spagestic commited on
Commit
79bc79a
·
1 Parent(s): fcfbd59

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 CHANGED
@@ -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
- self.c = c
24
- self.A = A
25
- self.b = b
26
- self.n = len(c)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # Process binary and integer variables
29
- self.binary_vars = [] if binary_vars is None else binary_vars
 
 
 
 
 
 
 
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.copy()
 
 
 
 
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
- for idx in self.integer_vars:
69
- if abs(round(x[idx]) - x[idx]) > 1e-6:
70
- return False
71
- return True
 
 
 
 
 
 
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
- for idx in self.integer_vars:
79
- fractional_part = abs(x[idx] - round(x[idx]))
80
- if fractional_part > max_fractional and fractional_part > 1e-6:
81
- max_fractional = fractional_part
82
- branching_var = idx
 
 
 
 
 
 
 
 
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
- except:
 
 
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
- self.graph.add_node(node_name, obj=objective_value, x=x_value,
117
- branch_var=branch_var, branch_cond=branch_cond)
118
-
119
- if parent is not None:
120
- # Use branch_var + 1 to show 1-indexed variables in the display
121
- label = f"x_{branch_var + 1} {branch_cond}"
122
- self.graph.add_edge(parent, node_name, label=label)
123
-
124
- return node_name
125
-
126
- def visualize_graph(self):
127
- """Visualize the branch and bound graph"""
128
- fig = plt.figure(figsize=(20, 8))
129
- pos = nx.spring_layout(self.graph) # Use spring layout instead of graphviz
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- # Node labels: Node name, Objective value and solution
132
- labels = {}
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
- plt.title("Branch and Bound Tree", fontsize=14)
154
- plt.axis('off')
155
- plt.tight_layout()
156
- return fig # Return the figure instead of showing it
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
-
159
  def display_steps_table(self):
160
- """Display the steps in tabular format"""
161
- headers = ["Node", "z", "x", "z*", "x*", "UB", "LB", "Z at end of stage"]
162
- return tabulate(self.steps_table, headers=headers, tablefmt="grid")
163
-
164
- def solve(self, verbose=True):
165
- """Solve the problem using branch and bound"""
166
- self.log_messages = [] # Initialize log for this run
167
- # Initialize bounds
168
- lower_bounds = [0] * self.n
169
- upper_bounds = [None] * self.n # None means unbounded
170
-
171
- # Set upper bounds for binary variables
172
- for idx in self.binary_vars:
173
- upper_bounds[idx] = 1
174
-
175
- # Create a priority queue for nodes (max heap for maximization, min heap for minimization)
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
- # Initial upper bound is the root objective
196
- upper_bound = obj_root
 
 
 
 
 
 
197
 
198
- # Check if the root solution is already integer-feasible
199
- if self.is_integer_feasible(x_root):
200
- self.log_messages.append("Root solution is integer-feasible! No need for branching.")
201
- self.best_solution = x_root
202
- self.best_objective = obj_root
203
 
204
- # Add to steps table
205
- active_nodes_str = "∅" if not self.active_nodes else "{" + ", ".join(self.active_nodes) + "}"
206
- self.steps_table.append([
207
- root_node, f"{obj_root:.2f}", f"({', '.join([f'{x:.2f}' for x in x_root])})",
208
- f"{self.best_objective:.2f}", f"({', '.join([f'{x:.2f}' for x in self.best_solution])})",
209
- f"{upper_bound:.2f}", f"{self.best_objective:.2f}", active_nodes_str
210
- ])
211
 
212
- steps_table_string = self.display_steps_table()
213
- self.log_messages.append(steps_table_string)
214
- fig = self.visualize_graph()
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
- self.log_messages.append(f"\nStep {self.nodes_explored + 1}: Exploring node {node_name}")
 
241
 
242
- # Remove from active nodes
243
- self.active_nodes.remove(node_name)
 
 
 
 
244
 
245
- # Branch on most fractional variable
246
- branch_var = self.get_branching_variable(self.graph.nodes[node_name]['x'])
247
- branch_val = self.graph.nodes[node_name]['x'][branch_var]
 
248
 
249
- # For binary variables, always branch with x=0 and x=1
250
- if branch_var in self.binary_vars:
251
- floor_val = 0
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
- # Solve the relaxation for this node
279
- x_floor, obj_floor = self.solve_relaxation(floor_lower_bounds, floor_upper_bounds)
280
 
281
- # Add node to graph
282
- self.add_node_to_graph(left_node, obj_floor if x_floor is not None else float('-inf'),
283
- x_floor, node_name, branch_var, branch_cond)
284
 
285
- # Process the floor branch
286
- if x_floor is None:
287
- self.log_messages.append(f" {left_node} is infeasible")
288
- else:
289
- self.log_messages.append(f" {left_node} relaxation objective: {obj_floor:.6f}")
290
- self.log_messages.append(f" {left_node} solution: {x_floor}")
291
 
