Kacemath commited on
Commit
747e054
·
1 Parent(s): 8c76379

New PL Model

Browse files
Files changed (3) hide show
  1. app.py +101 -27
  2. models/gurobi_models.py +490 -158
  3. ui/gradio_sections.py +130 -93
app.py CHANGED
@@ -1,43 +1,110 @@
 
 
1
  import gradio as gr
2
  import pandas as pd
3
- import os
4
- from ui.gradio_sections import (
5
- project_info_tab,
6
- production_planning_tab,
7
- vehicle_routing_tab,
8
- )
9
- from models.gurobi_models import solve_pl, solve_plne
10
 
11
  # Mock Data
12
- pl_df = pd.DataFrame(
13
- {
14
- "Product": ["A", "B", "C"],
15
- "Profit/Unit": [15, 20, 15],
16
- "Resource Usage": [3, 5, 2],
17
- }
 
 
 
18
  )
19
 
20
- plne_df = pd.DataFrame([
21
- {"Node": 0, "X": 50, "Y": 50, "Demand": 0},
22
- {"Node": 1, "X": 20, "Y": 20, "Demand": 10},
23
- {"Node": 2, "X": 80, "Y": 20, "Demand": 15},
24
- {"Node": 3, "X": 20, "Y": 80, "Demand": 10},
25
- {"Node": 4, "X": 80, "Y": 80, "Demand": 10},
26
- {"Node": 5, "X": 50, "Y": 10, "Demand": 20},
27
- ])
28
 
 
 
 
 
 
 
 
29
 
30
- # Descriptions
31
- pl_description = """
32
- ### 🏭 Production Planning (PL)
33
- Select the quantity of each product to produce to **maximize profit**, under limited resource constraints.
34
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
 
36
  plne_description = """
37
  ### 🚚 Capacitated Vehicle Routing Problem
38
  Provide node coordinates and demands, plus vehicle capacity and number of vehicles.
39
  """
40
 
 
 
 
 
 
 
 
 
 
41
  # Read and encode the PDF - go up one directory to find assets at project root
42
  favicon_path = os.path.join(os.path.dirname(__file__), "assets", "favicon.ico")
43
 
@@ -56,7 +123,14 @@ with gr.Blocks(title="Operations Research App") as ro_app:
56
  )
57
  with gr.Tabs():
58
  project_info_tab()
59
- production_planning_tab(pl_df, solve_pl, pl_description)
 
 
 
 
 
 
 
60
  vehicle_routing_tab(plne_df, solve_plne, plne_description)
61
 
62
  if __name__ == "__main__":
 
1
+ import os
2
+
3
  import gradio as gr
4
  import pandas as pd
5
+
6
+ from models.gurobi_models import solve_plne, solve_refinery_optimization
7
+ from ui.gradio_sections import oil_refinery_tab, project_info_tab, vehicle_routing_tab
 
 
 
 
8
 
9
  # Mock Data
10
+ plne_df = pd.DataFrame(
11
+ [
12
+ {"Node": 0, "X": 50, "Y": 50, "Demand": 0},
13
+ {"Node": 1, "X": 20, "Y": 20, "Demand": 10},
14
+ {"Node": 2, "X": 80, "Y": 20, "Demand": 15},
15
+ {"Node": 3, "X": 20, "Y": 80, "Demand": 10},
16
+ {"Node": 4, "X": 80, "Y": 80, "Demand": 10},
17
+ {"Node": 5, "X": 50, "Y": 10, "Demand": 20},
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
+ refinery_description = """
100
+ ### ⚙️ Oil Refinery Optimization Problem
101
+
102
+ **Scenario**
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
109
  favicon_path = os.path.join(os.path.dirname(__file__), "assets", "favicon.ico")
110
 
 
123
  )
124
  with gr.Tabs():
125
  project_info_tab()
