Spaces:
Runtime error
Runtime error
File size: 15,465 Bytes
1599566 f1254bd 79bc79a 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 d8c6327 1599566 f1254bd e422a0c f1254bd 79bc79a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
import numpy as np
from tabulate import tabulate
import gradio as gr
from maths.operations_research.utils import parse_matrix
from maths.operations_research.utils import parse_vector
def simplex_solver_with_steps(c, A, b, bounds):
"""
Solve LP using simplex method and display full tableau at each step
Parameters:
- c: Objective coefficients (for maximizing c'x)
- A: Constraint coefficients matrix
- b: Right-hand side of constraints
- bounds: Variable bounds as [(lower_1, upper_1), (lower_2, upper_2), ...]
Returns:
- x: Optimal solution
- optimal_value: Optimal objective value
- steps_log: List of strings detailing each step
"""
steps_log = []
steps_log.append("\n--- Starting Simplex Method ---")
steps_log.append(f"Objective: Maximize {' + '.join([f'{c[i]}x_{i}' for i in range(len(c))])}")
steps_log.append(f"Constraints:")
for i in range(len(b)):
constraint_str = ' + '.join([f"{A[i,j]}x_{j}" for j in range(A.shape[1])])
steps_log.append(f" {constraint_str} <= {b[i]}")
# Convert problem to standard form (for tableau method)
# First handle bounds by adding necessary constraints
A_with_bounds = A.copy()
b_with_bounds = b.copy()
for i, (lb, ub) in enumerate(bounds):
if lb is not None and lb > 0:
# For variables with lower bounds > 0, we'll substitute x_i = x_i' + lb
# This affects all constraints where x_i appears
for j in range(A.shape[0]):
b_with_bounds[j] -= A[j, i] * lb
# Number of variables and constraints
n_vars = len(c)
n_constraints = A.shape[0]
# Add slack variables to create standard form
# The tableau will have: [objective row | RHS]
# [-------------|----]
# [constraints | RHS]
# Initial tableau:
# First row is -c (negative of objective coefficients) and 0s for slack variables, then 0 (for max)
# The rest are constraint coefficients, then identity matrix for slack variables, then RHS
tableau = np.zeros((n_constraints + 1, n_vars + n_constraints + 1))
# Set the objective row (negated for maximization)
tableau[0, :n_vars] = -c
# Set the constraint coefficients
tableau[1:, :n_vars] = A_with_bounds
# Set the slack variable coefficients (identity matrix)
for i in range(n_constraints):
tableau[i + 1, n_vars + i] = 1
# Set the RHS
tableau[1:, -1] = b_with_bounds
# Base and non-base variables
base_vars = list(range(n_vars, n_vars + n_constraints)) # Slack variables are initially basic
# Function to print current tableau
def print_tableau(tableau, base_vars, steps_log_func):
headers = [f"x_{j}" for j in range(n_vars)] + [f"s_{j}" for j in range(n_constraints)] + ["RHS"]
rows = []
row_labels = ["z"] + [f"eq_{i}" for i in range(n_constraints)]
for i, row in enumerate(tableau):
rows.append([row_labels[i]] + [f"{val:.3f}" for val in row])
steps_log_func.append("\nCurrent Tableau:")
steps_log_func.append(tabulate(rows, headers=headers, tablefmt="grid"))
steps_log_func.append(f"Basic variables: {[f'x_{v}' if v < n_vars else f's_{v-n_vars}' for v in base_vars]}")
# Print initial tableau
steps_log.append("\nInitial tableau:")
print_tableau(tableau, base_vars, steps_log)
# Main simplex loop
iteration = 0
max_iterations = 100 # Prevent infinite loops
while iteration < max_iterations:
iteration += 1
steps_log.append(f"\n--- Iteration {iteration} ---")
# Find the entering variable (most negative coefficient in objective row for maximization)
entering_col = np.argmin(tableau[0, :-1])
if tableau[0, entering_col] >= -1e-10: # Small negative numbers due to floating-point errors
steps_log.append("Optimal solution reached - no negative coefficients in objective row")
break
steps_log.append(f"Entering variable: {'x_' + str(entering_col) if entering_col < n_vars else 's_' + str(entering_col - n_vars)}")
# Find the leaving variable using min ratio test
ratios = []
for i in range(1, n_constraints + 1):
if tableau[i, entering_col] <= 0:
ratios.append(np.inf) # Avoid division by zero or negative
else:
ratios.append(tableau[i, -1] / tableau[i, entering_col])
if all(r == np.inf for r in ratios):
steps_log.append("Unbounded solution - no leaving variable found")
return steps_log, None, float('inf') # Problem is unbounded
# Find the row with minimum ratio
leaving_row = np.argmin(ratios) + 1 # +1 because we skip the objective row
leaving_var = base_vars[leaving_row - 1]
steps_log.append(f"Leaving variable: {'x_' + str(leaving_var) if leaving_var < n_vars else 's_' + str(leaving_var - n_vars)}")
steps_log.append(f"Pivot element: {tableau[leaving_row, entering_col]:.3f} at row {leaving_row}, column {entering_col}")
# Perform pivot operation
# First, normalize the pivot row
pivot = tableau[leaving_row, entering_col]
tableau[leaving_row] = tableau[leaving_row] / pivot
# Update other rows
for i in range(tableau.shape[0]):
if i != leaving_row:
factor = tableau[i, entering_col]
tableau[i] = tableau[i] - factor * tableau[leaving_row]
# Update basic variables
base_vars[leaving_row - 1] = entering_col
# Print updated tableau
steps_log.append("\nAfter pivot:")
print_tableau(tableau, base_vars, steps_log)
if iteration == max_iterations:
steps_log.append("Max iterations reached without convergence")
return steps_log, None, None
# Extract solution
x = np.zeros(n_vars)
for i, var in enumerate(base_vars):
if var < n_vars: # If it's an original variable and not a slack
x[var] = tableau[i + 1, -1]
# Account for variable substitutions (if lower bounds were applied)
for i, (lb, _) in enumerate(bounds):
if lb is not None and lb > 0:
x[i] += lb
# Calculate objective value
optimal_value = np.dot(c, x)
steps_log.append("\n--- Simplex Method Complete ---")
steps_log.append(f"Optimal solution found: {x}")
steps_log.append(f"Optimal objective value: {optimal_value}")
return steps_log, x, optimal_value
# Gradio interface
def parse_input_list(s):
# Helper to parse comma/space/semicolon separated numbers
return [float(x) for x in s.replace(';', ' ').replace(',', ' ').split() if x.strip()]
def parse_matrix(s):
# Helper to parse matrix input as rows separated by newlines, columns by comma/space
return np.array([parse_input_list(row) for row in s.strip().split('\n') if row.strip()])
def parse_bounds(s):
# Helper to parse bounds as (lower, upper) per variable, e.g. "0, None\n0, None"
bounds = []
for row in s.strip().split('\n'):
parts = row.replace('None', 'None').replace('none', 'None').split(',')
lb = float(parts[0]) if parts[0].strip().lower() != 'none' else None
ub = float(parts[1]) if len(parts) > 1 and parts[1].strip().lower() != 'none' else None
bounds.append((lb, ub))
return bounds
def gradio_simplex(c_str, A_str, b_str, bounds_str):
try:
c = parse_input_list(c_str)
A = parse_matrix(A_str)
b = parse_input_list(b_str)
bounds = parse_bounds(bounds_str)
steps_log, x, optimal_value = simplex_solver_with_steps(c, A, b, bounds)
steps = '\n'.join(steps_log)
if x is not None:
solution = f"Optimal solution: {x}\nOptimal value: {optimal_value}"
else:
solution = "No optimal solution found."
return steps, solution
except Exception as e:
return f"Error: {e}", ""
with gr.Blocks() as demo:
gr.Markdown("""
# Simplex Solver with Steps (Gradio)
Enter your LP problem below. All inputs are required.
- **Objective coefficients (c):** e.g. `3, 2`
- **Constraint matrix (A):** one row per line, e.g. `1, 2`\n`2, 1`
- **RHS vector (b):** e.g. `6, 8`
- **Bounds:** one row per variable, lower and upper bound separated by comma, e.g. `0, None`\n`0, None`
""")
c_in = gr.Textbox(label="Objective coefficients (c)", value="3, 2")
A_in = gr.Textbox(label="Constraint matrix (A)", value="1, 2\n2, 1")
b_in = gr.Textbox(label="RHS vector (b)", value="6, 8")
bounds_in = gr.Textbox(label="Bounds", value="0, None\n0, None")
btn = gr.Button("Solve")
steps_out = gr.Textbox(label="Simplex Steps", lines=20)
sol_out = gr.Textbox(label="Solution")
btn.click(gradio_simplex, inputs=[c_in, A_in, b_in, bounds_in], outputs=[steps_out, sol_out])
def main():
demo.launch()
if __name__ == "__main__":
main()
def run_simplex_solver_interface(c_str, A_str, b_str, bounds_str):
"""
Wrapper function to connect simplex_solver_with_steps with Gradio interface.
"""
current_log_list = ["Initializing Simplex Solver (with Steps) Interface..."]
c = parse_vector(c_str)
if not c:
current_log_list.append("Error: Objective coefficients (c) could not be parsed or are empty.")
return "Error parsing c", "Error parsing c", "\n".join(current_log_list)
A = parse_matrix(A_str)
if A.size == 0:
current_log_list.append("Error: Constraint matrix (A) could not be parsed or is empty.")
return "Error parsing A", "Error parsing A", "\n".join(current_log_list)
b = parse_vector(b_str)
if not b:
current_log_list.append("Error: Constraint bounds (b) could not be parsed or are empty.")
return "Error parsing b", "Error parsing b", "\n".join(current_log_list)
variable_bounds = parse_bounds(bounds_str) # This returns a list of tuples or []
# parse_bounds includes gr.Warning on failure and returns []
if bounds_str and not variable_bounds: # If input was given but parsing failed
current_log_list.append("Error: Variable bounds string could not be parsed. Please check format.")
# parse_bounds already issues a gr.Warning
return "Error parsing var_bounds", "Error parsing var_bounds", "\n".join(current_log_list)
# Dimensional validation
if A.shape[0] != len(b):
current_log_list.append(f"Dimension mismatch: Number of rows in A ({A.shape[0]}) must equal length of b ({len(b)}).")
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
if A.shape[1] != len(c):
current_log_list.append(f"Dimension mismatch: Number of columns in A ({A.shape[1]}) must equal length of c ({len(c)}).")
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
if variable_bounds and len(variable_bounds) != len(c):
current_log_list.append(f"Dimension mismatch: Number of variable bounds pairs ({len(variable_bounds)}) must equal number of objective coefficients ({len(c)}).")
return "Dimension Error", "Dimension Error", "\n".join(current_log_list)
# If bounds_str is empty, parse_bounds returns [], which is fine for the solver if it expects default non-negativity or specific handling.
# 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.
# For this solver, bounds are crucial. If not provided, we should create default non-negative bounds.
if not variable_bounds:
variable_bounds = [(0, None) for _ in range(len(c))]
current_log_list.append("Defaulting to non-negative bounds for all variables as no specific bounds were provided.")
current_log_list.append("Inputs parsed and validated successfully.")
try:
# Ensure inputs are NumPy arrays as expected by some solvers (though this one might be flexible)
np_c = np.array(c)
np_b = np.array(b)
# Call the solver
# simplex_solver_with_steps returns: steps_log, x, optimal_value
steps_log, x_solution, opt_val = simplex_solver_with_steps(np_c, A, np_b, variable_bounds)
current_log_list.extend(steps_log) # steps_log is already a list of strings
solution_str = "N/A"
objective_str = "N/A"
if x_solution is not None:
solution_str = ", ".join([f"{val:.3f}" for val in x_solution])
else: # Check specific messages in log if solution is None
if any("Unbounded solution" in msg for msg in steps_log):
solution_str = "Unbounded solution"
elif any("infeasible" in msg.lower() for msg in steps_log): # More general infeasibility check
solution_str = "Infeasible solution"
if opt_val is not None:
if opt_val == float('inf') or opt_val == float('-inf'):
objective_str = str(opt_val)
else:
objective_str = f"{opt_val:.3f}"
return solution_str, objective_str, "\n".join(current_log_list)
except Exception as e:
gr.Error(f"An error occurred during solving with Simplex Method: {e}")
current_log_list.append(f"Runtime error in Simplex Method: {e}")
return "Solver error", "Solver error", "\n".join(current_log_list)
simplex_solver_interface = gr.Interface(
fn=run_simplex_solver_interface,
inputs=[
gr.Textbox(label="Objective Coefficients (c)", info="Comma-separated for maximization problem, e.g., 3,5"),
gr.Textbox(label="Constraint Matrix (A)", info="Rows separated by ';', elements by ',', e.g., 1,0;0,2;3,2. Assumes Ax <= b."),
gr.Textbox(label="Constraint RHS (b)", info="Comma-separated, e.g., 4,12,18"),
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).")
],
outputs=[
gr.Textbox(label="Optimal Solution (x)"),
gr.Textbox(label="Optimal Objective Value"),
gr.Textbox(label="Solver Steps and Tableaus", lines=20, interactive=False)
],
title="Simplex Method Solver (with Tableau Steps)",
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.",
examples=[
[ # Classic example from many textbooks (e.g., Taha, Operations Research)
"3,5", # c (Max Z = 3x1 + 5x2)
"1,0;0,2;3,2", # A
"4,12,18", # b
"0,None;0,None" # x1 >=0, x2 >=0 (explicitly stating non-negativity)
],
[ # Another common example
"5,4", # c (Max Z = 5x1 + 4x2)
"6,4;1,2; -1,1;0,1", # A
"24,6,1,2", #b
"0,None;0,None" # x1,x2 >=0
],
[ # Example that might be unbounded if not careful or show interesting steps
"1,1",
"1,-1;-1,1",
"1,1",
"0,None;0,None" # x1-x2 <=1, -x1+x2 <=1
]
],
flagging_mode="manual"
)
|