Source code for pm4py.util.lp.variants.pulp_solver

'''
    PM4Py – A Process Mining Library for Python
Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see this software project's root or
visit <https://www.gnu.org/licenses/>.

Website: https://processintelligence.solutions
Contact: info@processintelligence.solutions
'''
import sys
from enum import Enum

import numpy as np
import pulp
from pulp import LpProblem, LpMinimize, LpVariable, LpStatus, value, lpSum


[docs] class Parameters(Enum): REQUIRE_ILP = "require_ilp" INTEGRALITY = "integrality" BOUNDS = "bounds"
MIN_THRESHOLD = 1e-12 MAX_NUM_CONSTRAINTS = 7 # Maximum safe number of constraints (log10) # Solver function to maintain compatibility with different versions of PuLP if hasattr(pulp, "__version__"): # New interface from pulp import PULP_CBC_CMD def solver(prob): return prob.solve(PULP_CBC_CMD(msg=0)) else: # Old interface
[docs] def solver(prob): return prob.solve()
[docs] def get_variable_name(index): """Generates a variable name with leading zeros to ensure consistent length.""" return str(index).zfill(MAX_NUM_CONSTRAINTS)
[docs] def apply(c, Aub, bub, Aeq=None, beq=None, parameters=None): """ Solves a linear programming problem using PuLP with all inputs as Python lists or lists of lists. """ if type(Aub) is np.matrix: Aub = Aub.tolist() if type(bub) is np.matrix: bub = bub.tolist() if type(Aeq) is np.matrix: Aeq = Aeq.tolist() if type(beq) is np.matrix: beq = beq.tolist() if parameters is None: parameters = {} # Get parameters require_ilp = parameters.get("require_ilp", False) integrality = parameters.get("integrality", None) bounds = parameters.get("bounds", None) # Initialize the problem prob = LpProblem("LP_Problem", LpMinimize) # Define decision variables num_vars = len(c) # Validate integrality and bounds lists if integrality is not None and len(integrality) != num_vars: raise ValueError( "Length of 'integrality' list must be equal to the number of variables." ) if bounds is not None and len(bounds) != num_vars: raise ValueError( "Length of 'bounds' list must be equal to the number of variables." ) x_vars = [] for i in range(num_vars): var_name = f"x_{get_variable_name(i)}" # Determine variable bounds lb = None ub = None if bounds is not None: lb, ub = bounds[i] # Convert 'None' strings to actual None lb = None if lb == "None" else lb ub = None if ub == "None" else ub # Determine variable category (continuous or integer) if integrality is not None: # Use integrality list cat = "Integer" if integrality[i] else "Continuous" elif require_ilp: # All variables are integer cat = "Integer" else: # All variables are continuous cat = "Continuous" x_vars.append(LpVariable(var_name, lowBound=lb, upBound=ub, cat=cat)) # Build the objective function objective_expr = lpSum( c[j] * x_vars[j] for j in range(num_vars) if abs(c[j]) >= MIN_THRESHOLD ) prob += objective_expr, "Objective" # Add inequality constraints for i in range(len(Aub)): constraint_expr = lpSum( Aub[i][j] * x_vars[j] for j in range(num_vars) if abs(Aub[i][j]) >= MIN_THRESHOLD ) constraint_name = f"Inequality_Constraint_{get_variable_name(i)}" prob += constraint_expr <= bub[i], constraint_name # Add equality constraints, if any if Aeq is not None and beq is not None: for i in range(len(Aeq)): constraint_expr = lpSum( Aeq[i][j] * x_vars[j] for j in range(num_vars) if abs(Aeq[i][j]) >= MIN_THRESHOLD ) constraint_name = ( f"Equality_Constraint_{get_variable_name(i + len(Aub))}" ) prob += constraint_expr == beq[i], constraint_name # Solve the problem solver(prob) return prob
[docs] def get_prim_obj_from_sol(sol, parameters=None): """ Retrieves the objective value from the solved LP problem. """ return value(sol.objective)
[docs] def get_points_from_sol(sol, parameters=None): """ Retrieves the values of the decision variables from the solved LP problem. """ if parameters is None: parameters = {} maximize = parameters.get("maximize", False) return_when_none = parameters.get("return_when_none", False) var_corr = parameters.get("var_corr", {}) if LpStatus[sol.status] == "Optimal": # Extract variable values from the solution return [v.varValue for v in sol.variables()] elif return_when_none: # Return a list of default values if no solution is found default_value = sys.float_info.max if maximize else sys.float_info.min return [default_value] * len(var_corr) else: return None