Spaces:
Runtime error
Runtime error
File size: 16,606 Bytes
1599566 |
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 |
import numpy as np
from scipy.optimize import linprog # Using SciPy for robust LP solving
import warnings
from .solve_primal_directly import solve_primal_directly
def solve_lp_via_dual(objective_type, c, A, relations, b, TOLERANCE=1e-6):
"""
Solves an LP problem by formulating and solving the dual, then using
complementary slackness.
Args:
objective_type (str): 'max' or 'min'.
c (list or np.array): Primal objective coefficients.
A (list of lists or np.array): Primal constraint matrix LHS.
relations (list): Primal constraint relations ('<=', '>=', '=').
b (list or np.array): Primal constraint RHS.
Returns:
A tuple: (status, message, primal_solution, dual_solution, objective_value)
status: 0 for success, non-zero for failure.
message: Description of the outcome.
primal_solution: Dictionary of primal variable values (x).
dual_solution: Dictionary of dual variable values (p).
objective_value: Optimal objective value of the primal.
"""
primal_c = np.array(c, dtype=float)
primal_A = np.array(A, dtype=float)
primal_b = np.array(b, dtype=float)
primal_relations = relations
num_primal_vars = len(primal_c)
num_primal_constraints = len(primal_b)
print("--- Step 1: Formulate the Dual Problem ---")
# Standardize Primal for consistent dual formulation: Convert to Min problem
original_obj_type = objective_type.lower()
if original_obj_type == 'max':
print(" Primal is Max. Converting to Min by negating objective coefficients.")
primal_c_std = -primal_c
objective_sign_flip = -1.0
else:
print(" Primal is Min. Using original objective coefficients.")
primal_c_std = primal_c
objective_sign_flip = 1.0
# Handle constraint relations for dual formulation
# We'll formulate the dual based on a standard primal form:
# Min c'x s.t. Ax >= b, x >= 0
# Dual: Max p'b s.t. p'A <= c', p >= 0
# Let's adjust the input primal constraints to fit the Ax >= b form needed for this dual pair.
A_geq = []
b_geq = []
print(" Adjusting primal constraints to >= form for dual formulation:")
for i in range(num_primal_constraints):
rel = primal_relations[i]
a_row = primal_A[i]
b_val = primal_b[i]
if rel == '<=':
print(f" Constraint {i+1}: Multiplying by -1 ( {a_row} <= {b_val} -> {-a_row} >= {-b_val} )")
A_geq.append(-a_row)
b_geq.append(-b_val)
elif rel == '>=':
print(f" Constraint {i+1}: Keeping as >= ( {a_row} >= {b_val} )")
A_geq.append(a_row)
b_geq.append(b_val)
elif rel == '=':
# Represent equality as two inequalities: >= and <=
print(f" Constraint {i+1}: Splitting '=' into >= and <= ")
# >= part
print(f" Part 1: {a_row} >= {b_val}")
A_geq.append(a_row)
b_geq.append(b_val)
# <= part -> multiply by -1 to get >=
print(f" Part 2: {-a_row} >= {-b_val} (from {a_row} <= {b_val})")
A_geq.append(-a_row)
b_geq.append(-b_val)
else:
return 1, f"Invalid relation '{rel}' in constraint {i+1}", None, None, None
primal_A_std = np.array(A_geq, dtype=float)
primal_b_std = np.array(b_geq, dtype=float)
num_dual_vars = primal_A_std.shape[0] # One dual var per standardized constraint
# Now formulate the dual: Max p' * primal_b_std s.t. p' * primal_A_std <= primal_c_std, p >= 0
dual_c = primal_b_std # Coefficients for Maximize objective
dual_A = primal_A_std.T # Transpose A
dual_b = primal_c_std # RHS for dual constraints (<= type)
print("\n Dual Problem Formulation:")
print(f" Objective: Maximize p * [{', '.join(f'{bi:.2f}' for bi in dual_c)}]")
print(f" Subject to:")
for j in range(dual_A.shape[0]): # Iterate through dual constraints
print(f" {' + '.join(f'{dual_A[j, i]:.2f}*p{i+1}' for i in range(num_dual_vars))} <= {dual_b[j]:.2f}")
print(f" p_i >= 0 for i=1..{num_dual_vars}")
print("\n--- Step 2: Solve the Dual Problem using SciPy linprog ---")
# linprog solves Min problems, so we Max p*b by Min -p*b
# Constraints for linprog: A_ub @ x <= b_ub, A_eq @ x == b_eq
# Our dual is Max p*b s.t. p*A <= c. Let p be x for linprog.
# Maximize dual_c @ p => Minimize -dual_c @ p
# Subject to: dual_A @ p <= dual_b
# p >= 0 (default bounds)
c_linprog = -dual_c
A_ub_linprog = dual_A
b_ub_linprog = dual_b
# Use method='highs' which is the default and generally robust
# Options can be added for more control if needed
try:
result_dual = linprog(c_linprog, A_ub=A_ub_linprog, b_ub=b_ub_linprog, bounds=[(0, None)] * num_dual_vars, method='highs') # Using HiGHS solver
except ValueError as e:
# Sometimes specific solvers are not available or fail
print(f" SciPy linprog(method='highs') failed: {e}. Trying 'simplex'.")
try:
result_dual = linprog(c_linprog, A_ub=A_ub_linprog, b_ub=b_ub_linprog, bounds=[(0, None)] * num_dual_vars, method='simplex')
except Exception as e_simplex:
return 1, f"SciPy linprog failed for dual problem with both 'highs' and 'simplex': {e_simplex}", None, None, None
if not result_dual.success:
# Check status for specific reasons
if result_dual.status == 2: # Infeasible
msg = "Dual problem is infeasible. Primal problem is unbounded (or infeasible)."
elif result_dual.status == 3: # Unbounded
msg = "Dual problem is unbounded. Primal problem is infeasible."
else:
msg = f"Failed to solve the dual problem. Status: {result_dual.status} - {result_dual.message}"
return result_dual.status, msg, None, None, None
# Optimal dual solution found
optimal_dual_p = result_dual.x
optimal_dual_obj = -result_dual.fun # Negate back to get Max value
dual_solution_dict = {f'p{i+1}': optimal_dual_p[i] for i in range(num_dual_vars)}
print("\n Optimal Dual Solution Found:")
print(f" Dual Variables (p*):")
for i in range(num_dual_vars):
print(f" p{i+1} = {optimal_dual_p[i]:.6f}")
print(f" Optimal Dual Objective Value (Max p*b): {optimal_dual_obj:.6f}")
print("\n--- Step 3: Check Strong Duality ---")
# The optimal objective value of the dual should equal the optimal objective
# value of the primal (after adjusting for Min/Max conversion).
expected_primal_obj = optimal_dual_obj * objective_sign_flip
print(f" Strong duality implies the optimal primal objective value should be: {expected_primal_obj:.6f}")
print("\n--- Step 4 & 5: Use Complementary Slackness to find Primal Variables ---")
# Calculate Dual Slacks: dual_slack = dual_b - dual_A @ optimal_dual_p
# dual_b is primal_c_std
# dual_A is primal_A_std.T
# dual_slack_j = primal_c_std_j - (optimal_dual_p @ primal_A_std)_j
dual_slacks = dual_b - dual_A @ optimal_dual_p
print(" Calculating Dual Slacks (c'_j - p* A'_j):")
for j in range(num_primal_vars):
print(f" Dual Slack for primal var x{j+1}: {dual_slacks[j]:.6f}")
# Identify conditions from Complementary Slackness (CS)
# 1. Dual Slackness: If dual_slack_j > TOLERANCE, then primal x*_j = 0
# 2. Primal Slackness: If optimal_dual_p_i > TOLERANCE, then i-th standardized primal constraint is binding
# (primal_A_std[i] @ x* = primal_b_std[i])
binding_constraints_indices = []
zero_primal_vars_indices = []
print("\n Applying Complementary Slackness Conditions:")
# Dual Slackness
print(" From Dual Slackness (if c'_j - p* A'_j > 0, then x*_j = 0):")
for j in range(num_primal_vars):
if dual_slacks[j] > TOLERANCE:
print(f" Dual Slack for x{j+1} is {dual_slacks[j]:.4f} > 0 => x{j+1}* = 0")
zero_primal_vars_indices.append(j)
else:
print(f" Dual Slack for x{j+1} is {dual_slacks[j]:.4f} approx 0 => x{j+1}* may be non-zero")
# Primal Slackness
print(" From Primal Slackness (if p*_i > 0, then primal constraint i is binding):")
for i in range(num_dual_vars):
if optimal_dual_p[i] > TOLERANCE:
print(f" p*{i+1} = {optimal_dual_p[i]:.4f} > 0 => Primal constraint {i+1} (standardized) is binding.")
binding_constraints_indices.append(i)
else:
print(f" p*{i+1} = {optimal_dual_p[i]:.4f} approx 0 => Primal constraint {i+1} (standardized) may be non-binding.")
# Construct system of equations for non-zero primal variables
# Equations come from binding primal constraints.
# Variables are x*_j where j is NOT in zero_primal_vars_indices.
active_primal_vars_indices = [j for j in range(num_primal_vars) if j not in zero_primal_vars_indices]
num_active_primal_vars = len(active_primal_vars_indices)
print(f"\n Identifying system for active primal variables ({[f'x{j+1}' for j in active_primal_vars_indices]}):")
if num_active_primal_vars == 0:
# All primal vars are zero
primal_x_star = np.zeros(num_primal_vars)
print(" All primal variables determined to be 0 by dual slackness.")
elif len(binding_constraints_indices) < num_active_primal_vars:
print(f" Warning: Number of binding constraints ({len(binding_constraints_indices)}) identified is less than the number of potentially non-zero primal variables ({num_active_primal_vars}).")
print(" Complementary slackness alone might not be sufficient, or there might be degeneracy/multiple solutions.")
print(" Attempting to solve using available binding constraints, but result might be unreliable.")
# Pad with zero rows if necessary, or indicate underspecified system. For now, proceed cautiously.
matrix_A_sys = primal_A_std[binding_constraints_indices][:, active_primal_vars_indices]
vector_b_sys = primal_b_std[binding_constraints_indices]
else:
# We have at least as many binding constraints as active variables.
# Select num_active_primal_vars binding constraints to form a square system (if possible).
# If more binding constraints exist, they should be consistent.
# We take the first 'num_active_primal_vars' binding constraints.
if len(binding_constraints_indices) > num_active_primal_vars:
print(f" More binding constraints ({len(binding_constraints_indices)}) than active variables ({num_active_primal_vars}). Using the first {num_active_primal_vars}.")
matrix_A_sys = primal_A_std[binding_constraints_indices[:num_active_primal_vars]][:, active_primal_vars_indices]
vector_b_sys = primal_b_std[binding_constraints_indices[:num_active_primal_vars]]
print(" System Ax = b to solve:")
for r in range(matrix_A_sys.shape[0]):
print(f" {' + '.join(f'{matrix_A_sys[r, c]:.2f}*x{active_primal_vars_indices[c]+1}' for c in range(num_active_primal_vars))} = {vector_b_sys[r]:.2f}")
# Solve the system if possible
if num_active_primal_vars > 0:
try:
# Use numpy.linalg.solve for square systems, lstsq for potentially non-square
if matrix_A_sys.shape[0] == matrix_A_sys.shape[1]:
solved_active_vars = np.linalg.solve(matrix_A_sys, vector_b_sys)
elif matrix_A_sys.shape[0] > matrix_A_sys.shape[1]: # Overdetermined
print(" System is overdetermined. Using least squares solution.")
solved_active_vars, residuals, rank, s = np.linalg.lstsq(matrix_A_sys, vector_b_sys, rcond=None)
# Check if residuals are close to zero for consistency
if residuals and np.sum(residuals**2) > TOLERANCE * len(vector_b_sys):
print(f" Warning: Least squares solution has significant residuals ({np.sqrt(np.sum(residuals**2)):.4f}), CS conditions might be inconsistent?")
else: # Underdetermined
# Cannot uniquely solve. This shouldn't happen if dual was optimal and non-degenerate.
# Could use lstsq which gives one possible solution (minimum norm).
print(" System is underdetermined. Using least squares (minimum norm) solution.")
solved_active_vars, residuals, rank, s = np.linalg.lstsq(matrix_A_sys, vector_b_sys, rcond=None)
# Assign solved values back to the full primal_x_star vector
primal_x_star = np.zeros(num_primal_vars)
for i, active_idx in enumerate(active_primal_vars_indices):
primal_x_star[active_idx] = solved_active_vars[i]
print("\n Solved values for active primal variables:")
for i, active_idx in enumerate(active_primal_vars_indices):
print(f" x{active_idx+1}* = {solved_active_vars[i]:.6f}")
except np.linalg.LinAlgError:
print(" Error: Could not solve the system of equations derived from binding constraints (matrix may be singular).")
# Attempt to use linprog on the original primal as a fallback/check
print(" Attempting to solve primal directly with linprog as a fallback...")
primal_fallback_status, _, primal_fallback_sol, _, primal_fallback_obj = solve_primal_directly(
original_obj_type, c, A, relations, b)
if primal_fallback_status == 0:
print(" Fallback solution found.")
return 0, "Solved primal using fallback direct method after CS failure", primal_fallback_sol, dual_solution_dict, primal_fallback_obj
else:
return 1, "Failed to solve system from CS, and fallback primal solve also failed.", None, dual_solution_dict, None
# Assemble final primal solution dictionary
primal_solution_dict = {f'x{j+1}': primal_x_star[j] for j in range(num_primal_vars)}
# --- Step 6: Verify Primal Feasibility and Objective Value ---
print("\n--- Step 6: Verify Primal Solution ---")
feasible = True
print(" Checking primal constraints:")
for i in range(num_primal_constraints):
lhs_val = primal_A[i] @ primal_x_star
rhs_val = primal_b[i]
rel = primal_relations[i]
constraint_met = False
if rel == '<=':
constraint_met = lhs_val <= rhs_val + TOLERANCE
elif rel == '>=':
constraint_met = lhs_val >= rhs_val - TOLERANCE
elif rel == '=':
constraint_met = abs(lhs_val - rhs_val) < TOLERANCE
status_str = "Satisfied" if constraint_met else "VIOLATED"
print(f" Constraint {i+1}: {lhs_val:.4f} {rel} {rhs_val:.4f} -> {status_str}")
if not constraint_met:
feasible = False
print(" Checking non-negativity (x >= 0):")
non_negative = np.all(primal_x_star >= -TOLERANCE)
print(f" All x_j >= 0: {non_negative}")
if not non_negative:
feasible = False
print(f"Violating variables: {[f'x{j+1}={primal_x_star[j]:.4f}' for j in range(len(primal_x_star)) if primal_x_star[j] < -TOLERANCE]}")
final_primal_obj = primal_c @ primal_x_star # Using original primal c
print(f"\n Calculated Primal Objective Value: {final_primal_obj:.6f}")
print(f" Expected Primal Objective Value (from dual): {expected_primal_obj:.6f}")
if abs(final_primal_obj - expected_primal_obj) > TOLERANCE * (1 + abs(expected_primal_obj)):
print(" Warning: Calculated primal objective value significantly differs from the dual objective value!")
feasible = False # Consider this a failure if strong duality doesn't hold
if feasible:
print("\n--- Primal Solution Found Successfully via Dual ---")
return 0, "Optimal solution found via dual.", primal_solution_dict, dual_solution_dict, final_primal_obj
else:
print("\n--- Failed to Find Feasible Primal Solution via Dual ---")
print(" The derived primal solution violates constraints or non-negativity, or strong duality failed.")
# You might want to return the possibly incorrect solution for inspection or None
return 1, "Derived primal solution is infeasible or inconsistent.", primal_solution_dict, dual_solution_dict, final_primal_obj
|