Spaces:
Sleeping
Sleeping
Update
Browse files- achref/README.md +0 -60
- achref/infeasible.ilp +0 -36
- achref/src/config/__init__.py +0 -1
- achref/src/config/config.py +0 -39
- achref/src/logger/__init__.py +0 -1
- achref/src/models/gorubi_models.py +0 -255
- achref/src/models/infeasible.ilp +0 -24
- achref/src/plotting/plot_trajectories.py +0 -104
- app.py +7 -89
- assets/compte_rendu.pdf +2 -2
- models/gurobi_models.py +446 -204
- requirements.txt +2 -0
- ui/gradio_sections.py +344 -90
- utils/__init__.py +1 -0
- {achref/src/logger → utils}/logger.py +20 -11
achref/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 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
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
from .config import get_settings
|
|
|
|
|
|
achref/src/config/config.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 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
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
from .logger import get_logger
|
|
|
|
|
|
achref/src/models/gorubi_models.py
DELETED
|
@@ -1,255 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 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
CHANGED
|
@@ -3,10 +3,10 @@ import os
|
|
| 3 |
import gradio as gr
|
| 4 |
import pandas as pd
|
| 5 |
|
| 6 |
-
from models.gurobi_models import
|
| 7 |
-
from ui.gradio_sections import
|
| 8 |
|
| 9 |
-
# Mock Data
|
| 10 |
plne_df = pd.DataFrame(
|
| 11 |
[
|
| 12 |
{"Node": 0, "X": 50, "Y": 50, "Demand": 0},
|
|
@@ -18,91 +18,16 @@ plne_df = pd.DataFrame(
|
|
| 18 |
]
|
| 19 |
)
|
| 20 |
|
| 21 |
-
# Oil Refinery Optimization Mock Data
|
| 22 |
-
crude_df = pd.DataFrame(
|
| 23 |
-
[
|
| 24 |
-
{"Crude": "Light Crude", "Cost": 60, "Availability": 10000},
|
| 25 |
-
{"Crude": "Medium Crude", "Cost": 50, "Availability": 15000},
|
| 26 |
-
{"Crude": "Heavy Crude", "Cost": 45, "Availability": 12000},
|
| 27 |
-
]
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
product_df = pd.DataFrame(
|
| 31 |
-
[
|
| 32 |
-
{"Product": "Premium Gasoline", "Price": 90, "Demand": 5000},
|
| 33 |
-
{"Product": "Regular Gasoline", "Price": 80, "Demand": 7000},
|
| 34 |
-
{"Product": "Diesel", "Price": 75, "Demand": 8000},
|
| 35 |
-
]
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
yields_df = pd.DataFrame(
|
| 39 |
-
[
|
| 40 |
-
# Light Crude yields
|
| 41 |
-
{
|
| 42 |
-
"Crude": "Light Crude",
|
| 43 |
-
"Product": "Premium Gasoline",
|
| 44 |
-
"Yield": 0.4,
|
| 45 |
-
"Quality": 95,
|
| 46 |
-
},
|
| 47 |
-
{
|
| 48 |
-
"Crude": "Light Crude",
|
| 49 |
-
"Product": "Regular Gasoline",
|
| 50 |
-
"Yield": 0.3,
|
| 51 |
-
"Quality": 90,
|
| 52 |
-
},
|
| 53 |
-
{"Crude": "Light Crude", "Product": "Diesel", "Yield": 0.2, "Quality": 85},
|
| 54 |
-
# Medium Crude yields
|
| 55 |
-
{
|
| 56 |
-
"Crude": "Medium Crude",
|
| 57 |
-
"Product": "Premium Gasoline",
|
| 58 |
-
"Yield": 0.3,
|
| 59 |
-
"Quality": 85,
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"Crude": "Medium Crude",
|
| 63 |
-
"Product": "Regular Gasoline",
|
| 64 |
-
"Yield": 0.4,
|
| 65 |
-
"Quality": 80,
|
| 66 |
-
},
|
| 67 |
-
{"Crude": "Medium Crude", "Product": "Diesel", "Yield": 0.3, "Quality": 80},
|
| 68 |
-
# Heavy Crude yields
|
| 69 |
-
{
|
| 70 |
-
"Crude": "Heavy Crude",
|
| 71 |
-
"Product": "Premium Gasoline",
|
| 72 |
-
"Yield": 0.1,
|
| 73 |
-
"Quality": 75,
|
| 74 |
-
},
|
| 75 |
-
{
|
| 76 |
-
"Crude": "Heavy Crude",
|
| 77 |
-
"Product": "Regular Gasoline",
|
| 78 |
-
"Yield": 0.3,
|
| 79 |
-
"Quality": 70,
|
| 80 |
-
},
|
| 81 |
-
{"Crude": "Heavy Crude", "Product": "Diesel", "Yield": 0.5, "Quality": 75},
|
| 82 |
-
]
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
quality_reqs_df = pd.DataFrame(
|
| 86 |
-
[
|
| 87 |
-
{"Product": "Premium Gasoline", "MinQuality": 90},
|
| 88 |
-
{"Product": "Regular Gasoline", "MinQuality": 80},
|
| 89 |
-
{"Product": "Diesel", "MinQuality": 75},
|
| 90 |
-
]
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
# Descriptions
|
| 94 |
plne_description = """
|
| 95 |
### 🚚 Capacitated Vehicle Routing Problem
|
| 96 |
Provide node coordinates and demands, plus vehicle capacity and number of vehicles.
|
| 97 |
"""
|
| 98 |
|
| 99 |
-
|
| 100 |
-
###
|
| 101 |
|
| 102 |
-
|
| 103 |
-
An oil refinery wants to determine the optimal production plan for different fuel products (like diesel, premium gasoline, and regular gasoline)
|
| 104 |
-
using various crude oils. Each crude oil has different yields, costs, and qualities, and each product has its own demand,
|
| 105 |
-
quality requirements, and selling price.
|
| 106 |
"""
|
| 107 |
|
| 108 |
# Read and encode the PDF - go up one directory to find assets at project root
|
|
@@ -123,14 +48,7 @@ with gr.Blocks(title="Operations Research App") as ro_app:
|
|
| 123 |
)
|
| 124 |
with gr.Tabs():
|
| 125 |
project_info_tab()
|
| 126 |
-
|
| 127 |
-
crude_df,
|
| 128 |
-
product_df,
|
| 129 |
-
yields_df,
|
| 130 |
-
quality_reqs_df,
|
| 131 |
-
solve_refinery_optimization,
|
| 132 |
-
refinery_description,
|
| 133 |
-
)
|
| 134 |
vehicle_routing_tab(plne_df, solve_plne, plne_description)
|
| 135 |
|
| 136 |
if __name__ == "__main__":
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
import pandas as pd
|
| 5 |
|
| 6 |
+
from models.gurobi_models import solve_diet_problem, solve_plne
|
| 7 |
+
from ui.gradio_sections import diet_problem_tab, project_info_tab, vehicle_routing_tab
|
| 8 |
|
| 9 |
+
# Mock Data for Vehicle Routing Problem
|
| 10 |
plne_df = pd.DataFrame(
|
| 11 |
[
|
| 12 |
{"Node": 0, "X": 50, "Y": 50, "Demand": 0},
|
|
|
|
| 18 |
]
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
# Descriptions
|
| 22 |
plne_description = """
|
| 23 |
### 🚚 Capacitated Vehicle Routing Problem
|
| 24 |
Provide node coordinates and demands, plus vehicle capacity and number of vehicles.
|
| 25 |
"""
|
| 26 |
|
| 27 |
+
diet_description = """
|
| 28 |
+
### 🍎 Diet Problem
|
| 29 |
|
| 30 |
+
Find the optimal diet that minimizes cost while meeting nutritional requirements.
|
|
|
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
|
| 33 |
# Read and encode the PDF - go up one directory to find assets at project root
|
|
|
|
| 48 |
)
|
| 49 |
with gr.Tabs():
|
| 50 |
project_info_tab()
|
| 51 |
+
diet_problem_tab(solve_diet_problem, diet_description)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
vehicle_routing_tab(plne_df, solve_plne, plne_description)
|
| 53 |
|
| 54 |
if __name__ == "__main__":
|
assets/compte_rendu.pdf
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:232255ba8666363b58f53a53eaed299529b2428a0863e647a42af720dcc50517
|
| 3 |
+
size 1218613
|
models/gurobi_models.py
CHANGED
|
@@ -4,244 +4,486 @@ import matplotlib.pyplot as plt
|
|
| 4 |
import pandas as pd
|
| 5 |
from gurobipy import GRB, Model, quicksum
|
| 6 |
|
| 7 |
-
import
|
| 8 |
|
| 9 |
logger = logger.get_logger(__name__)
|
| 10 |
|
| 11 |
|
| 12 |
-
def
|
| 13 |
-
crude_data, product_data, crude_product_yields, product_quality_reqs
|
| 14 |
-
):
|
| 15 |
"""
|
| 16 |
-
Solves the
|
| 17 |
|
| 18 |
Args:
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
crude_product_yields (pd.DataFrame): DataFrame with columns ["Crude", "Product", "Yield", "Quality"]
|
| 22 |
-
product_quality_reqs (pd.DataFrame): DataFrame with columns ["Product", "MinQuality"]
|
| 23 |
|
| 24 |
Returns:
|
| 25 |
tuple: (result_df, fig) where result_df contains the optimal solution and fig is a matplotlib figure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"""
|
| 27 |
-
|
| 28 |
-
isinstance(df, pd.DataFrame)
|
| 29 |
-
for df in [crude_data, product_data, crude_product_yields, product_quality_reqs]
|
| 30 |
-
):
|
| 31 |
-
raise TypeError("All inputs must be pandas DataFrames")
|
| 32 |
-
|
| 33 |
-
# Validate required columns
|
| 34 |
-
if not set(["Crude", "Cost", "Availability"]).issubset(crude_data.columns):
|
| 35 |
-
raise ValueError("crude_data must contain columns: Crude, Cost, Availability")
|
| 36 |
-
if not set(["Product", "Price", "Demand"]).issubset(product_data.columns):
|
| 37 |
-
raise ValueError("product_data must contain columns: Product, Price, Demand")
|
| 38 |
-
if not set(["Crude", "Product", "Yield", "Quality"]).issubset(
|
| 39 |
-
crude_product_yields.columns
|
| 40 |
-
):
|
| 41 |
-
raise ValueError(
|
| 42 |
-
"crude_product_yields must contain columns: Crude, Product, Yield, Quality"
|
| 43 |
-
)
|
| 44 |
-
if not set(["Product", "MinQuality"]).issubset(product_quality_reqs.columns):
|
| 45 |
-
raise ValueError(
|
| 46 |
-
"product_quality_reqs must contain columns: Product, MinQuality"
|
| 47 |
-
)
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
demands = {row["Product"]: row["Demand"] for _, row in product_data.iterrows()}
|
| 66 |
-
min_qualities = {
|
| 67 |
-
row["Product"]: row["MinQuality"] for _, row in product_quality_reqs.iterrows()
|
| 68 |
-
}
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# Decision variables: amount of each crude oil used
|
| 79 |
-
x = {crude: model.addVar(name=f"x_{crude}", lb=0) for crude in crudes}
|
| 80 |
-
|
| 81 |
-
# Calculated variables: amount of each product produced from each crude
|
| 82 |
-
prod_from_crude = {}
|
| 83 |
-
for crude in crudes:
|
| 84 |
-
for product in products:
|
| 85 |
-
key = (crude, product)
|
| 86 |
-
if key in yields:
|
| 87 |
-
prod_from_crude[key] = yields[key] * x[crude]
|
| 88 |
-
|
| 89 |
-
# Calculate total production per product
|
| 90 |
-
total_production = {}
|
| 91 |
-
for product in products:
|
| 92 |
-
total_production[product] = quicksum(
|
| 93 |
-
prod_from_crude.get((crude, product), 0) for crude in crudes
|
| 94 |
-
)
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
prices[product] * total_production[product] for product in products
|
| 99 |
-
)
|
| 100 |
-
cost = quicksum(costs[crude] * x[crude] for crude in crudes)
|
| 101 |
-
model.setObjective(revenue - cost, GRB.MAXIMIZE)
|
| 102 |
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
)
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
)
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
prod_amount = sum(
|
| 151 |
-
prod_from_crude.get((crude, product), 0).getValue()
|
| 152 |
-
for crude in crudes
|
| 153 |
-
if (crude, product) in prod_from_crude
|
| 154 |
-
)
|
| 155 |
-
revenue = prod_amount * prices[product]
|
| 156 |
-
# Calculate average quality
|
| 157 |
-
quality_numerator = sum(
|
| 158 |
-
qualities.get((crude, product), 0)
|
| 159 |
-
* prod_from_crude.get((crude, product), 0).getValue()
|
| 160 |
-
for crude in crudes
|
| 161 |
-
if (crude, product) in prod_from_crude
|
| 162 |
-
)
|
| 163 |
-
avg_quality = quality_numerator / prod_amount if prod_amount > 0 else 0
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
)
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
axs[1, 1].set_title("Product Quality Analysis")
|
| 221 |
-
axs[1, 1].set_xticks(x)
|
| 222 |
-
axs[1, 1].set_xticklabels(products)
|
| 223 |
-
axs[1, 1].set_ylabel("Quality")
|
| 224 |
-
axs[1, 1].legend()
|
| 225 |
-
plt.setp(axs[1, 1].get_xticklabels(), rotation=45, ha="right")
|
| 226 |
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
"
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
def _validate_plne_input(data, vehicle_capacity, num_vehicles):
|
|
@@ -553,4 +795,4 @@ def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
|
|
| 553 |
except (ValueError, TypeError) as e:
|
| 554 |
raise RuntimeError(f"Error in solve_plne: {e}")
|
| 555 |
except Exception as e:
|
| 556 |
-
raise RuntimeError(f"Unexpected error in solve_plne: {e}")
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
from gurobipy import GRB, Model, quicksum
|
| 6 |
|
| 7 |
+
import utils.logger as logger
|
| 8 |
|
| 9 |
logger = logger.get_logger(__name__)
|
| 10 |
|
| 11 |
|
| 12 |
+
def solve_diet_problem(foods_df, requirements_df):
|
|
|
|
|
|
|
| 13 |
"""
|
| 14 |
+
Solves the Diet Problem using Linear Programming with flexible number of foods and nutrients.
|
| 15 |
|
| 16 |
Args:
|
| 17 |
+
foods_df (pd.DataFrame): DataFrame with columns ['Food', 'Cost'] + nutrient columns
|
| 18 |
+
requirements_df (pd.DataFrame): DataFrame with columns ['Nutrient', 'Minimum']
|
|
|
|
|
|
|
| 19 |
|
| 20 |
Returns:
|
| 21 |
tuple: (result_df, fig) where result_df contains the optimal solution and fig is a matplotlib figure
|
| 22 |
+
|
| 23 |
+
Raises:
|
| 24 |
+
TypeError: If inputs are not DataFrames or contain invalid data types
|
| 25 |
+
ValueError: If data validation fails or optimization problem cannot be solved
|
| 26 |
+
Exception: If Gurobi solver encounters an error
|
| 27 |
"""
|
| 28 |
+
logger.info("Starting Gurobi model for Diet Problem")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
try:
|
| 31 |
+
# Type validation
|
| 32 |
+
if not isinstance(foods_df, pd.DataFrame):
|
| 33 |
+
raise TypeError("foods_df must be a pandas DataFrame")
|
| 34 |
+
if not isinstance(requirements_df, pd.DataFrame):
|
| 35 |
+
raise TypeError("requirements_df must be a pandas DataFrame")
|
| 36 |
+
|
| 37 |
+
# Empty data validation
|
| 38 |
+
if foods_df.empty:
|
| 39 |
+
raise ValueError(
|
| 40 |
+
"Foods data cannot be empty. Please provide at least one food item."
|
| 41 |
+
)
|
| 42 |
+
if requirements_df.empty:
|
| 43 |
+
raise ValueError(
|
| 44 |
+
"Requirements data cannot be empty. Please provide at least one nutritional requirement."
|
| 45 |
+
)
|
| 46 |
|
| 47 |
+
# Required columns validation
|
| 48 |
+
required_food_cols = {"Food", "Cost"}
|
| 49 |
+
if not required_food_cols.issubset(foods_df.columns):
|
| 50 |
+
missing = required_food_cols - set(foods_df.columns)
|
| 51 |
+
raise ValueError(
|
| 52 |
+
f"Missing required columns in foods data: {missing}. Required columns: {required_food_cols}"
|
| 53 |
+
)
|
| 54 |
|
| 55 |
+
required_req_cols = {"Nutrient", "Minimum"}
|
| 56 |
+
if not required_req_cols.issubset(requirements_df.columns):
|
| 57 |
+
missing = required_req_cols - set(requirements_df.columns)
|
| 58 |
+
raise ValueError(
|
| 59 |
+
f"Missing required columns in requirements data: {missing}. Required columns: {required_req_cols}"
|
| 60 |
+
)
|
| 61 |
|
| 62 |
+
# Duplicate validation
|
| 63 |
+
if foods_df["Food"].duplicated().any():
|
| 64 |
+
duplicates = foods_df[foods_df["Food"].duplicated()]["Food"].tolist()
|
| 65 |
+
raise ValueError(
|
| 66 |
+
f"Duplicate food names found: {duplicates}. Each food must have a unique name."
|
| 67 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
if requirements_df["Nutrient"].duplicated().any():
|
| 70 |
+
duplicates = requirements_df[requirements_df["Nutrient"].duplicated()][
|
| 71 |
+
"Nutrient"
|
| 72 |
+
].tolist()
|
| 73 |
+
raise ValueError(
|
| 74 |
+
f"Duplicate nutrient names found: {duplicates}. Each nutrient must be unique."
|
| 75 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# Get nutrient columns (all columns except 'Food' and 'Cost')
|
| 78 |
+
nutrient_cols = [col for col in foods_df.columns if col not in ["Food", "Cost"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
if not nutrient_cols:
|
| 81 |
+
raise ValueError(
|
| 82 |
+
"No nutrient columns found in foods data. Please include at least one nutrient column (e.g., 'Protein', 'Fat', 'Carbs')."
|
| 83 |
+
)
|
| 84 |
|
| 85 |
+
# Data type validation
|
| 86 |
+
try:
|
| 87 |
+
foods_df["Cost"] = pd.to_numeric(foods_df["Cost"], errors="raise")
|
| 88 |
+
except (ValueError, TypeError) as e:
|
| 89 |
+
raise ValueError(
|
| 90 |
+
f"Cost column contains non-numeric values. All costs must be numbers. Error: {str(e)}"
|
| 91 |
+
)
|
| 92 |
|
| 93 |
+
try:
|
| 94 |
+
requirements_df["Minimum"] = pd.to_numeric(
|
| 95 |
+
requirements_df["Minimum"], errors="raise"
|
| 96 |
+
)
|
| 97 |
+
except (ValueError, TypeError) as e:
|
| 98 |
+
raise ValueError(
|
| 99 |
+
f"Minimum column contains non-numeric values. All requirements must be numbers. Error: {str(e)}"
|
| 100 |
+
)
|
| 101 |
|
| 102 |
+
for col in nutrient_cols:
|
| 103 |
+
try:
|
| 104 |
+
foods_df[col] = pd.to_numeric(foods_df[col], errors="raise")
|
| 105 |
+
except (ValueError, TypeError) as e:
|
| 106 |
+
raise ValueError(
|
| 107 |
+
f"Nutrient column '{col}' contains non-numeric values. All nutrient values must be numbers. Error: {str(e)}"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Value range validation
|
| 111 |
+
if (foods_df["Cost"] < 0).any():
|
| 112 |
+
negative_costs = foods_df[foods_df["Cost"] < 0]["Food"].tolist()
|
| 113 |
+
raise ValueError(
|
| 114 |
+
f"Negative costs found for foods: {negative_costs}. All costs must be non-negative."
|
| 115 |
)
|
| 116 |
+
|
| 117 |
+
if (requirements_df["Minimum"] <= 0).any():
|
| 118 |
+
non_positive_reqs = requirements_df[requirements_df["Minimum"] <= 0][
|
| 119 |
+
"Nutrient"
|
| 120 |
+
].tolist()
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f"Non-positive requirements found for nutrients: {non_positive_reqs}. All requirements must be positive."
|
| 123 |
)
|
| 124 |
|
| 125 |
+
for col in nutrient_cols:
|
| 126 |
+
if (foods_df[col] < 0).any():
|
| 127 |
+
negative_nutrients = foods_df[foods_df[col] < 0]["Food"].tolist()
|
| 128 |
+
raise ValueError(
|
| 129 |
+
f"Negative {col} values found for foods: {negative_nutrients}. All nutrient values must be non-negative."
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Check if any required nutrient is missing from foods data
|
| 133 |
+
missing_nutrients = set(requirements_df["Nutrient"]) - set(nutrient_cols)
|
| 134 |
+
if missing_nutrients:
|
| 135 |
+
raise ValueError(
|
| 136 |
+
f"Required nutrients missing from foods data: {missing_nutrients}. Please add these nutrient columns to your foods data."
|
| 137 |
+
)
|
| 138 |
|
| 139 |
+
# Check for zero costs (potential unbounded solution)
|
| 140 |
+
if (foods_df["Cost"] == 0).any():
|
| 141 |
+
zero_cost_foods = foods_df[foods_df["Cost"] == 0]["Food"].tolist()
|
| 142 |
+
logger.warning(
|
| 143 |
+
f"Foods with zero cost detected: {zero_cost_foods}. This may lead to unrealistic solutions."
|
| 144 |
+
)
|
| 145 |
|
| 146 |
+
# Feasibility pre-check: ensure at least one food provides each required nutrient
|
| 147 |
+
for _, req_row in requirements_df.iterrows():
|
| 148 |
+
nutrient = req_row["Nutrient"]
|
| 149 |
+
if nutrient in nutrient_cols:
|
| 150 |
+
max_nutrient = foods_df[nutrient].max()
|
| 151 |
+
if max_nutrient == 0:
|
| 152 |
+
raise ValueError(
|
| 153 |
+
f"No food provides the required nutrient '{nutrient}'. Problem is infeasible."
|
| 154 |
+
)
|
| 155 |
+
if max_nutrient < req_row["Minimum"]:
|
| 156 |
+
logger.warning(
|
| 157 |
+
f"Maximum {nutrient} content ({max_nutrient}) is less than requirement ({req_row['Minimum']}). May need multiple foods."
|
| 158 |
+
)
|
| 159 |
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"Data validation failed: {str(e)}")
|
| 162 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
+
# Create optimization model
|
| 165 |
+
try:
|
| 166 |
+
model = Model("DietProblem")
|
| 167 |
+
model.setParam("OutputFlag", 0)
|
| 168 |
+
|
| 169 |
+
# Set time limit to prevent infinite solving (60 seconds)
|
| 170 |
+
model.setParam("TimeLimit", 60)
|
| 171 |
+
|
| 172 |
+
logger.info("Creating decision variables...")
|
| 173 |
+
|
| 174 |
+
# Decision variables: units of each food
|
| 175 |
+
food_vars = {}
|
| 176 |
+
for i, food_name in enumerate(foods_df["Food"]):
|
| 177 |
+
if pd.isna(food_name) or str(food_name).strip() == "":
|
| 178 |
+
raise ValueError(
|
| 179 |
+
f"Empty or invalid food name at row {i}. All foods must have valid names."
|
| 180 |
+
)
|
| 181 |
+
food_vars[food_name] = model.addVar(name=f"Food_{i}_{food_name}", lb=0)
|
| 182 |
+
|
| 183 |
+
logger.info("Setting objective function...")
|
| 184 |
+
|
| 185 |
+
# Objective: Minimize total cost
|
| 186 |
+
model.setObjective(
|
| 187 |
+
quicksum(
|
| 188 |
+
foods_df.loc[foods_df["Food"] == food, "Cost"].iloc[0] * var
|
| 189 |
+
for food, var in food_vars.items()
|
| 190 |
+
),
|
| 191 |
+
GRB.MINIMIZE,
|
| 192 |
)
|
| 193 |
|
| 194 |
+
logger.info("Adding constraints...")
|
| 195 |
+
|
| 196 |
+
# Constraints: Nutritional requirements
|
| 197 |
+
for _, req_row in requirements_df.iterrows():
|
| 198 |
+
nutrient = req_row["Nutrient"]
|
| 199 |
+
min_requirement = req_row["Minimum"]
|
| 200 |
+
|
| 201 |
+
if pd.isna(nutrient) or str(nutrient).strip() == "":
|
| 202 |
+
raise ValueError(
|
| 203 |
+
"Empty or invalid nutrient name found. All nutrients must have valid names."
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
if nutrient in nutrient_cols:
|
| 207 |
+
model.addConstr(
|
| 208 |
+
quicksum(
|
| 209 |
+
foods_df.loc[foods_df["Food"] == food, nutrient].iloc[0] * var
|
| 210 |
+
for food, var in food_vars.items()
|
| 211 |
+
)
|
| 212 |
+
>= min_requirement,
|
| 213 |
+
name=f"{nutrient}_requirement",
|
| 214 |
+
)
|
| 215 |
+
else:
|
| 216 |
+
logger.warning(
|
| 217 |
+
f"Nutrient '{nutrient}' not found in foods data. Skipping constraint."
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
logger.info("Solving optimization model...")
|
| 221 |
+
|
| 222 |
+
# Solve the model
|
| 223 |
+
model.optimize()
|
| 224 |
+
|
| 225 |
+
# Enhanced status checking
|
| 226 |
+
if model.status == GRB.OPTIMAL:
|
| 227 |
+
logger.info("Optimal solution found successfully")
|
| 228 |
+
elif model.status == GRB.INFEASIBLE:
|
| 229 |
+
logger.error("Problem is infeasible")
|
| 230 |
+
# Try to compute IIS (Irreducible Inconsistent Subsystem) for debugging
|
| 231 |
+
try:
|
| 232 |
+
model.computeIIS()
|
| 233 |
+
iis_constraints = []
|
| 234 |
+
for constr in model.getConstrs():
|
| 235 |
+
if constr.IISConstr:
|
| 236 |
+
iis_constraints.append(constr.ConstrName)
|
| 237 |
+
if iis_constraints:
|
| 238 |
+
raise ValueError(
|
| 239 |
+
f"Problem is infeasible. Conflicting constraints: {iis_constraints}. Try reducing requirements or adding more diverse foods."
|
| 240 |
+
)
|
| 241 |
+
else:
|
| 242 |
+
raise ValueError(
|
| 243 |
+
"Problem is infeasible. The nutritional requirements cannot be met with the provided foods. Try reducing requirements or adding more foods with different nutritional profiles."
|
| 244 |
+
)
|
| 245 |
+
except Exception as iis_error:
|
| 246 |
+
logger.warning(f"Could not compute IIS: {str(iis_error)}")
|
| 247 |
+
raise ValueError(
|
| 248 |
+
"Problem is infeasible. The nutritional requirements cannot be met with the provided foods. Try reducing requirements or adding more diverse foods."
|
| 249 |
+
)
|
| 250 |
+
elif model.status == GRB.UNBOUNDED:
|
| 251 |
+
logger.error("Problem is unbounded")
|
| 252 |
+
raise ValueError(
|
| 253 |
+
"Problem is unbounded. This usually occurs when some foods have zero cost. Please ensure all foods have positive costs."
|
| 254 |
+
)
|
| 255 |
+
elif model.status == GRB.TIME_LIMIT:
|
| 256 |
+
logger.error("Time limit reached")
|
| 257 |
+
raise ValueError(
|
| 258 |
+
"Solver time limit reached (60 seconds). The problem may be too complex. Try simplifying the problem or contact support."
|
| 259 |
+
)
|
| 260 |
+
elif model.status == GRB.INTERRUPTED:
|
| 261 |
+
logger.error("Solver was interrupted")
|
| 262 |
+
raise ValueError("Solver was interrupted. Please try again.")
|
| 263 |
+
elif model.status == GRB.NUMERIC:
|
| 264 |
+
logger.error("Numerical difficulties encountered")
|
| 265 |
+
raise ValueError(
|
| 266 |
+
"Numerical difficulties encountered. Try using simpler numbers or scaling your data."
|
| 267 |
+
)
|
| 268 |
+
else:
|
| 269 |
+
logger.error(f"Unexpected solver status: {model.status}")
|
| 270 |
+
raise ValueError(
|
| 271 |
+
f"Failed to find an optimal solution. Solver status: {model.status}. Please check your input data."
|
| 272 |
+
)
|
| 273 |
|
| 274 |
+
except Exception as gurobi_error:
|
| 275 |
+
logger.error(f"Gurobi optimization error: {str(gurobi_error)}")
|
| 276 |
+
if "Gurobi" in str(type(gurobi_error)):
|
| 277 |
+
raise ValueError(
|
| 278 |
+
f"Gurobi solver error: {str(gurobi_error)}. Please check your Gurobi installation and license."
|
| 279 |
+
)
|
| 280 |
+
else:
|
| 281 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
+
# Extract results
|
| 284 |
+
try:
|
| 285 |
+
total_cost = model.objVal
|
| 286 |
+
logger.info(f"Optimal solution found with total cost: {total_cost:.2f}")
|
| 287 |
+
|
| 288 |
+
# Create results dataframe
|
| 289 |
+
result_rows = []
|
| 290 |
+
for food_name, var in food_vars.items():
|
| 291 |
+
try:
|
| 292 |
+
food_row = foods_df[foods_df["Food"] == food_name].iloc[0]
|
| 293 |
+
amount = var.X
|
| 294 |
+
|
| 295 |
+
result_row = {
|
| 296 |
+
"Food": food_name,
|
| 297 |
+
"Units": amount,
|
| 298 |
+
"Cost": food_row["Cost"] * amount,
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
# Add nutrient contributions
|
| 302 |
+
for nutrient in nutrient_cols:
|
| 303 |
+
result_row[nutrient] = food_row[nutrient] * amount
|
| 304 |
+
|
| 305 |
+
result_rows.append(result_row)
|
| 306 |
+
except Exception as extract_error:
|
| 307 |
+
logger.error(
|
| 308 |
+
f"Error extracting results for food '{food_name}': {str(extract_error)}"
|
| 309 |
+
)
|
| 310 |
+
raise ValueError(
|
| 311 |
+
f"Error processing results for food '{food_name}': {str(extract_error)}"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
result_df = pd.DataFrame(result_rows)
|
| 315 |
+
|
| 316 |
+
# Validate results
|
| 317 |
+
if result_df.empty:
|
| 318 |
+
raise ValueError(
|
| 319 |
+
"No results generated. This is unexpected after finding an optimal solution."
|
| 320 |
+
)
|
| 321 |
|
| 322 |
+
# Check if any solution violates non-negativity (shouldn't happen, but good to check)
|
| 323 |
+
if (result_df["Units"] < -1e-6).any():
|
| 324 |
+
negative_foods = result_df[result_df["Units"] < -1e-6]["Food"].tolist()
|
| 325 |
+
logger.warning(
|
| 326 |
+
f"Negative amounts found for foods (numerical error): {negative_foods}"
|
| 327 |
+
)
|
| 328 |
+
# Clamp negative values to zero
|
| 329 |
+
result_df["Units"] = result_df["Units"].clip(lower=0)
|
| 330 |
|
| 331 |
+
except Exception as result_error:
|
| 332 |
+
logger.error(f"Error extracting optimization results: {str(result_error)}")
|
| 333 |
+
raise ValueError(
|
| 334 |
+
f"Failed to extract optimization results: {str(result_error)}"
|
| 335 |
+
) # Create flexible visualizations
|
| 336 |
+
try:
|
| 337 |
+
fig, axs = plt.subplots(2, 2, figsize=(14, 10))
|
| 338 |
+
fig.suptitle("Diet Problem Optimization Results", fontsize=16, y=1.02)
|
| 339 |
+
|
| 340 |
+
# Plot 1: Food Units - only show foods with positive amounts
|
| 341 |
+
foods_with_amounts = result_df[result_df["Units"] > 0.001]
|
| 342 |
+
if not foods_with_amounts.empty:
|
| 343 |
+
colors = plt.cm.Set3(range(len(foods_with_amounts)))
|
| 344 |
+
axs[0, 0].bar(
|
| 345 |
+
foods_with_amounts["Food"], foods_with_amounts["Units"], color=colors
|
| 346 |
+
)
|
| 347 |
+
axs[0, 0].set_title("Optimal Food Quantities")
|
| 348 |
+
axs[0, 0].set_ylabel("Units")
|
| 349 |
+
axs[0, 0].tick_params(axis="x", rotation=45)
|
| 350 |
+
else:
|
| 351 |
+
axs[0, 0].text(
|
| 352 |
+
0.5,
|
| 353 |
+
0.5,
|
| 354 |
+
"No foods selected\n(all amounts are zero)",
|
| 355 |
+
ha="center",
|
| 356 |
+
va="center",
|
| 357 |
+
transform=axs[0, 0].transAxes,
|
| 358 |
+
)
|
| 359 |
+
axs[0, 0].set_title("Optimal Food Quantities")
|
| 360 |
|
| 361 |
+
# Plot 2: Cost Breakdown
|
| 362 |
+
if not foods_with_amounts.empty:
|
| 363 |
+
axs[0, 1].bar(
|
| 364 |
+
foods_with_amounts["Food"], foods_with_amounts["Cost"], color=colors
|
| 365 |
+
)
|
| 366 |
+
axs[0, 1].set_title("Cost per Food Type")
|
| 367 |
+
axs[0, 1].set_ylabel("Cost ($)")
|
| 368 |
+
axs[0, 1].tick_params(axis="x", rotation=45)
|
| 369 |
+
else:
|
| 370 |
+
axs[0, 1].text(
|
| 371 |
+
0.5,
|
| 372 |
+
0.5,
|
| 373 |
+
"No costs to display",
|
| 374 |
+
ha="center",
|
| 375 |
+
va="center",
|
| 376 |
+
transform=axs[0, 1].transAxes,
|
| 377 |
+
)
|
| 378 |
+
axs[0, 1].set_title("Cost per Food Type")
|
| 379 |
+
|
| 380 |
+
# Plot 3: Nutritional Requirements vs Achieved
|
| 381 |
+
achieved_nutrients = {}
|
| 382 |
+
for nutrient in nutrient_cols:
|
| 383 |
+
achieved_nutrients[nutrient] = result_df[nutrient].sum()
|
| 384 |
+
|
| 385 |
+
requirements_dict = dict(
|
| 386 |
+
zip(requirements_df["Nutrient"], requirements_df["Minimum"])
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
nutrients = list(nutrient_cols)
|
| 390 |
+
requirements = [requirements_dict.get(nut, 0) for nut in nutrients]
|
| 391 |
+
achieved = [achieved_nutrients.get(nut, 0) for nut in nutrients]
|
| 392 |
+
|
| 393 |
+
if nutrients:
|
| 394 |
+
x_pos = range(len(nutrients))
|
| 395 |
+
width = 0.35
|
| 396 |
+
axs[1, 0].bar(
|
| 397 |
+
[i - width / 2 for i in x_pos],
|
| 398 |
+
requirements,
|
| 399 |
+
width,
|
| 400 |
+
label="Required",
|
| 401 |
+
color="red",
|
| 402 |
+
alpha=0.7,
|
| 403 |
+
)
|
| 404 |
+
axs[1, 0].bar(
|
| 405 |
+
[i + width / 2 for i in x_pos],
|
| 406 |
+
achieved,
|
| 407 |
+
width,
|
| 408 |
+
label="Achieved",
|
| 409 |
+
color="green",
|
| 410 |
+
alpha=0.7,
|
| 411 |
+
)
|
| 412 |
+
axs[1, 0].set_title("Nutritional Requirements vs Achieved")
|
| 413 |
+
axs[1, 0].set_ylabel("Units")
|
| 414 |
+
axs[1, 0].set_xticks(x_pos)
|
| 415 |
+
axs[1, 0].set_xticklabels(nutrients, rotation=45)
|
| 416 |
+
axs[1, 0].legend()
|
| 417 |
+
else:
|
| 418 |
+
axs[1, 0].text(
|
| 419 |
+
0.5,
|
| 420 |
+
0.5,
|
| 421 |
+
"No nutrients to display",
|
| 422 |
+
ha="center",
|
| 423 |
+
va="center",
|
| 424 |
+
transform=axs[1, 0].transAxes,
|
| 425 |
+
)
|
| 426 |
+
axs[1, 0].set_title("Nutritional Requirements vs Achieved")
|
| 427 |
+
|
| 428 |
+
# Plot 4: Summary Information
|
| 429 |
+
summary_data = [("Total Cost", total_cost)]
|
| 430 |
+
for nutrient in nutrient_cols:
|
| 431 |
+
summary_data.append(
|
| 432 |
+
(f"Total {nutrient}", achieved_nutrients.get(nutrient, 0))
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
if summary_data:
|
| 436 |
+
summary_labels, summary_values = zip(*summary_data)
|
| 437 |
+
colors_summary = plt.cm.viridis(range(len(summary_data)))
|
| 438 |
+
|
| 439 |
+
bars = axs[1, 1].bar(summary_labels, summary_values, color=colors_summary)
|
| 440 |
+
axs[1, 1].set_title("Diet Summary")
|
| 441 |
+
axs[1, 1].set_ylabel("Value")
|
| 442 |
+
|
| 443 |
+
# Add value labels on bars
|
| 444 |
+
for bar, value in zip(bars, summary_values):
|
| 445 |
+
height = bar.get_height()
|
| 446 |
+
axs[1, 1].text(
|
| 447 |
+
bar.get_x() + bar.get_width() / 2.0,
|
| 448 |
+
height + max(summary_values) * 0.01,
|
| 449 |
+
f"{value:.2f}",
|
| 450 |
+
ha="center",
|
| 451 |
+
va="bottom",
|
| 452 |
+
fontsize=8,
|
| 453 |
+
)
|
| 454 |
+
axs[1, 1].tick_params(axis="x", rotation=45)
|
| 455 |
+
else:
|
| 456 |
+
axs[1, 1].text(
|
| 457 |
+
0.5,
|
| 458 |
+
0.5,
|
| 459 |
+
"No summary data to display",
|
| 460 |
+
ha="center",
|
| 461 |
+
va="center",
|
| 462 |
+
transform=axs[1, 1].transAxes,
|
| 463 |
+
)
|
| 464 |
+
axs[1, 1].set_title("Diet Summary")
|
| 465 |
+
|
| 466 |
+
fig.tight_layout()
|
| 467 |
+
|
| 468 |
+
logger.info("Visualization created successfully")
|
| 469 |
+
|
| 470 |
+
except Exception as plot_error:
|
| 471 |
+
logger.error(f"Error creating visualization: {str(plot_error)}")
|
| 472 |
+
# Create a simple fallback plot
|
| 473 |
+
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
|
| 474 |
+
ax.text(
|
| 475 |
+
0.5,
|
| 476 |
+
0.5,
|
| 477 |
+
f"Visualization Error\nOptimal Cost: ${total_cost:.2f}\nSee results table for details",
|
| 478 |
+
ha="center",
|
| 479 |
+
va="center",
|
| 480 |
+
transform=ax.transAxes,
|
| 481 |
+
fontsize=12,
|
| 482 |
+
)
|
| 483 |
+
ax.set_title("Diet Problem Results")
|
| 484 |
+
ax.axis("off")
|
| 485 |
+
|
| 486 |
+
return result_df, fig
|
| 487 |
|
| 488 |
|
| 489 |
def _validate_plne_input(data, vehicle_capacity, num_vehicles):
|
|
|
|
| 795 |
except (ValueError, TypeError) as e:
|
| 796 |
raise RuntimeError(f"Error in solve_plne: {e}")
|
| 797 |
except Exception as e:
|
| 798 |
+
raise RuntimeError(f"Unexpected error in solve_plne: {e}")
|
requirements.txt
CHANGED
|
@@ -7,6 +7,7 @@ charset-normalizer==3.4.2
|
|
| 7 |
click==8.1.8
|
| 8 |
contourpy==1.3.2
|
| 9 |
cycler==0.12.1
|
|
|
|
| 10 |
fastapi==0.115.12
|
| 11 |
ffmpy==0.5.0
|
| 12 |
filelock==3.18.0
|
|
@@ -41,6 +42,7 @@ pydub==0.25.1
|
|
| 41 |
Pygments==2.19.1
|
| 42 |
pyparsing==3.2.3
|
| 43 |
python-dateutil==2.9.0.post0
|
|
|
|
| 44 |
python-multipart==0.0.20
|
| 45 |
pytz==2025.2
|
| 46 |
PyYAML==6.0.2
|
|
|
|
| 7 |
click==8.1.8
|
| 8 |
contourpy==1.3.2
|
| 9 |
cycler==0.12.1
|
| 10 |
+
dotenv==0.9.9
|
| 11 |
fastapi==0.115.12
|
| 12 |
ffmpy==0.5.0
|
| 13 |
filelock==3.18.0
|
|
|
|
| 42 |
Pygments==2.19.1
|
| 43 |
pyparsing==3.2.3
|
| 44 |
python-dateutil==2.9.0.post0
|
| 45 |
+
python-dotenv==1.1.0
|
| 46 |
python-multipart==0.0.20
|
| 47 |
pytz==2025.2
|
| 48 |
PyYAML==6.0.2
|
ui/gradio_sections.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
|
| 7 |
-
import
|
| 8 |
|
| 9 |
logger = logger.get_logger(__name__)
|
| 10 |
|
|
@@ -13,21 +13,68 @@ def project_info_tab():
|
|
| 13 |
with gr.Tab("\U0001f4d8 Project Info"):
|
| 14 |
gr.Markdown(
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
)
|
| 32 |
pdf_path = os.path.join(
|
| 33 |
os.path.dirname(os.path.dirname(__file__)), "assets", "compte_rendu.pdf"
|
|
@@ -43,116 +90,323 @@ def project_info_tab():
|
|
| 43 |
)
|
| 44 |
|
| 45 |
|
| 46 |
-
def
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
mock_yields_data,
|
| 50 |
-
mock_quality_reqs,
|
| 51 |
-
solve_refinery_optimization,
|
| 52 |
-
refinery_description,
|
| 53 |
-
):
|
| 54 |
-
with gr.Tab("\U0001f3ed Oil Refinery Optimization (PL)"):
|
| 55 |
-
gr.Markdown(refinery_description)
|
| 56 |
|
| 57 |
# Add mathematical model description
|
| 58 |
gr.Markdown(
|
| 59 |
r"""
|
| 60 |
### 🧮 Mathematical Formulation
|
| 61 |
|
| 62 |
-
**Sets and Indices**
|
| 63 |
-
| Symbol | Description |
|
| 64 |
-
|-------------|-----------------------------------------|
|
| 65 |
-
| $$i=1,...,m$$ | Types of crude oil (inputs) |
|
| 66 |
-
| $$j=1,...,n$$ | Types of fuel products (outputs) |
|
| 67 |
-
|
| 68 |
**Parameters**
|
| 69 |
-
| Symbol | Description
|
| 70 |
-
|
| 71 |
-
| $$
|
| 72 |
-
| $$
|
| 73 |
-
| $$
|
| 74 |
-
| $$
|
| 75 |
-
| $$
|
| 76 |
-
| $$D_j$$ | Minimum demand (liters) for product $j$ |
|
| 77 |
-
| $$A_i$$ | Availability limit (liters) of crude $i$ |
|
| 78 |
|
| 79 |
**Decision Variables**
|
| 80 |
| Symbol | Description |
|
| 81 |
|-------------|-----------------------------------------|
|
| 82 |
-
| $$x_i$$
|
| 83 |
|
| 84 |
-
**Objective:**
|
| 85 |
$$
|
| 86 |
-
\text{
|
| 87 |
$$
|
| 88 |
|
| 89 |
**Constraints:**
|
| 90 |
-
1.
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
2. Demand Satisfaction:
|
| 94 |
-
$$\sum_{i=1}^m y_{ij} \cdot x_i \geq D_j \quad \forall j$$
|
| 95 |
-
|
| 96 |
-
3. Quality Requirements:
|
| 97 |
-
$$\sum_{i=1}^m q_{ij} \cdot y_{ij} \cdot x_i \geq Q_j^{min} \cdot \sum_{i=1}^m y_{ij} \cdot x_i \quad \forall j$$
|
| 98 |
|
| 99 |
-
|
| 100 |
-
$$x_i \geq 0 \quad \forall i$$
|
| 101 |
"""
|
| 102 |
)
|
| 103 |
|
| 104 |
-
with gr.
|
| 105 |
-
with gr.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
)
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
headers=["
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
| 117 |
)
|
| 118 |
|
| 119 |
-
with gr.
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
headers=["
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
| 131 |
)
|
| 132 |
|
| 133 |
-
solve_btn = gr.Button("Solve
|
| 134 |
status_output = gr.Textbox(label="Status", interactive=False)
|
| 135 |
results_table = gr.Dataframe(label="Optimization Results")
|
| 136 |
results_plot = gr.Plot(label="Results Visualization")
|
| 137 |
|
| 138 |
-
def
|
| 139 |
try:
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
)
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
except Exception as e:
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
solve_btn.click(
|
| 154 |
-
fn=
|
| 155 |
-
inputs=[
|
| 156 |
outputs=[results_table, results_plot, status_output],
|
| 157 |
)
|
| 158 |
|
|
@@ -264,4 +518,4 @@ def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
|
|
| 264 |
fn=_solve_vrp_with_floats,
|
| 265 |
inputs=[vrp_input, cap_input, k_input],
|
| 266 |
outputs=[result_table, result_plot, status_output],
|
| 267 |
-
)
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
|
| 7 |
+
import utils.logger as logger
|
| 8 |
|
| 9 |
logger = logger.get_logger(__name__)
|
| 10 |
|
|
|
|
| 13 |
with gr.Tab("\U0001f4d8 Project Info"):
|
| 14 |
gr.Markdown(
|
| 15 |
"""
|
| 16 |
+
# \U0001f393 GL3 - 2025 - Operational Research Project
|
| 17 |
+
This application demonstrates how **Linear Programming (PL)** and **Mixed-Integer Linear Programming (PLNE)** can be applied to solve real-world optimisation problems using **Gurobi**.
|
| 18 |
+
"""
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
gr.HTML(
|
| 22 |
+
"""
|
| 23 |
+
<style>
|
| 24 |
+
.member-card {
|
| 25 |
+
display: flex;
|
| 26 |
+
flex-direction: column;
|
| 27 |
+
align-items: center;
|
| 28 |
+
width: 160px;
|
| 29 |
+
text-align: center;
|
| 30 |
+
margin: 10px;
|
| 31 |
+
}
|
| 32 |
+
.member-card img {
|
| 33 |
+
width: 120px;
|
| 34 |
+
height: 140px;
|
| 35 |
+
object-fit: cover;
|
| 36 |
+
border-radius: 8px;
|
| 37 |
+
border: 1px solid #ccc;
|
| 38 |
+
}
|
| 39 |
+
.member-name {
|
| 40 |
+
margin-top: 8px;
|
| 41 |
+
font-weight: bold;
|
| 42 |
+
}
|
| 43 |
+
</style>
|
| 44 |
+
<div style="display: flex; flex-wrap: wrap; justify-content: center;">
|
| 45 |
+
<div class="member-card">
|
| 46 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/kacem_mathlouthi.jpg" alt="Kacem Mathlouthi">
|
| 47 |
+
<div class="member-name">Kacem Mathlouthi</div>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="member-card">
|
| 50 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/mohamed_amine_haouas.jpg" alt="Mohamed Amine Houas">
|
| 51 |
+
<div class="member-name">Mohamed Amine Houas</div>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="member-card">
|
| 54 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/oussema_kraiem.jpg" alt="Oussema Kraiem">
|
| 55 |
+
<div class="member-name">Oussema Kraiem</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="member-card">
|
| 58 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/mohamed_yassine_taieb.jpg" alt="Yassine Taieb">
|
| 59 |
+
<div class="member-name">Yassine Taieb</div>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="member-card">
|
| 62 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/youssef_sghairi.jpg" alt="Youssef Sghairi">
|
| 63 |
+
<div class="member-name">Youssef Sghairi</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="member-card">
|
| 66 |
+
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/youssef_aridhi.jpg" alt="Youssef Aaridhi">
|
| 67 |
+
<div class="member-name">Youssef Aaridhi</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
gr.Markdown(
|
| 74 |
+
"""
|
| 75 |
+
---
|
| 76 |
+
# \U0001f9fe Compte Rendu
|
| 77 |
+
"""
|
| 78 |
)
|
| 79 |
pdf_path = os.path.join(
|
| 80 |
os.path.dirname(os.path.dirname(__file__)), "assets", "compte_rendu.pdf"
|
|
|
|
| 90 |
)
|
| 91 |
|
| 92 |
|
| 93 |
+
def diet_problem_tab(solve_diet_problem, diet_description):
|
| 94 |
+
with gr.Tab("🍎 Diet Problem (PL)"):
|
| 95 |
+
gr.Markdown(diet_description)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# Add mathematical model description
|
| 98 |
gr.Markdown(
|
| 99 |
r"""
|
| 100 |
### 🧮 Mathematical Formulation
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
**Parameters**
|
| 103 |
+
| Symbol | Description |
|
| 104 |
+
|-------------|------------------------------------------------|
|
| 105 |
+
| $$I$$ | Set of available foods |
|
| 106 |
+
| $$J$$ | Set of nutrients |
|
| 107 |
+
| $$c_i$$ | Cost per unit of food i |
|
| 108 |
+
| $$n_{ij}$$ | Amount of nutrient j in one unit of food i |
|
| 109 |
+
| $$R_j$$ | Minimum requirement for nutrient j |
|
|
|
|
|
|
|
| 110 |
|
| 111 |
**Decision Variables**
|
| 112 |
| Symbol | Description |
|
| 113 |
|-------------|-----------------------------------------|
|
| 114 |
+
| $$x_i$$ | Units of food i to consume |
|
| 115 |
|
| 116 |
+
**Objective Function:**
|
| 117 |
$$
|
| 118 |
+
\text{Minimize} \quad Z = \sum_{i \in I} c_i \cdot x_i
|
| 119 |
$$
|
| 120 |
|
| 121 |
**Constraints:**
|
| 122 |
+
1. **Nutritional requirements:**
|
| 123 |
+
$$\sum_{i \in I} n_{ij} \cdot x_i \geq R_j \quad \forall j \in J$$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
+
2. **Non-negativity:**
|
| 126 |
+
$$x_i \geq 0 \quad \forall i \in I$$
|
| 127 |
"""
|
| 128 |
)
|
| 129 |
|
| 130 |
+
with gr.Row():
|
| 131 |
+
with gr.Column():
|
| 132 |
+
gr.Markdown("### 🍎 Foods Data")
|
| 133 |
+
gr.Markdown(
|
| 134 |
+
"Add foods with their costs and nutritional content per unit:"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Default foods data
|
| 138 |
+
default_foods = pd.DataFrame(
|
| 139 |
+
[
|
| 140 |
+
{"Food": "Food A", "Cost": 3.0, "Protein": 2.0, "Fat": 1.0},
|
| 141 |
+
{"Food": "Food B", "Cost": 2.0, "Protein": 1.0, "Fat": 2.0},
|
| 142 |
+
]
|
| 143 |
)
|
| 144 |
|
| 145 |
+
foods_input = gr.Dataframe(
|
| 146 |
+
value=default_foods,
|
| 147 |
+
headers=["Food", "Cost", "Protein", "Fat"],
|
| 148 |
+
datatype=["str", "number", "number", "number"],
|
| 149 |
+
col_count=(4, "dynamic"),
|
| 150 |
+
row_count=(2, "dynamic"),
|
| 151 |
+
label="Foods and Nutritional Content",
|
| 152 |
+
interactive=True,
|
| 153 |
)
|
| 154 |
|
| 155 |
+
with gr.Column():
|
| 156 |
+
gr.Markdown("### 🥗 Nutritional Requirements")
|
| 157 |
+
gr.Markdown("Specify minimum daily requirements for each nutrient:")
|
| 158 |
+
|
| 159 |
+
# Default requirements data
|
| 160 |
+
default_requirements = pd.DataFrame(
|
| 161 |
+
[
|
| 162 |
+
{"Nutrient": "Protein", "Minimum": 8.0},
|
| 163 |
+
{"Nutrient": "Fat", "Minimum": 6.0},
|
| 164 |
+
]
|
| 165 |
)
|
| 166 |
|
| 167 |
+
requirements_input = gr.Dataframe(
|
| 168 |
+
value=default_requirements,
|
| 169 |
+
headers=["Nutrient", "Minimum"],
|
| 170 |
+
datatype=["str", "number"],
|
| 171 |
+
col_count=(2, "fixed"),
|
| 172 |
+
row_count=(2, "dynamic"),
|
| 173 |
+
label="Nutritional Requirements",
|
| 174 |
+
interactive=True,
|
| 175 |
)
|
| 176 |
|
| 177 |
+
solve_btn = gr.Button("Solve Diet Problem", variant="primary")
|
| 178 |
status_output = gr.Textbox(label="Status", interactive=False)
|
| 179 |
results_table = gr.Dataframe(label="Optimization Results")
|
| 180 |
results_plot = gr.Plot(label="Results Visualization")
|
| 181 |
|
| 182 |
+
def _solve_diet_optimization(foods_df, requirements_df):
|
| 183 |
try:
|
| 184 |
+
logger.info("Starting diet optimization from UI")
|
| 185 |
+
|
| 186 |
+
# Input existence validation
|
| 187 |
+
if foods_df is None or len(foods_df) == 0:
|
| 188 |
+
return (
|
| 189 |
+
pd.DataFrame(),
|
| 190 |
+
None,
|
| 191 |
+
"❌ Error: Please provide foods data. Add at least one food item with its cost and nutritional content.",
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
if requirements_df is None or len(requirements_df) == 0:
|
| 195 |
+
return (
|
| 196 |
+
pd.DataFrame(),
|
| 197 |
+
None,
|
| 198 |
+
"❌ Error: Please provide requirements data. Add at least one nutritional requirement.",
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
# Convert to DataFrame if needed
|
| 202 |
+
if not isinstance(foods_df, pd.DataFrame):
|
| 203 |
+
try:
|
| 204 |
+
foods_df = pd.DataFrame(foods_df)
|
| 205 |
+
except Exception as e:
|
| 206 |
+
return (
|
| 207 |
+
pd.DataFrame(),
|
| 208 |
+
None,
|
| 209 |
+
f"❌ Error: Cannot convert foods data to DataFrame: {str(e)}",
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
if not isinstance(requirements_df, pd.DataFrame):
|
| 213 |
+
try:
|
| 214 |
+
requirements_df = pd.DataFrame(requirements_df)
|
| 215 |
+
except Exception as e:
|
| 216 |
+
return (
|
| 217 |
+
pd.DataFrame(),
|
| 218 |
+
None,
|
| 219 |
+
f"❌ Error: Cannot convert requirements data to DataFrame: {str(e)}",
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Remove completely empty rows
|
| 223 |
+
foods_df = foods_df.dropna(how="all")
|
| 224 |
+
requirements_df = requirements_df.dropna(how="all")
|
| 225 |
+
|
| 226 |
+
# Check if data still exists after cleaning
|
| 227 |
+
if foods_df.empty:
|
| 228 |
+
return (
|
| 229 |
+
pd.DataFrame(),
|
| 230 |
+
None,
|
| 231 |
+
"❌ Error: No valid foods data found. Please ensure at least one row has valid data.",
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
if requirements_df.empty:
|
| 235 |
+
return (
|
| 236 |
+
pd.DataFrame(),
|
| 237 |
+
None,
|
| 238 |
+
"❌ Error: No valid requirements data found. Please ensure at least one row has valid data.",
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Validate required columns
|
| 242 |
+
if "Food" not in foods_df.columns or "Cost" not in foods_df.columns:
|
| 243 |
+
return (
|
| 244 |
+
pd.DataFrame(),
|
| 245 |
+
None,
|
| 246 |
+
"❌ Error: Foods data must have 'Food' and 'Cost' columns. Please check your column headers.",
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if (
|
| 250 |
+
"Nutrient" not in requirements_df.columns
|
| 251 |
+
or "Minimum" not in requirements_df.columns
|
| 252 |
+
):
|
| 253 |
+
return (
|
| 254 |
+
pd.DataFrame(),
|
| 255 |
+
None,
|
| 256 |
+
"❌ Error: Requirements data must have 'Nutrient' and 'Minimum' columns. Please check your column headers.",
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# Check for missing values in critical columns
|
| 260 |
+
missing_food_names = foods_df["Food"].isna().sum()
|
| 261 |
+
if missing_food_names > 0:
|
| 262 |
+
return (
|
| 263 |
+
pd.DataFrame(),
|
| 264 |
+
None,
|
| 265 |
+
f"❌ Error: {missing_food_names} food(s) have missing names. All foods must have valid names.",
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
missing_costs = foods_df["Cost"].isna().sum()
|
| 269 |
+
if missing_costs > 0:
|
| 270 |
+
return (
|
| 271 |
+
pd.DataFrame(),
|
| 272 |
+
None,
|
| 273 |
+
f"❌ Error: {missing_costs} food(s) have missing costs. All foods must have valid costs.",
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
missing_nutrients = requirements_df["Nutrient"].isna().sum()
|
| 277 |
+
if missing_nutrients > 0:
|
| 278 |
+
return (
|
| 279 |
+
pd.DataFrame(),
|
| 280 |
+
None,
|
| 281 |
+
f"❌ Error: {missing_nutrients} requirement(s) have missing nutrient names. All requirements must have valid nutrient names.",
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
missing_minimums = requirements_df["Minimum"].isna().sum()
|
| 285 |
+
if missing_minimums > 0:
|
| 286 |
+
return (
|
| 287 |
+
pd.DataFrame(),
|
| 288 |
+
None,
|
| 289 |
+
f"❌ Error: {missing_minimums} requirement(s) have missing minimum values. All requirements must have valid minimum values.",
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Check for negative values
|
| 293 |
+
try:
|
| 294 |
+
numeric_cols = foods_df.select_dtypes(include=[float, int]).columns
|
| 295 |
+
numeric_cols = [
|
| 296 |
+
col for col in numeric_cols if col != "Food"
|
| 297 |
+
] # Exclude non-numeric columns
|
| 298 |
+
|
| 299 |
+
if (
|
| 300 |
+
len(numeric_cols) > 0
|
| 301 |
+
and (foods_df[numeric_cols] < 0).any().any()
|
| 302 |
+
):
|
| 303 |
+
negative_foods = []
|
| 304 |
+
for col in numeric_cols:
|
| 305 |
+
if (foods_df[col] < 0).any():
|
| 306 |
+
bad_foods = foods_df[foods_df[col] < 0]["Food"].tolist()
|
| 307 |
+
negative_foods.extend(
|
| 308 |
+
[f"{food} ({col})" for food in bad_foods]
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
return (
|
| 312 |
+
pd.DataFrame(),
|
| 313 |
+
None,
|
| 314 |
+
f"❌ Error: Negative values found: {', '.join(negative_foods[:5])}{'...' if len(negative_foods) > 5 else ''}. All numeric values must be non-negative.",
|
| 315 |
+
)
|
| 316 |
+
except Exception as numeric_error:
|
| 317 |
+
return (
|
| 318 |
+
pd.DataFrame(),
|
| 319 |
+
None,
|
| 320 |
+
f"❌ Error: Problem checking numeric values: {str(numeric_error)}. Please ensure all numeric columns contain valid numbers.",
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
try:
|
| 324 |
+
if (requirements_df["Minimum"] <= 0).any():
|
| 325 |
+
bad_requirements = requirements_df[
|
| 326 |
+
requirements_df["Minimum"] <= 0
|
| 327 |
+
]["Nutrient"].tolist()
|
| 328 |
+
return (
|
| 329 |
+
pd.DataFrame(),
|
| 330 |
+
None,
|
| 331 |
+
f"❌ Error: Non-positive requirements found for: {', '.join(bad_requirements)}. All requirements must be positive values.",
|
| 332 |
+
)
|
| 333 |
+
except Exception as req_error:
|
| 334 |
+
return (
|
| 335 |
+
pd.DataFrame(),
|
| 336 |
+
None,
|
| 337 |
+
f"❌ Error: Problem checking requirements: {str(req_error)}. Please ensure all requirement values are positive numbers.",
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
# Check for empty strings in food names
|
| 341 |
+
empty_food_names = foods_df[
|
| 342 |
+
foods_df["Food"].astype(str).str.strip() == ""
|
| 343 |
+
]["Food"].count()
|
| 344 |
+
if empty_food_names > 0:
|
| 345 |
+
return (
|
| 346 |
+
pd.DataFrame(),
|
| 347 |
+
None,
|
| 348 |
+
f"❌ Error: {empty_food_names} food(s) have empty names. All foods must have non-empty names.",
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Check for empty strings in nutrient names
|
| 352 |
+
empty_nutrient_names = requirements_df[
|
| 353 |
+
requirements_df["Nutrient"].astype(str).str.strip() == ""
|
| 354 |
+
]["Nutrient"].count()
|
| 355 |
+
if empty_nutrient_names > 0:
|
| 356 |
+
return (
|
| 357 |
+
pd.DataFrame(),
|
| 358 |
+
None,
|
| 359 |
+
f"❌ Error: {empty_nutrient_names} nutrient(s) have empty names. All nutrients must have non-empty names.",
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
logger.info("UI validation passed, calling solver...")
|
| 363 |
+
result_df, fig = solve_diet_problem(foods_df, requirements_df)
|
| 364 |
+
|
| 365 |
+
# Validate solver results
|
| 366 |
+
if result_df is None or result_df.empty:
|
| 367 |
+
return (
|
| 368 |
+
pd.DataFrame(),
|
| 369 |
+
None,
|
| 370 |
+
"❌ Error: Solver returned empty results. This is unexpected.",
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
# Check if solution makes sense
|
| 374 |
+
total_cost = (
|
| 375 |
+
result_df["Cost"].sum() if "Cost" in result_df.columns else 0
|
| 376 |
+
)
|
| 377 |
+
if total_cost < 0:
|
| 378 |
+
logger.warning(f"Negative total cost detected: {total_cost}")
|
| 379 |
+
|
| 380 |
+
logger.info(
|
| 381 |
+
f"Optimization completed successfully with total cost: {total_cost:.2f}"
|
| 382 |
+
)
|
| 383 |
+
return (
|
| 384 |
+
result_df,
|
| 385 |
+
fig,
|
| 386 |
+
f"✅ Solved Successfully! Optimal diet plan found with total cost: ${total_cost:.2f}",
|
| 387 |
)
|
| 388 |
+
|
| 389 |
+
except ValueError as ve:
|
| 390 |
+
logger.error(f"Validation error: {str(ve)}")
|
| 391 |
+
return pd.DataFrame(), None, f"❌ Validation Error: {str(ve)}"
|
| 392 |
+
except TypeError as te:
|
| 393 |
+
logger.error(f"Type error: {str(te)}")
|
| 394 |
+
return pd.DataFrame(), None, f"❌ Data Type Error: {str(te)}"
|
| 395 |
except Exception as e:
|
| 396 |
+
logger.error(f"Unexpected error in diet optimization: {str(e)}")
|
| 397 |
+
error_msg = str(e)
|
| 398 |
+
if "Gurobi" in error_msg:
|
| 399 |
+
return pd.DataFrame(), None, f"❌ Solver Error: {error_msg}"
|
| 400 |
+
elif "infeasible" in error_msg.lower():
|
| 401 |
+
return pd.DataFrame(), None, f"❌ Infeasible Problem: {error_msg}"
|
| 402 |
+
elif "unbounded" in error_msg.lower():
|
| 403 |
+
return pd.DataFrame(), None, f"❌ Unbounded Problem: {error_msg}"
|
| 404 |
+
else:
|
| 405 |
+
return pd.DataFrame(), None, f"❌ Unexpected Error: {error_msg}"
|
| 406 |
|
| 407 |
solve_btn.click(
|
| 408 |
+
fn=_solve_diet_optimization,
|
| 409 |
+
inputs=[foods_input, requirements_input],
|
| 410 |
outputs=[results_table, results_plot, status_output],
|
| 411 |
)
|
| 412 |
|
|
|
|
| 518 |
fn=_solve_vrp_with_floats,
|
| 519 |
inputs=[vrp_input, cap_input, k_input],
|
| 520 |
outputs=[result_table, result_plot, status_output],
|
| 521 |
+
)
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .logger import get_logger
|
{achref/src/logger → utils}/logger.py
RENAMED
|
@@ -9,7 +9,6 @@ from typing import Any, Callable
|
|
| 9 |
load_dotenv()
|
| 10 |
|
| 11 |
|
| 12 |
-
|
| 13 |
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
| 14 |
|
| 15 |
# Mapping of string levels to logging constants
|
|
@@ -18,33 +17,37 @@ LOG_LEVEL_MAPPING = {
|
|
| 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",
|
| 33 |
-
"INFO": "\033[32m",
|
| 34 |
"WARNING": "\033[33m", # Yellow
|
| 35 |
-
"ERROR": "\033[31m",
|
| 36 |
-
"CRITICAL": "\033[41m",
|
| 37 |
}
|
| 38 |
|
| 39 |
# Reset color
|
| 40 |
reset = "\033[0m"
|
| 41 |
|
| 42 |
# Apply color to the log level name
|
| 43 |
-
record.levelname =
|
|
|
|
|
|
|
| 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 |
|
|
@@ -52,7 +55,7 @@ def get_logger(name: str) -> logging.Logger:
|
|
| 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)
|
|
@@ -69,7 +72,7 @@ def get_logger(name: str) -> logging.Logger:
|
|
| 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
|
|
@@ -80,26 +83,32 @@ def get_logger(name: str) -> logging.Logger:
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
|
|
|
|
| 9 |
load_dotenv()
|
| 10 |
|
| 11 |
|
|
|
|
| 12 |
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
| 13 |
|
| 14 |
# Mapping of string levels to logging constants
|
|
|
|
| 17 |
"INFO": logging.INFO,
|
| 18 |
"WARNING": logging.WARNING,
|
| 19 |
"ERROR": logging.ERROR,
|
| 20 |
+
"CRITICAL": logging.CRITICAL,
|
| 21 |
}
|
| 22 |
|
| 23 |
# Set the actual logging level
|
| 24 |
LOG_LEVEL = LOG_LEVEL_MAPPING.get(LOG_LEVEL, logging.INFO) # Default to INFO if invalid
|
| 25 |
|
| 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 = (
|
| 44 |
+
f"{level_color_map.get(record.levelname, '')}{record.levelname}{reset}"
|
| 45 |
+
)
|
| 46 |
|
| 47 |
# Format the log message using the parent class's format method
|
| 48 |
return super().format(record)
|
| 49 |
|
| 50 |
+
|
| 51 |
def get_logger(name: str) -> logging.Logger:
|
| 52 |
"""Creates a logger object that reads its log level from .env.
|
| 53 |
|
|
|
|
| 55 |
name (str): name given to the logger
|
| 56 |
|
| 57 |
Returns:
|
| 58 |
+
logging.Logger: logger object to be used for logging
|
| 59 |
"""
|
| 60 |
# Create logger
|
| 61 |
logger = logging.getLogger(name)
|
|
|
|
| 72 |
"{asctime} | {levelname:<8} | {message}",
|
| 73 |
style="{",
|
| 74 |
datefmt="%Y-%m-%d %H:%M:%S",
|
| 75 |
+
use_colors=True,
|
| 76 |
)
|
| 77 |
|
| 78 |
# Add formatter to the handler
|
|
|
|
| 83 |
|
| 84 |
return logger
|
| 85 |
|
| 86 |
+
|
| 87 |
# Logger decorator implementation
|
| 88 |
def log_function_call(logger: logging.Logger) -> Callable:
|
| 89 |
"""A decorator that logs the function calls and results.
|
| 90 |
|
| 91 |
Args:
|
| 92 |
logger (logging.Logger): The logger instance to use for logging.
|
| 93 |
+
|
| 94 |
Returns:
|
| 95 |
Callable: A wrapper function that logs the execution details.
|
| 96 |
"""
|
| 97 |
+
|
| 98 |
def decorator(func: Callable) -> Callable:
|
| 99 |
@functools.wraps(func)
|
| 100 |
def wrapper(*args, **kwargs) -> Any:
|
| 101 |
# Log the function call with arguments
|
| 102 |
+
logger.debug(
|
| 103 |
+
f"Calling {func.__name__} with args: {args} and kwargs: {kwargs}"
|
| 104 |
+
)
|
| 105 |
result = func(*args, **kwargs)
|
| 106 |
# Log the function result
|
| 107 |
logger.debug(f"{func.__name__} returned {result}")
|
| 108 |
return result
|
| 109 |
+
|
| 110 |
return wrapper
|
| 111 |
+
|
| 112 |
return decorator
|
| 113 |
|
| 114 |
|