Spaces:
Sleeping
Sleeping
Feat: Project Deployment on huggingface spaces
Browse files- .gitignore +174 -0
- README.md +87 -1
- achref/README.md +60 -0
- achref/infeasible.ilp +36 -0
- achref/src/config/__init__.py +1 -0
- achref/src/config/config.py +39 -0
- achref/src/logger/__init__.py +1 -0
- achref/src/logger/logger.py +106 -0
- achref/src/models/gorubi_models.py +255 -0
- achref/src/models/infeasible.ilp +24 -0
- achref/src/plotting/plot_trajectories.py +104 -0
- app.py +63 -0
- assets/compte_rendu.pdf +0 -0
- assets/favicon.ico +0 -0
- assets/introduction-to-vrp.svg +119 -0
- models/gurobi_models.py +224 -0
- requirements.txt +4 -0
- ui/gradio_sections.py +230 -0
.gitignore
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
|
| 110 |
+
# pdm
|
| 111 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 112 |
+
#pdm.lock
|
| 113 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 114 |
+
# in version control.
|
| 115 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 116 |
+
.pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 121 |
+
__pypackages__/
|
| 122 |
+
|
| 123 |
+
# Celery stuff
|
| 124 |
+
celerybeat-schedule
|
| 125 |
+
celerybeat.pid
|
| 126 |
+
|
| 127 |
+
# SageMath parsed files
|
| 128 |
+
*.sage.py
|
| 129 |
+
|
| 130 |
+
# Environments
|
| 131 |
+
.env
|
| 132 |
+
.venv
|
| 133 |
+
env/
|
| 134 |
+
venv/
|
| 135 |
+
ENV/
|
| 136 |
+
env.bak/
|
| 137 |
+
venv.bak/
|
| 138 |
+
|
| 139 |
+
# Spyder project settings
|
| 140 |
+
.spyderproject
|
| 141 |
+
.spyproject
|
| 142 |
+
|
| 143 |
+
# Rope project settings
|
| 144 |
+
.ropeproject
|
| 145 |
+
|
| 146 |
+
# mkdocs documentation
|
| 147 |
+
/site
|
| 148 |
+
|
| 149 |
+
# mypy
|
| 150 |
+
.mypy_cache/
|
| 151 |
+
.dmypy.json
|
| 152 |
+
dmypy.json
|
| 153 |
+
|
| 154 |
+
# Pyre type checker
|
| 155 |
+
.pyre/
|
| 156 |
+
|
| 157 |
+
# pytype static type analyzer
|
| 158 |
+
.pytype/
|
| 159 |
+
|
| 160 |
+
# Cython debug symbols
|
| 161 |
+
cython_debug/
|
| 162 |
+
|
| 163 |
+
# PyCharm
|
| 164 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 165 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 166 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 167 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 168 |
+
#.idea/
|
| 169 |
+
|
| 170 |
+
# Ruff stuff:
|
| 171 |
+
.ruff_cache/
|
| 172 |
+
|
| 173 |
+
# PyPI configuration file
|
| 174 |
+
.pypirc
|
README.md
CHANGED
|
@@ -11,4 +11,90 @@ license: mit
|
|
| 11 |
short_description: This project demonstrates the use of Linear Programming (PL)
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
short_description: This project demonstrates the use of Linear Programming (PL)
|
| 12 |
---
|
| 13 |
|
| 14 |
+
<h1 align="center">Operations Research Web Application</h1>
|
| 15 |
+
|
| 16 |
+
<p align="center">
|
| 17 |
+
<img src="https://insat.rnu.tn/assets/images/logo_c.png" width="100" alt="INSAT Logo"><br><br>
|
| 18 |
+
<a href="https://kacemath-operations-research.hf.space/" target="_blank">
|
| 19 |
+
<img src="https://img.shields.io/badge/HuggingFace-Space-blue?logo=huggingface" alt="Try it on Hugging Face Spaces" />
|
| 20 |
+
</a>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
This project demonstrates the use of **Linear Programming (PL)** and **Mixed-Integer Linear Programming (PLNE)** for solving real-world optimisation problems using **Gurobi**. It uses **Gradio** to provide an interactive web interface.
|
| 24 |
+
|
| 25 |
+
## Features
|
| 26 |
+
|
| 27 |
+
- **Production Planning (PL):** Optimises the number of products to manufacture for maximum profit under resource constraints.
|
| 28 |
+
- **Staff Scheduling (PLNE):** Mock assignment of employees to shifts based on availability.
|
| 29 |
+
|
| 30 |
+
## Project Structure
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
.
|
| 34 |
+
├── app.py # Main entry point of the Gradio application
|
| 35 |
+
├── assets/
|
| 36 |
+
│ └── compte_rendu.pdf # Project report
|
| 37 |
+
├── models/
|
| 38 |
+
│ └── gurobi_models.py # Gurobi-based solvers for PL and PLNE
|
| 39 |
+
├── ui/
|
| 40 |
+
│ └── gradio_sections.py # UI layout and Gradio component logic
|
| 41 |
+
├── requirements.txt # Python dependencies
|
| 42 |
+
└── README.md # Project documentation
|
| 43 |
+
````
|
| 44 |
+
|
| 45 |
+
## Prerequisites
|
| 46 |
+
- Python 3.9 or higher
|
| 47 |
+
|
| 48 |
+
## Environment Setup
|
| 49 |
+
### 1. Clone the repository
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
git clone https://github.com/KacemMathlouthi/OperationsResearch.git
|
| 53 |
+
cd OperationsResearch
|
| 54 |
+
````
|
| 55 |
+
|
| 56 |
+
### 2. Create and activate a virtual environment
|
| 57 |
+
|
| 58 |
+
#### Linux/macOS
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
python3 -m venv venv
|
| 62 |
+
source venv/bin/activate
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
#### Windows
|
| 66 |
+
|
| 67 |
+
```cmd
|
| 68 |
+
python -m venv venv
|
| 69 |
+
venv\Scripts\activate
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### 3. Install dependencies
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
pip install -r requirements.txt
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Running the Application
|
| 79 |
+
|
| 80 |
+
Ensure you are in the project root directory and your virtual environment is activated:
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
python app.py
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
The application will launch locally at `http://127.0.0.1:7860/`.
|
| 87 |
+
|
| 88 |
+
## Usage
|
| 89 |
+
|
| 90 |
+
### Tabs Available:
|
| 91 |
+
|
| 92 |
+
* **Project Info:** Displays team information and a PDF report.
|
| 93 |
+
* **Production Planning (PL):** Solve and visualise a linear programming problem using product and resource data.
|
| 94 |
+
* **Staff Scheduling (PLNE):** Simulated assignment of employees to shifts based on availability.
|
| 95 |
+
|
| 96 |
+
## Notes
|
| 97 |
+
|
| 98 |
+
* Visualisations are generated with `matplotlib`.
|
| 99 |
+
* UI built with `Gradio Blocks` using tabbed layout.
|
| 100 |
+
* PDF report embedded with base64 encoding.
|
achref/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Satellite Collision Avoidance Web App
|
| 2 |
+
|
| 3 |
+
[](https://opensource.org/licenses/MIT)
|
| 4 |
+
|
| 5 |
+
A web-based interface for optimizing satellite trajectories to prevent collisions while minimizing fuel consumption and deviation from uncorrected paths. Built with Python/Gurobi for optimization and Three.js for 3D visualization.
|
| 6 |
+
|
| 7 |
+

|
| 8 |
+
|
| 9 |
+
### Optimization Objective
|
| 10 |
+
|
| 11 |
+
We now jointly minimize fuel usage and trajectory deviation from the nominal (no-thrust) path:
|
| 12 |
+
|
| 13 |
+
```math
|
| 14 |
+
\text{Minimize } \sum_{i=1}^N \sum_{t=0}^{T-1} \delta_{i,t}
|
| 15 |
+
\;+
|
| 16 |
+
\;\lambda \sum_{i=1}^N \sum_{t=0}^{T} \sum_{a\in\{x,y,z\}} d_{i,t}^a
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
where:
|
| 20 |
+
|
| 21 |
+
* \$\delta\_{i,t} \ge c\_{\text{fuel}}\sum\_{a\in{x,y,z}} |u\_{i,t}^a|\$ (fuel proxy)
|
| 22 |
+
* \$d\_{i,t}^a \ge |p\_{i,t}^a - p\_{i,t}^{a,\mathrm{nom}}|\$ (deviation from nominal trajectory)
|
| 23 |
+
* \$\lambda\$ is an optional weight balancing fuel vs. deviation (default \$\lambda=1\$).
|
| 24 |
+
|
| 25 |
+
### Features
|
| 26 |
+
|
| 27 |
+
* Interactive 3D visualization of satellite trajectories
|
| 28 |
+
* Configurable safety margins, thrust limits, and deviation weight
|
| 29 |
+
* Real-time MILP optimization backend
|
| 30 |
+
* Scenario export/import in JSON format
|
| 31 |
+
|
| 32 |
+
## Problem Formulation
|
| 33 |
+
|
| 34 |
+
### Orbital Dynamics
|
| 35 |
+
|
| 36 |
+
For satellite \$i\$ at time \$t\$ with mass \$m\_i\$ and thrust \$\mathbf{u}\_{i,t}\$:
|
| 37 |
+
|
| 38 |
+
```math
|
| 39 |
+
\begin{aligned}
|
| 40 |
+
\mathbf{p}_{i,t+1} &= \mathbf{p}_{i,t} + \mathbf{v}_{i,t} \Delta t + \frac{1}{2m_i} \mathbf{u}_{i,t} (\Delta t)^2 \\
|
| 41 |
+
\mathbf{v}_{i,t+1} &= \mathbf{v}_{i,t} + \frac{1}{m_i} \mathbf{u}_{i,t} \Delta t
|
| 42 |
+
\end{aligned}
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### Collision Avoidance
|
| 46 |
+
|
| 47 |
+
For all satellite pairs \$(i,j)\$ and times \$t \ge 1\$:
|
| 48 |
+
|
| 49 |
+
```math
|
| 50 |
+
\begin{aligned}
|
| 51 |
+
|p_{i,t}^x - p_{j,t}^x| &\ge d_{\text{safe}} \cdot b_{ij,t}^x \\
|
| 52 |
+
|p_{i,t}^y - p_{j,t}^y| &\ge d_{\text{safe}} \cdot b_{ij,t}^y \\
|
| 53 |
+
|p_{i,t}^z - p_{j,t}^z| &\ge d_{\text{safe}} \cdot b_{ij,t}^z \\
|
| 54 |
+
b_{ij,t}^x + b_{ij,t}^y + b_{ij,t}^z &\ge 1 \quad (b_{ij,t}^a \in \{0,1\})
|
| 55 |
+
\end{aligned}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### Additional Resources
|
| 59 |
+
|
| 60 |
+
For a comprehensive explanation of the problem statement, including mathematical formulations and assumptions, refer to the [Problem Statement](./assets/problem_statement.md). This document provides in-depth details about the orbital dynamics, collision avoidance constraints, and optimization objectives used in this project.
|
achref/infeasible.ilp
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\ Model SatelliteLP_WithTracking_copy
|
| 2 |
+
\ LP format - for model browsing. Use MPS format to capture full model detail.
|
| 3 |
+
Minimize
|
| 4 |
+
|
| 5 |
+
Subject To
|
| 6 |
+
init_p_Sat1_1: p_Sat1_0[1] = 0
|
| 7 |
+
init_v_Sat1_1: v_Sat1_0[1] = 0
|
| 8 |
+
dyn_p_Sat1_0_1: - 0.0005 u_Sat1_0[1] - p_Sat1_0[1] + p_Sat1_1[1]
|
| 9 |
+
- 0.1 v_Sat1_0[1] = 0
|
| 10 |
+
dyn_v_Sat1_0_1: - 0.01 u_Sat1_0[1] - v_Sat1_0[1] + v_Sat1_1[1] = 0
|
| 11 |
+
dyn_p_Sat1_1_1: - 0.0005 u_Sat1_1[1] - p_Sat1_1[1] + p_Sat1_2[1]
|
| 12 |
+
- 0.1 v_Sat1_1[1] = 0
|
| 13 |
+
init_p_Sat2_1: p_Sat2_0[1] = 1.732051
|
| 14 |
+
init_v_Sat2_1: v_Sat2_0[1] = -0.0866025
|
| 15 |
+
dyn_p_Sat2_0_1: - 0.0005 u_Sat2_0[1] - p_Sat2_0[1] + p_Sat2_1[1]
|
| 16 |
+
- 0.1 v_Sat2_0[1] = 0
|
| 17 |
+
dyn_v_Sat2_0_1: - 0.01 u_Sat2_0[1] - v_Sat2_0[1] + v_Sat2_1[1] = 0
|
| 18 |
+
dyn_p_Sat2_1_1: - 0.0005 u_Sat2_1[1] - p_Sat2_1[1] + p_Sat2_2[1]
|
| 19 |
+
- 0.1 v_Sat2_1[1] = 0
|
| 20 |
+
con_dy_Sat1_Sat2_2: - p_Sat1_2[1] + p_Sat2_2[1] + dy_Sat1_Sat2_2 = 0
|
| 21 |
+
Bounds
|
| 22 |
+
-infinity <= u_Sat1_0[1] <= 100
|
| 23 |
+
-infinity <= u_Sat1_1[1] <= 100
|
| 24 |
+
u_Sat2_0[1] >= -100
|
| 25 |
+
u_Sat2_1[1] >= -100
|
| 26 |
+
p_Sat1_0[1] free
|
| 27 |
+
p_Sat1_1[1] free
|
| 28 |
+
p_Sat1_2[1] free
|
| 29 |
+
v_Sat1_0[1] free
|
| 30 |
+
v_Sat1_1[1] free
|
| 31 |
+
p_Sat2_0[1] free
|
| 32 |
+
p_Sat2_1[1] free
|
| 33 |
+
p_Sat2_2[1] free
|
| 34 |
+
v_Sat2_0[1] free
|
| 35 |
+
v_Sat2_1[1] free
|
| 36 |
+
End
|
achref/src/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .config import get_settings
|
achref/src/config/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
from pydantic_settings import BaseSettings
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv(".env")
|
| 7 |
+
|
| 8 |
+
class Settings(BaseSettings):
|
| 9 |
+
"""
|
| 10 |
+
Settings class for this application.
|
| 11 |
+
Utilizes the BaseSettings from Pydantic for environment variables.
|
| 12 |
+
"""
|
| 13 |
+
# Python setup
|
| 14 |
+
PYTHONPATH: str = None
|
| 15 |
+
|
| 16 |
+
class Config:
|
| 17 |
+
env_file = ".env"
|
| 18 |
+
extra = "ignore"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@lru_cache(maxsize=None)
|
| 22 |
+
def get_settings() -> Settings:
|
| 23 |
+
"""
|
| 24 |
+
Function to get and cache settings.
|
| 25 |
+
The settings are cached to avoid repeated disk I/O.
|
| 26 |
+
"""
|
| 27 |
+
environment = os.getenv("ENVIRONMENT", "local")
|
| 28 |
+
|
| 29 |
+
if environment == "local":
|
| 30 |
+
settings_file = ".env"
|
| 31 |
+
elif environment == "dev":
|
| 32 |
+
settings_file = ".env.dev"
|
| 33 |
+
elif environment == "prod":
|
| 34 |
+
settings_file = ".env.prod"
|
| 35 |
+
else:
|
| 36 |
+
raise ValueError(f"Invalid environment: {environment}")
|
| 37 |
+
|
| 38 |
+
# Load settings from the respective .env file
|
| 39 |
+
return Settings(_env_file=settings_file) # type: ignore
|
achref/src/logger/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .logger import get_logger
|
achref/src/logger/logger.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import logging
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from uvicorn.logging import ColourizedFormatter
|
| 7 |
+
from typing import Any, Callable
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
| 14 |
+
|
| 15 |
+
# Mapping of string levels to logging constants
|
| 16 |
+
LOG_LEVEL_MAPPING = {
|
| 17 |
+
"DEBUG": logging.DEBUG,
|
| 18 |
+
"INFO": logging.INFO,
|
| 19 |
+
"WARNING": logging.WARNING,
|
| 20 |
+
"ERROR": logging.ERROR,
|
| 21 |
+
"CRITICAL": logging.CRITICAL
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# Set the actual logging level
|
| 25 |
+
LOG_LEVEL = LOG_LEVEL_MAPPING.get(LOG_LEVEL, logging.INFO) # Default to INFO if invalid
|
| 26 |
+
|
| 27 |
+
# Custom colorized formatter to apply colors specifically to log levels
|
| 28 |
+
class CustomColourizedFormatter(ColourizedFormatter):
|
| 29 |
+
def format(self, record: logging.LogRecord) -> str:
|
| 30 |
+
# Define color mappings for different log levels
|
| 31 |
+
level_color_map = {
|
| 32 |
+
"DEBUG": "\033[34m", # Blue
|
| 33 |
+
"INFO": "\033[32m", # Green
|
| 34 |
+
"WARNING": "\033[33m", # Yellow
|
| 35 |
+
"ERROR": "\033[31m", # Red
|
| 36 |
+
"CRITICAL": "\033[41m", # Red background
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# Reset color
|
| 40 |
+
reset = "\033[0m"
|
| 41 |
+
|
| 42 |
+
# Apply color to the log level name
|
| 43 |
+
record.levelname = f"{level_color_map.get(record.levelname, '')}{record.levelname}{reset}"
|
| 44 |
+
|
| 45 |
+
# Format the log message using the parent class's format method
|
| 46 |
+
return super().format(record)
|
| 47 |
+
|
| 48 |
+
def get_logger(name: str) -> logging.Logger:
|
| 49 |
+
"""Creates a logger object that reads its log level from .env.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
name (str): name given to the logger
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
logging.Logger: logger object to be used for logging
|
| 56 |
+
"""
|
| 57 |
+
# Create logger
|
| 58 |
+
logger = logging.getLogger(name)
|
| 59 |
+
logger.setLevel(LOG_LEVEL) # Set level based on .env file
|
| 60 |
+
|
| 61 |
+
# Prevent adding multiple handlers if already exists
|
| 62 |
+
if not logger.hasHandlers():
|
| 63 |
+
# Create console handler
|
| 64 |
+
ch = logging.StreamHandler(sys.stdout)
|
| 65 |
+
ch.setLevel(LOG_LEVEL) # Set handler level
|
| 66 |
+
|
| 67 |
+
# Create a custom formatter with colored log levels
|
| 68 |
+
formatter = CustomColourizedFormatter(
|
| 69 |
+
"{asctime} | {levelname:<8} | {message}",
|
| 70 |
+
style="{",
|
| 71 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 72 |
+
use_colors=True
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Add formatter to the handler
|
| 76 |
+
ch.setFormatter(formatter)
|
| 77 |
+
|
| 78 |
+
# Add handler to the logger
|
| 79 |
+
logger.addHandler(ch)
|
| 80 |
+
|
| 81 |
+
return logger
|
| 82 |
+
|
| 83 |
+
# Logger decorator implementation
|
| 84 |
+
def log_function_call(logger: logging.Logger) -> Callable:
|
| 85 |
+
"""A decorator that logs the function calls and results.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
logger (logging.Logger): The logger instance to use for logging.
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
Callable: A wrapper function that logs the execution details.
|
| 92 |
+
"""
|
| 93 |
+
def decorator(func: Callable) -> Callable:
|
| 94 |
+
@functools.wraps(func)
|
| 95 |
+
def wrapper(*args, **kwargs) -> Any:
|
| 96 |
+
# Log the function call with arguments
|
| 97 |
+
logger.debug(f"Calling {func.__name__} with args: {args} and kwargs: {kwargs}")
|
| 98 |
+
result = func(*args, **kwargs)
|
| 99 |
+
# Log the function result
|
| 100 |
+
logger.debug(f"{func.__name__} returned {result}")
|
| 101 |
+
return result
|
| 102 |
+
return wrapper
|
| 103 |
+
return decorator
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
logger = logging.getLogger(__name__)
|
achref/src/models/gorubi_models.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gurobipy as gp
|
| 2 |
+
from gurobipy import GRB
|
| 3 |
+
from itertools import combinations
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
from mpl_toolkits.mplot3d import Axes3D
|
| 6 |
+
import re
|
| 7 |
+
import matplotlib.animation as animation
|
| 8 |
+
|
| 9 |
+
def animate_satellite_trajectories(model, time_steps, interval=200):
|
| 10 |
+
"""
|
| 11 |
+
Creates a 3D animated plot of satellite trajectories and thrust vectors.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
model (gp.Model): Solved Gurobi model containing:
|
| 15 |
+
- position variables named `p_{sat}_{t}[coord]`
|
| 16 |
+
- thrust variables named `u_{sat}_{t}[coord]`
|
| 17 |
+
time_steps (list of int): List of time step indices used in the optimization.
|
| 18 |
+
interval (int): Delay between frames in milliseconds.
|
| 19 |
+
"""
|
| 20 |
+
# Extract data containers
|
| 21 |
+
positions = {}
|
| 22 |
+
thrusts = {}
|
| 23 |
+
# Patterns to match variable names
|
| 24 |
+
p_pattern = re.compile(r'p_(\w+)_(\d+)\[(\d+)\]')
|
| 25 |
+
u_pattern = re.compile(r'u_(\w+)_(\d+)\[(\d+)\]')
|
| 26 |
+
|
| 27 |
+
# Populate positions and thrusts dictionaries
|
| 28 |
+
for var in model.getVars():
|
| 29 |
+
# Position variables
|
| 30 |
+
pm = p_pattern.match(var.VarName)
|
| 31 |
+
if pm:
|
| 32 |
+
sat, t_str, coord_str = pm.groups()
|
| 33 |
+
t = int(t_str)
|
| 34 |
+
coord = int(coord_str)
|
| 35 |
+
positions.setdefault(sat, {}).setdefault(t, [0.0, 0.0, 0.0])[coord] = var.Xn
|
| 36 |
+
continue
|
| 37 |
+
# Thrust variables
|
| 38 |
+
um = u_pattern.match(var.VarName)
|
| 39 |
+
if um:
|
| 40 |
+
sat, t_str, coord_str = um.groups()
|
| 41 |
+
t = int(t_str)
|
| 42 |
+
coord = int(coord_str)
|
| 43 |
+
thrusts.setdefault(sat, {}).setdefault(t, [0.0, 0.0, 0.0])[coord] = var.Xn
|
| 44 |
+
|
| 45 |
+
sats = sorted(positions.keys())
|
| 46 |
+
|
| 47 |
+
# Prepare 3D figure
|
| 48 |
+
fig = plt.figure(figsize=(10, 8))
|
| 49 |
+
ax = fig.add_subplot(111, projection='3d')
|
| 50 |
+
ax.set_xlabel('X (m)')
|
| 51 |
+
ax.set_ylabel('Y (m)')
|
| 52 |
+
ax.set_zlabel('Z (m)')
|
| 53 |
+
ax.set_title('Satellite Trajectories and Thrust Animation')
|
| 54 |
+
|
| 55 |
+
# Plot objects for trajectories, end markers, and quivers
|
| 56 |
+
traj_lines = {sat: ax.plot([], [], [], label=f'{sat} traj')[0] for sat in sats}
|
| 57 |
+
end_markers = {sat: ax.plot([], [], [], marker='o', linestyle='')[0] for sat in sats}
|
| 58 |
+
quiver_objs = {sat: None for sat in sats}
|
| 59 |
+
|
| 60 |
+
# Set axis limits based on all positions
|
| 61 |
+
all_x = [positions[s][t][0] for s in sats for t in positions[s]]
|
| 62 |
+
all_y = [positions[s][t][1] for s in sats for t in positions[s]]
|
| 63 |
+
all_z = [positions[s][t][2] for s in sats for t in positions[s]]
|
| 64 |
+
ax.set_xlim(min(all_x), max(all_x))
|
| 65 |
+
ax.set_ylim(min(all_y), max(all_y))
|
| 66 |
+
ax.set_zlim(min(all_z), max(all_z))
|
| 67 |
+
ax.legend()
|
| 68 |
+
|
| 69 |
+
def update(frame):
|
| 70 |
+
# Remove previous quivers
|
| 71 |
+
for sat in sats:
|
| 72 |
+
if quiver_objs[sat] is not None:
|
| 73 |
+
quiver_objs[sat].remove()
|
| 74 |
+
|
| 75 |
+
# Update each satellite's trajectory, marker, and thrust arrow
|
| 76 |
+
for sat in sats:
|
| 77 |
+
# Trajectory up to current frame
|
| 78 |
+
xs = [positions[sat][t][0] for t in time_steps if t <= frame]
|
| 79 |
+
ys = [positions[sat][t][1] for t in time_steps if t <= frame]
|
| 80 |
+
zs = [positions[sat][t][2] for t in time_steps if t <= frame]
|
| 81 |
+
traj_lines[sat].set_data(xs, ys)
|
| 82 |
+
traj_lines[sat].set_3d_properties(zs)
|
| 83 |
+
|
| 84 |
+
# End marker at last position
|
| 85 |
+
if xs:
|
| 86 |
+
end_markers[sat].set_data([xs[-1]], [ys[-1]])
|
| 87 |
+
end_markers[sat].set_3d_properties([zs[-1]])
|
| 88 |
+
|
| 89 |
+
# Thrust arrow at this frame
|
| 90 |
+
ux, uy, uz = thrusts.get(sat, {}).get(frame, [0.0, 0.0, 0.0])
|
| 91 |
+
# Draw new quiver
|
| 92 |
+
quiver_objs[sat] = ax.quiver(
|
| 93 |
+
xs[-1], ys[-1], zs[-1], ux, uy, uz,
|
| 94 |
+
length=1e3, normalize=True
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Return artists to update
|
| 98 |
+
artists = list(traj_lines.values()) + list(end_markers.values())
|
| 99 |
+
artists += [q for q in quiver_objs.values() if q is not None]
|
| 100 |
+
return artists
|
| 101 |
+
|
| 102 |
+
# Create animation
|
| 103 |
+
ani = animation.FuncAnimation(
|
| 104 |
+
fig, update, frames=time_steps, interval=interval, blit=False
|
| 105 |
+
)
|
| 106 |
+
ani.save("satellite_trajectories.gif", writer='imagemagick', fps=5)
|
| 107 |
+
plt.show()
|
| 108 |
+
# ---------------------------
|
| 109 |
+
# SCALED DATA INITIALIZATION
|
| 110 |
+
# ---------------------------
|
| 111 |
+
# Units:
|
| 112 |
+
# • distance unit = 10 km
|
| 113 |
+
# • time unit = 10 s
|
| 114 |
+
# • velocity unit = 1 km/s
|
| 115 |
+
# • thrust unit = 1 kN
|
| 116 |
+
# • mass unit = 0.01 kg
|
| 117 |
+
|
| 118 |
+
time_steps = list(range(20))
|
| 119 |
+
dt = 10.0 # 10 s
|
| 120 |
+
F_max = 10.0 # 100 kN
|
| 121 |
+
c_fuel = 100 # same relative cost
|
| 122 |
+
m_i = 1000.0 # 0.01 kg
|
| 123 |
+
# safety distance scaled: 0.00001 units = 0.1 m
|
| 124 |
+
# we apply collision avoidance only from t=1 onward to respect initial positions
|
| 125 |
+
d_safe = 1
|
| 126 |
+
|
| 127 |
+
initial_state = {
|
| 128 |
+
"Sat1": { "position": [20000/10000, 0.0, 0.0], "velocity": [-100/1000, 0.0, 0.0] },
|
| 129 |
+
"Sat2": { "position": [-10000/10000, 17320.51/10000, 0.0], "velocity": [ 50/1000, -86.6025/1000, 0.0] },
|
| 130 |
+
"Sat3": { "position": [-10000/10000,-17320.51/10000, 0.0], "velocity": [ 50/1000, 86.6025/1000, 0.0] },
|
| 131 |
+
}
|
| 132 |
+
satellites = list(initial_state.keys())
|
| 133 |
+
|
| 134 |
+
# ------------------------------------
|
| 135 |
+
# PRECOMPUTE NOMINAL (UNCORRECTED) TRAJECTORIES
|
| 136 |
+
# ------------------------------------
|
| 137 |
+
# Nominal case: no thrust (u=0)
|
| 138 |
+
nominal_positions = {sat: {0: initial_state[sat]['position'][:] } for sat in satellites}
|
| 139 |
+
for sat in satellites:
|
| 140 |
+
pos = nominal_positions[sat]
|
| 141 |
+
vel = initial_state[sat]['velocity']
|
| 142 |
+
for t in time_steps[:-1]:
|
| 143 |
+
# constant velocity motion
|
| 144 |
+
next_pos = [pos[t][i] + vel[i] * dt for i in range(3)]
|
| 145 |
+
pos[t+1] = next_pos
|
| 146 |
+
|
| 147 |
+
# ------------------------------------
|
| 148 |
+
# ORBITAL DYNAMICS FUNCTION
|
| 149 |
+
# ------------------------------------
|
| 150 |
+
def add_orbital_dynamics(model, sat, time_steps, dt, mass, u, initial_pos, initial_vel):
|
| 151 |
+
p = {t: model.addVars(3, lb=-GRB.INFINITY, ub=GRB.INFINITY, name=f"p_{sat}_{t}") for t in time_steps}
|
| 152 |
+
v = {t: model.addVars(3, lb=-GRB.INFINITY, ub=GRB.INFINITY, name=f"v_{sat}_{t}") for t in time_steps}
|
| 153 |
+
|
| 154 |
+
# initial conditions
|
| 155 |
+
for coord in range(3):
|
| 156 |
+
model.addConstr(p[0][coord] == initial_pos[sat][coord], name=f"init_p_{sat}_{coord}")
|
| 157 |
+
model.addConstr(v[0][coord] == initial_vel[sat][coord], name=f"init_v_{sat}_{coord}")
|
| 158 |
+
|
| 159 |
+
# dynamics for t=0..T-1
|
| 160 |
+
for t in time_steps[:-1]:
|
| 161 |
+
for coord in range(3):
|
| 162 |
+
model.addConstr(
|
| 163 |
+
p[t+1][coord] == p[t][coord]
|
| 164 |
+
+ v[t][coord] * dt
|
| 165 |
+
+ 0.5 * (u[sat, t][coord] / mass) * dt * dt,
|
| 166 |
+
name=f"dyn_p_{sat}_{t}_{coord}"
|
| 167 |
+
)
|
| 168 |
+
model.addConstr(
|
| 169 |
+
v[t+1][coord] == v[t][coord]
|
| 170 |
+
+ (u[sat, t][coord] / mass) * dt,
|
| 171 |
+
name=f"dyn_v_{sat}_{t}_{coord}"
|
| 172 |
+
)
|
| 173 |
+
return p, v
|
| 174 |
+
|
| 175 |
+
# ---------------------------
|
| 176 |
+
# BUILD & SOLVE WITH TRACKING PENALTY
|
| 177 |
+
# ---------------------------
|
| 178 |
+
|
| 179 |
+
def solve_lp_model():
|
| 180 |
+
model = gp.Model("SatelliteLP_WithTracking")
|
| 181 |
+
|
| 182 |
+
# decision vars: thrust u, fuel cost delta
|
| 183 |
+
u = {}
|
| 184 |
+
delta = {}
|
| 185 |
+
for sat in satellites:
|
| 186 |
+
for t in time_steps[:-1]:
|
| 187 |
+
u[sat, t] = model.addVars(3, lb=-F_max, ub=F_max, name=f"u_{sat}_{t}")
|
| 188 |
+
delta[sat, t] = model.addVar(lb=0.0, name=f"delta_{sat}_{t}")
|
| 189 |
+
for k in range(3):
|
| 190 |
+
abs_u = model.addVar(lb=0.0, name=f"abs_u_{sat}_{t}_{k}")
|
| 191 |
+
model.addGenConstrAbs(abs_u, u[sat, t][k], name=f"absConstr_{sat}_{t}_{k}")
|
| 192 |
+
model.addConstr(delta[sat, t] >= c_fuel * abs_u, name=f"fuelLink_{sat}_{t}_{k}")
|
| 193 |
+
|
| 194 |
+
# dynamics and collect position vars
|
| 195 |
+
initial_pos = {s: initial_state[s]["position"] for s in satellites}
|
| 196 |
+
initial_vel = {s: initial_state[s]["velocity"] for s in satellites}
|
| 197 |
+
positions = {}
|
| 198 |
+
for sat in satellites:
|
| 199 |
+
p, v = add_orbital_dynamics(model, sat, time_steps, dt, m_i, u, initial_pos, initial_vel)
|
| 200 |
+
positions[sat] = p
|
| 201 |
+
|
| 202 |
+
# collision avoidance as before
|
| 203 |
+
for t in time_steps[1:]:
|
| 204 |
+
for i, j in combinations(satellites, 2):
|
| 205 |
+
dx = model.addVar(name=f"dx_{i}_{j}_{t}")
|
| 206 |
+
dy = model.addVar(name=f"dy_{i}_{j}_{t}")
|
| 207 |
+
dz = model.addVar(name=f"dz_{i}_{j}_{t}")
|
| 208 |
+
model.addConstr(dx == positions[i][t][0] - positions[j][t][0], name=f"con_dx_{i}_{j}_{t}")
|
| 209 |
+
model.addConstr(dy == positions[i][t][1] - positions[j][t][1], name=f"con_dy_{i}_{j}_{t}")
|
| 210 |
+
model.addConstr(dz == positions[i][t][2] - positions[j][t][2], name=f"con_dz_{i}_{j}_{t}")
|
| 211 |
+
absx = model.addVar(lb=0.0, name=f"absx_{i}_{j}_{t}")
|
| 212 |
+
absy = model.addVar(lb=0.0, name=f"absy_{i}_{j}_{t}")
|
| 213 |
+
absz = model.addVar(lb=0.0, name=f"absz_{i}_{j}_{t}")
|
| 214 |
+
model.addGenConstrAbs(absx, dx, name=f"absxConstr_{i}_{j}_{t}")
|
| 215 |
+
model.addGenConstrAbs(absy, dy, name=f"absyConstr_{i}_{j}_{t}")
|
| 216 |
+
model.addGenConstrAbs(absz, dz, name=f"abszConstr_{i}_{j}_{t}")
|
| 217 |
+
bx = model.addVar(vtype=GRB.BINARY, name=f"bx_{i}_{j}_{t}")
|
| 218 |
+
by = model.addVar(vtype=GRB.BINARY, name=f"by_{i}_{j}_{t}")
|
| 219 |
+
bz = model.addVar(vtype=GRB.BINARY, name=f"bz_{i}_{j}_{t}")
|
| 220 |
+
model.addConstr(absx >= d_safe * bx, name=f"safe_x_{i}_{j}_{t}")
|
| 221 |
+
model.addConstr(absy >= d_safe * by, name=f"safe_y_{i}_{j}_{t}")
|
| 222 |
+
model.addConstr(absz >= d_safe * bz, name=f"safe_z_{i}_{j}_{t}")
|
| 223 |
+
model.addConstr(bx + by + bz >= 1, name=f"sep_sum_{i}_{j}_{t}")
|
| 224 |
+
|
| 225 |
+
# tracking penalty: absolute deviation from nominal
|
| 226 |
+
tracking_dev = {}
|
| 227 |
+
for sat in satellites:
|
| 228 |
+
for t in time_steps:
|
| 229 |
+
for coord in range(3):
|
| 230 |
+
dev = model.addVar(lb=0.0, name=f"dev_{sat}_{t}_{coord}")
|
| 231 |
+
tracking_dev[sat, t, coord] = dev
|
| 232 |
+
# deviation = p - p_nom
|
| 233 |
+
p_var = positions[sat][t][coord]
|
| 234 |
+
p_nom = nominal_positions[sat][t][coord]
|
| 235 |
+
# p_var - p_nom <= dev and -(p_var - p_nom) <= dev
|
| 236 |
+
model.addConstr(p_var - p_nom <= dev, name=f"dev_pos_{sat}_{t}_{coord}")
|
| 237 |
+
model.addConstr(p_nom - p_var <= dev, name=f"dev_neg_{sat}_{t}_{coord}")
|
| 238 |
+
|
| 239 |
+
# objective: fuel + tracking deviation
|
| 240 |
+
obj = gp.quicksum(delta[s, t] for s in satellites for t in time_steps[:-1]) \
|
| 241 |
+
+ gp.quicksum(tracking_dev[s, t, c] for s in satellites for t in time_steps for c in range(3))
|
| 242 |
+
model.setObjective(obj, GRB.MINIMIZE)
|
| 243 |
+
|
| 244 |
+
# solve
|
| 245 |
+
model.optimize()
|
| 246 |
+
if model.status == GRB.INFEASIBLE:
|
| 247 |
+
model.computeIIS()
|
| 248 |
+
model.write("infeasible.ilp")
|
| 249 |
+
print("Model infeasible; IIS written to 'infeasible.ilp'.")
|
| 250 |
+
else:
|
| 251 |
+
print("Optimal solution found.")
|
| 252 |
+
animate_satellite_trajectories(model, time_steps)
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
solve_lp_model()
|
achref/src/models/infeasible.ilp
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\ Model SatelliteLP_WithTracking_copy
|
| 2 |
+
\ LP format - for model browsing. Use MPS format to capture full model detail.
|
| 3 |
+
Minimize
|
| 4 |
+
|
| 5 |
+
Subject To
|
| 6 |
+
init_p_Sat1_1: p_Sat1_0[1] = 0
|
| 7 |
+
init_v_Sat1_1: v_Sat1_0[1] = 0
|
| 8 |
+
dyn_p_Sat1_0_1: - 0.0125 u_Sat1_0[1] - p_Sat1_0[1] + p_Sat1_1[1]
|
| 9 |
+
- 5 v_Sat1_0[1] = 0
|
| 10 |
+
init_p_Sat2_1: p_Sat2_0[1] = 1.732051
|
| 11 |
+
init_v_Sat2_1: v_Sat2_0[1] = -0.0866025
|
| 12 |
+
dyn_p_Sat2_0_1: - 0.0125 u_Sat2_0[1] - p_Sat2_0[1] + p_Sat2_1[1]
|
| 13 |
+
- 5 v_Sat2_0[1] = 0
|
| 14 |
+
con_dy_Sat1_Sat2_1: - p_Sat1_1[1] + p_Sat2_1[1] + dy_Sat1_Sat2_1 = 0
|
| 15 |
+
Bounds
|
| 16 |
+
-infinity <= u_Sat1_0[1] <= 10
|
| 17 |
+
u_Sat2_0[1] >= -10
|
| 18 |
+
p_Sat1_0[1] free
|
| 19 |
+
p_Sat1_1[1] free
|
| 20 |
+
v_Sat1_0[1] free
|
| 21 |
+
p_Sat2_0[1] free
|
| 22 |
+
p_Sat2_1[1] free
|
| 23 |
+
v_Sat2_0[1] free
|
| 24 |
+
End
|
achref/src/plotting/plot_trajectories.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
from mpl_toolkits.mplot3d import Axes3D
|
| 3 |
+
import re
|
| 4 |
+
import matplotlib.animation as animation
|
| 5 |
+
|
| 6 |
+
def animate_satellite_trajectories(model, time_steps, interval=200):
|
| 7 |
+
"""
|
| 8 |
+
Creates a 3D animated plot of satellite trajectories and thrust vectors.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
model (gp.Model): Solved Gurobi model containing:
|
| 12 |
+
- position variables named `p_{sat}_{t}[coord]`
|
| 13 |
+
- thrust variables named `u_{sat}_{t}[coord]`
|
| 14 |
+
time_steps (list of int): List of time step indices used in the optimization.
|
| 15 |
+
interval (int): Delay between frames in milliseconds.
|
| 16 |
+
"""
|
| 17 |
+
# Extract data containers
|
| 18 |
+
positions = {}
|
| 19 |
+
thrusts = {}
|
| 20 |
+
# Patterns to match variable names
|
| 21 |
+
p_pattern = re.compile(r'p_(\w+)_(\d+)\[(\d+)\]')
|
| 22 |
+
u_pattern = re.compile(r'u_(\w+)_(\d+)\[(\d+)\]')
|
| 23 |
+
|
| 24 |
+
# Populate positions and thrusts dictionaries
|
| 25 |
+
for var in model.getVars():
|
| 26 |
+
# Position variables
|
| 27 |
+
pm = p_pattern.match(var.VarName)
|
| 28 |
+
if pm:
|
| 29 |
+
sat, t_str, coord_str = pm.groups()
|
| 30 |
+
t = int(t_str)
|
| 31 |
+
coord = int(coord_str)
|
| 32 |
+
positions.setdefault(sat, {}).setdefault(t, [0.0, 0.0, 0.0])[coord] = var.Xn
|
| 33 |
+
continue
|
| 34 |
+
# Thrust variables
|
| 35 |
+
um = u_pattern.match(var.VarName)
|
| 36 |
+
if um:
|
| 37 |
+
sat, t_str, coord_str = um.groups()
|
| 38 |
+
t = int(t_str)
|
| 39 |
+
coord = int(coord_str)
|
| 40 |
+
thrusts.setdefault(sat, {}).setdefault(t, [0.0, 0.0, 0.0])[coord] = var.Xn
|
| 41 |
+
|
| 42 |
+
sats = sorted(positions.keys())
|
| 43 |
+
|
| 44 |
+
# Prepare 3D figure
|
| 45 |
+
fig = plt.figure(figsize=(10, 8))
|
| 46 |
+
ax = fig.add_subplot(111, projection='3d')
|
| 47 |
+
ax.set_xlabel('X (m)')
|
| 48 |
+
ax.set_ylabel('Y (m)')
|
| 49 |
+
ax.set_zlabel('Z (m)')
|
| 50 |
+
ax.set_title('Satellite Trajectories and Thrust Animation')
|
| 51 |
+
|
| 52 |
+
# Plot objects for trajectories, end markers, and quivers
|
| 53 |
+
traj_lines = {sat: ax.plot([], [], [], label=f'{sat} traj')[0] for sat in sats}
|
| 54 |
+
end_markers = {sat: ax.plot([], [], [], marker='o', linestyle='')[0] for sat in sats}
|
| 55 |
+
quiver_objs = {sat: None for sat in sats}
|
| 56 |
+
|
| 57 |
+
# Set axis limits based on all positions
|
| 58 |
+
all_x = [positions[s][t][0] for s in sats for t in positions[s]]
|
| 59 |
+
all_y = [positions[s][t][1] for s in sats for t in positions[s]]
|
| 60 |
+
all_z = [positions[s][t][2] for s in sats for t in positions[s]]
|
| 61 |
+
ax.set_xlim(min(all_x), max(all_x))
|
| 62 |
+
ax.set_ylim(min(all_y), max(all_y))
|
| 63 |
+
ax.set_zlim(min(all_z), max(all_z))
|
| 64 |
+
ax.legend()
|
| 65 |
+
|
| 66 |
+
def update(frame):
|
| 67 |
+
# Remove previous quivers
|
| 68 |
+
for sat in sats:
|
| 69 |
+
if quiver_objs[sat] is not None:
|
| 70 |
+
quiver_objs[sat].remove()
|
| 71 |
+
|
| 72 |
+
# Update each satellite's trajectory, marker, and thrust arrow
|
| 73 |
+
for sat in sats:
|
| 74 |
+
# Trajectory up to current frame
|
| 75 |
+
xs = [positions[sat][t][0] for t in time_steps if t <= frame]
|
| 76 |
+
ys = [positions[sat][t][1] for t in time_steps if t <= frame]
|
| 77 |
+
zs = [positions[sat][t][2] for t in time_steps if t <= frame]
|
| 78 |
+
traj_lines[sat].set_data(xs, ys)
|
| 79 |
+
traj_lines[sat].set_3d_properties(zs)
|
| 80 |
+
|
| 81 |
+
# End marker at last position
|
| 82 |
+
if xs:
|
| 83 |
+
end_markers[sat].set_data([xs[-1]], [ys[-1]])
|
| 84 |
+
end_markers[sat].set_3d_properties([zs[-1]])
|
| 85 |
+
|
| 86 |
+
# Thrust arrow at this frame
|
| 87 |
+
ux, uy, uz = thrusts.get(sat, {}).get(frame, [0.0, 0.0, 0.0])
|
| 88 |
+
# Draw new quiver
|
| 89 |
+
quiver_objs[sat] = ax.quiver(
|
| 90 |
+
xs[-1], ys[-1], zs[-1], ux, uy, uz,
|
| 91 |
+
length=1e3, normalize=True
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Return artists to update
|
| 95 |
+
artists = list(traj_lines.values()) + list(end_markers.values())
|
| 96 |
+
artists += [q for q in quiver_objs.values() if q is not None]
|
| 97 |
+
return artists
|
| 98 |
+
|
| 99 |
+
# Create animation
|
| 100 |
+
ani = animation.FuncAnimation(
|
| 101 |
+
fig, update, frames=time_steps, interval=interval, blit=False
|
| 102 |
+
)
|
| 103 |
+
ani.save("satellite_trajectories.gif", writer='imagemagick', fps=5)
|
| 104 |
+
plt.show()
|
app.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from ui.gradio_sections import (
|
| 5 |
+
project_info_tab,
|
| 6 |
+
production_planning_tab,
|
| 7 |
+
vehicle_routing_tab,
|
| 8 |
+
)
|
| 9 |
+
from models.gurobi_models import solve_pl, solve_plne
|
| 10 |
+
|
| 11 |
+
# Mock Data
|
| 12 |
+
pl_df = pd.DataFrame(
|
| 13 |
+
{
|
| 14 |
+
"Product": ["A", "B", "C"],
|
| 15 |
+
"Profit/Unit": [15, 20, 15],
|
| 16 |
+
"Resource Usage": [3, 5, 2],
|
| 17 |
+
}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
plne_df = pd.DataFrame([
|
| 21 |
+
{"Node": 0, "X": 50, "Y": 50, "Demand": 0},
|
| 22 |
+
{"Node": 1, "X": 20, "Y": 20, "Demand": 10},
|
| 23 |
+
{"Node": 2, "X": 80, "Y": 20, "Demand": 15},
|
| 24 |
+
{"Node": 3, "X": 20, "Y": 80, "Demand": 10},
|
| 25 |
+
{"Node": 4, "X": 80, "Y": 80, "Demand": 10},
|
| 26 |
+
{"Node": 5, "X": 50, "Y": 10, "Demand": 20},
|
| 27 |
+
])
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Descriptions
|
| 31 |
+
pl_description = """
|
| 32 |
+
### 🏭 Production Planning (PL)
|
| 33 |
+
Select the quantity of each product to produce to **maximize profit**, under limited resource constraints.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
plne_description = """
|
| 37 |
+
### 🚚 Capacitated Vehicle Routing Problem
|
| 38 |
+
Provide node coordinates and demands, plus vehicle capacity and number of vehicles.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
# Read and encode the PDF - go up one directory to find assets at project root
|
| 42 |
+
favicon_path = os.path.join(os.path.dirname(__file__), "assets", "favicon.ico")
|
| 43 |
+
|
| 44 |
+
# Assemble UI
|
| 45 |
+
with gr.Blocks(title="Operations Research App") as ro_app:
|
| 46 |
+
gr.Markdown(
|
| 47 |
+
"""
|
| 48 |
+
<div style="display: flex; align-items: center; gap: 20px;">
|
| 49 |
+
<img src="https://insat.rnu.tn/assets/images/logo_c.png" width="100">
|
| 50 |
+
<div>
|
| 51 |
+
<h1 style="margin-bottom: 5px;">Operations Research Project</h1>
|
| 52 |
+
<p style="margin-top: 0; font-size: 14px; color: gray;">Choose the convenient tab to solve a problem</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
"""
|
| 56 |
+
)
|
| 57 |
+
with gr.Tabs():
|
| 58 |
+
project_info_tab()
|
| 59 |
+
production_planning_tab(pl_df, solve_pl, pl_description)
|
| 60 |
+
vehicle_routing_tab(plne_df, solve_plne, plne_description)
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
ro_app.launch(favicon_path=favicon_path)
|
assets/compte_rendu.pdf
ADDED
|
Binary file (49.7 kB). View file
|
|
|
assets/favicon.ico
ADDED
|
|
assets/introduction-to-vrp.svg
ADDED
|
|
models/gurobi_models.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from gurobipy import Model, GRB
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import math
|
| 4 |
+
from gurobipy import quicksum
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import achref.src.logger as logger
|
| 7 |
+
|
| 8 |
+
logger = logger.get_logger(__name__)
|
| 9 |
+
|
| 10 |
+
def solve_pl(data, total_resource=100):
|
| 11 |
+
model = Model("ProductionPlanning")
|
| 12 |
+
logger.info("Starting Gurobi model for production planning")
|
| 13 |
+
logger.info("Data: %s", data)
|
| 14 |
+
# Turn off solver output
|
| 15 |
+
model.setParam("OutputFlag", 0)
|
| 16 |
+
|
| 17 |
+
# Extract product info
|
| 18 |
+
products = data["Product"].tolist()
|
| 19 |
+
profits = data["Profit/Unit"].tolist()
|
| 20 |
+
usage = data["Resource Usage"].tolist()
|
| 21 |
+
|
| 22 |
+
# Create decision variables
|
| 23 |
+
x = {
|
| 24 |
+
prod: model.addVar(name=f"x_{prod}", lb=0, vtype=GRB.CONTINUOUS)
|
| 25 |
+
for prod in products
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# Objective: Maximise total profit
|
| 29 |
+
model.setObjective(
|
| 30 |
+
sum(profits[i] * x[products[i]] for i in range(len(products))),
|
| 31 |
+
GRB.MAXIMIZE,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Constraint: total resource usage <= available
|
| 35 |
+
model.addConstr(
|
| 36 |
+
sum(usage[i] * x[products[i]] for i in range(len(products))) <= total_resource,
|
| 37 |
+
name="ResourceConstraint",
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Solve
|
| 41 |
+
model.optimize()
|
| 42 |
+
|
| 43 |
+
# Extract results
|
| 44 |
+
result_df = pd.DataFrame({
|
| 45 |
+
"Product": products,
|
| 46 |
+
"Quantity Produced": [x[prod].X for prod in products],
|
| 47 |
+
"Profit": [x[prod].X * profits[i] for i, prod in enumerate(products)]
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
result_df["Profit per Resource"] = result_df["Profit"] / data["Resource Usage"]
|
| 51 |
+
|
| 52 |
+
# Prepare a single figure with 5 subplots
|
| 53 |
+
fig, axs = plt.subplots(3, 2, figsize=(14, 12))
|
| 54 |
+
fig.suptitle("Production Planning Visualisations", fontsize=16, y=1.02)
|
| 55 |
+
|
| 56 |
+
# Plot 1: Quantity Produced
|
| 57 |
+
axs[0, 0].bar(result_df["Product"], result_df["Quantity Produced"])
|
| 58 |
+
axs[0, 0].set_title("Quantity Produced per Product")
|
| 59 |
+
|
| 60 |
+
# Plot 2: Stacked Bar (Quantity + Profit)
|
| 61 |
+
axs[0, 1].bar(result_df["Product"], result_df["Quantity Produced"], label="Quantity")
|
| 62 |
+
axs[0, 1].bar(result_df["Product"], result_df["Profit"],
|
| 63 |
+
bottom=result_df["Quantity Produced"], label="Profit", alpha=0.6)
|
| 64 |
+
axs[0, 1].set_title("Stacked Bar: Quantity + Profit")
|
| 65 |
+
axs[0, 1].legend()
|
| 66 |
+
|
| 67 |
+
# Plot 3: Pie Chart of Profit
|
| 68 |
+
axs[1, 0].pie(result_df["Profit"], labels=result_df["Product"], autopct='%1.1f%%')
|
| 69 |
+
axs[1, 0].set_title("Profit Share per Product")
|
| 70 |
+
|
| 71 |
+
# Plot 4: Cumulative Profit
|
| 72 |
+
df_sorted = result_df.sort_values(by="Profit", ascending=False).reset_index(drop=True)
|
| 73 |
+
df_sorted["Cumulative Profit"] = df_sorted["Profit"].cumsum()
|
| 74 |
+
axs[1, 1].plot(df_sorted["Product"], df_sorted["Cumulative Profit"], marker="o")
|
| 75 |
+
axs[1, 1].set_title("Cumulative Profit by Product")
|
| 76 |
+
axs[1, 1].set_ylabel("Cumulative Profit")
|
| 77 |
+
|
| 78 |
+
# Plot 5: Profit per Resource
|
| 79 |
+
axs[2, 0].bar(result_df["Product"], result_df["Profit per Resource"])
|
| 80 |
+
axs[2, 0].set_title("Profit per Unit of Resource Used")
|
| 81 |
+
axs[2, 0].set_ylabel("Efficiency")
|
| 82 |
+
|
| 83 |
+
# Remove unused subplot (bottom right)
|
| 84 |
+
axs[2, 1].axis('off')
|
| 85 |
+
|
| 86 |
+
fig.tight_layout()
|
| 87 |
+
|
| 88 |
+
return result_df, fig
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
|
| 92 |
+
"""
|
| 93 |
+
data: DataFrame with columns ["Node","X","Y","Demand"]
|
| 94 |
+
vehicle_capacity: capacity Q of each vehicle
|
| 95 |
+
num_vehicles: number of vehicles K
|
| 96 |
+
Returns: (routes_df, fig)
|
| 97 |
+
- routes_df: DataFrame with columns ["Route","Sequence","Load","Distance"]
|
| 98 |
+
- fig: matplotlib.figure.Figure with the route‐map and summary bars
|
| 99 |
+
"""
|
| 100 |
+
# 1. Parse inputs
|
| 101 |
+
coords = {int(r.Node): (r.X, r.Y) for _, r in data.iterrows()}
|
| 102 |
+
demand = {int(r.Node): r.Demand for _, r in data.iterrows()}
|
| 103 |
+
nodes = list(coords.keys())
|
| 104 |
+
depot = 0
|
| 105 |
+
customers = [i for i in nodes if i != depot]
|
| 106 |
+
Q = vehicle_capacity
|
| 107 |
+
K = num_vehicles
|
| 108 |
+
|
| 109 |
+
# 2. Precompute distances
|
| 110 |
+
cost = {
|
| 111 |
+
(i, j): math.hypot(coords[i][0] - coords[j][0],
|
| 112 |
+
coords[i][1] - coords[j][1])
|
| 113 |
+
for i in nodes for j in nodes if i != j
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# 3. Build model
|
| 117 |
+
m = Model("CVRP")
|
| 118 |
+
m.setParam("OutputFlag", 0)
|
| 119 |
+
|
| 120 |
+
# Decision vars
|
| 121 |
+
x = m.addVars(cost.keys(), vtype=GRB.BINARY, name="x")
|
| 122 |
+
u = m.addVars(nodes, lb=0, ub=Q, vtype=GRB.CONTINUOUS, name="u")
|
| 123 |
+
|
| 124 |
+
# Objective
|
| 125 |
+
m.setObjective(quicksum(cost[i, j] * x[i, j] for i, j in cost), GRB.MINIMIZE)
|
| 126 |
+
|
| 127 |
+
# Degree constraints
|
| 128 |
+
m.addConstrs(
|
| 129 |
+
(quicksum(x[i, j] for j in nodes if j != i) == 1 for i in customers),
|
| 130 |
+
name="leave"
|
| 131 |
+
)
|
| 132 |
+
m.addConstrs(
|
| 133 |
+
(quicksum(x[i, j] for i in nodes if i != j) == 1 for j in customers),
|
| 134 |
+
name="enter"
|
| 135 |
+
)
|
| 136 |
+
# Depot flow
|
| 137 |
+
m.addConstr(quicksum(x[depot, j] for j in customers) == K, "dep_out")
|
| 138 |
+
m.addConstr(quicksum(x[i, depot] for i in customers) == K, "dep_in")
|
| 139 |
+
|
| 140 |
+
# MTZ subtour‐elimination & capacity
|
| 141 |
+
m.addConstrs(
|
| 142 |
+
(u[i] - u[j] + Q * x[i, j] <= Q - demand[j]
|
| 143 |
+
for i in customers for j in customers if i != j),
|
| 144 |
+
name="mtz"
|
| 145 |
+
)
|
| 146 |
+
m.addConstr(u[depot] == 0, "depot_load")
|
| 147 |
+
|
| 148 |
+
# Solve
|
| 149 |
+
m.optimize()
|
| 150 |
+
|
| 151 |
+
# 4. Extract x‐values
|
| 152 |
+
sol = m.getAttr('x', x)
|
| 153 |
+
|
| 154 |
+
# 4a. Find exactly which customers each vehicle leaves the depot to serve
|
| 155 |
+
starts = [ j for (i,j),val in sol.items()
|
| 156 |
+
if i == depot and val > 0.5 ]
|
| 157 |
+
# sanity check
|
| 158 |
+
if len(starts) != K:
|
| 159 |
+
raise ValueError(f"Expected {K} routes out of depot, got {len(starts)}")
|
| 160 |
+
|
| 161 |
+
# 4b. Build a succ map for ALL non‐depot nodes (each has exactly 1 outgoing)
|
| 162 |
+
succ = { i: j for (i,j),val in sol.items()
|
| 163 |
+
if i != depot and val > 0.5 }
|
| 164 |
+
|
| 165 |
+
# 4c. Now reconstruct each of the K routes
|
| 166 |
+
routes = []
|
| 167 |
+
for start in starts:
|
| 168 |
+
route = [depot, start]
|
| 169 |
+
cur = start
|
| 170 |
+
while cur != depot:
|
| 171 |
+
nxt = succ[cur]
|
| 172 |
+
route.append(nxt)
|
| 173 |
+
cur = nxt
|
| 174 |
+
routes.append(route)
|
| 175 |
+
|
| 176 |
+
# 5. Build result DataFrame
|
| 177 |
+
rows = []
|
| 178 |
+
for ridx, route in enumerate(routes, start=1):
|
| 179 |
+
load = sum(demand[n] for n in route if n != depot)
|
| 180 |
+
dist = sum(
|
| 181 |
+
math.hypot(coords[route[i]][0] - coords[route[i+1]][0],
|
| 182 |
+
coords[route[i]][1] - coords[route[i+1]][1])
|
| 183 |
+
for i in range(len(route)-1)
|
| 184 |
+
)
|
| 185 |
+
rows.append({
|
| 186 |
+
"Route": ridx,
|
| 187 |
+
"Sequence": "→".join(str(n) for n in route),
|
| 188 |
+
"Load": load,
|
| 189 |
+
"Distance": dist
|
| 190 |
+
})
|
| 191 |
+
routes_df = pd.DataFrame(rows)
|
| 192 |
+
|
| 193 |
+
# 6. Create plots
|
| 194 |
+
fig, axs = plt.subplots(1, 2, figsize=(14,6))
|
| 195 |
+
|
| 196 |
+
# Plot A: Map of routes
|
| 197 |
+
ax = axs[0]
|
| 198 |
+
ax.scatter(*zip(*[coords[i] for i in customers]),
|
| 199 |
+
c='blue', label='Customers')
|
| 200 |
+
ax.scatter(*coords[depot], c='red', s=100, label='Depot')
|
| 201 |
+
colors = plt.cm.get_cmap('tab10', K)
|
| 202 |
+
|
| 203 |
+
for ridx, route in enumerate(routes):
|
| 204 |
+
pts = [coords[n] for n in route]
|
| 205 |
+
xs, ys = zip(*pts)
|
| 206 |
+
ax.plot(xs, ys, '-o', color=colors(ridx), label=f'Route {ridx+1}')
|
| 207 |
+
ax.set_title("Vehicle Routes")
|
| 208 |
+
ax.legend(loc='upper right')
|
| 209 |
+
|
| 210 |
+
# Plot B: Route loads & distances
|
| 211 |
+
ax2 = axs[1]
|
| 212 |
+
bar_width = 0.35
|
| 213 |
+
idx = range(len(routes_df))
|
| 214 |
+
ax2.bar(idx, routes_df["Load"], bar_width, label="Load")
|
| 215 |
+
ax2.bar([i+bar_width for i in idx], routes_df["Distance"],
|
| 216 |
+
bar_width, label="Distance")
|
| 217 |
+
ax2.set_xticks([i+bar_width/2 for i in idx])
|
| 218 |
+
ax2.set_xticklabels([f"R{r}" for r in routes_df["Route"]])
|
| 219 |
+
ax2.set_ylabel("Units / Distance")
|
| 220 |
+
ax2.set_title("Load vs Distance per Route")
|
| 221 |
+
ax2.legend()
|
| 222 |
+
|
| 223 |
+
fig.tight_layout()
|
| 224 |
+
return routes_df, fig
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-dotenv==1.1.0
|
| 2 |
+
pydantic==2.8.2
|
| 3 |
+
pydantic_settings==2.4.0
|
| 4 |
+
gurobipy==12.0.1
|
ui/gradio_sections.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
import sys
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import achref.src.logger as logger
|
| 7 |
+
|
| 8 |
+
logger = logger.get_logger(__name__)
|
| 9 |
+
|
| 10 |
+
def project_info_tab():
|
| 11 |
+
with gr.Tab("📘 Project Info"):
|
| 12 |
+
gr.Markdown(
|
| 13 |
+
"""
|
| 14 |
+
# 🎓 GL3 - 2025 - Operational Research Project
|
| 15 |
+
This application demonstrates how **Linear Programming (PL)** and **Mixed-Integer Linear Programming (PLNE)** can be applied to solve real-world optimisation problems using **Gurobi**.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
# 👥 Project Members
|
| 19 |
+
- **Kacem Mathlouthi** — GL3/2
|
| 20 |
+
- **Mohamed Amine Houas** — GL3/1
|
| 21 |
+
- **Oussema Kraiem** — GL3/2
|
| 22 |
+
- **Yassine Taieb** — GL3/2
|
| 23 |
+
- **Youssef Sghairi** — GL3/2
|
| 24 |
+
- **Youssef Aaridhi** — GL3/2
|
| 25 |
+
- **Achref Ben Ammar** — GL3/1
|
| 26 |
+
---
|
| 27 |
+
# 🧾 Compte Rendu
|
| 28 |
+
"""
|
| 29 |
+
)
|
| 30 |
+
pdf_path = os.path.join(
|
| 31 |
+
os.path.dirname(os.path.dirname(__file__)), "assets", "compte_rendu.pdf"
|
| 32 |
+
)
|
| 33 |
+
with open(pdf_path, "rb") as pdf_file:
|
| 34 |
+
encoded_pdf = base64.b64encode(pdf_file.read()).decode("utf-8")
|
| 35 |
+
|
| 36 |
+
# Display using data URI
|
| 37 |
+
gr.HTML(
|
| 38 |
+
f"""
|
| 39 |
+
<embed src="data:application/pdf;base64,{encoded_pdf}" type="application/pdf" width="100%" height="1200px">
|
| 40 |
+
"""
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def production_planning_tab(mock_pl_df, solve_pl_gurobi, pl_description):
|
| 45 |
+
with gr.Tab("🏭 Production Planning (PL)"):
|
| 46 |
+
gr.Markdown(pl_description)
|
| 47 |
+
|
| 48 |
+
# Add mathematical model description
|
| 49 |
+
gr.Markdown(
|
| 50 |
+
r"""
|
| 51 |
+
### 🧮 Mathematical Formulation
|
| 52 |
+
|
| 53 |
+
Let:
|
| 54 |
+
| Symbol | Description |
|
| 55 |
+
|-------------|-----------------------------------------|
|
| 56 |
+
| $$x_i$$ | Number of units to produce for product i |
|
| 57 |
+
| $$p_i$$ | Profit per unit for product i |
|
| 58 |
+
| $$r_i$$ | Resource usage per unit for product i |
|
| 59 |
+
| $$R$$ | Total resource available |
|
| 60 |
+
|
| 61 |
+
**Objective:**
|
| 62 |
+
$$
|
| 63 |
+
\text{Maximise} \quad \sum_i p_i \cdot x_i
|
| 64 |
+
$$
|
| 65 |
+
|
| 66 |
+
**Constraint:**
|
| 67 |
+
$$
|
| 68 |
+
\sum_i r_i \cdot x_i \leq R \quad \text{and} \quad x_i \geq 0
|
| 69 |
+
$$
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
with gr.Row():
|
| 74 |
+
input_pl = gr.Dataframe(
|
| 75 |
+
headers=["Product", "Profit/Unit", "Resource Usage"],
|
| 76 |
+
value=mock_pl_df,
|
| 77 |
+
label="Input Product Data",
|
| 78 |
+
)
|
| 79 |
+
total_resource_input = gr.Number(
|
| 80 |
+
value=100, label="Total Resource Available (R)"
|
| 81 |
+
)
|
| 82 |
+
solve_btn_pl = gr.Button("Solve Production Problem")
|
| 83 |
+
|
| 84 |
+
result_table_pl = gr.Dataframe(label="Optimised Result")
|
| 85 |
+
|
| 86 |
+
# Create plot output placeholders
|
| 87 |
+
result_plot_combined = gr.Plot(label="Data Visualisation")
|
| 88 |
+
|
| 89 |
+
def _solve_with_floats(df, R):
|
| 90 |
+
df["Profit/Unit"] = df["Profit/Unit"].astype(float)
|
| 91 |
+
df["Resource Usage"] = df["Resource Usage"].astype(float)
|
| 92 |
+
return solve_pl_gurobi(df, total_resource=R)
|
| 93 |
+
|
| 94 |
+
solve_btn_pl.click(
|
| 95 |
+
fn=_solve_with_floats,
|
| 96 |
+
inputs=[input_pl, total_resource_input],
|
| 97 |
+
outputs=[
|
| 98 |
+
result_table_pl,
|
| 99 |
+
result_plot_combined,
|
| 100 |
+
]
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# in gradio_sections.py
|
| 105 |
+
|
| 106 |
+
def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
|
| 107 |
+
|
| 108 |
+
with gr.Tab("🚚 Vehicle Routing (PLNE)"):
|
| 109 |
+
gr.Markdown(plne_description)
|
| 110 |
+
# Log the Python path of the project
|
| 111 |
+
gr.HTML(
|
| 112 |
+
'<img src="https://pyvrp.readthedocs.io/en/latest/_images/introduction-to-vrp.svg" '
|
| 113 |
+
'alt="VRP Problem Illustration" width="600px" />'
|
| 114 |
+
)
|
| 115 |
+
gr.Markdown(
|
| 116 |
+
r"""
|
| 117 |
+
### 🧮 Mathematical Formulation (Capacitated VRP)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
| Symbol | Description |
|
| 121 |
+
|--------------------------------|---------------------------------------------------------------|
|
| 122 |
+
| $$i,j \in N=\{0,\dots,n\}$$ | Nodes (0 = depot, 1..n = customers) |
|
| 123 |
+
| $$K$$ | Number of vehicles |
|
| 124 |
+
| $$c_{ij}$$ | Travel cost (distance) from node `i` to node `j` |
|
| 125 |
+
| $$d_i$$ | Demand at customer `i` |
|
| 126 |
+
| $$Q$$ | Vehicle capacity |
|
| 127 |
+
| $$x_{ij}\in\{0,1\}$$ | 1 if a vehicle travels directly from `i` to `j` |
|
| 128 |
+
| $$u_i\ge0$$ | Load on the vehicle immediately after visiting node `i` |
|
| 129 |
+
|
| 130 |
+
**Objective**
|
| 131 |
+
$$
|
| 132 |
+
\min \sum_{i\in N}\sum_{\substack{j\in N \\ j\neq i}} c_{ij}\,x_{ij}
|
| 133 |
+
$$
|
| 134 |
+
Minimize the **total travel cost** of all vehicles.
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
**Subject to**
|
| 139 |
+
|
| 140 |
+
1. **Degree constraints**
|
| 141 |
+
$$
|
| 142 |
+
\sum_{j\neq i} x_{ij} = 1
|
| 143 |
+
\quad \forall\, i\neq0
|
| 144 |
+
$$
|
| 145 |
+
$$
|
| 146 |
+
\sum_{i\neq j} x_{ij} = 1
|
| 147 |
+
\quad \forall\, j\neq0
|
| 148 |
+
$$
|
| 149 |
+
> *Explanation:* Every customer `i` must have exactly one vehicle leaving it and one arriving—ensuring each customer is visited exactly once.
|
| 150 |
+
|
| 151 |
+
2. **Depot flow**
|
| 152 |
+
$$
|
| 153 |
+
\sum_{j>0} x_{0j} = K
|
| 154 |
+
$$
|
| 155 |
+
$$
|
| 156 |
+
\sum_{i>0} x_{i0} = K
|
| 157 |
+
$$
|
| 158 |
+
> *Explanation:* Exactly `K` vehicles depart from the depot and `K` return, so all vehicles are used and end back at the depot.
|
| 159 |
+
|
| 160 |
+
3. **MTZ subtour-elimination & capacity**
|
| 161 |
+
$$
|
| 162 |
+
u_i - u_j + Q\,x_{ij} \le Q - d_j
|
| 163 |
+
\quad \forall\,i\neq j,\; i,j>0
|
| 164 |
+
$$
|
| 165 |
+
$$
|
| 166 |
+
u_0 = 0
|
| 167 |
+
$$
|
| 168 |
+
$$
|
| 169 |
+
0 \le u_i \le Q
|
| 170 |
+
$$
|
| 171 |
+
> *Explanation:*
|
| 172 |
+
> - If `x_{ij}=1`, then $$u_j \ge u_i + d_j$$ enforcing vehicle capacity.
|
| 173 |
+
> - These constraints also prevent any customer‐only loops (subtours), because load can’t reset without returning to the depot.
|
| 174 |
+
> - We fix `u_0=0` at the depot and bound `u_i` by capacity `Q`.
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
---
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
"""
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
vrp_input = gr.Dataframe(
|
| 185 |
+
headers=["Node", "X", "Y", "Demand"],
|
| 186 |
+
value=mock_plne_df,
|
| 187 |
+
label="Input Vehicle Routing Data",
|
| 188 |
+
)
|
| 189 |
+
with gr.Row():
|
| 190 |
+
cap_input = gr.Number(value=40, label="Vehicle capacity (Q)")
|
| 191 |
+
k_input = gr.Number(value=2, label="Number of vehicles (K)")
|
| 192 |
+
solve_btn = gr.Button("Solve VRP")
|
| 193 |
+
status_output = gr.Textbox(label="Status", interactive=False)
|
| 194 |
+
result_table = gr.Dataframe(label="Routes Summary")
|
| 195 |
+
result_plot = gr.Plot(label="Route Map & Summary")
|
| 196 |
+
|
| 197 |
+
def _solve_vrp_with_floats(df, Q, K):
|
| 198 |
+
df["X"] = df["X"].astype(float)
|
| 199 |
+
df["Y"] = df["Y"].astype(float)
|
| 200 |
+
df["Demand"] = df["Demand"].astype(float)
|
| 201 |
+
|
| 202 |
+
# skip depot (assumed Node==0) when checking
|
| 203 |
+
custs = df[df["Node"] != 0]
|
| 204 |
+
|
| 205 |
+
try:
|
| 206 |
+
# 1) any single demand > Q?
|
| 207 |
+
too_big = custs[custs["Demand"] > Q]
|
| 208 |
+
if not too_big.empty:
|
| 209 |
+
bad = int(too_big["Node"].iloc[0])
|
| 210 |
+
raise ValueError(f"Client {bad} demand ({too_big['Demand'].iloc[0]}) exceeds capacity Q={Q}")
|
| 211 |
+
|
| 212 |
+
# 2) total demand > Q*K?
|
| 213 |
+
total = custs["Demand"].sum()
|
| 214 |
+
if total > Q * K:
|
| 215 |
+
raise ValueError(f"Total demand ({total}) exceeds fleet capacity Q*K={Q*K}")
|
| 216 |
+
|
| 217 |
+
# all good → call solver
|
| 218 |
+
routes_df, fig = solve_plne(df, vehicle_capacity=Q, num_vehicles=K)
|
| 219 |
+
return routes_df, fig, "All Good"
|
| 220 |
+
except ValueError as e:
|
| 221 |
+
# on error show empty table/plot + message
|
| 222 |
+
return pd.DataFrame(), None, str(e)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
solve_btn.click(
|
| 226 |
+
fn=_solve_vrp_with_floats,
|
| 227 |
+
inputs=[vrp_input, cap_input, k_input],
|
| 228 |
+
outputs=[result_table, result_plot, status_output],
|
| 229 |
+
)
|
| 230 |
+
|