Spaces:
Runtime error
Refactor Operations Research Interfaces and Add Utility Functions
Browse files- Moved the DualSimplexSolver and BranchAndBoundSolver interfaces to their respective modules for better organization.
- Removed the operations_research_interface.py file as it was redundant.
- Added utility functions for parsing vectors, matrices, relations, and bounds into a new utils.py file to streamline input handling across different solvers.
- Updated the DualSimplexSolver interface to utilize the new parsing utilities and improved error handling.
- Enhanced the Branch and Bound interface to ensure proper input validation and error messaging.
- Refactored the Simplex Solver interface to incorporate the new utility functions and improve clarity in input processing.
- Added detailed examples and descriptions for the Gradio interfaces to enhance user experience.
- maths/operations_research/BranchAndBoundSolver.py +709 -300
- maths/operations_research/DualSimplexSolver.py +138 -0
- maths/operations_research/operations_research_interface.py +0 -481
- maths/operations_research/operations_research_tab.py +3 -3
- maths/operations_research/simplex_solver_with_steps.py +127 -0
- maths/operations_research/utils.py +90 -0
@@ -6,6 +6,9 @@ import networkx as nx
|
|
6 |
import matplotlib.pyplot as plt
|
7 |
from tabulate import tabulate
|
8 |
from scipy.optimize import linprog
|
|
|
|
|
|
|
9 |
|
10 |
|
11 |
class BranchAndBoundSolver:
|
@@ -19,20 +22,64 @@ class BranchAndBoundSolver:
|
|
19 |
- integer_vars: Indices of variables that must be integers
|
20 |
- binary_vars: Indices of variables that must be binary (0 or 1)
|
21 |
- maximize: True for maximization, False for minimization
|
|
|
|
|
|
|
|
|
22 |
"""
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
31 |
# If integer_vars not specified, assume all non-binary variables are integers
|
32 |
if integer_vars is None:
|
33 |
self.integer_vars = list(range(self.n))
|
34 |
else:
|
35 |
-
self.integer_vars = integer_vars
|
|
|
|
|
|
|
|
|
36 |
|
37 |
# Add binary variables to integer variables list if they're not already there
|
38 |
for idx in self.binary_vars:
|
@@ -55,337 +102,699 @@ class BranchAndBoundSolver:
|
|
55 |
self.steps_table = []
|
56 |
|
57 |
# Set of active nodes
|
58 |
-
self.active_nodes = set()
|
59 |
-
|
60 |
-
# For logging messages
|
61 |
self.log_messages = []
|
62 |
-
|
63 |
def is_integer_feasible(self, x):
|
64 |
-
"""Check if the solution satisfies integer constraints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
if x is None:
|
66 |
return False
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
def get_branching_variable(self, x):
|
74 |
-
"""Select most fractional variable to branch on
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
max_fractional = -1
|
76 |
branching_var = -1
|
77 |
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
return branching_var
|
85 |
-
|
86 |
def solve_relaxation(self, lower_bounds, upper_bounds):
|
87 |
-
"""Solve the continuous relaxation with given bounds
|
88 |
-
x = cp.Variable(self.n)
|
89 |
-
|
90 |
-
# Set the objective - maximize c'x or minimize -c'x
|
91 |
-
if self.maximize:
|
92 |
-
objective = cp.Maximize(self.c @ x)
|
93 |
-
else:
|
94 |
-
objective = cp.Minimize(self.c @ x)
|
95 |
-
|
96 |
-
# Basic constraints Ax <= b
|
97 |
-
constraints = [self.A @ x <= self.b]
|
98 |
-
|
99 |
-
# Add bounds
|
100 |
-
for i in range(self.n):
|
101 |
-
if lower_bounds[i] is not None:
|
102 |
-
constraints.append(x[i] >= lower_bounds[i])
|
103 |
-
if upper_bounds[i] is not None:
|
104 |
-
constraints.append(x[i] <= upper_bounds[i])
|
105 |
-
|
106 |
-
prob = cp.Problem(objective, constraints)
|
107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
objective_value = prob.solve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
return x.value, objective_value
|
111 |
-
|
|
|
|
|
112 |
return None, float('-inf') if self.maximize else float('inf')
|
113 |
-
|
|
|
|
|
|
|
114 |
def add_node_to_graph(self, node_name, objective_value, x_value, parent=None, branch_var=None, branch_cond=None):
|
115 |
-
"""Add a node to the branch and bound graph
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
"""
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
-
|
132 |
-
|
133 |
-
for node, data in self.graph.nodes(data=True):
|
134 |
-
if data.get('x') is not None:
|
135 |
-
x_str = ', '.join([f"{x:.2f}" for x in data['x']])
|
136 |
-
labels[node] = f"{node}\n({data['obj']:.2f}, ({x_str}))"
|
137 |
-
else:
|
138 |
-
labels[node] = f"{node}\nInfeasible"
|
139 |
-
|
140 |
-
# Edge labels: Branching conditions
|
141 |
-
edge_labels = nx.get_edge_attributes(self.graph, 'label')
|
142 |
-
|
143 |
-
# Draw nodes
|
144 |
-
nx.draw_networkx_nodes(self.graph, pos, node_size=2000, node_color='skyblue')
|
145 |
-
|
146 |
-
# Draw edges
|
147 |
-
nx.draw_networkx_edges(self.graph, pos, width=1.5, arrowsize=20, edge_color='gray')
|
148 |
-
|
149 |
-
# Draw labels
|
150 |
-
nx.draw_networkx_labels(self.graph, pos, labels, font_size=10, font_family='sans-serif')
|
151 |
-
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels, font_size=10, font_family='sans-serif')
|
152 |
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
|
158 |
-
|
159 |
def display_steps_table(self):
|
160 |
-
"""Display the steps in tabular format
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
# We use negative values for maximization to simulate max heap with Python's min heap
|
177 |
-
node_queue = PriorityQueue()
|
178 |
-
|
179 |
-
# Solve the root relaxation
|
180 |
-
self.log_messages.append("Step 1: Solving root relaxation (continuous problem)")
|
181 |
-
x_root, obj_root = self.solve_relaxation(lower_bounds, upper_bounds)
|
182 |
-
|
183 |
-
if x_root is None:
|
184 |
-
self.log_messages.append("Root problem infeasible")
|
185 |
-
fig = self.visualize_graph() # Still generate graph even if infeasible at root
|
186 |
-
return None, float('-inf') if self.maximize else float('inf'), self.log_messages, fig
|
187 |
-
|
188 |
-
# Add root node to the graph
|
189 |
-
root_node = "S0"
|
190 |
-
self.add_node_to_graph(root_node, obj_root, x_root)
|
191 |
-
|
192 |
-
self.log_messages.append(f"Root relaxation objective: {obj_root:.6f}")
|
193 |
-
self.log_messages.append(f"Root solution: {x_root}")
|
194 |
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
self.
|
202 |
-
self.best_objective = obj_root
|
203 |
|
204 |
-
#
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
])
|
211 |
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
return self.best_solution, self.best_objective, self.log_messages, fig
|
216 |
-
|
217 |
-
# Add root node to the queue and active nodes set
|
218 |
-
priority = -obj_root if self.maximize else obj_root
|
219 |
-
node_queue.put((priority, self.nodes_explored, root_node, lower_bounds.copy(), upper_bounds.copy()))
|
220 |
-
self.active_nodes.add(root_node)
|
221 |
-
|
222 |
-
# Add entry to steps table for root node
|
223 |
-
active_nodes_str = "{" + ", ".join(self.active_nodes) + "}"
|
224 |
-
lb_str = "-" if self.best_objective == float('-inf') else f"{self.best_objective:.2f}"
|
225 |
-
x_star_str = "-" if self.best_solution is None else f"({', '.join([f'{x:.2f}' for x in self.best_solution])})"
|
226 |
-
|
227 |
-
self.steps_table.append([
|
228 |
-
root_node, f"{obj_root:.2f}", f"({', '.join([f'{x:.2f}' for x in x_root])})",
|
229 |
-
lb_str, x_star_str, f"{upper_bound:.2f}", lb_str, active_nodes_str
|
230 |
-
])
|
231 |
-
|
232 |
-
self.log_messages.append("\nStarting branch and bound process:")
|
233 |
-
node_counter = 1
|
234 |
-
|
235 |
-
while not node_queue.empty():
|
236 |
-
# Get the node with the highest objective (for maximization)
|
237 |
-
priority, _, node_name, node_lower_bounds, node_upper_bounds = node_queue.get()
|
238 |
-
self.nodes_explored += 1
|
239 |
|
240 |
-
|
|
|
241 |
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
|
|
244 |
|
245 |
-
|
246 |
-
|
247 |
-
|
|
|
248 |
|
249 |
-
#
|
250 |
-
|
251 |
-
|
252 |
-
ceil_val = 1
|
253 |
-
self.log_messages.append(f" Branching on binary variable x_{branch_var + 1} with value {branch_val:.6f}")
|
254 |
-
self.log_messages.append(f" Creating two branches: x_{branch_var + 1} = 0 and x_{branch_var + 1} = 1")
|
255 |
-
else:
|
256 |
-
floor_val = math.floor(branch_val)
|
257 |
-
ceil_val = math.ceil(branch_val)
|
258 |
-
self.log_messages.append(f" Branching on variable x_{branch_var + 1} with value {branch_val:.6f}")
|
259 |
-
self.log_messages.append(f" Creating two branches: x_{branch_var + 1} ≤ {floor_val} and x_{branch_var + 1} ≥ {ceil_val}")
|
260 |
-
|
261 |
-
# Process left branch (floor)
|
262 |
-
left_node = f"S{node_counter}"
|
263 |
-
node_counter += 1
|
264 |
-
|
265 |
-
# Create the "floor" branch
|
266 |
-
floor_lower_bounds = node_lower_bounds.copy()
|
267 |
-
floor_upper_bounds = node_upper_bounds.copy()
|
268 |
-
|
269 |
-
# For binary variables, set both bounds to 0 (x=0)
|
270 |
-
if branch_var in self.binary_vars:
|
271 |
-
floor_lower_bounds[branch_var] = 0
|
272 |
-
floor_upper_bounds[branch_var] = 0
|
273 |
-
branch_cond = f"= 0"
|
274 |
-
else:
|
275 |
-
floor_upper_bounds[branch_var] = floor_val
|
276 |
-
branch_cond = f"≤ {floor_val}"
|
277 |
|
278 |
-
|
279 |
-
|
280 |
|
281 |
-
#
|
282 |
-
|
283 |
-
x_floor, node_name, branch_var, branch_cond)
|
284 |
|
285 |
-
#
|
286 |
-
if
|
287 |
-
self.log_messages.append(
|
288 |
-
|
289 |
-
self.
|
290 |
-
self.log_messages.append(f" {left_node} solution: {x_floor}")
|
291 |
|
292 |
-
#
|
293 |
-
|
294 |
-
|
295 |
-
self.
|
296 |
-
|
297 |
-
|
|
|
|
|
|
|
|
|
298 |
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
priority = -obj_floor if self.maximize else obj_floor
|
304 |
-
node_queue.put((priority, self.nodes_explored, left_node,
|
305 |
-
floor_lower_bounds.copy(), floor_upper_bounds.copy()))
|
306 |
-
self.active_nodes.add(left_node)
|
307 |
-
|
308 |
-
# Process right branch (ceil)
|
309 |
-
right_node = f"S{node_counter}"
|
310 |
-
node_counter += 1
|
311 |
-
|
312 |
-
# Create the "ceil" branch
|
313 |
-
ceil_lower_bounds = node_lower_bounds.copy()
|
314 |
-
ceil_upper_bounds = node_upper_bounds.copy()
|
315 |
-
|
316 |
-
# For binary variables, set both bounds to 1 (x=1)
|
317 |
-
if branch_var in self.binary_vars:
|
318 |
-
ceil_lower_bounds[branch_var] = 1
|
319 |
-
ceil_upper_bounds[branch_var] = 1
|
320 |
-
branch_cond = f"= 1"
|
321 |
-
else:
|
322 |
-
ceil_lower_bounds[branch_var] = ceil_val
|
323 |
-
branch_cond = f"≥ {ceil_val}"
|
324 |
-
|
325 |
-
# Solve the relaxation for this node
|
326 |
-
x_ceil, obj_ceil = self.solve_relaxation(ceil_lower_bounds, ceil_upper_bounds)
|
327 |
|
328 |
-
# Add node to
|
329 |
-
|
330 |
-
|
|
|
331 |
|
332 |
-
#
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
self.
|
337 |
-
self.log_messages.append(f" {right_node} solution: {x_ceil}")
|
338 |
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
360 |
else:
|
361 |
-
|
362 |
-
|
363 |
-
#
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
self.
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
import matplotlib.pyplot as plt
|
7 |
from tabulate import tabulate
|
8 |
from scipy.optimize import linprog
|
9 |
+
import gradio as gr
|
10 |
+
from maths.operations_research.utils import parse_matrix
|
11 |
+
from maths.operations_research.utils import parse_vector
|
12 |
|
13 |
|
14 |
class BranchAndBoundSolver:
|
|
|
22 |
- integer_vars: Indices of variables that must be integers
|
23 |
- binary_vars: Indices of variables that must be binary (0 or 1)
|
24 |
- maximize: True for maximization, False for minimization
|
25 |
+
|
26 |
+
Raises:
|
27 |
+
- ValueError: If input dimensions are inconsistent or invalid indices provided
|
28 |
+
- TypeError: If inputs are not numeric arrays
|
29 |
"""
|
30 |
+
# Input validation
|
31 |
+
if not hasattr(c, '__len__') or len(c) == 0:
|
32 |
+
raise ValueError("Objective coefficients 'c' must be a non-empty array-like object")
|
33 |
+
|
34 |
+
if not hasattr(A, 'shape') or A.size == 0:
|
35 |
+
raise ValueError("Constraint matrix 'A' must be a non-empty 2D array")
|
36 |
+
|
37 |
+
if not hasattr(b, '__len__') or len(b) == 0:
|
38 |
+
raise ValueError("Constraint bounds 'b' must be a non-empty array-like object")
|
39 |
+
|
40 |
+
# Convert inputs to numpy arrays for consistency
|
41 |
+
try:
|
42 |
+
self.c = np.asarray(c, dtype=float)
|
43 |
+
self.A = np.asarray(A, dtype=float)
|
44 |
+
self.b = np.asarray(b, dtype=float)
|
45 |
+
except (ValueError, TypeError) as e:
|
46 |
+
raise TypeError(f"All inputs must be convertible to numeric arrays: {e}")
|
47 |
+
|
48 |
+
# Validate dimensions
|
49 |
+
if self.A.ndim != 2:
|
50 |
+
raise ValueError(f"Constraint matrix 'A' must be 2D, got {self.A.ndim}D")
|
51 |
+
|
52 |
+
if self.c.ndim != 1:
|
53 |
+
raise ValueError(f"Objective coefficients 'c' must be 1D, got {self.c.ndim}D")
|
54 |
+
|
55 |
+
if self.b.ndim != 1:
|
56 |
+
raise ValueError(f"Constraint bounds 'b' must be 1D, got {self.b.ndim}D")
|
57 |
+
|
58 |
+
if self.A.shape[0] != len(self.b):
|
59 |
+
raise ValueError(f"Number of rows in A ({self.A.shape[0]}) must match length of b ({len(self.b)})")
|
60 |
+
|
61 |
+
if self.A.shape[1] != len(self.c):
|
62 |
+
raise ValueError(f"Number of columns in A ({self.A.shape[1]}) must match length of c ({len(self.c)})")
|
63 |
|
64 |
+
self.n = len(self.c)
|
65 |
+
|
66 |
+
# Process binary and integer variables with validation
|
67 |
+
self.binary_vars = [] if binary_vars is None else list(binary_vars)
|
68 |
+
|
69 |
+
# Validate binary variable indices
|
70 |
+
for idx in self.binary_vars:
|
71 |
+
if not isinstance(idx, (int, np.integer)) or idx < 0 or idx >= self.n:
|
72 |
+
raise ValueError(f"Binary variable index {idx} is invalid. Must be integer in range [0, {self.n-1}]")
|
73 |
|
74 |
# If integer_vars not specified, assume all non-binary variables are integers
|
75 |
if integer_vars is None:
|
76 |
self.integer_vars = list(range(self.n))
|
77 |
else:
|
78 |
+
self.integer_vars = list(integer_vars)
|
79 |
+
# Validate integer variable indices
|
80 |
+
for idx in self.integer_vars:
|
81 |
+
if not isinstance(idx, (int, np.integer)) or idx < 0 or idx >= self.n:
|
82 |
+
raise ValueError(f"Integer variable index {idx} is invalid. Must be integer in range [0, {self.n-1}]")
|
83 |
|
84 |
# Add binary variables to integer variables list if they're not already there
|
85 |
for idx in self.binary_vars:
|
|
|
102 |
self.steps_table = []
|
103 |
|
104 |
# Set of active nodes
|
105 |
+
self.active_nodes = set() # For logging messages
|
|
|
|
|
106 |
self.log_messages = []
|
107 |
+
|
108 |
def is_integer_feasible(self, x):
|
109 |
+
"""Check if the solution satisfies integer constraints
|
110 |
+
|
111 |
+
Args:
|
112 |
+
x: Solution vector to check
|
113 |
+
|
114 |
+
Returns:
|
115 |
+
bool: True if solution satisfies integer constraints, False otherwise
|
116 |
+
"""
|
117 |
if x is None:
|
118 |
return False
|
119 |
|
120 |
+
try:
|
121 |
+
for idx in self.integer_vars:
|
122 |
+
if idx >= len(x):
|
123 |
+
raise IndexError(f"Integer variable index {idx} exceeds solution vector length {len(x)}")
|
124 |
+
if abs(round(x[idx]) - x[idx]) > 1e-6:
|
125 |
+
return False
|
126 |
+
return True
|
127 |
+
except (IndexError, TypeError) as e:
|
128 |
+
self.log_messages.append(f"Error checking integer feasibility: {e}")
|
129 |
+
return False
|
130 |
|
131 |
def get_branching_variable(self, x):
|
132 |
+
"""Select most fractional variable to branch on
|
133 |
+
|
134 |
+
Args:
|
135 |
+
x: Solution vector
|
136 |
+
|
137 |
+
Returns:
|
138 |
+
int: Index of variable to branch on, or -1 if no fractional variables found
|
139 |
+
"""
|
140 |
+
if x is None:
|
141 |
+
return -1
|
142 |
+
|
143 |
max_fractional = -1
|
144 |
branching_var = -1
|
145 |
|
146 |
+
try:
|
147 |
+
for idx in self.integer_vars:
|
148 |
+
if idx >= len(x):
|
149 |
+
self.log_messages.append(f"Warning: Integer variable index {idx} exceeds solution vector length {len(x)}")
|
150 |
+
continue
|
151 |
+
|
152 |
+
fractional_part = abs(x[idx] - round(x[idx]))
|
153 |
+
if fractional_part > max_fractional and fractional_part > 1e-6:
|
154 |
+
max_fractional = fractional_part
|
155 |
+
branching_var = idx
|
156 |
+
except (IndexError, TypeError) as e:
|
157 |
+
self.log_messages.append(f"Error finding branching variable: {e}")
|
158 |
+
return -1
|
159 |
|
160 |
return branching_var
|
161 |
+
|
162 |
def solve_relaxation(self, lower_bounds, upper_bounds):
|
163 |
+
"""Solve the continuous relaxation with given bounds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
|
165 |
+
Args:
|
166 |
+
lower_bounds: List of lower bounds for variables
|
167 |
+
upper_bounds: List of upper bounds for variables
|
168 |
+
|
169 |
+
Returns:
|
170 |
+
tuple: (solution_vector, objective_value) or (None, inf/-inf) if infeasible
|
171 |
+
"""
|
172 |
try:
|
173 |
+
# Validate bounds
|
174 |
+
if len(lower_bounds) != self.n or len(upper_bounds) != self.n:
|
175 |
+
raise ValueError(f"Bounds must have length {self.n}, got {len(lower_bounds)}, {len(upper_bounds)}")
|
176 |
+
|
177 |
+
x = cp.Variable(self.n)
|
178 |
+
|
179 |
+
# Set the objective - maximize c'x or minimize -c'x
|
180 |
+
if self.maximize:
|
181 |
+
objective = cp.Maximize(self.c @ x)
|
182 |
+
else:
|
183 |
+
objective = cp.Minimize(self.c @ x)
|
184 |
+
|
185 |
+
# Basic constraints Ax <= b
|
186 |
+
constraints = [self.A @ x <= self.b]
|
187 |
+
|
188 |
+
# Add bounds with validation
|
189 |
+
for i in range(self.n):
|
190 |
+
if lower_bounds[i] is not None:
|
191 |
+
if not np.isfinite(lower_bounds[i]):
|
192 |
+
raise ValueError(f"Lower bound for variable {i} must be finite, got {lower_bounds[i]}")
|
193 |
+
constraints.append(x[i] >= lower_bounds[i])
|
194 |
+
if upper_bounds[i] is not None:
|
195 |
+
if not np.isfinite(upper_bounds[i]):
|
196 |
+
raise ValueError(f"Upper bound for variable {i} must be finite, got {upper_bounds[i]}")
|
197 |
+
constraints.append(x[i] <= upper_bounds[i])
|
198 |
+
|
199 |
+
# Check for contradictory bounds
|
200 |
+
if (lower_bounds[i] is not None and upper_bounds[i] is not None and
|
201 |
+
lower_bounds[i] > upper_bounds[i]):
|
202 |
+
self.log_messages.append(f"Warning: Contradictory bounds for variable {i}: [{lower_bounds[i]}, {upper_bounds[i]}]")
|
203 |
+
|
204 |
+
prob = cp.Problem(objective, constraints)
|
205 |
+
|
206 |
+
# Solve with error handling
|
207 |
objective_value = prob.solve()
|
208 |
+
|
209 |
+
# Check solver status
|
210 |
+
if prob.status in ['infeasible', 'unbounded']:
|
211 |
+
self.log_messages.append(f"Relaxation problem status: {prob.status}")
|
212 |
+
return None, float('-inf') if self.maximize else float('inf')
|
213 |
+
elif prob.status != 'optimal':
|
214 |
+
self.log_messages.append(f"Relaxation solver warning: {prob.status}")
|
215 |
+
# Validate solution
|
216 |
+
if x.value is None:
|
217 |
+
return None, float('-inf') if self.maximize else float('inf')
|
218 |
+
|
219 |
+
# Check for numerical issues
|
220 |
+
if not np.isfinite(objective_value):
|
221 |
+
self.log_messages.append(f"Warning: Non-finite objective value: {objective_value}")
|
222 |
+
return None, float('-inf') if self.maximize else float('inf')
|
223 |
+
|
224 |
return x.value, objective_value
|
225 |
+
|
226 |
+
except cp.error.SolverError as e:
|
227 |
+
self.log_messages.append(f"CVXPY solver error: {e}")
|
228 |
return None, float('-inf') if self.maximize else float('inf')
|
229 |
+
except Exception as e:
|
230 |
+
self.log_messages.append(f"Unexpected error in solve_relaxation: {e}")
|
231 |
+
return None, float('-inf') if self.maximize else float('inf')
|
232 |
+
|
233 |
def add_node_to_graph(self, node_name, objective_value, x_value, parent=None, branch_var=None, branch_cond=None):
|
234 |
+
"""Add a node to the branch and bound graph
|
235 |
+
|
236 |
+
Args:
|
237 |
+
node_name: Unique identifier for the node
|
238 |
+
objective_value: Objective value at this node
|
239 |
+
x_value: Solution vector at this node
|
240 |
+
parent: Parent node name (for edges)
|
241 |
+
branch_var: Variable being branched on
|
242 |
+
branch_cond: Branching condition string
|
243 |
+
|
244 |
+
Returns:
|
245 |
+
str: The node name that was added
|
246 |
+
"""
|
247 |
+
try:
|
248 |
+
# Validate inputs
|
249 |
+
if not isinstance(node_name, str):
|
250 |
+
raise ValueError(f"Node name must be string, got {type(node_name)}")
|
251 |
+
|
252 |
+
if objective_value is not None and not np.isfinite(objective_value):
|
253 |
+
self.log_messages.append(f"Warning: Non-finite objective value for node {node_name}: {objective_value}")
|
254 |
+
|
255 |
+
self.graph.add_node(node_name, obj=objective_value, x=x_value,
|
256 |
+
branch_var=branch_var, branch_cond=branch_cond)
|
257 |
+
|
258 |
+
if parent is not None:
|
259 |
+
if parent not in self.graph.nodes:
|
260 |
+
self.log_messages.append(f"Warning: Parent node {parent} not found in graph")
|
261 |
+
else:
|
262 |
+
# Use branch_var + 1 to show 1-indexed variables in the display if branch_var is not None and branch_cond is not None:
|
263 |
+
label = f"x_{branch_var + 1} {branch_cond}"
|
264 |
+
self.graph.add_edge(parent, node_name, label=label)
|
265 |
+
|
266 |
+
return node_name
|
267 |
+
|
268 |
+
except Exception as e:
|
269 |
+
self.log_messages.append(f"Error adding node {node_name} to graph: {e}")
|
270 |
+
return node_name # Return the name even if there was an error
|
271 |
|
272 |
+
def visualize_graph(self):
|
273 |
+
"""Visualize the branch and bound graph
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
|
275 |
+
Returns:
|
276 |
+
matplotlib.figure.Figure: The graph visualization figure
|
277 |
+
"""
|
278 |
+
try:
|
279 |
+
if len(self.graph.nodes) == 0:
|
280 |
+
# Create empty plot if no nodes
|
281 |
+
fig = plt.figure(figsize=(10, 6))
|
282 |
+
plt.text(0.5, 0.5, 'No nodes to display', ha='center', va='center', transform=plt.gca().transAxes)
|
283 |
+
plt.title("Branch and Bound Tree (Empty)", fontsize=14)
|
284 |
+
plt.axis('off')
|
285 |
+
return fig
|
286 |
+
|
287 |
+
fig = plt.figure(figsize=(20, 8))
|
288 |
+
|
289 |
+
# Use hierarchical layout if possible, fall back to spring layout
|
290 |
+
try:
|
291 |
+
pos = nx.nx_agraph.graphviz_layout(self.graph, prog='dot')
|
292 |
+
except:
|
293 |
+
try:
|
294 |
+
pos = nx.spring_layout(self.graph, k=3, iterations=50)
|
295 |
+
except:
|
296 |
+
# Fallback to simple circular layout
|
297 |
+
pos = nx.circular_layout(self.graph)
|
298 |
+
|
299 |
+
# Node labels: Node name, Objective value and solution
|
300 |
+
labels = {}
|
301 |
+
for node, data in self.graph.nodes(data=True):
|
302 |
+
try:
|
303 |
+
if data.get('x') is not None and len(data['x']) > 0:
|
304 |
+
x_str = ', '.join([f"{x:.2f}" for x in data['x']])
|
305 |
+
obj_val = data.get('obj', 'N/A')
|
306 |
+
if isinstance(obj_val, (int, float)) and np.isfinite(obj_val):
|
307 |
+
labels[node] = f"{node}\n({obj_val:.2f}, ({x_str}))"
|
308 |
+
else:
|
309 |
+
labels[node] = f"{node}\n(N/A, ({x_str}))"
|
310 |
+
else:
|
311 |
+
labels[node] = f"{node}\nInfeasible"
|
312 |
+
except Exception as e:
|
313 |
+
labels[node] = f"{node}\nError: {str(e)[:20]}..."
|
314 |
+
|
315 |
+
# Edge labels: Branching conditions
|
316 |
+
edge_labels = nx.get_edge_attributes(self.graph, 'label')
|
317 |
+
|
318 |
+
# Draw components with error handling
|
319 |
+
try:
|
320 |
+
nx.draw_networkx_nodes(self.graph, pos, node_size=2000, node_color='skyblue')
|
321 |
+
except Exception as e:
|
322 |
+
self.log_messages.append(f"Warning: Could not draw nodes: {e}")
|
323 |
+
|
324 |
+
try:
|
325 |
+
nx.draw_networkx_edges(self.graph, pos, width=1.5, arrowsize=20, edge_color='gray')
|
326 |
+
except Exception as e:
|
327 |
+
self.log_messages.append(f"Warning: Could not draw edges: {e}")
|
328 |
+
|
329 |
+
try:
|
330 |
+
nx.draw_networkx_labels(self.graph, pos, labels, font_size=10, font_family='sans-serif')
|
331 |
+
except Exception as e:
|
332 |
+
self.log_messages.append(f"Warning: Could not draw node labels: {e}")
|
333 |
+
|
334 |
+
try:
|
335 |
+
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels,
|
336 |
+
font_size=10, font_family='sans-serif')
|
337 |
+
except Exception as e:
|
338 |
+
self.log_messages.append(f"Warning: Could not draw edge labels: {e}")
|
339 |
+
|
340 |
+
plt.title("Branch and Bound Tree", fontsize=14)
|
341 |
+
plt.axis('off')
|
342 |
+
plt.tight_layout()
|
343 |
+
return fig
|
344 |
+
|
345 |
+
except Exception as e:
|
346 |
+
self.log_messages.append(f"Error creating graph visualization: {e}")
|
347 |
+
# Return a simple error plot
|
348 |
+
fig = plt.figure(figsize=(10, 6))
|
349 |
+
plt.text(0.5, 0.5, f'Error creating visualization:\n{str(e)}',
|
350 |
+
ha='center', va='center', transform=plt.gca().transAxes)
|
351 |
+
plt.title("Branch and Bound Tree (Error)", fontsize=14)
|
352 |
+
plt.axis('off')
|
353 |
+
return fig
|
354 |
|
|
|
355 |
def display_steps_table(self):
|
356 |
+
"""Display the steps in tabular format
|
357 |
+
Returns:
|
358 |
+
str: Formatted table string
|
359 |
+
"""
|
360 |
+
try:
|
361 |
+
if not self.steps_table:
|
362 |
+
return "No steps recorded."
|
363 |
+
|
364 |
+
headers = ["Node", "z", "x", "z*", "x*", "UB", "LB", "Z at end of stage"]
|
365 |
+
return tabulate(self.steps_table, headers=headers, tablefmt="grid")
|
366 |
+
except Exception as e:
|
367 |
+
self.log_messages.append(f"Error creating steps table: {e}")
|
368 |
+
return f"Error displaying steps table: {e}"
|
369 |
+
|
370 |
+
def solve(self, verbose=True, max_iterations=1000):
|
371 |
+
"""Solve the problem using branch and bound
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
372 |
|
373 |
+
Args:
|
374 |
+
verbose: Whether to log detailed information
|
375 |
+
max_iterations: Maximum number of iterations to prevent infinite loops
|
376 |
+
|
377 |
+
Returns:
|
378 |
+
tuple: (best_solution, best_objective, log_messages, visualization_figure)
|
379 |
+
"""
|
380 |
+
self.log_messages = [] # Initialize log for this run
|
381 |
|
382 |
+
try:
|
383 |
+
# Initialize bounds with validation
|
384 |
+
lower_bounds = [0.0] * self.n
|
385 |
+
upper_bounds = [None] * self.n # None means unbounded
|
|
|
386 |
|
387 |
+
# Set upper bounds for binary variables
|
388 |
+
for idx in self.binary_vars:
|
389 |
+
if idx < len(upper_bounds):
|
390 |
+
upper_bounds[idx] = 1.0
|
391 |
+
else:
|
392 |
+
raise IndexError(f"Binary variable index {idx} exceeds problem dimension {self.n}")
|
|
|
393 |
|
394 |
+
# Create a priority queue for nodes
|
395 |
+
node_queue = PriorityQueue()
|
396 |
+
iteration_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
+
# Solve the root relaxation
|
399 |
+
self.log_messages.append("Step 1: Solving root relaxation (continuous problem)")
|
400 |
|
401 |
+
try:
|
402 |
+
x_root, obj_root = self.solve_relaxation(lower_bounds, upper_bounds)
|
403 |
+
except Exception as e:
|
404 |
+
self.log_messages.append(f"Error solving root relaxation: {e}")
|
405 |
+
fig = self.visualize_graph()
|
406 |
+
return None, float('-inf') if self.maximize else float('inf'), self.log_messages, fig
|
407 |
|
408 |
+
if x_root is None:
|
409 |
+
self.log_messages.append("Root problem infeasible")
|
410 |
+
fig = self.visualize_graph()
|
411 |
+
return None, float('-inf') if self.maximize else float('inf'), self.log_messages, fig
|
412 |
|
413 |
+
# Add root node to the graph
|
414 |
+
root_node = "S0"
|
415 |
+
self.add_node_to_graph(root_node, obj_root, x_root)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
416 |
|
417 |
+
self.log_messages.append(f"Root relaxation objective: {obj_root:.6f}")
|
418 |
+
self.log_messages.append(f"Root solution: {x_root}")
|
419 |
|
420 |
+
# Initial upper bound is the root objective
|
421 |
+
upper_bound = obj_root
|
|
|
422 |
|
423 |
+
# Check if the root solution is already integer-feasible
|
424 |
+
if self.is_integer_feasible(x_root):
|
425 |
+
self.log_messages.append("Root solution is integer-feasible! No need for branching.")
|
426 |
+
self.best_solution = x_root.copy()
|
427 |
+
self.best_objective = obj_root
|
|
|
428 |
|
429 |
+
# Add to steps table
|
430 |
+
try:
|
431 |
+
active_nodes_str = "∅" if not self.active_nodes else "{" + ", ".join(self.active_nodes) + "}"
|
432 |
+
self.steps_table.append([
|
433 |
+
root_node, f"{obj_root:.2f}", f"({', '.join([f'{x:.2f}' for x in x_root])})",
|
434 |
+
f"{self.best_objective:.2f}", f"({', '.join([f'{x:.2f}' for x in self.best_solution])})",
|
435 |
+
f"{upper_bound:.2f}", f"{self.best_objective:.2f}", active_nodes_str
|
436 |
+
])
|
437 |
+
except Exception as e:
|
438 |
+
self.log_messages.append(f"Error updating steps table: {e}")
|
439 |
|
440 |
+
steps_table_string = self.display_steps_table()
|
441 |
+
self.log_messages.append(steps_table_string)
|
442 |
+
fig = self.visualize_graph()
|
443 |
+
return self.best_solution, self.best_objective, self.log_messages, fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
|
445 |
+
# Add root node to the queue and active nodes set
|
446 |
+
priority = -obj_root if self.maximize else obj_root
|
447 |
+
node_queue.put((priority, self.nodes_explored, root_node, lower_bounds.copy(), upper_bounds.copy()))
|
448 |
+
self.active_nodes.add(root_node)
|
449 |
|
450 |
+
# Add entry to steps table for root node
|
451 |
+
try:
|
452 |
+
active_nodes_str = "{" + ", ".join(self.active_nodes) + "}"
|
453 |
+
lb_str = "-" if self.best_objective == float('-inf') else f"{self.best_objective:.2f}"
|
454 |
+
x_star_str = "-" if self.best_solution is None else f"({', '.join([f'{x:.2f}' for x in self.best_solution])})"
|
|
|
455 |
|
456 |
+
self.steps_table.append([
|
457 |
+
root_node, f"{obj_root:.2f}", f"({', '.join([f'{x:.2f}' for x in x_root])})",
|
458 |
+
lb_str, x_star_str, f"{upper_bound:.2f}", lb_str, active_nodes_str
|
459 |
+
])
|
460 |
+
except Exception as e:
|
461 |
+
self.log_messages.append(f"Error updating initial steps table: {e}")
|
462 |
+
|
463 |
+
self.log_messages.append("\nStarting branch and bound process:")
|
464 |
+
node_counter = 1
|
465 |
+
|
466 |
+
while not node_queue.empty() and iteration_count < max_iterations:
|
467 |
+
iteration_count += 1
|
468 |
|
469 |
+
try:
|
470 |
+
# Get the node with the highest objective (for maximization)
|
471 |
+
priority, _, node_name, node_lower_bounds, node_upper_bounds = node_queue.get()
|
472 |
+
self.nodes_explored += 1
|
473 |
+
|
474 |
+
self.log_messages.append(f"\nStep {self.nodes_explored + 1}: Exploring node {node_name}")
|
475 |
+
|
476 |
+
# Remove from active nodes
|
477 |
+
if node_name in self.active_nodes:
|
478 |
+
self.active_nodes.remove(node_name)
|
479 |
+
|
480 |
+
# Branch on most fractional variable
|
481 |
+
if node_name not in self.graph.nodes:
|
482 |
+
self.log_messages.append(f"Warning: Node {node_name} not found in graph")
|
483 |
+
continue
|
484 |
+
|
485 |
+
current_solution = self.graph.nodes[node_name].get('x')
|
486 |
+
if current_solution is None:
|
487 |
+
self.log_messages.append(f"Warning: No solution found for node {node_name}")
|
488 |
+
continue
|
489 |
+
|
490 |
+
branch_var = self.get_branching_variable(current_solution)
|
491 |
+
if branch_var == -1:
|
492 |
+
self.log_messages.append(f"Warning: No branching variable found for node {node_name}")
|
493 |
+
continue
|
494 |
+
|
495 |
+
branch_val = current_solution[branch_var]
|
496 |
+
|
497 |
+
# Process branches with enhanced error handling
|
498 |
+
self._process_branches(node_name, branch_var, branch_val, node_lower_bounds,
|
499 |
+
node_upper_bounds, node_queue, node_counter)
|
500 |
+
node_counter += 2 # Two new nodes created
|
501 |
+
|
502 |
+
# Update upper bound
|
503 |
+
if not node_queue.empty():
|
504 |
+
next_priority = node_queue.queue[0][0]
|
505 |
+
upper_bound = -next_priority if self.maximize else next_priority
|
506 |
+
else:
|
507 |
+
upper_bound = self.best_objective
|
508 |
+
|
509 |
+
# Add to steps table
|
510 |
+
self._add_step_to_table(node_name, upper_bound)
|
511 |
+
|
512 |
+
except Exception as e:
|
513 |
+
self.log_messages.append(f"Error processing node {node_name if 'node_name' in locals() else 'unknown'}: {e}")
|
514 |
+
continue
|
515 |
+
|
516 |
+
# Check for max iterations exceeded
|
517 |
+
if iteration_count >= max_iterations:
|
518 |
+
self.log_messages.append(f"Warning: Maximum iterations ({max_iterations}) reached. Solution may not be optimal.")
|
519 |
+
|
520 |
+
self.log_messages.append("\nBranch and bound completed!")
|
521 |
+
self.log_messages.append(f"Nodes explored: {self.nodes_explored}")
|
522 |
+
self.log_messages.append(f"Iterations: {iteration_count}")
|
523 |
+
|
524 |
+
if self.best_solution is not None:
|
525 |
+
self.log_messages.append(f"Optimal objective: {self.best_objective:.6f}")
|
526 |
+
self.log_messages.append(f"Optimal solution: {self.best_solution}")
|
527 |
else:
|
528 |
+
self.log_messages.append("No feasible integer solution found")
|
529 |
+
|
530 |
+
# Append steps table string to log
|
531 |
+
steps_table_string = self.display_steps_table()
|
532 |
+
self.log_messages.append(steps_table_string)
|
533 |
+
|
534 |
+
# Visualize the graph
|
535 |
+
fig = self.visualize_graph()
|
536 |
+
|
537 |
+
return self.best_solution, self.best_objective, self.log_messages, fig
|
538 |
+
|
539 |
+
except Exception as e:
|
540 |
+
self.log_messages.append(f"Critical error in solve method: {e}")
|
541 |
+
fig = self.visualize_graph()
|
542 |
+
return None, float('-inf') if self.maximize else float('inf'), self.log_messages, fig
|
543 |
+
|
544 |
+
|
545 |
+
def solve_branch_and_bound_interface(c_str, A_str, b_str, integer_vars_str, binary_vars_str, maximize_bool):
|
546 |
+
"""
|
547 |
+
Enhanced wrapper function to connect BranchAndBoundSolver with Gradio interface.
|
548 |
+
Provides comprehensive input validation, error handling, and user-friendly error messages.
|
549 |
+
"""
|
550 |
+
log_messages = []
|
551 |
+
|
552 |
+
try:
|
553 |
+
# Enhanced input validation with detailed error messages
|
554 |
+
log_messages.append("Starting input validation...")
|
555 |
+
|
556 |
+
# Validate and parse objective coefficients
|
557 |
+
if not c_str or not c_str.strip():
|
558 |
+
log_messages.append("Error: Objective coefficients (c) cannot be empty. Please provide comma-separated values (e.g., '3,2').")
|
559 |
+
return "Error: Empty objective coefficients", "Error: Invalid input", "\n".join(log_messages), None
|
560 |
+
|
561 |
+
try:
|
562 |
+
c = parse_vector(c_str)
|
563 |
+
if not c or len(c) == 0:
|
564 |
+
log_messages.append("Error: Objective coefficients (c) could not be parsed or resulted in empty vector.")
|
565 |
+
log_messages.append("Please ensure format is comma-separated numbers (e.g., '3,2,1').")
|
566 |
+
return "Error parsing c", "Error parsing c", "\n".join(log_messages), None
|
567 |
+
except Exception as e:
|
568 |
+
log_messages.append(f"Error parsing objective coefficients (c): {str(e)}")
|
569 |
+
log_messages.append("Expected format: comma-separated numbers (e.g., '3,2,1')")
|
570 |
+
return "Error parsing c", "Error parsing c", "\n".join(log_messages), None
|
571 |
+
|
572 |
+
# Validate and parse constraint matrix
|
573 |
+
if not A_str or not A_str.strip():
|
574 |
+
log_messages.append("Error: Constraint matrix (A) cannot be empty. Please provide semicolon-separated rows with comma-separated values.")
|
575 |
+
log_messages.append("Example format: '1,2;3,4' for a 2x2 matrix.")
|
576 |
+
return "Error: Empty constraint matrix", "Error: Invalid input", "\n".join(log_messages), None
|
577 |
+
|
578 |
+
try:
|
579 |
+
A = parse_matrix(A_str)
|
580 |
+
if A.size == 0:
|
581 |
+
log_messages.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
582 |
+
log_messages.append("Expected format: rows separated by ';', elements by ',' (e.g., '1,2;3,4')")
|
583 |
+
return "Error parsing A", "Error parsing A", "\n".join(log_messages), None
|
584 |
+
except Exception as e:
|
585 |
+
log_messages.append(f"Error parsing constraint matrix (A): {str(e)}")
|
586 |
+
log_messages.append("Expected format: rows separated by ';', elements by ',' (e.g., '1,2;3,4')")
|
587 |
+
return "Error parsing A", "Error parsing A", "\n".join(log_messages), None
|
588 |
+
|
589 |
+
# Validate and parse constraint bounds
|
590 |
+
if not b_str or not b_str.strip():
|
591 |
+
log_messages.append("Error: Constraint bounds (b) cannot be empty. Please provide comma-separated values.")
|
592 |
+
log_messages.append("Example format: '10,8,3' for three constraints.")
|
593 |
+
return "Error: Empty constraint bounds", "Error: Invalid input", "\n".join(log_messages), None
|
594 |
+
|
595 |
+
try:
|
596 |
+
b = parse_vector(b_str)
|
597 |
+
if not b or len(b) == 0:
|
598 |
+
log_messages.append("Error: Constraint bounds (b) could not be parsed or resulted in empty vector.")
|
599 |
+
log_messages.append("Expected format: comma-separated numbers (e.g., '10,8,3')")
|
600 |
+
return "Error parsing b", "Error parsing b", "\n".join(log_messages), None
|
601 |
+
except Exception as e:
|
602 |
+
log_messages.append(f"Error parsing constraint bounds (b): {str(e)}")
|
603 |
+
log_messages.append("Expected format: comma-separated numbers (e.g., '10,8,3')")
|
604 |
+
return "Error parsing b", "Error parsing b", "\n".join(log_messages), None
|
605 |
+
|
606 |
+
# Enhanced dimension validation
|
607 |
+
if A.shape[0] != len(b):
|
608 |
+
log_messages.append(f"Error: Dimension mismatch between constraints matrix and bounds.")
|
609 |
+
log_messages.append(f"Matrix A has {A.shape[0]} rows but vector b has {len(b)} elements.")
|
610 |
+
log_messages.append("Each row in A represents one constraint, so A and b must have matching dimensions.")
|
611 |
+
return "Dimension mismatch A vs b", "Dimension mismatch A vs b", "\n".join(log_messages), None
|
612 |
+
|
613 |
+
if A.shape[1] != len(c):
|
614 |
+
log_messages.append(f"Error: Dimension mismatch between objective and constraints.")
|
615 |
+
log_messages.append(f"Matrix A has {A.shape[1]} columns but objective vector c has {len(c)} elements.")
|
616 |
+
log_messages.append("Each column in A represents one variable, so A and c must have matching dimensions.")
|
617 |
+
return "Dimension mismatch A vs c", "Dimension mismatch A vs c", "\n".join(log_messages), None
|
618 |
+
|
619 |
+
# Enhanced integer variables parsing with better error handling
|
620 |
+
integer_vars = []
|
621 |
+
if integer_vars_str and integer_vars_str.strip():
|
622 |
+
try:
|
623 |
+
# Handle special case for "all"
|
624 |
+
if integer_vars_str.strip().lower() == "all":
|
625 |
+
integer_vars = list(range(len(c)))
|
626 |
+
log_messages.append(f"All {len(c)} variables set as integer variables.")
|
627 |
+
else:
|
628 |
+
integer_vars = [int(x.strip()) for x in integer_vars_str.split(',') if x.strip()]
|
629 |
+
if not integer_vars:
|
630 |
+
log_messages.append("Warning: Integer variables string provided but no valid indices found.")
|
631 |
+
elif not all(0 <= i < len(c) for i in integer_vars):
|
632 |
+
invalid_indices = [i for i in integer_vars if not (0 <= i < len(c))]
|
633 |
+
log_messages.append(f"Error: Integer variable indices out of bounds: {invalid_indices}")
|
634 |
+
log_messages.append(f"Valid range is 0 to {len(c)-1} (0-indexed).")
|
635 |
+
return "Error: Invalid integer variable indices", "Error: Invalid input", "\n".join(log_messages), None
|
636 |
+
elif len(set(integer_vars)) != len(integer_vars):
|
637 |
+
duplicates = [i for i in set(integer_vars) if integer_vars.count(i) > 1]
|
638 |
+
log_messages.append(f"Warning: Duplicate integer variable indices found: {duplicates}. Removing duplicates.")
|
639 |
+
integer_vars = list(set(integer_vars))
|
640 |
+
except ValueError as e:
|
641 |
+
log_messages.append(f"Error parsing integer variable indices: {str(e)}")
|
642 |
+
log_messages.append("Please use comma-separated 0-indexed integers (e.g., '0,1,2') or 'all' for all variables.")
|
643 |
+
return "Error parsing integer_vars", "Error parsing integer_vars", "\n".join(log_messages), None
|
644 |
+
|
645 |
+
# Enhanced binary variables parsing with better error handling
|
646 |
+
binary_vars = []
|
647 |
+
if binary_vars_str and binary_vars_str.strip():
|
648 |
+
try:
|
649 |
+
binary_vars = [int(x.strip()) for x in binary_vars_str.split(',') if x.strip()]
|
650 |
+
if not binary_vars:
|
651 |
+
log_messages.append("Warning: Binary variables string provided but no valid indices found.")
|
652 |
+
elif not all(0 <= i < len(c) for i in binary_vars):
|
653 |
+
invalid_indices = [i for i in binary_vars if not (0 <= i < len(c))]
|
654 |
+
log_messages.append(f"Error: Binary variable indices out of bounds: {invalid_indices}")
|
655 |
+
log_messages.append(f"Valid range is 0 to {len(c)-1} (0-indexed).")
|
656 |
+
return "Error: Invalid binary variable indices", "Error: Invalid input", "\n".join(log_messages), None
|
657 |
+
elif len(set(binary_vars)) != len(binary_vars):
|
658 |
+
duplicates = [i for i in set(binary_vars) if binary_vars.count(i) > 1]
|
659 |
+
log_messages.append(f"Warning: Duplicate binary variable indices found: {duplicates}. Removing duplicates.")
|
660 |
+
binary_vars = list(set(binary_vars))
|
661 |
+
except ValueError as e:
|
662 |
+
log_messages.append(f"Error parsing binary variable indices: {str(e)}")
|
663 |
+
log_messages.append("Please use comma-separated 0-indexed integers (e.g., '0,1,2').")
|
664 |
+
return "Error parsing binary_vars", "Error parsing binary_vars", "\n".join(log_messages), None
|
665 |
+
|
666 |
+
# Check for overlap between integer and binary variables
|
667 |
+
if integer_vars and binary_vars:
|
668 |
+
overlap = set(integer_vars) & set(binary_vars)
|
669 |
+
if overlap:
|
670 |
+
log_messages.append(f"Warning: Variables {list(overlap)} are specified as both integer and binary.")
|
671 |
+
log_messages.append("Binary variables are automatically treated as integer. Removing from integer list.")
|
672 |
+
integer_vars = [i for i in integer_vars if i not in overlap]
|
673 |
+
|
674 |
+
# Log successful parsing
|
675 |
+
log_messages.append("✓ Input validation completed successfully.")
|
676 |
+
log_messages.append(f"✓ Problem size: {len(c)} variables, {A.shape[0]} constraints")
|
677 |
+
if integer_vars:
|
678 |
+
log_messages.append(f"✓ Integer variables: {integer_vars}")
|
679 |
+
if binary_vars:
|
680 |
+
log_messages.append(f"✓ Binary variables: {binary_vars}")
|
681 |
+
log_messages.append(f"✓ Optimization direction: {'Maximize' if maximize_bool else 'Minimize'}")
|
682 |
+
log_messages.append("Starting solver...")
|
683 |
+
|
684 |
+
# Create solver with enhanced error handling
|
685 |
+
try:
|
686 |
+
solver = BranchAndBoundSolver(
|
687 |
+
c=np.array(c),
|
688 |
+
A=A,
|
689 |
+
b=np.array(b),
|
690 |
+
integer_vars=integer_vars if integer_vars else None,
|
691 |
+
binary_vars=binary_vars if binary_vars else None,
|
692 |
+
maximize=maximize_bool
|
693 |
+
)
|
694 |
+
except Exception as e:
|
695 |
+
log_messages.append(f"Error creating solver instance: {str(e)}")
|
696 |
+
log_messages.append("This could indicate incompatible problem dimensions or invalid parameter values.")
|
697 |
+
return "Solver creation error", "Solver creation error", "\n".join(log_messages), None
|
698 |
+
|
699 |
+
# Solve with enhanced error handling
|
700 |
+
try:
|
701 |
+
best_solution, best_objective, solver_log, fig = solver.solve()
|
702 |
+
log_messages.extend(solver_log) # Add solver's internal log
|
703 |
+
except Exception as e:
|
704 |
+
log_messages.append(f"Error during solving: {str(e)}")
|
705 |
+
log_messages.append("The solver encountered an unexpected error. Check input validity and problem formulation.")
|
706 |
+
# Try to get partial results
|
707 |
+
try:
|
708 |
+
fig = solver.visualize_graph() if hasattr(solver, 'visualize_graph') else None
|
709 |
+
except:
|
710 |
+
fig = None
|
711 |
+
return "Solver execution error", "Solver execution error", "\n".join(log_messages), fig
|
712 |
+
|
713 |
+
# Enhanced result formatting with better error handling
|
714 |
+
try:
|
715 |
+
if best_solution is not None:
|
716 |
+
solution_str = ", ".join([f"{val:.6f}" for val in best_solution])
|
717 |
+
objective_str = f"{best_objective:.6f}"
|
718 |
+
log_messages.append(f"✓ Optimal solution found: x = ({solution_str})")
|
719 |
+
log_messages.append(f"✓ Optimal objective value: {objective_str}")
|
720 |
+
else:
|
721 |
+
solution_str = "No feasible integer solution found."
|
722 |
+
if best_objective == float('-inf'):
|
723 |
+
objective_str = "Unbounded (maximization)"
|
724 |
+
log_messages.append("Problem appears to be unbounded for maximization.")
|
725 |
+
elif best_objective == float('inf'):
|
726 |
+
objective_str = "Unbounded (minimization)"
|
727 |
+
log_messages.append("Problem appears to be unbounded for minimization.")
|
728 |
+
else:
|
729 |
+
objective_str = "Infeasible"
|
730 |
+
log_messages.append("No feasible solution exists for the given constraints.")
|
731 |
+
except Exception as e:
|
732 |
+
log_messages.append(f"Error formatting results: {str(e)}")
|
733 |
+
solution_str = "Result formatting error"
|
734 |
+
objective_str = "Result formatting error"
|
735 |
+
|
736 |
+
return solution_str, objective_str, "\n".join(log_messages), fig
|
737 |
+
|
738 |
+
except Exception as e:
|
739 |
+
# Catch-all error handler for unexpected exceptions
|
740 |
+
log_messages.append(f"Unexpected error in interface function: {str(e)}")
|
741 |
+
log_messages.append("Please check your input format and try again.")
|
742 |
+
log_messages.append("If the problem persists, there may be an issue with the solver implementation.")
|
743 |
+
return "Unexpected error", "Unexpected error", "\n".join(log_messages), None
|
744 |
+
|
745 |
+
|
746 |
+
branch_and_bound_interface = gr.Interface(
|
747 |
+
fn=solve_branch_and_bound_interface,
|
748 |
+
inputs=[
|
749 |
+
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated, e.g., 3,2"),
|
750 |
+
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 2,1; 1,1; 1,0"),
|
751 |
+
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 10,8,3. Must be Ax <= b form."),
|
752 |
+
gr.Textbox(label="Integer Variable Indices (optional)", info="Comma-separated, 0-indexed. If empty and no binary vars, all vars are continuous for B&B (effectively LP). If empty but binary vars exist, only binary are integer. If 'all', all vars are integer."), # Adjusted info
|
753 |
+
gr.Textbox(label="Binary Variable Indices (optional)", info="Comma-separated, 0-indexed, e.g., 0,1"),
|
754 |
+
gr.Checkbox(label="Maximize?", value=True)
|
755 |
+
],
|
756 |
+
outputs=[
|
757 |
+
gr.Textbox(label="Optimal Solution (x)"),
|
758 |
+
gr.Textbox(label="Optimal Objective Value (z)"),
|
759 |
+
gr.Textbox(label="Solver Log and Steps", lines=15, interactive=False),
|
760 |
+
gr.Plot(label="Branch and Bound Tree")
|
761 |
+
],
|
762 |
+
title="Branch and Bound Solver for Mixed Integer Linear Programs (MILP)",
|
763 |
+
description="Solves MILPs (Ax <= b) using the Branch and Bound method. Specify integer/binary variables or leave empty if not applicable (for pure LP).",
|
764 |
+
examples=[
|
765 |
+
[ # Example 1: Knapsack-like problem (from a common example)
|
766 |
+
"8,11,6,4", # c: Objective (maximize)
|
767 |
+
"5,7,4,3; 1,1,1,1", # A: Constraints matrix
|
768 |
+
"14, 4", # b: Constraints RHS
|
769 |
+
"", # integer_vars: all variables are effectively integer due to binary constraint if specified, or by default if integer_vars=None
|
770 |
+
"0,1,2,3", # binary_vars: All variables are binary
|
771 |
+
True # Maximize
|
772 |
+
],
|
773 |
+
[ # Example 2: Simple MILP
|
774 |
+
"3,2,4", # c
|
775 |
+
"1,1,1; 2,1,0", # A
|
776 |
+
"10,5", # b
|
777 |
+
"0,2", # integer_vars: x0 and x2 are integer
|
778 |
+
"", # binary_vars
|
779 |
+
True # Maximize
|
780 |
+
],
|
781 |
+
[ # Example 3: Minimization problem (from a common example)
|
782 |
+
"3,5", # c (minimize)
|
783 |
+
"-1,0; 0,-1; 3,2", # A (original constraints might be >=, converted to <= by multiplying with -1)
|
784 |
+
"0,0,18", # b (original might be >=0, >=0, <=18. For >=0, we write -x <= 0)
|
785 |
+
"0,1", # integer_vars
|
786 |
+
"", # binary_vars
|
787 |
+
False # Minimize
|
788 |
+
],
|
789 |
+
[ # Example 4: Provided in task description
|
790 |
+
"5,4", #c
|
791 |
+
"1,1;2,0;0,1", #A
|
792 |
+
"5,6,3", #b
|
793 |
+
"0,1", #integer
|
794 |
+
"", #binary
|
795 |
+
True #maximize
|
796 |
+
]
|
797 |
+
],
|
798 |
+
flagging_mode="manual"
|
799 |
+
)
|
800 |
+
|
@@ -2,6 +2,27 @@ import numpy as np
|
|
2 |
import sys
|
3 |
from .solve_lp_via_dual import solve_lp_via_dual
|
4 |
from .solve_primal_directly import solve_primal_directly
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
TOLERANCE = 1e-9
|
7 |
|
@@ -440,3 +461,120 @@ class DualSimplexSolver:
|
|
440 |
|
441 |
return objective_str, solution_details_str
|
442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import sys
|
3 |
from .solve_lp_via_dual import solve_lp_via_dual
|
4 |
from .solve_primal_directly import solve_primal_directly
|
5 |
+
import gradio as gr
|
6 |
+
from maths.operations_research.utils import parse_matrix
|
7 |
+
from maths.operations_research.utils import parse_vector
|
8 |
+
|
9 |
+
|
10 |
+
def parse_relations(input_str: str) -> list[str]:
|
11 |
+
"""Parses a comma-separated string of relations into a list of strings."""
|
12 |
+
if not input_str:
|
13 |
+
return []
|
14 |
+
try:
|
15 |
+
relations = [r.strip() for r in input_str.split(',')]
|
16 |
+
valid_relations = {"<=", ">=", "="}
|
17 |
+
if not all(r in valid_relations for r in relations):
|
18 |
+
invalid_rels = [r for r in relations if r not in valid_relations]
|
19 |
+
gr.Warning(f"Invalid relation(s) found: {', '.join(invalid_rels)}. Allowed relations are: '<=', '>=', '='.")
|
20 |
+
return []
|
21 |
+
return relations
|
22 |
+
except Exception as e: # Catch any other unexpected errors during parsing
|
23 |
+
gr.Warning(f"Error parsing relations: '{input_str}'. Error: {e}")
|
24 |
+
return []
|
25 |
+
|
26 |
|
27 |
TOLERANCE = 1e-9
|
28 |
|
|
|
461 |
|
462 |
return objective_str, solution_details_str
|
463 |
|
464 |
+
|
465 |
+
def solve_dual_simplex_interface(objective_type_str, c_str, A_str, relations_str, b_str):
|
466 |
+
"""
|
467 |
+
Wrapper function to connect DualSimplexSolver with Gradio interface.
|
468 |
+
"""
|
469 |
+
current_log = ["Initializing Dual Simplex Solver Interface..."]
|
470 |
+
|
471 |
+
c = parse_vector(c_str)
|
472 |
+
if not c:
|
473 |
+
current_log.append("Error: Objective coefficients (c) could not be parsed or are empty.")
|
474 |
+
return "Error parsing c", "Error parsing c", "\n".join(current_log)
|
475 |
+
|
476 |
+
A = parse_matrix(A_str)
|
477 |
+
if A.size == 0:
|
478 |
+
current_log.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
479 |
+
return "Error parsing A", "Error parsing A", "\n".join(current_log)
|
480 |
+
|
481 |
+
b = parse_vector(b_str)
|
482 |
+
if not b:
|
483 |
+
current_log.append("Error: Constraint bounds (b) could not be parsed or are empty.")
|
484 |
+
return "Error parsing b", "Error parsing b", "\n".join(current_log)
|
485 |
+
|
486 |
+
relations = parse_relations(relations_str)
|
487 |
+
if not relations:
|
488 |
+
current_log.append("Error: Constraint relations could not be parsed, are empty, or contain invalid symbols.")
|
489 |
+
return "Error parsing relations", "Error parsing relations", "\n".join(current_log)
|
490 |
+
|
491 |
+
# Basic dimensional validation
|
492 |
+
if A.shape[0] != len(b):
|
493 |
+
current_log.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of b ({len(b)}).")
|
494 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
495 |
+
if A.shape[1] != len(c):
|
496 |
+
current_log.append(f"Dimension mismatch: Number of columns in A ({A.shape[1]}) must equal length of c ({len(c)}).")
|
497 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
498 |
+
if A.shape[0] != len(relations):
|
499 |
+
current_log.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of relations ({len(relations)}).")
|
500 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
501 |
+
|
502 |
+
current_log.append("Inputs parsed and validated successfully.")
|
503 |
+
|
504 |
+
try:
|
505 |
+
solver = DualSimplexSolver(objective_type_str, c, A, relations, b)
|
506 |
+
current_log.append("DualSimplexSolver instantiated.")
|
507 |
+
|
508 |
+
# The solve method now returns: final_solution_str, final_objective_str, log_messages, is_fallback_used_str
|
509 |
+
solution_str, objective_str, solver_log_messages, fallback_info = solver.solve()
|
510 |
+
|
511 |
+
current_log.extend(solver_log_messages)
|
512 |
+
current_log.append(f"Fallback Status: {fallback_info}")
|
513 |
+
|
514 |
+
return solution_str, objective_str, "\n".join(current_log)
|
515 |
+
|
516 |
+
except Exception as e:
|
517 |
+
gr.Error(f"An error occurred during solving with Dual Simplex: {e}")
|
518 |
+
current_log.append(f"Runtime error in Dual Simplex: {e}")
|
519 |
+
return "Solver error", "Solver error", "\n".join(current_log)
|
520 |
+
|
521 |
+
dual_simplex_interface = gr.Interface(
|
522 |
+
fn=solve_dual_simplex_interface,
|
523 |
+
inputs=[
|
524 |
+
gr.Radio(label="Objective Type", choices=["max", "min"], value="max"),
|
525 |
+
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated, e.g., 4,1"),
|
526 |
+
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 3,1; 4,3; 1,2"),
|
527 |
+
gr.Textbox(label="Constraint Relations", info="Comma-separated, e.g., >=,>=,>="), # Dual simplex typically starts from Ax >= b for max problems if to be converted to <=
|
528 |
+
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 3,6,4")
|
529 |
+
],
|
530 |
+
outputs=[
|
531 |
+
gr.Textbox(label="Optimal Solution (Variables)"),
|
532 |
+
gr.Textbox(label="Optimal Objective Value"),
|
533 |
+
gr.Textbox(label="Solver Log, Tableau Steps, and Fallback Info", lines=15, interactive=False)
|
534 |
+
],
|
535 |
+
title="Dual Simplex Solver for Linear Programs (LP)",
|
536 |
+
description="Solves LPs using the Dual Simplex method. This method is often efficient when an initial basic solution is dual feasible but primal infeasible (e.g. after adding cuts). Input Ax R b where R can be '>=', '<=', or '='.",
|
537 |
+
examples=[
|
538 |
+
[ # Example 1: Max problem, standard form for dual simplex often has >= constraints initially
|
539 |
+
# Maximize Z = 4x1 + x2
|
540 |
+
# Subject to:
|
541 |
+
# 3x1 + x2 >= 3 --> -3x1 - x2 <= -3
|
542 |
+
# 4x1 + 3x2 >= 6 --> -4x1 - 3x2 <= -6
|
543 |
+
# x1 + 2x2 >= 4 --> -x1 - 2x2 <= -4 (Mistake in common example, should be <= to be interesting for dual or needs specific setup)
|
544 |
+
# Let's use a more typical dual simplex starting point:
|
545 |
+
# Min C = 2x1 + x2 (so Max -2x1 -x2)
|
546 |
+
# s.t. x1 + x2 >= 5
|
547 |
+
# 2x1 + x2 >= 6
|
548 |
+
# x1, x2 >=0
|
549 |
+
# Becomes: Max Z' = -2x1 -x2
|
550 |
+
# -x1 -x2 <= -5
|
551 |
+
# -2x1 -x2 <= -6
|
552 |
+
"max", "-2,-1", "-1,-1;-2,-1", "<=,<=", "-5,-6" # This is already in <= form, good for dual if RHS is neg.
|
553 |
+
],
|
554 |
+
[ # Example 2: (Taken from a standard textbook example for Dual Simplex)
|
555 |
+
# Minimize Z = 3x1 + 2x2 + x3
|
556 |
+
# Subject to:
|
557 |
+
# 3x1 + x2 + x3 >= 3
|
558 |
+
# -3x1 + 3x2 + x3 >= 6
|
559 |
+
# x1 + x2 + x3 <= 3 (This constraint makes it interesting)
|
560 |
+
# x1,x2,x3 >=0
|
561 |
+
# For Gradio: obj_type='min', c="3,2,1", A="3,1,1;-3,3,1;1,1,1", relations=">=,>=,<=", b="3,6,3"
|
562 |
+
"min", "3,2,1", "3,1,1;-3,3,1;1,1,1", ">=,>=,<=", "3,6,3"
|
563 |
+
],
|
564 |
+
[ # Example from problem description (slightly modified for typical dual simplex)
|
565 |
+
# Maximize Z = 3x1 + 2x2
|
566 |
+
# Subject to:
|
567 |
+
# 2x1 + x2 <= 18 (Original)
|
568 |
+
# x1 + x2 <= 12 (Original)
|
569 |
+
# x1 <= 5 (Original)
|
570 |
+
# To make it a dual simplex start, we might have transformed it from something else,
|
571 |
+
# or expect some RHS to be negative after initial setup.
|
572 |
+
# For a direct input that might be dual feasible but primal infeasible:
|
573 |
+
# Max Z = x1 + x2
|
574 |
+
# s.t. -2x1 - x2 <= -10 (i.e. 2x1 + x2 >= 10)
|
575 |
+
# -x1 - 2x2 <= -10 (i.e. x1 + 2x2 >= 10)
|
576 |
+
"max", "1,1", "-2,-1;-1,-2", "<=,<=", "-10,-10"
|
577 |
+
]
|
578 |
+
],
|
579 |
+
flagging_mode="manual"
|
580 |
+
)
|
@@ -1,481 +0,0 @@
|
|
1 |
-
"""Gradio interfaces for Operations Research solvers."""
|
2 |
-
import gradio as gr
|
3 |
-
import numpy as np
|
4 |
-
|
5 |
-
def parse_vector(input_str: str, dtype=float) -> list:
|
6 |
-
"""Parses a comma-separated string into a list of numbers."""
|
7 |
-
if not input_str:
|
8 |
-
return []
|
9 |
-
try:
|
10 |
-
return [dtype(x.strip()) for x in input_str.split(',')]
|
11 |
-
except ValueError:
|
12 |
-
gr.Warning(f"Could not parse vector: '{input_str}'. Please use comma-separated numbers (e.g., '1,2,3.5').")
|
13 |
-
return []
|
14 |
-
|
15 |
-
def parse_matrix(input_str: str) -> np.ndarray:
|
16 |
-
"""Parses a string (rows separated by semicolons, elements by commas) into a NumPy array."""
|
17 |
-
if not input_str:
|
18 |
-
return np.array([])
|
19 |
-
try:
|
20 |
-
rows = input_str.split(';')
|
21 |
-
matrix = []
|
22 |
-
num_cols = -1
|
23 |
-
for i, row_str in enumerate(rows):
|
24 |
-
if not row_str.strip(): continue # Allow empty rows if they result from trailing semicolons
|
25 |
-
|
26 |
-
row = [float(x.strip()) for x in row_str.split(',')]
|
27 |
-
if num_cols == -1:
|
28 |
-
num_cols = len(row)
|
29 |
-
elif len(row) != num_cols:
|
30 |
-
raise ValueError(f"Row {i+1} has {len(row)} elements, expected {num_cols}.")
|
31 |
-
matrix.append(row)
|
32 |
-
|
33 |
-
if not matrix: # If all rows were empty or input_str was just ';'
|
34 |
-
return np.array([])
|
35 |
-
|
36 |
-
return np.array(matrix)
|
37 |
-
except ValueError as e:
|
38 |
-
gr.Warning(f"Could not parse matrix: '{input_str}'. Error: {e}. Expected format: '1,2;3,4'. Ensure all rows have the same number of columns.")
|
39 |
-
return np.array([])
|
40 |
-
|
41 |
-
def parse_relations(input_str: str) -> list[str]:
|
42 |
-
"""Parses a comma-separated string of relations into a list of strings."""
|
43 |
-
if not input_str:
|
44 |
-
return []
|
45 |
-
try:
|
46 |
-
relations = [r.strip() for r in input_str.split(',')]
|
47 |
-
valid_relations = {"<=", ">=", "="}
|
48 |
-
if not all(r in valid_relations for r in relations):
|
49 |
-
invalid_rels = [r for r in relations if r not in valid_relations]
|
50 |
-
gr.Warning(f"Invalid relation(s) found: {', '.join(invalid_rels)}. Allowed relations are: '<=', '>=', '='.")
|
51 |
-
return []
|
52 |
-
return relations
|
53 |
-
except Exception as e: # Catch any other unexpected errors during parsing
|
54 |
-
gr.Warning(f"Error parsing relations: '{input_str}'. Error: {e}")
|
55 |
-
return []
|
56 |
-
|
57 |
-
def parse_bounds(input_str: str) -> list[tuple]:
|
58 |
-
"""
|
59 |
-
Parses a string representing variable bounds into a list of tuples.
|
60 |
-
Format: "lower1,upper1; lower2,upper2; ..." (e.g., "0,None; 0,10; None,None")
|
61 |
-
'None' (case-insensitive) is used for no bound.
|
62 |
-
"""
|
63 |
-
if not input_str:
|
64 |
-
return []
|
65 |
-
bounds_list = []
|
66 |
-
try:
|
67 |
-
pairs = input_str.split(';')
|
68 |
-
for i, pair_str in enumerate(pairs):
|
69 |
-
if not pair_str.strip(): continue # Allow for trailing semicolons or empty entries
|
70 |
-
|
71 |
-
parts = pair_str.split(',')
|
72 |
-
if len(parts) != 2:
|
73 |
-
raise ValueError(f"Bound pair '{pair_str}' (entry {i+1}) does not have two elements. Expected format 'lower,upper'.")
|
74 |
-
|
75 |
-
lower_str, upper_str = parts[0].strip().lower(), parts[1].strip().lower()
|
76 |
-
|
77 |
-
lower = None if lower_str == 'none' else float(lower_str)
|
78 |
-
upper = None if upper_str == 'none' else float(upper_str)
|
79 |
-
|
80 |
-
if lower is not None and upper is not None and lower > upper:
|
81 |
-
raise ValueError(f"Lower bound {lower} cannot be greater than upper bound {upper} for pair '{pair_str}' (entry {i+1}).")
|
82 |
-
|
83 |
-
bounds_list.append((lower, upper))
|
84 |
-
return bounds_list
|
85 |
-
except ValueError as e:
|
86 |
-
gr.Warning(f"Could not parse bounds: '{input_str}'. Error: {e}. Expected format: 'lower1,upper1; lower2,upper2; ...' e.g., '0,None; 0,10'. Use 'None' for no bound.")
|
87 |
-
return []
|
88 |
-
except Exception as e: # Catch any other unexpected parsing errors
|
89 |
-
gr.Warning(f"Unexpected error parsing bounds: '{input_str}'. Error: {e}")
|
90 |
-
return []
|
91 |
-
|
92 |
-
# --- Branch and Bound Interface ---
|
93 |
-
from .BranchAndBoundSolver import BranchAndBoundSolver
|
94 |
-
|
95 |
-
def solve_branch_and_bound_interface(c_str, A_str, b_str, integer_vars_str, binary_vars_str, maximize_bool):
|
96 |
-
"""
|
97 |
-
Wrapper function to connect BranchAndBoundSolver with Gradio interface.
|
98 |
-
"""
|
99 |
-
log_messages = []
|
100 |
-
|
101 |
-
c = parse_vector(c_str)
|
102 |
-
if not c:
|
103 |
-
log_messages.append("Error: Objective coefficients (c) could not be parsed or are empty.")
|
104 |
-
return "Error parsing c", "Error parsing c", "\n".join(log_messages), None
|
105 |
-
|
106 |
-
A = parse_matrix(A_str)
|
107 |
-
if A.size == 0: # Check if array is empty
|
108 |
-
log_messages.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
109 |
-
return "Error parsing A", "Error parsing A", "\n".join(log_messages), None
|
110 |
-
|
111 |
-
b = parse_vector(b_str)
|
112 |
-
if not b:
|
113 |
-
log_messages.append("Error: Constraint bounds (b) could not be parsed or are empty.")
|
114 |
-
return "Error parsing b", "Error parsing b", "\n".join(log_messages), None
|
115 |
-
|
116 |
-
# Validate dimensions
|
117 |
-
if A.shape[0] != len(b):
|
118 |
-
log_messages.append(f"Error: Number of rows in A ({A.shape[0]}) does not match number of elements in b ({len(b)}).")
|
119 |
-
return "Dimension mismatch A vs b", "Dimension mismatch A vs b", "\n".join(log_messages), None
|
120 |
-
if A.shape[1] != len(c):
|
121 |
-
log_messages.append(f"Error: Number of columns in A ({A.shape[1]}) does not match number of elements in c ({len(c)}).")
|
122 |
-
return "Dimension mismatch A vs c", "Dimension mismatch A vs c", "\n".join(log_messages), None
|
123 |
-
|
124 |
-
integer_vars = []
|
125 |
-
if integer_vars_str:
|
126 |
-
try:
|
127 |
-
integer_vars = [int(x.strip()) for x in integer_vars_str.split(',')]
|
128 |
-
if not all(0 <= i < len(c) for i in integer_vars):
|
129 |
-
raise ValueError("Integer variable indices out of bounds.")
|
130 |
-
except ValueError as e:
|
131 |
-
log_messages.append(f"Error parsing integer variable indices: {e}. Please use comma-separated 0-indexed integers (e.g., '0,1').")
|
132 |
-
return "Error parsing integer_vars", "Error parsing integer_vars", "\n".join(log_messages), None
|
133 |
-
|
134 |
-
binary_vars = []
|
135 |
-
if binary_vars_str:
|
136 |
-
try:
|
137 |
-
binary_vars = [int(x.strip()) for x in binary_vars_str.split(',')]
|
138 |
-
if not all(0 <= i < len(c) for i in binary_vars):
|
139 |
-
raise ValueError("Binary variable indices out of bounds.")
|
140 |
-
except ValueError as e:
|
141 |
-
log_messages.append(f"Error parsing binary variable indices: {e}. Please use comma-separated 0-indexed integers (e.g., '0,1').")
|
142 |
-
return "Error parsing binary_vars", "Error parsing binary_vars", "\n".join(log_messages), None
|
143 |
-
|
144 |
-
# Solver expects None if no specific integer_vars are given (meaning all are integer)
|
145 |
-
# However, our current B&B implementation defaults integer_vars to all if None.
|
146 |
-
# For clarity with user input (where empty means "no specific integer vars beyond binary"),
|
147 |
-
# we'll pass the list. If it's empty, and binary_vars is also empty, it implies a continuous LP for B&B,
|
148 |
-
# or if binary_vars is not empty, those will be treated as integer.
|
149 |
-
# The B&B solver correctly adds binary_vars to its internal integer_vars list.
|
150 |
-
|
151 |
-
log_messages.append("Inputs parsed successfully. Starting solver...")
|
152 |
-
|
153 |
-
try:
|
154 |
-
solver = BranchAndBoundSolver(c=np.array(c), A=A, b=np.array(b),
|
155 |
-
integer_vars=integer_vars if integer_vars else None, # Pass None if list is empty for solver's default logic
|
156 |
-
binary_vars=binary_vars if binary_vars else None, # Pass None if list is empty
|
157 |
-
maximize=maximize_bool)
|
158 |
-
|
159 |
-
best_solution, best_objective, solver_log, fig = solver.solve()
|
160 |
-
|
161 |
-
log_messages.extend(solver_log) # Add solver's internal log
|
162 |
-
|
163 |
-
if best_solution is not None:
|
164 |
-
solution_str = ", ".join([f"{val:.3f}" for val in best_solution])
|
165 |
-
objective_str = f"{best_objective:.3f}"
|
166 |
-
else:
|
167 |
-
solution_str = "No feasible integer solution found."
|
168 |
-
objective_str = "N/A"
|
169 |
-
if best_objective == float('-inf') or best_objective == float('inf'): # Check if it was due to infeasibility
|
170 |
-
objective_str = "Infeasible or unbounded"
|
171 |
-
|
172 |
-
|
173 |
-
return solution_str, objective_str, "\n".join(log_messages), fig
|
174 |
-
|
175 |
-
except Exception as e:
|
176 |
-
gr.Error(f"An error occurred during solving: {e}")
|
177 |
-
log_messages.append(f"Runtime error: {e}")
|
178 |
-
return "Solver error", "Solver error", "\n".join(log_messages), None
|
179 |
-
|
180 |
-
|
181 |
-
branch_and_bound_interface = gr.Interface(
|
182 |
-
fn=solve_branch_and_bound_interface,
|
183 |
-
inputs=[
|
184 |
-
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated, e.g., 3,2"),
|
185 |
-
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 2,1; 1,1; 1,0"),
|
186 |
-
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 10,8,3. Must be Ax <= b form."),
|
187 |
-
gr.Textbox(label="Integer Variable Indices (optional)", info="Comma-separated, 0-indexed. If empty and no binary vars, all vars are continuous for B&B (effectively LP). If empty but binary vars exist, only binary are integer. If 'all', all vars are integer."), # Adjusted info
|
188 |
-
gr.Textbox(label="Binary Variable Indices (optional)", info="Comma-separated, 0-indexed, e.g., 0,1"),
|
189 |
-
gr.Checkbox(label="Maximize?", value=True)
|
190 |
-
],
|
191 |
-
outputs=[
|
192 |
-
gr.Textbox(label="Optimal Solution (x)"),
|
193 |
-
gr.Textbox(label="Optimal Objective Value (z)"),
|
194 |
-
gr.Textbox(label="Solver Log and Steps", lines=15, interactive=False),
|
195 |
-
gr.Plot(label="Branch and Bound Tree")
|
196 |
-
],
|
197 |
-
title="Branch and Bound Solver for Mixed Integer Linear Programs (MILP)",
|
198 |
-
description="Solves MILPs (Ax <= b) using the Branch and Bound method. Specify integer/binary variables or leave empty if not applicable (for pure LP).",
|
199 |
-
examples=[
|
200 |
-
[ # Example 1: Knapsack-like problem (from a common example)
|
201 |
-
"8,11,6,4", # c: Objective (maximize)
|
202 |
-
"5,7,4,3; 1,1,1,1", # A: Constraints matrix
|
203 |
-
"14; 4", # b: Constraints RHS
|
204 |
-
"", # integer_vars: all variables are effectively integer due to binary constraint if specified, or by default if integer_vars=None
|
205 |
-
"0,1,2,3", # binary_vars: All variables are binary
|
206 |
-
True # Maximize
|
207 |
-
],
|
208 |
-
[ # Example 2: Simple MILP
|
209 |
-
"3,2,4", # c
|
210 |
-
"1,1,1; 2,1,0", # A
|
211 |
-
"10; 5", # b
|
212 |
-
"0,2", # integer_vars: x0 and x2 are integer
|
213 |
-
"", # binary_vars
|
214 |
-
True # Maximize
|
215 |
-
],
|
216 |
-
[ # Example 3: Minimization problem (from a common example)
|
217 |
-
"3,5", # c (minimize)
|
218 |
-
"-1,0; 0,-1; 3,2", # A (original constraints might be >=, converted to <= by multiplying by -1)
|
219 |
-
"0;0;18", # b (original might be >=0, >=0, <=18. For >=0, we write -x <= 0)
|
220 |
-
"0,1", # integer_vars
|
221 |
-
"", # binary_vars
|
222 |
-
False # Minimize
|
223 |
-
],
|
224 |
-
[ # Example 4: Provided in task description
|
225 |
-
"5,4", #c
|
226 |
-
"1,1;2,0;0,1", #A
|
227 |
-
"5,6,3", #b
|
228 |
-
"0,1", #integer
|
229 |
-
"", #binary
|
230 |
-
True #maximize
|
231 |
-
]
|
232 |
-
],
|
233 |
-
flagging_mode="manual"
|
234 |
-
)
|
235 |
-
|
236 |
-
# --- Simplex Solver (with Steps) Interface ---
|
237 |
-
from .simplex_solver_with_steps import simplex_solver_with_steps
|
238 |
-
|
239 |
-
def run_simplex_solver_interface(c_str, A_str, b_str, bounds_str):
|
240 |
-
"""
|
241 |
-
Wrapper function to connect simplex_solver_with_steps with Gradio interface.
|
242 |
-
"""
|
243 |
-
current_log_list = ["Initializing Simplex Solver (with Steps) Interface..."]
|
244 |
-
|
245 |
-
c = parse_vector(c_str)
|
246 |
-
if not c:
|
247 |
-
current_log_list.append("Error: Objective coefficients (c) could not be parsed or are empty.")
|
248 |
-
return "Error parsing c", "Error parsing c", "\n".join(current_log_list)
|
249 |
-
|
250 |
-
A = parse_matrix(A_str)
|
251 |
-
if A.size == 0:
|
252 |
-
current_log_list.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
253 |
-
return "Error parsing A", "Error parsing A", "\n".join(current_log_list)
|
254 |
-
|
255 |
-
b = parse_vector(b_str)
|
256 |
-
if not b:
|
257 |
-
current_log_list.append("Error: Constraint bounds (b) could not be parsed or are empty.")
|
258 |
-
return "Error parsing b", "Error parsing b", "\n".join(current_log_list)
|
259 |
-
|
260 |
-
variable_bounds = parse_bounds(bounds_str) # This returns a list of tuples or []
|
261 |
-
# parse_bounds includes gr.Warning on failure and returns []
|
262 |
-
if bounds_str and not variable_bounds: # If input was given but parsing failed
|
263 |
-
current_log_list.append("Error: Variable bounds string could not be parsed. Please check format.")
|
264 |
-
# parse_bounds already issues a gr.Warning
|
265 |
-
return "Error parsing var_bounds", "Error parsing var_bounds", "\n".join(current_log_list)
|
266 |
-
|
267 |
-
|
268 |
-
# Dimensional validation
|
269 |
-
if A.shape[0] != len(b):
|
270 |
-
current_log_list.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of b ({len(b)}).")
|
271 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
272 |
-
if A.shape[1] != len(c):
|
273 |
-
current_log_list.append(f"Dimension mismatch: Number of columns in A ({A.shape[1]}) must equal length of c ({len(c)}).")
|
274 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
275 |
-
if variable_bounds and len(variable_bounds) != len(c):
|
276 |
-
current_log_list.append(f"Dimension mismatch: Number of variable bounds pairs ({len(variable_bounds)}) must equal number of objective coefficients ({len(c)}).")
|
277 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
278 |
-
|
279 |
-
# If bounds_str is empty, parse_bounds returns [], which is fine for the solver if it expects default non-negativity or specific handling.
|
280 |
-
# The simplex_solver_with_steps expects a list of tuples, and an empty list if no specific bounds beyond non-negativity are implied by problem setup.
|
281 |
-
# For this solver, bounds are crucial. If not provided, we should create default non-negative bounds.
|
282 |
-
if not variable_bounds:
|
283 |
-
variable_bounds = [(0, None) for _ in range(len(c))]
|
284 |
-
current_log_list.append("Defaulting to non-negative bounds for all variables as no specific bounds were provided.")
|
285 |
-
|
286 |
-
|
287 |
-
current_log_list.append("Inputs parsed and validated successfully.")
|
288 |
-
|
289 |
-
try:
|
290 |
-
# Ensure inputs are NumPy arrays as expected by some solvers (though this one might be flexible)
|
291 |
-
np_c = np.array(c)
|
292 |
-
np_b = np.array(b)
|
293 |
-
|
294 |
-
# Call the solver
|
295 |
-
# simplex_solver_with_steps returns: steps_log, x, optimal_value
|
296 |
-
steps_log, x_solution, opt_val = simplex_solver_with_steps(np_c, A, np_b, variable_bounds)
|
297 |
-
|
298 |
-
current_log_list.extend(steps_log) # steps_log is already a list of strings
|
299 |
-
|
300 |
-
solution_str = "N/A"
|
301 |
-
objective_str = "N/A"
|
302 |
-
|
303 |
-
if x_solution is not None:
|
304 |
-
solution_str = ", ".join([f"{val:.3f}" for val in x_solution])
|
305 |
-
else: # Check specific messages in log if solution is None
|
306 |
-
if any("Unbounded solution" in msg for msg in steps_log):
|
307 |
-
solution_str = "Unbounded solution"
|
308 |
-
elif any("infeasible" in msg.lower() for msg in steps_log): # More general infeasibility check
|
309 |
-
solution_str = "Infeasible solution"
|
310 |
-
|
311 |
-
|
312 |
-
if opt_val is not None:
|
313 |
-
if opt_val == float('inf') or opt_val == float('-inf'):
|
314 |
-
objective_str = str(opt_val)
|
315 |
-
else:
|
316 |
-
objective_str = f"{opt_val:.3f}"
|
317 |
-
|
318 |
-
return solution_str, objective_str, "\n".join(current_log_list)
|
319 |
-
|
320 |
-
except Exception as e:
|
321 |
-
gr.Error(f"An error occurred during solving with Simplex Method: {e}")
|
322 |
-
current_log_list.append(f"Runtime error in Simplex Method: {e}")
|
323 |
-
return "Solver error", "Solver error", "\n".join(current_log_list)
|
324 |
-
|
325 |
-
simplex_solver_interface = gr.Interface(
|
326 |
-
fn=run_simplex_solver_interface,
|
327 |
-
inputs=[
|
328 |
-
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated for maximization problem, e.g., 3,5"),
|
329 |
-
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 1,0;0,2;3,2. Assumes Ax <= b."),
|
330 |
-
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 4,12,18"),
|
331 |
-
gr.Textbox(label="Variable Bounds (L,U) per variable", info="Pairs like L1,U1; L2,U2;... Use 'None' for no bound. E.g., '0,None;0,10'. If empty, defaults to non-negative for all variables (0,None).")
|
332 |
-
],
|
333 |
-
outputs=[
|
334 |
-
gr.Textbox(label="Optimal Solution (x)"),
|
335 |
-
gr.Textbox(label="Optimal Objective Value"),
|
336 |
-
gr.Textbox(label="Solver Steps and Tableaus", lines=20, interactive=False)
|
337 |
-
],
|
338 |
-
title="Simplex Method Solver (with Tableau Steps)",
|
339 |
-
description="Solves Linear Programs (maximization, Ax <= b) using the Simplex method and displays detailed tableau iterations. Assumes variables are non-negative if bounds not specified.",
|
340 |
-
examples=[
|
341 |
-
[ # Classic example from many textbooks (e.g., Taha, Operations Research)
|
342 |
-
"3,5", # c (Max Z = 3x1 + 5x2)
|
343 |
-
"1,0;0,2;3,2", # A
|
344 |
-
"4,12,18", # b
|
345 |
-
"0,None;0,None" # x1 >=0, x2 >=0 (explicitly stating non-negativity)
|
346 |
-
],
|
347 |
-
[ # Another common example
|
348 |
-
"5,4", # c (Max Z = 5x1 + 4x2)
|
349 |
-
"6,4;1,2; -1,1;0,1", # A
|
350 |
-
"24,6,1,2", #b
|
351 |
-
"0,None;0,None" # x1,x2 >=0
|
352 |
-
],
|
353 |
-
[ # Example that might be unbounded if not careful or show interesting steps
|
354 |
-
"1,1",
|
355 |
-
"1,-1;-1,1",
|
356 |
-
"1,1",
|
357 |
-
"0,None;0,None" # x1-x2 <=1, -x1+x2 <=1
|
358 |
-
]
|
359 |
-
],
|
360 |
-
flagging_mode="manual"
|
361 |
-
)
|
362 |
-
|
363 |
-
# --- Dual Simplex Interface ---
|
364 |
-
from .DualSimplexSolver import DualSimplexSolver
|
365 |
-
|
366 |
-
def solve_dual_simplex_interface(objective_type_str, c_str, A_str, relations_str, b_str):
|
367 |
-
"""
|
368 |
-
Wrapper function to connect DualSimplexSolver with Gradio interface.
|
369 |
-
"""
|
370 |
-
current_log = ["Initializing Dual Simplex Solver Interface..."]
|
371 |
-
|
372 |
-
c = parse_vector(c_str)
|
373 |
-
if not c:
|
374 |
-
current_log.append("Error: Objective coefficients (c) could not be parsed or are empty.")
|
375 |
-
return "Error parsing c", "Error parsing c", "\n".join(current_log)
|
376 |
-
|
377 |
-
A = parse_matrix(A_str)
|
378 |
-
if A.size == 0:
|
379 |
-
current_log.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
380 |
-
return "Error parsing A", "Error parsing A", "\n".join(current_log)
|
381 |
-
|
382 |
-
b = parse_vector(b_str)
|
383 |
-
if not b:
|
384 |
-
current_log.append("Error: Constraint bounds (b) could not be parsed or are empty.")
|
385 |
-
return "Error parsing b", "Error parsing b", "\n".join(current_log)
|
386 |
-
|
387 |
-
relations = parse_relations(relations_str)
|
388 |
-
if not relations:
|
389 |
-
current_log.append("Error: Constraint relations could not be parsed, are empty, or contain invalid symbols.")
|
390 |
-
return "Error parsing relations", "Error parsing relations", "\n".join(current_log)
|
391 |
-
|
392 |
-
# Basic dimensional validation
|
393 |
-
if A.shape[0] != len(b):
|
394 |
-
current_log.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of b ({len(b)}).")
|
395 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
396 |
-
if A.shape[1] != len(c):
|
397 |
-
current_log.append(f"Dimension mismatch: Number of columns in A ({A.shape[1]}) must equal length of c ({len(c)}).")
|
398 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
399 |
-
if A.shape[0] != len(relations):
|
400 |
-
current_log.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of relations ({len(relations)}).")
|
401 |
-
return "Dimension Error", "Dimension Error", "\n".join(current_log)
|
402 |
-
|
403 |
-
current_log.append("Inputs parsed and validated successfully.")
|
404 |
-
|
405 |
-
try:
|
406 |
-
solver = DualSimplexSolver(objective_type_str, c, A, relations, b)
|
407 |
-
current_log.append("DualSimplexSolver instantiated.")
|
408 |
-
|
409 |
-
# The solve method now returns: final_solution_str, final_objective_str, log_messages, is_fallback_used_str
|
410 |
-
solution_str, objective_str, solver_log_messages, fallback_info = solver.solve()
|
411 |
-
|
412 |
-
current_log.extend(solver_log_messages)
|
413 |
-
current_log.append(f"Fallback Status: {fallback_info}")
|
414 |
-
|
415 |
-
return solution_str, objective_str, "\n".join(current_log)
|
416 |
-
|
417 |
-
except Exception as e:
|
418 |
-
gr.Error(f"An error occurred during solving with Dual Simplex: {e}")
|
419 |
-
current_log.append(f"Runtime error in Dual Simplex: {e}")
|
420 |
-
return "Solver error", "Solver error", "\n".join(current_log)
|
421 |
-
|
422 |
-
dual_simplex_interface = gr.Interface(
|
423 |
-
fn=solve_dual_simplex_interface,
|
424 |
-
inputs=[
|
425 |
-
gr.Radio(label="Objective Type", choices=["max", "min"], value="max"),
|
426 |
-
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated, e.g., 4,1"),
|
427 |
-
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 3,1; 4,3; 1,2"),
|
428 |
-
gr.Textbox(label="Constraint Relations", info="Comma-separated, e.g., >=,>=,>="), # Dual simplex typically starts from Ax >= b for max problems if to be converted to <=
|
429 |
-
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 3,6,4")
|
430 |
-
],
|
431 |
-
outputs=[
|
432 |
-
gr.Textbox(label="Optimal Solution (Variables)"),
|
433 |
-
gr.Textbox(label="Optimal Objective Value"),
|
434 |
-
gr.Textbox(label="Solver Log, Tableau Steps, and Fallback Info", lines=15, interactive=False)
|
435 |
-
],
|
436 |
-
title="Dual Simplex Solver for Linear Programs (LP)",
|
437 |
-
description="Solves LPs using the Dual Simplex method. This method is often efficient when an initial basic solution is dual feasible but primal infeasible (e.g. after adding cuts). Input Ax R b where R can be '>=', '<=', or '='.",
|
438 |
-
examples=[
|
439 |
-
[ # Example 1: Max problem, standard form for dual simplex often has >= constraints initially
|
440 |
-
# Maximize Z = 4x1 + x2
|
441 |
-
# Subject to:
|
442 |
-
# 3x1 + x2 >= 3 --> -3x1 - x2 <= -3
|
443 |
-
# 4x1 + 3x2 >= 6 --> -4x1 - 3x2 <= -6
|
444 |
-
# x1 + 2x2 >= 4 --> -x1 - 2x2 <= -4 (Mistake in common example, should be <= to be interesting for dual or needs specific setup)
|
445 |
-
# Let's use a more typical dual simplex starting point:
|
446 |
-
# Min C = 2x1 + x2 (so Max -2x1 -x2)
|
447 |
-
# s.t. x1 + x2 >= 5
|
448 |
-
# 2x1 + x2 >= 6
|
449 |
-
# x1, x2 >=0
|
450 |
-
# Becomes: Max Z' = -2x1 -x2
|
451 |
-
# -x1 -x2 <= -5
|
452 |
-
# -2x1 -x2 <= -6
|
453 |
-
"max", "-2,-1", "-1,-1;-2,-1", "<=,<=", "-5,-6" # This is already in <= form, good for dual if RHS is neg.
|
454 |
-
],
|
455 |
-
[ # Example 2: (Taken from a standard textbook example for Dual Simplex)
|
456 |
-
# Minimize Z = 3x1 + 2x2 + x3
|
457 |
-
# Subject to:
|
458 |
-
# 3x1 + x2 + x3 >= 3
|
459 |
-
# -3x1 + 3x2 + x3 >= 6
|
460 |
-
# x1 + x2 + x3 <= 3 (This constraint makes it interesting)
|
461 |
-
# x1,x2,x3 >=0
|
462 |
-
# For Gradio: obj_type='min', c="3,2,1", A="3,1,1;-3,3,1;1,1,1", relations=">=,>=,<=", b="3,6,3"
|
463 |
-
"min", "3,2,1", "3,1,1;-3,3,1;1,1,1", ">=,>=,<=", "3,6,3"
|
464 |
-
],
|
465 |
-
[ # Example from problem description (slightly modified for typical dual simplex)
|
466 |
-
# Maximize Z = 3x1 + 2x2
|
467 |
-
# Subject to:
|
468 |
-
# 2x1 + x2 <= 18 (Original)
|
469 |
-
# x1 + x2 <= 12 (Original)
|
470 |
-
# x1 <= 5 (Original)
|
471 |
-
# To make it a dual simplex start, we might have transformed it from something else,
|
472 |
-
# or expect some RHS to be negative after initial setup.
|
473 |
-
# For a direct input that might be dual feasible but primal infeasible:
|
474 |
-
# Max Z = x1 + x2
|
475 |
-
# s.t. -2x1 - x2 <= -10 (i.e. 2x1 + x2 >= 10)
|
476 |
-
# -x1 - 2x2 <= -10 (i.e. x1 + 2x2 >= 10)
|
477 |
-
"max", "1,1", "-2,-1;-1,-2", "<=,<=", "-10,-10"
|
478 |
-
]
|
479 |
-
],
|
480 |
-
flagging_mode="manual"
|
481 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,7 +1,7 @@
|
|
1 |
import gradio as gr
|
2 |
-
from maths.operations_research.
|
3 |
-
|
4 |
-
|
5 |
|
6 |
operations_research_interfaces_list = [
|
7 |
branch_and_bound_interface, dual_simplex_interface, simplex_solver_interface
|
|
|
1 |
import gradio as gr
|
2 |
+
from maths.operations_research.BranchAndBoundSolver import branch_and_bound_interface
|
3 |
+
from maths.operations_research.DualSimplexSolver import dual_simplex_interface
|
4 |
+
from maths.operations_research.simplex_solver_with_steps import simplex_solver_interface
|
5 |
|
6 |
operations_research_interfaces_list = [
|
7 |
branch_and_bound_interface, dual_simplex_interface, simplex_solver_interface
|
@@ -1,6 +1,8 @@
|
|
1 |
import numpy as np
|
2 |
from tabulate import tabulate
|
3 |
import gradio as gr
|
|
|
|
|
4 |
|
5 |
|
6 |
def simplex_solver_with_steps(c, A, b, bounds):
|
@@ -222,3 +224,128 @@ def main():
|
|
222 |
if __name__ == "__main__":
|
223 |
main()
|
224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import numpy as np
|
2 |
from tabulate import tabulate
|
3 |
import gradio as gr
|
4 |
+
from maths.operations_research.utils import parse_matrix
|
5 |
+
from maths.operations_research.utils import parse_vector
|
6 |
|
7 |
|
8 |
def simplex_solver_with_steps(c, A, b, bounds):
|
|
|
224 |
if __name__ == "__main__":
|
225 |
main()
|
226 |
|
227 |
+
|
228 |
+
def run_simplex_solver_interface(c_str, A_str, b_str, bounds_str):
|
229 |
+
"""
|
230 |
+
Wrapper function to connect simplex_solver_with_steps with Gradio interface.
|
231 |
+
"""
|
232 |
+
current_log_list = ["Initializing Simplex Solver (with Steps) Interface..."]
|
233 |
+
|
234 |
+
c = parse_vector(c_str)
|
235 |
+
if not c:
|
236 |
+
current_log_list.append("Error: Objective coefficients (c) could not be parsed or are empty.")
|
237 |
+
return "Error parsing c", "Error parsing c", "\n".join(current_log_list)
|
238 |
+
|
239 |
+
A = parse_matrix(A_str)
|
240 |
+
if A.size == 0:
|
241 |
+
current_log_list.append("Error: Constraint matrix (A) could not be parsed or is empty.")
|
242 |
+
return "Error parsing A", "Error parsing A", "\n".join(current_log_list)
|
243 |
+
|
244 |
+
b = parse_vector(b_str)
|
245 |
+
if not b:
|
246 |
+
current_log_list.append("Error: Constraint bounds (b) could not be parsed or are empty.")
|
247 |
+
return "Error parsing b", "Error parsing b", "\n".join(current_log_list)
|
248 |
+
|
249 |
+
variable_bounds = parse_bounds(bounds_str) # This returns a list of tuples or []
|
250 |
+
# parse_bounds includes gr.Warning on failure and returns []
|
251 |
+
if bounds_str and not variable_bounds: # If input was given but parsing failed
|
252 |
+
current_log_list.append("Error: Variable bounds string could not be parsed. Please check format.")
|
253 |
+
# parse_bounds already issues a gr.Warning
|
254 |
+
return "Error parsing var_bounds", "Error parsing var_bounds", "\n".join(current_log_list)
|
255 |
+
|
256 |
+
|
257 |
+
# Dimensional validation
|
258 |
+
if A.shape[0] != len(b):
|
259 |
+
current_log_list.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of b ({len(b)}).")
|
260 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
261 |
+
if A.shape[1] != len(c):
|
262 |
+
current_log_list.append(f"Dimension mismatch: Number of columns in A ({A.shape[1]}) must equal length of c ({len(c)}).")
|
263 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
264 |
+
if variable_bounds and len(variable_bounds) != len(c):
|
265 |
+
current_log_list.append(f"Dimension mismatch: Number of variable bounds pairs ({len(variable_bounds)}) must equal number of objective coefficients ({len(c)}).")
|
266 |
+
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
|
267 |
+
|
268 |
+
# If bounds_str is empty, parse_bounds returns [], which is fine for the solver if it expects default non-negativity or specific handling.
|
269 |
+
# The simplex_solver_with_steps expects a list of tuples, and an empty list if no specific bounds beyond non-negativity are implied by problem setup.
|
270 |
+
# For this solver, bounds are crucial. If not provided, we should create default non-negative bounds.
|
271 |
+
if not variable_bounds:
|
272 |
+
variable_bounds = [(0, None) for _ in range(len(c))]
|
273 |
+
current_log_list.append("Defaulting to non-negative bounds for all variables as no specific bounds were provided.")
|
274 |
+
|
275 |
+
|
276 |
+
current_log_list.append("Inputs parsed and validated successfully.")
|
277 |
+
|
278 |
+
try:
|
279 |
+
# Ensure inputs are NumPy arrays as expected by some solvers (though this one might be flexible)
|
280 |
+
np_c = np.array(c)
|
281 |
+
np_b = np.array(b)
|
282 |
+
|
283 |
+
# Call the solver
|
284 |
+
# simplex_solver_with_steps returns: steps_log, x, optimal_value
|
285 |
+
steps_log, x_solution, opt_val = simplex_solver_with_steps(np_c, A, np_b, variable_bounds)
|
286 |
+
|
287 |
+
current_log_list.extend(steps_log) # steps_log is already a list of strings
|
288 |
+
|
289 |
+
solution_str = "N/A"
|
290 |
+
objective_str = "N/A"
|
291 |
+
|
292 |
+
if x_solution is not None:
|
293 |
+
solution_str = ", ".join([f"{val:.3f}" for val in x_solution])
|
294 |
+
else: # Check specific messages in log if solution is None
|
295 |
+
if any("Unbounded solution" in msg for msg in steps_log):
|
296 |
+
solution_str = "Unbounded solution"
|
297 |
+
elif any("infeasible" in msg.lower() for msg in steps_log): # More general infeasibility check
|
298 |
+
solution_str = "Infeasible solution"
|
299 |
+
|
300 |
+
|
301 |
+
if opt_val is not None:
|
302 |
+
if opt_val == float('inf') or opt_val == float('-inf'):
|
303 |
+
objective_str = str(opt_val)
|
304 |
+
else:
|
305 |
+
objective_str = f"{opt_val:.3f}"
|
306 |
+
|
307 |
+
return solution_str, objective_str, "\n".join(current_log_list)
|
308 |
+
|
309 |
+
except Exception as e:
|
310 |
+
gr.Error(f"An error occurred during solving with Simplex Method: {e}")
|
311 |
+
current_log_list.append(f"Runtime error in Simplex Method: {e}")
|
312 |
+
return "Solver error", "Solver error", "\n".join(current_log_list)
|
313 |
+
|
314 |
+
simplex_solver_interface = gr.Interface(
|
315 |
+
fn=run_simplex_solver_interface,
|
316 |
+
inputs=[
|
317 |
+
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated for maximization problem, e.g., 3,5"),
|
318 |
+
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 1,0;0,2;3,2. Assumes Ax <= b."),
|
319 |
+
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 4,12,18"),
|
320 |
+
gr.Textbox(label="Variable Bounds (L,U) per variable", info="Pairs like L1,U1; L2,U2;... Use 'None' for no bound. E.g., '0,None;0,10'. If empty, defaults to non-negative for all variables (0,None).")
|
321 |
+
],
|
322 |
+
outputs=[
|
323 |
+
gr.Textbox(label="Optimal Solution (x)"),
|
324 |
+
gr.Textbox(label="Optimal Objective Value"),
|
325 |
+
gr.Textbox(label="Solver Steps and Tableaus", lines=20, interactive=False)
|
326 |
+
],
|
327 |
+
title="Simplex Method Solver (with Tableau Steps)",
|
328 |
+
description="Solves Linear Programs (maximization, Ax <= b) using the Simplex method and displays detailed tableau iterations. Assumes variables are non-negative if bounds not specified.",
|
329 |
+
examples=[
|
330 |
+
[ # Classic example from many textbooks (e.g., Taha, Operations Research)
|
331 |
+
"3,5", # c (Max Z = 3x1 + 5x2)
|
332 |
+
"1,0;0,2;3,2", # A
|
333 |
+
"4,12,18", # b
|
334 |
+
"0,None;0,None" # x1 >=0, x2 >=0 (explicitly stating non-negativity)
|
335 |
+
],
|
336 |
+
[ # Another common example
|
337 |
+
"5,4", # c (Max Z = 5x1 + 4x2)
|
338 |
+
"6,4;1,2; -1,1;0,1", # A
|
339 |
+
"24,6,1,2", #b
|
340 |
+
"0,None;0,None" # x1,x2 >=0
|
341 |
+
],
|
342 |
+
[ # Example that might be unbounded if not careful or show interesting steps
|
343 |
+
"1,1",
|
344 |
+
"1,-1;-1,1",
|
345 |
+
"1,1",
|
346 |
+
"0,None;0,None" # x1-x2 <=1, -x1+x2 <=1
|
347 |
+
]
|
348 |
+
],
|
349 |
+
flagging_mode="manual"
|
350 |
+
)
|
351 |
+
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Gradio interfaces for Operations Research solvers."""
|
2 |
+
import gradio as gr
|
3 |
+
import numpy as np
|
4 |
+
|
5 |
+
def parse_vector(input_str: str, dtype=float) -> list:
|
6 |
+
"""Parses a comma-separated string into a list of numbers."""
|
7 |
+
if not input_str:
|
8 |
+
return []
|
9 |
+
try:
|
10 |
+
return [dtype(x.strip()) for x in input_str.split(',')]
|
11 |
+
except ValueError:
|
12 |
+
gr.Warning(f"Could not parse vector: '{input_str}'. Please use comma-separated numbers (e.g., '1,2,3.5').")
|
13 |
+
return []
|
14 |
+
|
15 |
+
def parse_matrix(input_str: str) -> np.ndarray:
|
16 |
+
"""Parses a string (rows separated by semicolons, elements by commas) into a NumPy array."""
|
17 |
+
if not input_str:
|
18 |
+
return np.array([])
|
19 |
+
try:
|
20 |
+
rows = input_str.split(';')
|
21 |
+
matrix = []
|
22 |
+
num_cols = -1
|
23 |
+
for i, row_str in enumerate(rows):
|
24 |
+
if not row_str.strip(): continue # Allow empty rows if they result from trailing semicolons
|
25 |
+
|
26 |
+
row = [float(x.strip()) for x in row_str.split(',')]
|
27 |
+
if num_cols == -1:
|
28 |
+
num_cols = len(row)
|
29 |
+
elif len(row) != num_cols:
|
30 |
+
raise ValueError(f"Row {i+1} has {len(row)} elements, expected {num_cols}.")
|
31 |
+
matrix.append(row)
|
32 |
+
|
33 |
+
if not matrix: # If all rows were empty or input_str was just ';'
|
34 |
+
return np.array([])
|
35 |
+
|
36 |
+
return np.array(matrix)
|
37 |
+
except ValueError as e:
|
38 |
+
gr.Warning(f"Could not parse matrix: '{input_str}'. Error: {e}. Expected format: '1,2;3,4'. Ensure all rows have the same number of columns.")
|
39 |
+
return np.array([])
|
40 |
+
|
41 |
+
def parse_relations(input_str: str) -> list[str]:
|
42 |
+
"""Parses a comma-separated string of relations into a list of strings."""
|
43 |
+
if not input_str:
|
44 |
+
return []
|
45 |
+
try:
|
46 |
+
relations = [r.strip() for r in input_str.split(',')]
|
47 |
+
valid_relations = {"<=", ">=", "="}
|
48 |
+
if not all(r in valid_relations for r in relations):
|
49 |
+
invalid_rels = [r for r in relations if r not in valid_relations]
|
50 |
+
gr.Warning(f"Invalid relation(s) found: {', '.join(invalid_rels)}. Allowed relations are: '<=', '>=', '='.")
|
51 |
+
return []
|
52 |
+
return relations
|
53 |
+
except Exception as e: # Catch any other unexpected errors during parsing
|
54 |
+
gr.Warning(f"Error parsing relations: '{input_str}'. Error: {e}")
|
55 |
+
return []
|
56 |
+
|
57 |
+
def parse_bounds(input_str: str) -> list[tuple]:
|
58 |
+
"""
|
59 |
+
Parses a string representing variable bounds into a list of tuples.
|
60 |
+
Format: "lower1,upper1; lower2,upper2; ..." (e.g., "0,None; 0,10; None,None")
|
61 |
+
'None' (case-insensitive) is used for no bound.
|
62 |
+
"""
|
63 |
+
if not input_str:
|
64 |
+
return []
|
65 |
+
bounds_list = []
|
66 |
+
try:
|
67 |
+
pairs = input_str.split(';')
|
68 |
+
for i, pair_str in enumerate(pairs):
|
69 |
+
if not pair_str.strip(): continue # Allow for trailing semicolons or empty entries
|
70 |
+
|
71 |
+
parts = pair_str.split(',')
|
72 |
+
if len(parts) != 2:
|
73 |
+
raise ValueError(f"Bound pair '{pair_str}' (entry {i+1}) does not have two elements. Expected format 'lower,upper'.")
|
74 |
+
|
75 |
+
lower_str, upper_str = parts[0].strip().lower(), parts[1].strip().lower()
|
76 |
+
|
77 |
+
lower = None if lower_str == 'none' else float(lower_str)
|
78 |
+
upper = None if upper_str == 'none' else float(upper_str)
|
79 |
+
|
80 |
+
if lower is not None and upper is not None and lower > upper:
|
81 |
+
raise ValueError(f"Lower bound {lower} cannot be greater than upper bound {upper} for pair '{pair_str}' (entry {i+1}).")
|
82 |
+
|
83 |
+
bounds_list.append((lower, upper))
|
84 |
+
return bounds_list
|
85 |
+
except ValueError as e:
|
86 |
+
gr.Warning(f"Could not parse bounds: '{input_str}'. Error: {e}. Expected format: 'lower1,upper1; lower2,upper2; ...' e.g., '0,None; 0,10'. Use 'None' for no bound.")
|
87 |
+
return []
|
88 |
+
except Exception as e: # Catch any other unexpected parsing errors
|
89 |
+
gr.Warning(f"Unexpected error parsing bounds: '{input_str}'. Error: {e}")
|
90 |
+
return []
|