126
+ oil_refinery_tab(
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__":
models/gurobi_models.py CHANGED
@@ -1,91 +1,349 @@
1
- from gurobipy import Model, GRB
2
- import pandas as pd
3
  import math
4
- from gurobipy import quicksum
5
  import matplotlib.pyplot as plt
 
 
 
6
  import achref.src.logger as logger
7
 
8
  logger = logger.get_logger(__name__)
9
 
10
- def solve_pl(data, total_resource=100):
11
- model = Model("ProductionPlanning")
12
- logger.info("Starting Gurobi model for production planning")
13
- logger.info("Data: %s", data)
14
- # Turn off solver output
15
- model.setParam("OutputFlag", 0)
16
-
17
- # Extract product info
18
- products = data["Product"].tolist()
19
- profits = data["Profit/Unit"].tolist()
20
- usage = data["Resource Usage"].tolist()
21
-
22
- # Create decision variables
23
- x = {
24
- prod: model.addVar(name=f"x_{prod}", lb=0, vtype=GRB.CONTINUOUS)
25
- for prod in products
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
- # Objective: Maximise total profit
29
- model.setObjective(
30
- sum(profits[i] * x[products[i]] for i in range(len(products))),
31
- GRB.MAXIMIZE,
32
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- # Constraint: total resource usage <= available
35
- model.addConstr(
36
- sum(usage[i] * x[products[i]] for i in range(len(products))) <= total_resource,
37
- name="ResourceConstraint",
38
  )
 
 
 
 
 
 
 
 
39
 
40
- # Solve
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  model.optimize()
42
 
 
 
 
 
43
  # Extract results
44
- result_df = pd.DataFrame({
45
- "Product": products,
46
- "Quantity Produced": [x[prod].X for prod in products],
47
- "Profit": [x[prod].X * profits[i] for i, prod in enumerate(products)]
48
- })
49
-
50
- result_df["Profit per Resource"] = result_df["Profit"] / data["Resource Usage"]
51
-
52
- # Prepare a single figure with 5 subplots
53
- fig, axs = plt.subplots(3, 2, figsize=(14, 12))
54
- fig.suptitle("Production Planning Visualisations", fontsize=16, y=1.02)
55
-
56
- # Plot 1: Quantity Produced
57
- axs[0, 0].bar(result_df["Product"], result_df["Quantity Produced"])
58
- axs[0, 0].set_title("Quantity Produced per Product")
59
-
60
- # Plot 2: Stacked Bar (Quantity + Profit)
61
- axs[0, 1].bar(result_df["Product"], result_df["Quantity Produced"], label="Quantity")
62
- axs[0, 1].bar(result_df["Product"], result_df["Profit"],
63
- bottom=result_df["Quantity Produced"], label="Profit", alpha=0.6)
64
- axs[0, 1].set_title("Stacked Bar: Quantity + Profit")
65
- axs[0, 1].legend()
66
-
67
- # Plot 3: Pie Chart of Profit
68
- axs[1, 0].pie(result_df["Profit"], labels=result_df["Product"], autopct='%1.1f%%')
69
- axs[1, 0].set_title("Profit Share per Product")
70
-
71
- # Plot 4: Cumulative Profit
72
- df_sorted = result_df.sort_values(by="Profit", ascending=False).reset_index(drop=True)
73
- df_sorted["Cumulative Profit"] = df_sorted["Profit"].cumsum()
74
- axs[1, 1].plot(df_sorted["Product"], df_sorted["Cumulative Profit"], marker="o")
75
- axs[1, 1].set_title("Cumulative Profit by Product")
76
- axs[1, 1].set_ylabel("Cumulative Profit")
77
-
78
- # Plot 5: Profit per Resource
79
- axs[2, 0].bar(result_df["Product"], result_df["Profit per Resource"])
80
- axs[2, 0].set_title("Profit per Unit of Resource Used")
81
- axs[2, 0].set_ylabel("Efficiency")
82
-
83
- # Remove unused subplot (bottom right)
84
- axs[2, 1].axis('off')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  fig.tight_layout()
87
 
88
- return result_df, fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
 
91
  def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
@@ -97,128 +355,202 @@ def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
97
  - routes_df: DataFrame with columns ["Route","Sequence","Load","Distance"]
98
  - fig: matplotlib.figure.Figure with the route‐map and summary bars
99
  """
100
- # 1. Parse inputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  coords = {int(r.Node): (r.X, r.Y) for _, r in data.iterrows()}
102
  demand = {int(r.Node): r.Demand for _, r in data.iterrows()}
103
  nodes = list(coords.keys())
104
  depot = 0
 
 
105
  customers = [i for i in nodes if i != depot]
106
- Q = vehicle_capacity
107
- K = num_vehicles
108
-
109
- # 2. Precompute distances
110
- cost = {
111
- (i, j): math.hypot(coords[i][0] - coords[j][0],
112
- coords[i][1] - coords[j][1])
113
- for i in nodes for j in nodes if i != j
114
- }
115
-
116
- # 3. Build model
117
- m = Model("CVRP")
118
- m.setParam("OutputFlag", 0)
119
-
120
- # Decision vars
121
- x = m.addVars(cost.keys(), vtype=GRB.BINARY, name="x")
122
- u = m.addVars(nodes, lb=0, ub=Q, vtype=GRB.CONTINUOUS, name="u")
123
 
124
- # Objective
125
- m.setObjective(quicksum(cost[i, j] * x[i, j] for i, j in cost), GRB.MINIMIZE)
126
 
127
- # Degree constraints
128
- m.addConstrs(
129
- (quicksum(x[i, j] for j in nodes if j != i) == 1 for i in customers),
130
- name="leave"
131
- )
132
- m.addConstrs(
133
- (quicksum(x[i, j] for i in nodes if i != j) == 1 for j in customers),
134
- name="enter"
135
- )
136
- # Depot flow
137
- m.addConstr(quicksum(x[depot, j] for j in customers) == K, "dep_out")
138
- m.addConstr(quicksum(x[i, depot] for i in customers) == K, "dep_in")
139
-
140
- # MTZ subtour‐elimination & capacity
141
- m.addConstrs(
142
- (u[i] - u[j] + Q * x[i, j] <= Q - demand[j]
143
- for i in customers for j in customers if i != j),
144
- name="mtz"
145
- )
146
- m.addConstr(u[depot] == 0, "depot_load")
147
-
148
- # Solve
149
- m.optimize()
150
 
151
- # 4. Extract x‐values
152
- sol = m.getAttr('x', x)
153
 
154
- # 4a. Find exactly which customers each vehicle leaves the depot to serve
155
- starts = [ j for (i,j),val in sol.items()
156
- if i == depot and val > 0.5 ]
157
- # sanity check
158
  if len(starts) != K:
159
  raise ValueError(f"Expected {K} routes out of depot, got {len(starts)}")
160
-
161
- # 4b. Build a succ map for ALL non‐depot nodes (each has exactly 1 outgoing)
162
- succ = { i: j for (i,j),val in sol.items()
163
- if i != depot and val > 0.5 }
164
-
165
- # 4c. Now reconstruct each of the K routes
166
  routes = []
167
  for start in starts:
168
  route = [depot, start]
169
  cur = start
170
  while cur != depot:
171
- nxt = succ[cur]
 
 
172
  route.append(nxt)
173
  cur = nxt
174
  routes.append(route)
 
 
175
 
176
- # 5. Build result DataFrame
177
  rows = []
178
  for ridx, route in enumerate(routes, start=1):
179
  load = sum(demand[n] for n in route if n != depot)
180
  dist = sum(
181
- math.hypot(coords[route[i]][0] - coords[route[i+1]][0],
182
- coords[route[i]][1] - coords[route[i+1]][1])
183
- for i in range(len(route)-1)
184
- )
185
- rows.append({
186
- "Route": ridx,
187
- "Sequence": "→".join(str(n) for n in route),
188
- "Load": load,
189
- "Distance": dist
190
- })
191
- routes_df = pd.DataFrame(rows)
192
-
193
- # 6. Create plots
194
- fig, axs = plt.subplots(1, 2, figsize=(14,6))
195
-
196
- # Plot A: Map of routes
197
- ax = axs[0]
198
- ax.scatter(*zip(*[coords[i] for i in customers]),
199
- c='blue', label='Customers')
200
- ax.scatter(*coords[depot], c='red', s=100, label='Depot')
201
- colors = plt.cm.get_cmap('tab10', K)
202
 
 
 
 
 
 
 
203
  for ridx, route in enumerate(routes):
204
  pts = [coords[n] for n in route]
205
  xs, ys = zip(*pts)
206
- ax.plot(xs, ys, '-o', color=colors(ridx), label=f'Route {ridx+1}')
207
  ax.set_title("Vehicle Routes")
208
- ax.legend(loc='upper right')
209
-
210
- # Plot B: Route loads & distances
211
  ax2 = axs[1]
212
  bar_width = 0.35
213
  idx = range(len(routes_df))
214
  ax2.bar(idx, routes_df["Load"], bar_width, label="Load")
215
- ax2.bar([i+bar_width for i in idx], routes_df["Distance"],
216
- bar_width, label="Distance")
217
- ax2.set_xticks([i+bar_width/2 for i in idx])
 
218
  ax2.set_xticklabels([f"R{r}" for r in routes_df["Route"]])
219
  ax2.set_ylabel("Units / Distance")
220
  ax2.set_title("Load vs Distance per Route")
221
  ax2.legend()
222
-
223
  fig.tight_layout()
224
- return routes_df, fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import math
2
+
3
  import matplotlib.pyplot as plt
4
+ import pandas as pd
5
+ from gurobipy import GRB, Model, quicksum
6
+
7
  import achref.src.logger as logger
8
 
9
  logger = logger.get_logger(__name__)
10
 
11
+
12
+ def solve_refinery_optimization(
13
+ crude_data, product_data, crude_product_yields, product_quality_reqs
14
+ ):
15
+ """
16
+ Solves the Oil Refinery Optimization Problem.
17
+
18
+ Args:
19
+ crude_data (pd.DataFrame): DataFrame with columns ["Crude", "Cost", "Availability"]
20
+ product_data (pd.DataFrame): DataFrame with columns ["Product", "Price", "Demand"]
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
+ if not all(
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
+ logger.info("Starting Gurobi model for Oil Refinery Optimization")
50
+
51
+ # Create optimization model
52
+ model = Model("OilRefineryOptimization")
53
+ model.setParam("OutputFlag", 0)
54
+
55
+ # Get unique crudes and products
56
+ crudes = crude_data["Crude"].unique().tolist()
57
+ products = product_data["Product"].unique().tolist()
58
+
59
+ # Extract data
60
+ costs = {row["Crude"]: row["Cost"] for _, row in crude_data.iterrows()}
61
+ availability = {
62
+ row["Crude"]: row["Availability"] for _, row in crude_data.iterrows()
63
+ }
64
+ prices = {row["Product"]: row["Price"] for _, row in product_data.iterrows()}
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
+ # Create a dictionary for yields and qualities
71
+ yields = {}
72
+ qualities = {}
73
+ for _, row in crude_product_yields.iterrows():
74
+ crude, product = row["Crude"], row["Product"]
75
+ yields[(crude, product)] = row["Yield"]
76
+ qualities[(crude, product)] = row["Quality"]
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
+ # Objective: Maximize profit (revenue - cost)
97
+ revenue = quicksum(
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
+ # Constraints:
104
+
105
+ # 1. Crude availability constraints
106
+ for crude in crudes:
107
+ model.addConstr(x[crude] <= availability[crude], name=f"avail_{crude}")
108
 
109
+ # 2. Demand satisfaction constraints
110
+ for product in products:
111
+ model.addConstr(
112
+ total_production[product] >= demands[product], name=f"demand_{product}"
113
+ )
114
+
115
+ # 3. Quality constraints
116
+ for product in products:
117
+ if product in min_qualities:
118
+ # Linearized quality constraint: sum(q_ij * y_ij * x_i) >= Q_j^min * sum(y_ij * x_i)
119
+ quality_numerator = quicksum(
120
+ qualities.get((crude, product), 0)
121
+ * yields.get((crude, product), 0)
122
+ * x[crude]
123
+ for crude in crudes
124
+ if (crude, product) in yields
125
+ )
126
+ model.addConstr(
127
+ quality_numerator >= min_qualities[product] * total_production[product],
128
+ name=f"quality_{product}",
129
+ )
130
+
131
+ # Solve the model
132
  model.optimize()
133
 
134
+ # Check if optimal solution was found
135
+ if model.status != GRB.OPTIMAL:
136
+ raise ValueError("Failed to find an optimal solution")
137
+
138
  # Extract results
139
+ crude_results = pd.DataFrame(
140
+ {
141
+ "Crude": crudes,
142
+ "Amount Used": [x[crude].X for crude in crudes],
143
+ "Cost": [x[crude].X * costs[crude] for crude in crudes],
144
+ }
145
+ )
146
+
147
+ # Calculate production results
148
+ prod_results = []
149
+ for product in products:
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
+ prod_results.append(
166
+ {
167
+ "Product": product,
168
+ "Amount Produced": prod_amount,
169
+ "Revenue": revenue,
170
+ "Average Quality": avg_quality,
171
+ "Min Quality Required": min_qualities.get(product, "N/A"),
172
+ }
173
+ )
174
+
175
+ product_results_df = pd.DataFrame(prod_results)
176
+
177
+ # Create visualizations
178
+ fig, axs = plt.subplots(2, 2, figsize=(14, 10))
179
+ fig.suptitle("Oil Refinery Optimization Results", fontsize=16, y=1.02)
180
+
181
+ # Plot 1: Crude Oil Usage
182
+ axs[0, 0].bar(crude_results["Crude"], crude_results["Amount Used"])
183
+ axs[0, 0].set_title("Crude Oil Usage")
184
+ axs[0, 0].set_ylabel("Amount (Liters)")
185
+ plt.setp(axs[0, 0].get_xticklabels(), rotation=45, ha="right")
186
+
187
+ # Plot 2: Product Production
188
+ axs[0, 1].bar(product_results_df["Product"], product_results_df["Amount Produced"])
189
+ axs[0, 1].set_title("Product Production")
190
+ axs[0, 1].set_ylabel("Amount (Liters)")
191
+ plt.setp(axs[0, 1].get_xticklabels(), rotation=45, ha="right")
192
+
193
+ # Plot 3: Profit/Cost Breakdown
194
+ revenue_total = product_results_df["Revenue"].sum()
195
+ cost_total = crude_results["Cost"].sum()
196
+ profit = revenue_total - cost_total
197
+ axs[1, 0].bar(
198
+ ["Revenue", "Cost", "Profit"],
199
+ [revenue_total, cost_total, profit],
200
+ color=["green", "red", "blue"],
201
+ )
202
+ axs[1, 0].set_title("Financial Summary")
203
+ axs[1, 0].set_ylabel("Amount ($)")
204
+
205
+ # Plot 4: Quality vs Requirements
206
+ products = product_results_df["Product"]
207
+ achieved_quality = product_results_df["Average Quality"]
208
+ required_quality = pd.to_numeric(
209
+ product_results_df["Min Quality Required"], errors="coerce"
210
+ )
211
+
212
+ x = range(len(products))
213
+ width = 0.35
214
+ axs[1, 1].bar(
215
+ [i - width / 2 for i in x], achieved_quality, width, label="Achieved Quality"
216
+ )
217
+ axs[1, 1].bar(
218
+ [i + width / 2 for i in x], required_quality, width, label="Required Quality"
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
  fig.tight_layout()
228
 
229
+ # Combine results for return
230
+ combined_results = {
231
+ "Crude Usage": crude_results,
232
+ "Product Production": product_results_df,
233
+ "Total Profit": profit,
234
+ }
235
+
236
+ # Convert to a results dataframe with all key information
237
+ result_summary = pd.DataFrame(
238
+ {
239
+ "Category": ["Total Revenue", "Total Cost", "Total Profit"],
240
+ "Value": [revenue_total, cost_total, profit],
241
+ }
242
+ )
243
+
244
+ return product_results_df, fig
245
+
246
+
247
+ def _validate_plne_input(data, vehicle_capacity, num_vehicles):
248
+ required_cols = {"Node", "X", "Y", "Demand"}
249
+ if not isinstance(data, pd.DataFrame):
250
+ raise TypeError("Input 'data' must be a pandas DataFrame.")
251
+ if not required_cols.issubset(data.columns):
252
+ missing = required_cols - set(data.columns)
253
+ raise ValueError(f"Missing required columns in 'data': {missing}")
254
+ if not isinstance(vehicle_capacity, (int, float)) or vehicle_capacity <= 0:
255
+ raise ValueError("'vehicle_capacity' must be a positive number.")
256
+ if not isinstance(num_vehicles, int) or num_vehicles <= 0:
257
+ raise ValueError("'num_vehicles' must be a positive integer.")
258
+
259
+
260
+ def _prepare_plne_data(data):
261
+ coords = {int(r.Node): (r.X, r.Y) for _, r in data.iterrows()}
262
+ demand = {int(r.Node): r.Demand for _, r in data.iterrows()}
263
+ nodes = list(coords.keys())
264
+ depot = 0
265
+ if depot not in nodes:
266
+ raise ValueError("Depot node (0) is missing from input.")
267
+ customers = [i for i in nodes if i != depot]
268
+ return coords, demand, nodes, depot, customers
269
+
270
+
271
+ def _compute_distance_matrix(coords, nodes):
272
+ return {
273
+ (i, j): math.hypot(coords[i][0] - coords[j][0], coords[i][1] - coords[j][1])
274
+ for i in nodes
275
+ for j in nodes
276
+ if i != j
277
+ }
278
+
279
+
280
+ def _reconstruct_routes(sol, depot, K):
281
+ starts = [j for (i, j), val in sol.items() if i == depot and val > 0.5]
282
+ if len(starts) != K:
283
+ raise ValueError(f"Expected {K} routes out of depot, got {len(starts)}")
284
+ succ = {i: j for (i, j), val in sol.items() if i != depot and val > 0.5}
285
+ routes = []
286
+ for start in starts:
287
+ route = [depot, start]
288
+ cur = start
289
+ while cur != depot:
290
+ nxt = succ.get(cur)
291
+ if nxt is None:
292
+ raise ValueError(f"Incomplete route starting at node {start}.")
293
+ route.append(nxt)
294
+ cur = nxt
295
+ routes.append(route)
296
+ return routes
297
+
298
+
299
+ def _build_routes_df(routes, demand, coords, depot):
300
+ rows = []
301
+ for ridx, route in enumerate(routes, start=1):
302
+ load = sum(demand[n] for n in route if n != depot)
303
+ dist = sum(
304
+ math.hypot(
305
+ coords[route[i]][0] - coords[route[i + 1]][0],
306
+ coords[route[i]][1] - coords[route[i + 1]][1],
307
+ )
308
+ for i in range(len(route) - 1)
309
+ )
310
+ rows.append(
311
+ {
312
+ "Route": ridx,
313
+ "Sequence": "→".join(str(n) for n in route),
314
+ "Load": load,
315
+ "Distance": dist,
316
+ }
317
+ )
318
+ return pd.DataFrame(rows)
319
+
320
+
321
+ def _plot_plne(routes, routes_df, coords, customers, depot, K):
322
+ fig, axs = plt.subplots(1, 2, figsize=(14, 6))
323
+ ax = axs[0]
324
+ ax.scatter(*zip(*[coords[i] for i in customers]), c="blue", label="Customers")
325
+ ax.scatter(*coords[depot], c="red", s=100, label="Depot")
326
+ colors = plt.cm.get_cmap("tab10", K)
327
+ for ridx, route in enumerate(routes):
328
+ pts = [coords[n] for n in route]
329
+ xs, ys = zip(*pts)
330
+ ax.plot(xs, ys, "-o", color=colors(ridx), label=f"Route {ridx+1}")
331
+ ax.set_title("Vehicle Routes")
332
+ ax.legend(loc="upper right")
333
+ ax2 = axs[1]
334
+ bar_width = 0.35
335
+ idx = range(len(routes_df))
336
+ ax2.bar(idx, routes_df["Load"], bar_width, label="Load")
337
+ ax2.bar(
338
+ [i + bar_width for i in idx], routes_df["Distance"], bar_width, label="Distance"
339
+ )
340
+ ax2.set_xticks([i + bar_width / 2 for i in idx])
341
+ ax2.set_xticklabels([f"R{r}" for r in routes_df["Route"]])
342
+ ax2.set_ylabel("Units / Distance")
343
+ ax2.set_title("Load vs Distance per Route")
344
+ ax2.legend()
345
+ fig.tight_layout()
346
+ return fig
347
 
348
 
349
  def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
 
355
  - routes_df: DataFrame with columns ["Route","Sequence","Load","Distance"]
356
  - fig: matplotlib.figure.Figure with the route‐map and summary bars
357
  """
358
+ try:
359
+ _validate_plne_input(data, vehicle_capacity, num_vehicles)
360
+ coords, demand, nodes, depot, customers = _prepare_plne_data(data)
361
+ Q, K = vehicle_capacity, num_vehicles
362
+ cost = _compute_distance_matrix(coords, nodes)
363
+
364
+ m = Model("CVRP")
365
+ m.setParam("OutputFlag", 0)
366
+ x = m.addVars(cost.keys(), vtype=GRB.BINARY, name="x")
367
+ u = m.addVars(nodes, lb=0, ub=Q, vtype=GRB.CONTINUOUS, name="u")
368
+ m.setObjective(quicksum(cost[i, j] * x[i, j] for i, j in cost), GRB.MINIMIZE)
369
+ m.addConstrs(
370
+ (quicksum(x[i, j] for j in nodes if j != i) == 1 for i in customers),
371
+ "leave",
372
+ )
373
+ m.addConstrs(
374
+ (quicksum(x[i, j] for i in nodes if i != j) == 1 for j in customers),
375
+ "enter",
376
+ )
377
+ m.addConstr(quicksum(x[depot, j] for j in customers) == K, "dep_out")
378
+ m.addConstr(quicksum(x[i, depot] for i in customers) == K, "dep_in")
379
+ m.addConstrs(
380
+ (
381
+ u[i] - u[j] + Q * x[i, j] <= Q - demand[j]
382
+ for i in customers
383
+ for j in customers
384
+ if i != j
385
+ ),
386
+ name="mtz",
387
+ )
388
+ m.addConstr(u[depot] == 0, "depot_load")
389
+ m.optimize()
390
+ if m.status != GRB.OPTIMAL:
391
+ raise ValueError("Gurobi failed to find an optimal solution.")
392
+ sol = m.getAttr("x", x)
393
+ routes = _reconstruct_routes(sol, depot, K)
394
+ routes_df = _build_routes_df(routes, demand, coords, depot)
395
+ fig = _plot_plne(routes, routes_df, coords, customers, depot, K)
396
+ return routes_df, fig
397
+ except (ValueError, TypeError) as e:
398
+ raise RuntimeError(f"Error in solve_plne: {e}")
399
+ except Exception as e:
400
+ raise RuntimeError(f"Unexpected error in solve_plne: {e}")
401
+
402
+
403
+ def _validate_plne_input(data, vehicle_capacity, num_vehicles):
404
+ required_cols = {"Node", "X", "Y", "Demand"}
405
+ if not isinstance(data, pd.DataFrame):
406
+ raise TypeError("Input 'data' must be a pandas DataFrame.")
407
+ if not required_cols.issubset(data.columns):
408
+ missing = required_cols - set(data.columns)
409
+ raise ValueError(f"Missing required columns in 'data': {missing}")
410
+ if not isinstance(vehicle_capacity, (int, float)) or vehicle_capacity <= 0:
411
+ raise ValueError("'vehicle_capacity' must be a positive number.")
412
+ if not isinstance(num_vehicles, int) or num_vehicles <= 0:
413
+ raise ValueError("'num_vehicles' must be a positive integer.")
414
+
415
+
416
+ def _prepare_plne_data(data):
417
  coords = {int(r.Node): (r.X, r.Y) for _, r in data.iterrows()}
418
  demand = {int(r.Node): r.Demand for _, r in data.iterrows()}
419
  nodes = list(coords.keys())
420
  depot = 0
421
+ if depot not in nodes:
422
+ raise ValueError("Depot node (0) is missing from input.")
423
  customers = [i for i in nodes if i != depot]
424
+ return coords, demand, nodes, depot, customers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
 
 
426
 
427
+ def _compute_distance_matrix(coords, nodes):
428
+ return {
429
+ (i, j): math.hypot(coords[i][0] - coords[j][0], coords[i][1] - coords[j][1])
430
+ for i in nodes
431
+ for j in nodes
432
+ if i != j
433
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
 
 
435
 
436
+ def _reconstruct_routes(sol, depot, K):
437
+ starts = [j for (i, j), val in sol.items() if i == depot and val > 0.5]
 
 
438
  if len(starts) != K:
439
  raise ValueError(f"Expected {K} routes out of depot, got {len(starts)}")
440
+ succ = {i: j for (i, j), val in sol.items() if i != depot and val > 0.5}
 
 
 
 
 
441
  routes = []
442
  for start in starts:
443
  route = [depot, start]
444
  cur = start
445
  while cur != depot:
446
+ nxt = succ.get(cur)
447
+ if nxt is None:
448
+ raise ValueError(f"Incomplete route starting at node {start}.")
449
  route.append(nxt)
450
  cur = nxt
451
  routes.append(route)
452
+ return routes
453
+
454
 
455
+ def _build_routes_df(routes, demand, coords, depot):
456
  rows = []
457
  for ridx, route in enumerate(routes, start=1):
458
  load = sum(demand[n] for n in route if n != depot)
459
  dist = sum(
460
+ math.hypot(
461
+ coords[route[i]][0] - coords[route[i + 1]][0],
462
+ coords[route[i]][1] - coords[route[i + 1]][1],
463
+ )
464
+ for i in range(len(route) - 1)
465
+ )
466
+ rows.append(
467
+ {
468
+ "Route": ridx,
469
+ "Sequence": "→".join(str(n) for n in route),
470
+ "Load": load,
471
+ "Distance": dist,
472
+ }
473
+ )
474
+ return pd.DataFrame(rows)
475
+
 
 
 
 
 
476
 
477
+ def _plot_plne(routes, routes_df, coords, customers, depot, K):
478
+ fig, axs = plt.subplots(1, 2, figsize=(14, 6))
479
+ ax = axs[0]
480
+ ax.scatter(*zip(*[coords[i] for i in customers]), c="blue", label="Customers")
481
+ ax.scatter(*coords[depot], c="red", s=100, label="Depot")
482
+ colors = plt.cm.get_cmap("tab10", K)
483
  for ridx, route in enumerate(routes):
484
  pts = [coords[n] for n in route]
485
  xs, ys = zip(*pts)
486
+ ax.plot(xs, ys, "-o", color=colors(ridx), label=f"Route {ridx+1}")
487
  ax.set_title("Vehicle Routes")
488
+ ax.legend(loc="upper right")
 
 
489
  ax2 = axs[1]
490
  bar_width = 0.35
491
  idx = range(len(routes_df))
492
  ax2.bar(idx, routes_df["Load"], bar_width, label="Load")
493
+ ax2.bar(
494
+ [i + bar_width for i in idx], routes_df["Distance"], bar_width, label="Distance"
495
+ )
496
+ ax2.set_xticks([i + bar_width / 2 for i in idx])
497
  ax2.set_xticklabels([f"R{r}" for r in routes_df["Route"]])
498
  ax2.set_ylabel("Units / Distance")
499
  ax2.set_title("Load vs Distance per Route")
500
  ax2.legend()
 
501
  fig.tight_layout()
502
+ return fig
503
+
504
+
505
+ def solve_plne(data: pd.DataFrame, vehicle_capacity: float, num_vehicles: int):
506
+ """
507
+ data: DataFrame with columns ["Node","X","Y","Demand"]
508
+ vehicle_capacity: capacity Q of each vehicle
509
+ num_vehicles: number of vehicles K
510
+ Returns: (routes_df, fig)
511
+ - routes_df: DataFrame with columns ["Route","Sequence","Load","Distance"]
512
+ - fig: matplotlib.figure.Figure with the route‐map and summary bars
513
+ """
514
+ try:
515
+ _validate_plne_input(data, vehicle_capacity, num_vehicles)
516
+ coords, demand, nodes, depot, customers = _prepare_plne_data(data)
517
+ Q, K = vehicle_capacity, num_vehicles
518
+ cost = _compute_distance_matrix(coords, nodes)
519
+
520
+ m = Model("CVRP")
521
+ m.setParam("OutputFlag", 0)
522
+ x = m.addVars(cost.keys(), vtype=GRB.BINARY, name="x")
523
+ u = m.addVars(nodes, lb=0, ub=Q, vtype=GRB.CONTINUOUS, name="u")
524
+ m.setObjective(quicksum(cost[i, j] * x[i, j] for i, j in cost), GRB.MINIMIZE)
525
+ m.addConstrs(
526
+ (quicksum(x[i, j] for j in nodes if j != i) == 1 for i in customers),
527
+ "leave",
528
+ )
529
+ m.addConstrs(
530
+ (quicksum(x[i, j] for i in nodes if i != j) == 1 for j in customers),
531
+ "enter",
532
+ )
533
+ m.addConstr(quicksum(x[depot, j] for j in customers) == K, "dep_out")
534
+ m.addConstr(quicksum(x[i, depot] for i in customers) == K, "dep_in")
535
+ m.addConstrs(
536
+ (
537
+ u[i] - u[j] + Q * x[i, j] <= Q - demand[j]
538
+ for i in customers
539
+ for j in customers
540
+ if i != j
541
+ ),
542
+ name="mtz",
543
+ )
544
+ m.addConstr(u[depot] == 0, "depot_load")
545
+ m.optimize()
546
+ if m.status != GRB.OPTIMAL:
547
+ raise ValueError("Gurobi failed to find an optimal solution.")
548
+ sol = m.getAttr("x", x)
549
+ routes = _reconstruct_routes(sol, depot, K)
550
+ routes_df = _build_routes_df(routes, demand, coords, depot)
551
+ fig = _plot_plne(routes, routes_df, coords, customers, depot, K)
552
+ return routes_df, fig
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}")
ui/gradio_sections.py CHANGED
@@ -1,21 +1,23 @@
1
- import gradio as gr
2
- import os
3
  import base64
4
- import sys
5
- import pandas as pd
 
 
 
6
  import achref.src.logger as logger
7
 
8
  logger = logger.get_logger(__name__)
9
 
 
10
  def project_info_tab():
11
- with gr.Tab("📘 Project Info"):
12
  gr.Markdown(
13
  """
14
- # 🎓 GL3 - 2025 - Operational Research Project
15
  This application demonstrates how **Linear Programming (PL)** and **Mixed-Integer Linear Programming (PLNE)** can be applied to solve real-world optimisation problems using **Gurobi**.
16
 
17
  ---
18
- # 👥 Project Members
19
  - **Kacem Mathlouthi** — GL3/2
20
  - **Mohamed Amine Houas** — GL3/1
21
  - **Oussema Kraiem** — GL3/2
@@ -24,7 +26,7 @@ def project_info_tab():
24
  - **Youssef Aaridhi** — GL3/2
25
  - **Achref Ben Ammar** — GL3/1
26
  ---
27
- # 🧾 Compte Rendu
28
  """
29
  )
30
  pdf_path = os.path.join(
@@ -41,80 +43,130 @@ def project_info_tab():
41
  )
42
 
43
 
44
- def production_planning_tab(mock_pl_df, solve_pl_gurobi, pl_description):
45
- with gr.Tab("🏭 Production Planning (PL)"):
46
- gr.Markdown(pl_description)
 
 
 
 
 
 
 
47
 
48
  # Add mathematical model description
49
  gr.Markdown(
50
  r"""
51
  ### 🧮 Mathematical Formulation
52
 
53
- Let:
54
  | Symbol | Description |
55
  |-------------|-----------------------------------------|
56
- | $$x_i$$ | Number of units to produce for product i |
57
- | $$p_i$$ | Profit per unit for product i |
58
- | $$r_i$$ | Resource usage per unit for product i |
59
- | $$R$$ | Total resource available |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  **Objective:**
62
  $$
63
- \text{Maximise} \quad \sum_i p_i \cdot x_i
64
  $$
65
 
66
- **Constraint:**
67
- $$
68
- \sum_i r_i \cdot x_i \leq R \quad \text{and} \quad x_i \geq 0
69
- $$
 
 
 
 
 
 
 
 
70
  """
71
  )
72
 
73
- with gr.Row():
74
- input_pl = gr.Dataframe(
75
- headers=["Product", "Profit/Unit", "Resource Usage"],
76
- value=mock_pl_df,
77
- label="Input Product Data",
78
- )
79
- total_resource_input = gr.Number(
80
- value=100, label="Total Resource Available (R)"
81
- )
82
- solve_btn_pl = gr.Button("Solve Production Problem")
83
 
84
- result_table_pl = gr.Dataframe(label="Optimised Result")
85
-
86
- # Create plot output placeholders
87
- result_plot_combined = gr.Plot(label="Data Visualisation")
 
 
88
 
89
- def _solve_with_floats(df, R):
90
- df["Profit/Unit"] = df["Profit/Unit"].astype(float)
91
- df["Resource Usage"] = df["Resource Usage"].astype(float)
92
- return solve_pl_gurobi(df, total_resource=R)
93
-
94
- solve_btn_pl.click(
95
- fn=_solve_with_floats,
96
- inputs=[input_pl, total_resource_input],
97
- outputs=[
98
- result_table_pl,
99
- result_plot_combined,
100
- ]
101
- )
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- # in gradio_sections.py
105
 
106
  def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
107
-
108
- with gr.Tab("🚚 Vehicle Routing (PLNE)"):
109
  gr.Markdown(plne_description)
110
- # Log the Python path of the project
111
  gr.HTML(
112
  '<img src="https://pyvrp.readthedocs.io/en/latest/_images/introduction-to-vrp.svg" '
113
  'alt="VRP Problem Illustration" width="600px" />'
114
  )
115
  gr.Markdown(
116
  r"""
117
- ### 🧮 Mathematical Formulation (Capacitated VRP)
118
 
119
 
120
  | Symbol | Description |
@@ -146,7 +198,6 @@ def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
146
  \sum_{i\neq j} x_{ij} = 1
147
  \quad \forall\, j\neq0
148
  $$
149
- > *Explanation:* Every customer `i` must have exactly one vehicle leaving it and one arriving—ensuring each customer is visited exactly once.
150
 
151
  2. **Depot flow**
152
  $$
@@ -155,7 +206,6 @@ def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
155
  $$
156
  \sum_{i>0} x_{i0} = K
157
  $$
158
- > *Explanation:* Exactly `K` vehicles depart from the depot and `K` return, so all vehicles are used and end back at the depot.
159
 
160
  3. **MTZ subtour-elimination & capacity**
161
  $$
@@ -168,63 +218,50 @@ def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
168
  $$
169
  0 \le u_i \le Q
170
  $$
171
- > *Explanation:*
172
- > - If `x_{ij}=1`, then $$u_j \ge u_i + d_j$$ enforcing vehicle capacity.
173
- > - These constraints also prevent any customer‐only loops (subtours), because load can’t reset without returning to the depot.
174
- > - We fix `u_0=0` at the depot and bound `u_i` by capacity `Q`.
175
-
176
-
177
- ---
178
-
179
-
180
-
181
- """
182
  )
183
-
184
  vrp_input = gr.Dataframe(
185
- headers=["Node", "X", "Y", "Demand"],
186
- value=mock_plne_df,
187
- label="Input Vehicle Routing Data",
188
- )
189
  with gr.Row():
190
  cap_input = gr.Number(value=40, label="Vehicle capacity (Q)")
191
- k_input = gr.Number(value=2, label="Number of vehicles (K)")
192
  solve_btn = gr.Button("Solve VRP")
193
  status_output = gr.Textbox(label="Status", interactive=False)
194
  result_table = gr.Dataframe(label="Routes Summary")
195
- result_plot = gr.Plot(label="Route Map & Summary")
196
 
197
  def _solve_vrp_with_floats(df, Q, K):
198
- df["X"] = df["X"].astype(float)
199
- df["Y"] = df["Y"].astype(float)
200
- df["Demand"] = df["Demand"].astype(float)
201
-
202
- # skip depot (assumed Node==0) when checking
203
- custs = df[df["Node"] != 0]
204
-
205
  try:
206
- # 1) any single demand > Q?
 
 
 
 
 
207
  too_big = custs[custs["Demand"] > Q]
208
  if not too_big.empty:
209
  bad = int(too_big["Node"].iloc[0])
210
- raise ValueError(f"Client {bad} demand ({too_big['Demand'].iloc[0]}) exceeds capacity Q={Q}")
 
 
211
 
212
- # 2) total demand > Q*K?
213
  total = custs["Demand"].sum()
214
  if total > Q * K:
215
- raise ValueError(f"Total demand ({total}) exceeds fleet capacity Q*K={Q*K}")
 
 
216
 
217
- # all good → call solver
218
  routes_df, fig = solve_plne(df, vehicle_capacity=Q, num_vehicles=K)
219
- return routes_df, fig, "All Good"
220
- except ValueError as e:
221
- # on error show empty table/plot + message
222
- return pd.DataFrame(), None, str(e)
223
-
224
-
225
  solve_btn.click(
226
  fn=_solve_vrp_with_floats,
227
  inputs=[vrp_input, cap_input, k_input],
228
  outputs=[result_table, result_plot, status_output],
229
- )
230
-
 
 
 
1
  import base64
2
+ import os
3
+
4
+ import gradio as gr
5
+ import pandas as pd
6
+
7
  import achref.src.logger as logger
8
 
9
  logger = logger.get_logger(__name__)
10
 
11
+
12
  def project_info_tab():
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
+ # \U0001f465 Project Members
21
  - **Kacem Mathlouthi** — GL3/2
22
  - **Mohamed Amine Houas** — GL3/1
23
  - **Oussema Kraiem** — GL3/2
 
26
  - **Youssef Aaridhi** — GL3/2
27
  - **Achref Ben Ammar** — GL3/1
28
  ---
29
+ # \U0001f9fe Compte Rendu
30
  """
31
  )
32
  pdf_path = os.path.join(
 
43
  )
44
 
45
 
46
+ def oil_refinery_tab(
47
+ mock_crude_data,
48
+ mock_product_data,
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
+ | $$c_i$$ | Cost per unit of crude $i$ |
72
+ | $$p_j$$ | Selling price per unit of product $j$ |
73
+ | $$y_{ij}$$ | Yield of product $j$ from crude $i$ (liters of product per liter of crude) |
74
+ | $$q_{ij}$$ | Quality contribution of crude $i$ to product $j$ |
75
+ | $$Q_j^{min}$$ | Minimum average quality required for product $j$ |
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$$ | Amount of crude oil $i$ used (liters) |
83
 
84
  **Objective:**
85
  $$
86
+ \text{Maximize} \quad \left(\sum_{j=1}^n p_j \cdot \sum_{i=1}^m y_{ij} \cdot x_i - \sum_{i=1}^m c_i \cdot x_i\right)
87
  $$
88
 
89
+ **Constraints:**
90
+ 1. Crude Availability:
91
+ $$x_i \leq A_i \quad \forall i$$
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
+ 4. Non-negativity:
100
+ $$x_i \geq 0 \quad \forall i$$
101
  """
102
  )
103
 
104
+ with gr.Tabs():
105
+ with gr.TabItem("Crude Oils"):
106
+ crude_input = gr.Dataframe(
107
+ headers=["Crude", "Cost", "Availability"],
108
+ value=mock_crude_data,
109
+ label="Crude Oil Data",
110
+ )
 
 
 
111
 
112
+ with gr.TabItem("Products"):
113
+ product_input = gr.Dataframe(
114
+ headers=["Product", "Price", "Demand"],
115
+ value=mock_product_data,
116
+ label="Product Data",
117
+ )
118
 
119
+ with gr.TabItem("Yields & Quality"):
120
+ yields_input = gr.Dataframe(
121
+ headers=["Crude", "Product", "Yield", "Quality"],
122
+ value=mock_yields_data,
123
+ label="Yield & Quality Data",
124
+ )
 
 
 
 
 
 
 
125
 
126
+ with gr.TabItem("Quality Requirements"):
127
+ quality_reqs_input = gr.Dataframe(
128
+ headers=["Product", "MinQuality"],
129
+ value=mock_quality_reqs,
130
+ label="Quality Requirements",
131
+ )
132
+
133
+ solve_btn = gr.Button("Solve Refinery Optimization Problem")
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 _solve_refinery_problem(crude_df, product_df, yields_df, quality_df):
139
+ try:
140
+ # Convert all numeric columns to float
141
+ for df in [crude_df, product_df, yields_df, quality_df]:
142
+ for col in df.columns:
143
+ if col not in ["Crude", "Product"]:
144
+ df[col] = pd.to_numeric(df[col], errors="coerce")
145
+
146
+ result_df, fig = solve_refinery_optimization(
147
+ crude_df, product_df, yields_df, quality_df
148
+ )
149
+ return result_df, fig, "Solved Successfully"
150
+ except Exception as e:
151
+ return pd.DataFrame(), None, f"❌ Error: {str(e)}"
152
+
153
+ solve_btn.click(
154
+ fn=_solve_refinery_problem,
155
+ inputs=[crude_input, product_input, yields_input, quality_reqs_input],
156
+ outputs=[results_table, results_plot, status_output],
157
+ )
158
 
 
159
 
160
  def vehicle_routing_tab(mock_plne_df, solve_plne, plne_description):
161
+ with gr.Tab("\U0001f69a Vehicle Routing (PLNE)"):
 
162
  gr.Markdown(plne_description)
 
163
  gr.HTML(
164
  '<img src="https://pyvrp.readthedocs.io/en/latest/_images/introduction-to-vrp.svg" '
165
  'alt="VRP Problem Illustration" width="600px" />'
166
  )
167
  gr.Markdown(
168
  r"""
169
+ ### \U0001F9EE Mathematical Formulation (Capacitated VRP)
170
 
171
 
172
  | Symbol | Description |
 
198
  \sum_{i\neq j} x_{ij} = 1
199
  \quad \forall\, j\neq0
200
  $$
 
201
 
202
  2. **Depot flow**
203
  $$
 
206
  $$
207
  \sum_{i>0} x_{i0} = K
208
  $$
 
209
 
210
  3. **MTZ subtour-elimination & capacity**
211
  $$
 
218
  $$
219
  0 \le u_i \le Q
220
  $$
221
+ """
 
 
 
 
 
 
 
 
 
 
222
  )
223
+
224
  vrp_input = gr.Dataframe(
225
+ headers=["Node", "X", "Y", "Demand"],
226
+ value=mock_plne_df,
227
+ label="Input Vehicle Routing Data",
228
+ )
229
  with gr.Row():
230
  cap_input = gr.Number(value=40, label="Vehicle capacity (Q)")
231
+ k_input = gr.Number(value=2, label="Number of vehicles (K)")
232
  solve_btn = gr.Button("Solve VRP")
233
  status_output = gr.Textbox(label="Status", interactive=False)
234
  result_table = gr.Dataframe(label="Routes Summary")
235
+ result_plot = gr.Plot(label="Route Map & Summary")
236
 
237
  def _solve_vrp_with_floats(df, Q, K):
 
 
 
 
 
 
 
238
  try:
239
+ df["X"] = df["X"].astype(float)
240
+ df["Y"] = df["Y"].astype(float)
241
+ df["Demand"] = df["Demand"].astype(float)
242
+
243
+ custs = df[df["Node"] != 0]
244
+
245
  too_big = custs[custs["Demand"] > Q]
246
  if not too_big.empty:
247
  bad = int(too_big["Node"].iloc[0])
248
+ raise ValueError(
249
+ f"Client {bad} demand ({too_big['Demand'].iloc[0]}) exceeds capacity Q={Q}"
250
+ )
251
 
 
252
  total = custs["Demand"].sum()
253
  if total > Q * K:
254
+ raise ValueError(
255
+ f"Total demand ({total}) exceeds fleet capacity Q*K={Q*K}"
256
+ )
257
 
 
258
  routes_df, fig = solve_plne(df, vehicle_capacity=Q, num_vehicles=K)
259
+ return routes_df, fig, "Solved Successfully"
260
+ except Exception as e:
261
+ return pd.DataFrame(), None, f"❌ Error: {str(e)}"
262
+
 
 
263
  solve_btn.click(
264
  fn=_solve_vrp_with_floats,
265
  inputs=[vrp_input, cap_input, k_input],
266
  outputs=[result_table, result_plot, status_output],
267
+ )