Kacemath commited on
Commit
bb1c643
·
1 Parent(s): a789dba

Feat: Project Deployment on huggingface spaces

Browse files
.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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ![App Screenshot](./assets/satellite_trajectories.gif)
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
+