OperationsResearch / ui /gradio_sections.py
Kacemath's picture
Update
b43bf80
import base64
import os
import gradio as gr
import pandas as pd
import utils.logger as logger
logger = logger.get_logger(__name__)
def project_info_tab():
with gr.Tab("\U0001f4d8 Project Info"):
gr.Markdown(
"""
# \U0001f393 GL3 - 2025 - Operational Research Project
This application demonstrates how **Linear Programming (PL)** and **Mixed-Integer Linear Programming (PLNE)** can be applied to solve real-world optimisation problems using **Gurobi**.
"""
)
gr.HTML(
"""
<style>
.member-card {
display: flex;
flex-direction: column;
align-items: center;
width: 160px;
text-align: center;
margin: 10px;
}
.member-card img {
width: 120px;
height: 140px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ccc;
}
.member-name {
margin-top: 8px;
font-weight: bold;
}
</style>
<div style="display: flex; flex-wrap: wrap; justify-content: center;">
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/kacem_mathlouthi.jpg" alt="Kacem Mathlouthi">
<div class="member-name">Kacem Mathlouthi</div>
</div>
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/mohamed_amine_haouas.jpg" alt="Mohamed Amine Houas">
<div class="member-name">Mohamed Amine Houas</div>
</div>
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/oussema_kraiem.jpg" alt="Oussema Kraiem">
<div class="member-name">Oussema Kraiem</div>
</div>
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/mohamed_yassine_taieb.jpg" alt="Yassine Taieb">
<div class="member-name">Yassine Taieb</div>
</div>
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/youssef_sghairi.jpg" alt="Youssef Sghairi">
<div class="member-name">Youssef Sghairi</div>
</div>
<div class="member-card">
<img src="https://raw.githubusercontent.com/KacemMathlouthi/OperationsResearch/main/assets/images/youssef_aridhi.jpg" alt="Youssef Aaridhi">
<div class="member-name">Youssef Aaridhi</div>
</div>
</div>
"""
)
gr.Markdown(
"""
---
# \U0001f9fe Compte Rendu
"""
)
pdf_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "assets", "compte_rendu.pdf"
)
with open(pdf_path, "rb") as pdf_file:
encoded_pdf = base64.b64encode(pdf_file.read()).decode("utf-8")
# Display using data URI
gr.HTML(
f"""
<embed src="data:application/pdf;base64,{encoded_pdf}" type="application/pdf" width="100%" height="1200px">
"""
)
def diet_problem_tab(solve_diet_problem, diet_description):
with gr.Tab("🍎 Diet Problem (PL)"):
gr.Markdown(diet_description)
# Add mathematical model description
gr.Markdown(
r"""
### 🧮 Mathematical Formulation
**Parameters**
| Symbol | Description |
|-------------|------------------------------------------------|
| $$I$$ | Set of available foods |
| $$J$$ | Set of nutrients |
| $$c_i$$ | Cost per unit of food i |
| $$n_{ij}$$ | Amount of nutrient j in one unit of food i |
| $$R_j$$ | Minimum requirement for nutrient j |
**Decision Variables**
| Symbol | Description |
|-------------|-----------------------------------------|
| $$x_i$$ | Units of food i to consume |
**Objective Function:**
$$
\text{Minimize} \quad Z = \sum_{i \in I} c_i \cdot x_i
$$
**Constraints:**
1. **Nutritional requirements:**
$$\sum_{i \in I} n_{ij} \cdot x_i \geq R_j \quad \forall j \in J$$
2. **Non-negativity:**
$$x_i \geq 0 \quad \forall i \in I$$
"""
)
with gr.Row():
with gr.Column():
gr.Markdown("### 🍎 Foods Data")
gr.Markdown(
"Add foods with their costs and nutritional content per unit:"
)
# Default foods data
default_foods = pd.DataFrame(
[
{"Food": "Food A", "Cost": 3.0, "Protein": 2.0, "Fat": 1.0},
{"Food": "Food B", "Cost": 2.0, "Protein": 1.0, "Fat": 2.0},
]
)
foods_input = gr.Dataframe(
value=default_foods,
headers=["Food", "Cost", "Protein", "Fat"],
datatype=["str", "number", "number", "number"],
col_count=(4, "dynamic"),
row_count=(2, "dynamic"),
label="Foods and Nutritional Content",
interactive=True,
)
with gr.Column():
gr.Markdown("### 🥗 Nutritional Requirements")
gr.Markdown("Specify minimum daily requirements for each nutrient:")
# Default requirements data
default_requirements = pd.DataFrame(
[
{"Nutrient": "Protein", "Minimum": 8.0},
{"Nutrient": "Fat", "Minimum": 6.0},
]
)
requirements_input = gr.Dataframe(
value=default_requirements,
headers=["Nutrient", "Minimum"],
datatype=["str", "number"],
col_count=(2, "fixed"),
row_count=(2, "dynamic"),
label="Nutritional Requirements",
interactive=True,
)
solve_btn = gr.Button("Solve Diet Problem", variant="primary")
status_output = gr.Textbox(label="Status", interactive=False)
results_table = gr.Dataframe(label="Optimization Results")
results_plot = gr.Plot(label="Results Visualization")
def _solve_diet_optimization(foods_df, requirements_df):
try:
logger.info("Starting diet optimization from UI")
# Input existence validation
if foods_df is None or len(foods_df) == 0:
return (
pd.DataFrame(),
None,
"❌ Error: Please provide foods data. Add at least one food item with its cost and nutritional content.",
)
if requirements_df is None or len(requirements_df) == 0:
return (
pd.DataFrame(),
None,
"❌ Error: Please provide requirements data. Add at least one nutritional requirement.",
)
# Convert to DataFrame if needed
if not isinstance(foods_df, pd.DataFrame):
try:
foods_df = pd.DataFrame(foods_df)
except Exception as e:
return (
pd.DataFrame(),
None,
f"❌ Error: Cannot convert foods data to DataFrame: {str(e)}",
)
if not isinstance(requirements_df, pd.DataFrame):
try:
requirements_df = pd.DataFrame(requirements_df)
except Exception as e:
return (
pd.DataFrame(),
None,
f"❌ Error: Cannot convert requirements data to DataFrame: {str(e)}",
)
# Remove completely empty rows
foods_df = foods_df.dropna(how="all")
requirements_df = requirements_df.dropna(how="all")
# Check if data still exists after cleaning
if foods_df.empty:
return (
pd.DataFrame(),
None,
"❌ Error: No valid foods data found. Please ensure at least one row has valid data.",
)
if requirements_df.empty:
return (
pd.DataFrame(),
None,
"❌ Error: No valid requirements data found. Please ensure at least one row has valid data.",
)
# Validate required columns
if "Food" not in foods_df.columns or "Cost" not in foods_df.columns:
return (
pd.DataFrame(),
None,
"❌ Error: Foods data must have 'Food' and 'Cost' columns. Please check your column headers.",
)
if (
"Nutrient" not in requirements_df.columns
or "Minimum" not in requirements_df.columns
):
return (
pd.DataFrame(),
None,
"❌ Error: Requirements data must have 'Nutrient' and 'Minimum' columns. Please check your column headers.",
)
# Check for missing values in critical columns
missing_food_names = foods_df["Food"].isna().sum()
if missing_food_names > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {missing_food_names} food(s) have missing names. All foods must have valid names.",
)
missing_costs = foods_df["Cost"].isna().sum()
if missing_costs > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {missing_costs} food(s) have missing costs. All foods must have valid costs.",
)
missing_nutrients = requirements_df["Nutrient"].isna().sum()
if missing_nutrients > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {missing_nutrients} requirement(s) have missing nutrient names. All requirements must have valid nutrient names.",
)
missing_minimums = requirements_df["Minimum"].isna().sum()
if missing_minimums > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {missing_minimums} requirement(s) have missing minimum values. All requirements must have valid minimum values.",
)
# Check for negative values
try:
numeric_cols = foods_df.select_dtypes(include=[float, int]).columns
numeric_cols = [
col for col in numeric_cols if col != "Food"
] # Exclude non-numeric columns
if (
len(numeric_cols) > 0
and (foods_df[numeric_cols] < 0).any().any()
):
negative_foods = []
for col in numeric_cols:
if (foods_df[col] < 0).any():
bad_foods = foods_df[foods_df[col] < 0]["Food"].tolist()
negative_foods.extend(
[f"{food} ({col})" for food in bad_foods]
)
return (
pd.DataFrame(),
None,
f"❌ Error: Negative values found: {', '.join(negative_foods[:5])}{'...' if len(negative_foods) > 5 else ''}. All numeric values must be non-negative.",
)
except Exception as numeric_error:
return (
pd.DataFrame(),
None,
f"❌ Error: Problem checking numeric values: {str(numeric_error)}. Please ensure all numeric columns contain valid numbers.",
)
try:
if (requirements_df["Minimum"] <= 0).any():
bad_requirements = requirements_df[
requirements_df["Minimum"] <= 0
]["Nutrient"].tolist()
return (
pd.DataFrame(),
None,
f"❌ Error: Non-positive requirements found for: {', '.join(bad_requirements)}. All requirements must be positive values.",
)
except Exception as req_error:
return (
pd.DataFrame(),
None,
f"❌ Error: Problem checking requirements: {str(req_error)}. Please ensure all requirement values are positive numbers.",
)
# Check for empty strings in food names
empty_food_names = foods_df[
foods_df["Food"].astype(str).str.strip() == ""
]["Food"].count()
if empty_food_names > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {empty_food_names} food(s) have empty names. All foods must have non-empty names.",
)
# Check for empty strings in nutrient names
empty_nutrient_names = requirements_df[
requirements_df["Nutrient"].astype(str).str.strip() == ""
]["Nutrient"].count()
if empty_nutrient_names > 0:
return (
pd.DataFrame(),
None,
f"❌ Error: {empty_nutrient_names} nutrient(s) have empty names. All nutrients must have non-empty names.",
)
logger.info("UI validation passed, calling solver...")
result_df, fig = solve_diet_problem(foods_df, requirements_df)
# Validate solver results
if result_df is None or result_df.empty:
return (
pd.DataFrame(),
None,
"❌ Error: Solver returned empty results. This is unexpected.",
)
# Check if solution makes sense
total_cost = (
result_df["Cost"].sum() if "Cost" in result_df.columns else 0
)
if total_cost < 0:
logger.warning(f"Negative total cost detected: {total_cost}")
logger.info(
f"Optimization completed successfully with total cost: {total_cost:.2f}"
)
return (
result_df,
fig,
f"✅ Solved Successfully! Optimal diet plan found with total cost: ${total_cost:.2f}",
)
except ValueError as ve:
logger.error(f"Validation error: {str(ve)}")
return pd.DataFrame(), None, f"❌ Validation Error: {str(ve)}"
except TypeError as te:
logger.error(f"Type error: {str(te)}")
return pd.DataFrame(), None, f"❌ Data Type Error: {str(te)}"
except Exception as e:
logger.error(f"Unexpected error in diet optimization: {str(e)}")
error_msg = str(e)
if "Gurobi" in error_msg:
return pd.DataFrame(), None, f"❌ Solver Error: {error_msg}"
elif "infeasible" in error_msg.lower():
return pd.DataFrame(), None, f"❌ Infeasible Problem: {error_msg}"
elif "unbounded" in error_msg.lower():
return pd.DataFrame(), None, f"❌ Unbounded Problem: {error_msg}"
else:
return pd.DataFrame(), None, f"❌ Unexpected Error: {error_msg}"
solve_btn.click(
fn=_solve_diet_optimization,
inputs=[foods_input, requirements_input],
outputs=[results_table, results_plot, status_output],
)
def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
with gr.Tab("\U0001f69a Vehicle Routing (PLNE)"):
gr.Markdown(plne_description)
gr.HTML(
'<img src="https://pyvrp.readthedocs.io/en/latest/_images/introduction-to-vrp.svg" '
'alt="VRP Problem Illustration" width="600px" />'
)
gr.Markdown(
r"""
### \U0001F9EE Mathematical Formulation (Capacitated VRP)
| Symbol | Description |
|--------------------------------|---------------------------------------------------------------|
| $$i,j \in N=\{0,\dots,n\}$$ | Nodes (0 = depot, 1..n = customers) |
| $$K$$ | Number of vehicles |
| $$c_{ij}$$ | Travel cost (distance) from node `i` to node `j` |
| $$d_i$$ | Demand at customer `i` |
| $$Q$$ | Vehicle capacity |
| $$x_{ij}\in\{0,1\}$$ | 1 if a vehicle travels directly from `i` to `j` |
| $$u_i\ge0$$ | Load on the vehicle immediately after visiting node `i` |
**Objective**
$$
\min \sum_{i\in N}\sum_{\substack{j\in N \\ j\neq i}} c_{ij}\,x_{ij}
$$
Minimize the **total travel cost** of all vehicles.
---
**Subject to**
1. **Degree constraints**
$$
\sum_{j\neq i} x_{ij} = 1
\quad \forall\, i\neq0
$$
$$
\sum_{i\neq j} x_{ij} = 1
\quad \forall\, j\neq0
$$
2. **Depot flow**
$$
\sum_{j>0} x_{0j} = K
$$
$$
\sum_{i>0} x_{i0} = K
$$
3. **MTZ subtour-elimination & capacity**
$$
u_i - u_j + Q\,x_{ij} \le Q - d_j
\quad \forall\,i\neq j,\; i,j>0
$$
$$
u_0 = 0
$$
$$
0 \le u_i \le Q
$$
"""
)
vrp_input = gr.Dataframe(
headers=["Node", "X", "Y", "Demand"],
value=mock_plne_df,
label="Input Vehicle Routing Data",
)
with gr.Row():
cap_input = gr.Number(value=40, label="Vehicle capacity (Q)")
k_input = gr.Number(value=2, label="Number of vehicles (K)")
solve_btn = gr.Button("Solve VRP")
status_output = gr.Textbox(label="Status", interactive=False)
result_table = gr.Dataframe(label="Routes Summary")
result_plot = gr.Plot(label="Route Map & Summary")
def _solve_vrp_with_floats(df, Q, K):
try:
df["X"] = df["X"].astype(float)
df["Y"] = df["Y"].astype(float)
df["Demand"] = df["Demand"].astype(float)
custs = df[df["Node"] != 0]
too_big = custs[custs["Demand"] > Q]
if not too_big.empty:
bad = int(too_big["Node"].iloc[0])
raise ValueError(
f"Client {bad} demand ({too_big['Demand'].iloc[0]}) exceeds capacity Q={Q}"
)
total = custs["Demand"].sum()
if total > Q * K:
raise ValueError(
f"Total demand ({total}) exceeds fleet capacity Q*K={Q*K}"
)
routes_df, fig = solve_plne(df, vehicle_capacity=Q, num_vehicles=K)
return routes_df, fig, "Solved Successfully"
except Exception as e:
return pd.DataFrame(), None, f"❌ Error: {str(e)}"
solve_btn.click(
fn=_solve_vrp_with_floats,
inputs=[vrp_input, cap_input, k_input],
outputs=[result_table, result_plot, status_output],
)