292
- # Check if integer feasible and update best solution if needed
293
- if self.is_integer_feasible(x_floor) and ((self.maximize and obj_floor > self.best_objective) or
294
- (not self.maximize and obj_floor < self.best_objective)):
295
- self.best_solution = x_floor.copy()
296
- self.best_objective = obj_floor
297
- self.log_messages.append(f" Found new best integer solution with objective {self.best_objective:.6f}")
 
 
 
 
298
 
299
- # Add to queue if not fathomed
300
- if ((self.maximize and obj_floor > self.best_objective) or
301
- (not self.maximize and obj_floor < self.best_objective)):
302
- if not self.is_integer_feasible(x_floor): # Only branch if not integer feasible
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 graph
329
- self.add_node_to_graph(right_node, obj_ceil if x_ceil is not None else float('-inf'),
330
- x_ceil, node_name, branch_var, branch_cond)
 
331
 
332
- # Process the ceil branch
333
- if x_ceil is None:
334
- self.log_messages.append(f" {right_node} is infeasible")
335
- else:
336
- self.log_messages.append(f" {right_node} relaxation objective: {obj_ceil:.6f}")
337
- self.log_messages.append(f" {right_node} solution: {x_ceil}")
338
 
339
- # Check if integer feasible and update best solution if needed
340
- if self.is_integer_feasible(x_ceil) and ((self.maximize and obj_ceil > self.best_objective) or
341
- (not self.maximize and obj_ceil < self.best_objective)):
342
- self.best_solution = x_ceil.copy()
343
- self.best_objective = obj_ceil
344
- self.log_messages.append(f" Found new best integer solution with objective {self.best_objective:.6f}")
 
 
 
 
 
 
345
 
346
- # Add to queue if not fathomed
347
- if ((self.maximize and obj_ceil > self.best_objective) or
348
- (not self.maximize and obj_ceil < self.best_objective)):
349
- if not self.is_integer_feasible(x_ceil): # Only branch if not integer feasible
350
- priority = -obj_ceil if self.maximize else obj_ceil
351
- node_queue.put((priority, self.nodes_explored, right_node,
352
- ceil_lower_bounds.copy(), ceil_upper_bounds.copy()))
353
- self.active_nodes.add(right_node)
354
-
355
- # Update upper bound as the best objective in the remaining nodes
356
- if not node_queue.empty():
357
- # Upper bound is the best possible objective in the remaining nodes
358
- next_priority = node_queue.queue[0][0]
359
- upper_bound = -next_priority if self.maximize else next_priority
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  else:
361
- upper_bound = self.best_objective
362
-
363
- # Add to steps table
364
- active_nodes_str = "∅" if not self.active_nodes else "{" + ", ".join(self.active_nodes) + "}"
365
- lb_str = f"{self.best_objective:.2f}" if self.best_objective != float('-inf') else "-"
366
- x_star_str = "-" if self.best_solution is None else f"({', '.join([f'{x:.2f}' for x in self.best_solution])})"
367
-
368
- self.steps_table.append([
369
- node_name,
370
- f"{self.graph.nodes[node_name]['obj']:.2f}",
371
- f"({', '.join([f'{x:.2f}' for x in self.graph.nodes[node_name]['x']])})",
372
- lb_str, x_star_str, f"{upper_bound:.2f}", lb_str, active_nodes_str
373
- ])
374
-
375
- self.log_messages.append("\nBranch and bound completed!")
376
- self.log_messages.append(f"Nodes explored: {self.nodes_explored}")
377
-
378
- if self.best_solution is not None:
379
- self.log_messages.append(f"Optimal objective: {self.best_objective:.6f}")
380
- self.log_messages.append(f"Optimal solution: {self.best_solution}")
381
- else:
382
- self.log_messages.append("No feasible integer solution found")
383
-
384
- # Append steps table string to log
385
- steps_table_string = self.display_steps_table()
386
- self.log_messages.append(steps_table_string)
387
-
388
- # Visualize the graph
389
- fig = self.visualize_graph()
390
-
391
- return self.best_solution, self.best_objective, self.log_messages, fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
maths/operations_research/DualSimplexSolver.py CHANGED
@@ -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
+ )
maths/operations_research/operations_research_interface.py DELETED
@@ -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
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
maths/operations_research/operations_research_tab.py CHANGED
@@ -1,7 +1,7 @@
1
  import gradio as gr
2
- from maths.operations_research.operations_research_interface import (
3
- branch_and_bound_interface, dual_simplex_interface, simplex_solver_interface
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
maths/operations_research/simplex_solver_with_steps.py CHANGED
@@ -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
+
maths/operations_research/utils.py ADDED
@@ -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 []