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"
)