From 37cc2889410a6d9ee3d898da7cabbed98daefa8a Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 22 Jul 2025 13:47:53 -0700 Subject: [PATCH 01/29] Add python API --- .../cuopt/linear_programming/__init__.py | 1 + .../cuopt/cuopt/linear_programming/problem.py | 358 ++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 python/cuopt/cuopt/linear_programming/problem.py diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index 4d88382ebd..f49060ca08 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -17,6 +17,7 @@ from cuopt.linear_programming.data_model import DataModel from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve +from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solver_settings import ( PDLPSolverMode, SolverMethod, diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py new file mode 100644 index 0000000000..01fe1af5bf --- /dev/null +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -0,0 +1,358 @@ +import cuopt.linear_programming.data_model as data_model +import cuopt.linear_programming.solver as solver + +import enum +import numpy as np + +# The type of a variable is either continuous, binary, or integer +CONTINUOUS = 'C' +BINARY = 'B' +INTEGER = 'I' + +# Variable objects hold a reference to the problem they were created from as well as info +# about there index, lower bound, upper bound, objective coefficient and variable type. +# You can add and subtract Variables to scalars and other Variables to form LinearExpression objects. +# You can multiply variables by a scalar to form a LinearExpression object +class Variable: + def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, vname=''): + self.problem = problem + self.index = index + self.LB = lb + self.UB = ub + self.Obj = obj + self.Value = 0.0 + self.VType = vtype + self.VName = vname + def getIndex(self): + return self.index + def getValue(self): + return self.Value + def getObjectiveCoefficient(self): + return self.Obj + def setObjectiveCoefficient(self, val): + self.Obj = val + def setLowerBound(self, val): + self.LB = val + def getLowerBound(self): + return self.LB + def setUpperBound(self, val): + self.UB = val + def getUpperBound(self): + return self.UB + def setVariableType(self, val): + self.VType = val + def getVariableType(self): + return self.VType + def setVariableName(self, val): + self.VName = val + def getVariableName(self): + return self.VName + def __add__(self, other): + match other: + case int() | float(): + return LinearExpression([self], [1.0], float(other)) + case Variable(): + # Change? + return LinearExpression([self, other], [1.0, 1.0], 0.0) + case LinearExpression(): + return other + self + case _: + raise ValueError('Cannot add type %s to variable' % type(other).__name__) + def __radd__(self, other): + return self + other + def __sub__(self, other): + match other: + case int() | float(): + return LinearExpression([self], [1.0], -float(other)) + case Variable(): + return LinearExpression([self, other], [1.0, -1.0], 0.0) + case LinearExpression(): + # self - other -> other * -1.0 + self + return other * -1.0 + self + case _: + raise ValueError('Cannot subtract type %s from variable' % type(other).__name__) + def __rsub__(self, other): + # other - self -> other + self * -1.0 + return other + self * -1.0 + def __mul__(self, other): + match other: + case int() | float(): + return LinearExpression([self], [float(other)], 0.0) + case _: + raise ValueError('Cannot multiply type %s with variable' % type(other).__name__) + def __rmul__(self, other): + return self * other + + +# LinearExpressions contain a set of variables, the coefficients for the variables, and a constant +# LinearExpressions can be used to create constraints and the objective in the Problem +# LinearExpressions can be added and subtracted with other LinearExpressions and Variables +# LinearExpressions can be multiplied and divided by scalars +# LinearExpressions can be compared with scalars, Variables, and other LinearExpressions to create Constraints +class LinearExpression: + def __init__(self, vars, coefficients, constant): + self.vars = vars + self.coefficients = coefficients + self.constant = constant + def getVars(self): + return self.vars + def getVar(self, i): + return self.vars[i] + def getCoefficients(self): + return self.coefficients + def getCoefficient(self, i): + return self.coefficients[i] + def getConstant(self): + return self.constant + def zipVarCoefficients(self): + return zip(self.vars, self.coefficients) + def getValue(self): + value = 0.0 + for i, var in enumerate(self.vars): + value += var.Value * self.coefficients[i] + return value + def size(self): + return len(self.vars) + def __len__(self): + return len(self.vars) + def __add__(self, other): + match other: + case int() | float(): + self.constant += float(other) + return self + case Variable(): + self.vars.append(other) + self.coefficients.append(1.0) + return self + case LinearExpression(): + self.vars.extend(other.vars) + self.coefficients.extend(other.coefficients) + self.constant += other.constant + return self + case _: + raise ValueError("Can't add type %s to Linear Expression" % type(other).__name__) + def __radd__(self, other): + return self + other + def __sub__(self, other): + match other: + case int() | float(): + self.constant -= float(other) + return self + case Variable(): + self.vars.append(other) + self.coefficients.append(-1.0) + return self + case LinearExpression(): + self.vars.extend(other.vars) + for coeff in other.coefficients: # Same Time Complexity as extend O(k), k = nelements to append + self.coefficients.append(-coeff) + self.constant -= other.constant + return self + case _: + raise ValueError("Can't sub type %s from LinearExpression" % type(other).__name__) + def __rsub__(self, other): + # other - self -> other + self * -1.0 + return other + self * -1.0 + def __mul__(self, other): + match other: + case int() | float(): + self.coefficients = [coeff * float(other) for coeff in self.coefficients] + self.constant = self.constant * float(other) + return self + case _: + raise ValueError("Can't multiply type %s by LinearExpresson" % type(other).__name__) + def __rmul__(self, other): + return self * other + def __div__(self, other): + match other: + case int() | float(): + self.coefficients = [coeff / float(other) for coeff in self.coefficients] + self.constant = self.constant / float(other) + return self + case _: + raise ValueError("Can't divide LinearExpression by type %s" % type(other).__name__) + def __le__(self, other): + match other: + case int() | float(): + return Constraint(self, CONSTRAINT_LE, float(other)) + case Variable() | LinearExpression(): + # expr1 <= expr2 -> expr1 - expr2 <= 0 + expr = self - other + return Constraint(expr, CONSTRAINT_LE, 0.0) + def __ge__(self, other): + match other: + case int() | float(): + return Constraint(self, CONSTRAINT_GE, float(other)) + case Variable() | LinearExpression(): + # expr1 >= expr2 -> expr1 - expr2 >= 0 + expr = self - other + return Constraint(expr, CONSTRAINT_GE, 0.0) + def __eq__(self, other): + match other: + case int() | float(): + return Constraint(self, CONSTRAINT_EQ, float(other)) + case Variable() | LinearExpression(): + # expr1 == expr2 -> expr1 - expr2 == 0 + expr = self - other + return Constraint(expr, CONSTRAINT_EQ, 0.0) + +# The sense of a constraint is either less than or equal, greater than or equal, or equal +CONSTRAINT_LE = 'L' +CONSTRAINT_GE = 'G' +CONSTRAINT_EQ = 'E' + +# A constraint contains a linear expression, the sense of the constraint, and the right-hand side of the constraint +class Constraint: + def __init__(self, expr, sense, rhs, name=''): + self.vindex_coeff_dict = {} + nz = len(expr.getVars()) + for i in range(nz): + v_idx = expr.vars[i].index + v_coeff = expr.coefficients[i] + self.vindex_coeff_dict[v_idx] = self.vindex_coeff_dict[v_idx] + v_coeff if v_idx in self.vindex_coeff_dict else v_coeff + self.Sense = sense + self.RHS = rhs - expr.getConstant() + self.CName = name + def getName(self): + return CName + def getSense(self): + return self.Sense + def getRHS(self): + return self.RHS + def getCoefficient(self, var): + v_idx = var.index + return vindex_coeff_dict[v_idx] + +# The sense of a problem is either minimize or maximize +MINIMIZE = 0 +MAXIMIZE = 1 + +# A Problem defines a Linear Program or Mixed Integer Program +# Variable can be be created by calling addVariable() +# Constraints can be added by calling addConstraint() +# The objective can be set by calling setObjective() +# The problem data is formed when calling optimize() +class Problem: + def __init__(self, model_name=''): + self.Name = model_name + self.vars = [] + self.constrs = [] + self.ObjSense = MINIMIZE + self.Obj = None + self.ObjCon = 0.0 + self.Status = -1 + self.IsMIP = False + + self.rhs = None + self.row_sense = None + self.row_pointers = None + self.column_indicies = None + self.values = None + self.lower_bound = None + self.upper_bound = None + self.var_type = None + + def addVar(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, name=''): + n = len(self.vars) + if vtype == INTEGER or vtype == BINARY: + self.IsMIP = True + var = Variable(self, n, lb, ub, obj, vtype, name) + self.vars.append(var) + return var + + def addConstr(self, constr, name=''): + n = len(self.constrs) + match constr: + case Constraint(): + constr.index = n + constr.cname = name + self.constrs.append(constr) + case _: + raise ValueError("addConstraint requires a Constraint object") + + def setObjective(self, expr, sense=MINIMIZE): + self.ObjSense = sense + match expr: + case int() | float(): + for var in self.vars: + var.setObjectiveCoefficient(0.0) + self.ObjCon = float(expr) + case Variable(): + for var in self.vars: + var.setObjectiveCoefficient(0.0) + if var.getIndex() == expr.getIndex(): + var.setObjectiveCoefficient(1.0) + case LinearExpression(): + for var, coeff in expr.zipVarCoefficients(): + self.vars[var.getIndex()].setObjectiveCoefficient(coeff) + case _: + raise ValueError('Objective must be a LinearExpression or a constant') + self.Obj = expr + + def getObjective(self): + return self.Obj + + def getVars(self): + return self.vars + + def getConstrs(self): + return self.constrs + + @property + def NumVars(self): + return len(self.vars) + + @property + def NumConstrs(self): + return len(self.constrs) + + @property + def NumNZs(self): + nnz = 0 + for constr in self.constrs: + nnz += len(constr.vindex_coeff_dict) + + def optimize(self): + # iterate through the constraints and construct the constraint matrix and the rhs + m = len(self.constrs) + n = len(self.vars) + self.row_pointers = [0] + self.column_indicies = [] + self.values = [] + self.rhs = [] + self.row_sense = [] + for constr in self.constrs: + self.column_indicies.extend(list(constr.vindex_coeff_dict.keys())) + self.values.extend(list(constr.vindex_coeff_dict.values())) + self.row_pointers.append(len(self.column_indicies)) + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + + self.objective = [] + self.lower_bound, self.upper_bound = [], [] + self.var_type = [] + + for j in range(n): + self.objective.append(self.vars[j].getObjectiveCoefficient()) + self.var_type.append(self.vars[j].getVariableType()) + self.lower_bound.append(self.vars[j].getLowerBound()) + self.upper_bound.append(self.vars[j].getUpperBound()) + + # Initialize datamodel + dm = data_model.DataModel() + dm.set_csr_constraint_matrix(np.array(self.values), np.array(self.column_indicies), np.array(self.row_pointers)) + dm.set_maximize(self.ObjSense) + dm.set_constraint_bounds(np.array(self.rhs)) + dm.set_row_types(np.array(self.row_sense)) + dm.set_objective_coefficients(np.array(self.objective)) + dm.set_variable_lower_bounds(np.array(self.lower_bound)) + dm.set_variable_upper_bounds(np.array(self.upper_bound)) + dm.set_variable_types(np.array(self.var_type)) + + # Initialize solver_settings + + # Call Solver + solution = solver.Solve(dm) + + # Post Solve + return solution From e70c8a6feb167ecfdcc083b2aebf3510d77cb874 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 22 Jul 2025 21:59:13 -0700 Subject: [PATCH 02/29] extract solution into variables and attributes --- .../cuopt/cuopt/linear_programming/problem.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 01fe1af5bf..95634ab4de 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1,5 +1,6 @@ import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver +import cuopt.linear_programming.solver_settings as solver_settings import enum import numpy as np @@ -206,6 +207,7 @@ class Constraint: def __init__(self, expr, sense, rhs, name=''): self.vindex_coeff_dict = {} nz = len(expr.getVars()) + self.vars = expr.vars for i in range(nz): v_idx = expr.vars[i].index v_coeff = expr.coefficients[i] @@ -222,6 +224,12 @@ def getRHS(self): def getCoefficient(self, var): v_idx = var.index return vindex_coeff_dict[v_idx] + @property + def Slack(self): + lhs = 0.0 + for var in self.vars: + lhs += var.Value * self.vindex_coeff_dict[var.index] + return self.RHS - lhs # The sense of a problem is either minimize or maximize MINIMIZE = 0 @@ -242,6 +250,7 @@ def __init__(self, model_name=''): self.ObjCon = 0.0 self.Status = -1 self.IsMIP = False + self.Settings = solver_settings.SolverSettings() self.rhs = None self.row_sense = None @@ -265,7 +274,7 @@ def addConstr(self, constr, name=''): match constr: case Constraint(): constr.index = n - constr.cname = name + constr.CName = name self.constrs.append(constr) case _: raise ValueError("addConstraint requires a Constraint object") @@ -311,6 +320,25 @@ def NumNZs(self): nnz = 0 for constr in self.constrs: nnz += len(constr.vindex_coeff_dict) + return nnz + + def post_solve(self, solution): + self.Status = solution.get_termination_status() + self.SolveTime = solution.get_solve_time() + + if solution.problem_category == 0: + self.SolutionStats = solution.get_lp_stats() + else: + self.SolutionStats = solution.get_milp_stats() + + primal_sol = solution.get_primal_solution() + reduced_cost = solution.get_reduced_cost() + if len(primal_sol) > 0: + for var in self.vars: + var.Value = primal_sol[var.index] + if not self.IsMIP: + var.RC = reduced_cost[var.index] + self.ObjVal = self.Obj.getValue() def optimize(self): # iterate through the constraints and construct the constraint matrix and the rhs @@ -349,10 +377,8 @@ def optimize(self): dm.set_variable_upper_bounds(np.array(self.upper_bound)) dm.set_variable_types(np.array(self.var_type)) - # Initialize solver_settings - # Call Solver - solution = solver.Solve(dm) + solution = solver.Solve(dm, self.Settings) # Post Solve - return solution + self.post_solve(solution) From a20d6b3b161ffe01c40fee156b300b1e74980808 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 28 Jul 2025 09:39:04 -0700 Subject: [PATCH 03/29] address review comments, iadd, isub --- .../cuopt/cuopt/linear_programming/problem.py | 105 +++++++++++++----- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 95634ab4de..6ac760b607 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -7,7 +7,6 @@ # The type of a variable is either continuous, binary, or integer CONTINUOUS = 'C' -BINARY = 'B' INTEGER = 'I' # Variable objects hold a reference to the problem they were created from as well as info @@ -22,8 +21,9 @@ def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTI self.UB = ub self.Obj = obj self.Value = 0.0 - self.VType = vtype - self.VName = vname + self.ReducedCost = float('nan') + self.VariableType = vtype + self.VariableName = vname def getIndex(self): return self.index def getValue(self): @@ -41,13 +41,13 @@ def setUpperBound(self, val): def getUpperBound(self): return self.UB def setVariableType(self, val): - self.VType = val + self.VariableType = val def getVariableType(self): - return self.VType + return self.VariableType def setVariableName(self, val): - self.VName = val + self.VariableName = val def getVariableName(self): - return self.VName + return self.VariableName def __add__(self, other): match other: case int() | float(): @@ -95,9 +95,9 @@ def __init__(self, vars, coefficients, constant): self.vars = vars self.coefficients = coefficients self.constant = constant - def getVars(self): + def getVariables(self): return self.vars - def getVar(self, i): + def getVariable(self, i): return self.vars[i] def getCoefficients(self): return self.coefficients @@ -112,11 +112,9 @@ def getValue(self): for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] return value - def size(self): - return len(self.vars) def __len__(self): return len(self.vars) - def __add__(self, other): + def __iadd__(self, other): match other: case int() | float(): self.constant += float(other) @@ -132,9 +130,22 @@ def __add__(self, other): return self case _: raise ValueError("Can't add type %s to Linear Expression" % type(other).__name__) + def __add__(self, other): + match other: + case int() | float(): + return LinearExpression(self.vars, self.coefficients, self.constant + float(other)) + case Variable(): + vars = self.vars + [other] + coeffs = self.coefficients + [1.0] + return LinearExpression(vars, coeffs, self.constant) + case LinearExpression(): + vars = self.vars + [other.vars] + coeffs = self.coefficients + [other.coefficients] + constant = self.constant + other.constant + return LinearExpression(vars, coeffs, constant) def __radd__(self, other): return self + other - def __sub__(self, other): + def __isub__(self, other): match other: case int() | float(): self.constant -= float(other) @@ -151,6 +162,23 @@ def __sub__(self, other): return self case _: raise ValueError("Can't sub type %s from LinearExpression" % type(other).__name__) + def __sub__(self, other): + match other: + case int() | float(): + return LinearExpression(self.vars, self.coefficients, self.constant - float(other)) + case Variable(): + vars = self.vars + [other] + coeffs = self.coefficients + [-1.0] + return LinearExpression(vars, coeffs, self.constant) + case LinearExpression(): + vars = self.vars + [other.vars] + coeffs = [] + for i in self.coefficients: + coeffs.append(i) + for i in other.coefficients: + coeffs.append[-1.0*i] + constant = self.constant - other.constant + return LinearExpression(vars, coeffs, constant) def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 @@ -206,7 +234,7 @@ def __eq__(self, other): class Constraint: def __init__(self, expr, sense, rhs, name=''): self.vindex_coeff_dict = {} - nz = len(expr.getVars()) + nz = len(expr) self.vars = expr.vars for i in range(nz): v_idx = expr.vars[i].index @@ -214,9 +242,12 @@ def __init__(self, expr, sense, rhs, name=''): self.vindex_coeff_dict[v_idx] = self.vindex_coeff_dict[v_idx] + v_coeff if v_idx in self.vindex_coeff_dict else v_coeff self.Sense = sense self.RHS = rhs - expr.getConstant() - self.CName = name + self.ConstraintName = name + self.DualValue = float('nan') + def __len__(self): + return len(self.vindex_coeff_dict) def getName(self): - return CName + return ConstraintName def getSense(self): return self.Sense def getRHS(self): @@ -247,7 +278,7 @@ def __init__(self, model_name=''): self.constrs = [] self.ObjSense = MINIMIZE self.Obj = None - self.ObjCon = 0.0 + self.ObjConstant = 0.0 self.Status = -1 self.IsMIP = False self.Settings = solver_settings.SolverSettings() @@ -261,7 +292,12 @@ def __init__(self, model_name=''): self.upper_bound = None self.var_type = None - def addVar(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, name=''): + class dict_to_object: + def __init__(self, mdict): + for key, value in mdict.items(): + setattr(self, key, value) + + def addVariable(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, name=''): n = len(self.vars) if vtype == INTEGER or vtype == BINARY: self.IsMIP = True @@ -269,12 +305,12 @@ def addVar(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, name=''): self.vars.append(var) return var - def addConstr(self, constr, name=''): + def addConstraint(self, constr, name=''): n = len(self.constrs) match constr: case Constraint(): constr.index = n - constr.CName = name + constr.ConstraintName = name self.constrs.append(constr) case _: raise ValueError("addConstraint requires a Constraint object") @@ -301,27 +337,37 @@ def setObjective(self, expr, sense=MINIMIZE): def getObjective(self): return self.Obj - def getVars(self): + def getVariabless(self): return self.vars - def getConstrs(self): + def getConstraints(self): return self.constrs @property - def NumVars(self): + def NumVariables(self): return len(self.vars) @property - def NumConstrs(self): + def NumConstraints(self): return len(self.constrs) @property def NumNZs(self): nnz = 0 for constr in self.constrs: - nnz += len(constr.vindex_coeff_dict) + nnz += len(constr) return nnz + def getCSR(self): + csr_dict = {'row_pointers' : [0], + 'column_indices' : [], + 'values' : []} + for constr in self.constrs: + csr_dict['column_indices'].extend(list(constr.vindex_coeff_dict.keys())) + csr_dict['values'].extend(list(constr.vindex_coeff_dict.values())) + csr_dict['row_pointers'].append(len(csr_dict['column_indices'])) + return self.dict_to_object(csr_dict) + def post_solve(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() @@ -337,10 +383,15 @@ def post_solve(self, solution): for var in self.vars: var.Value = primal_sol[var.index] if not self.IsMIP: - var.RC = reduced_cost[var.index] + var.ReducedCost = reduced_cost[var.index] + if not self.IsMIP: + dual_sol = solution.get_dual_solution() + if len(dual_sol) > 0: + for i, constr in enumerate(self.constrs): + constr.DualValue = dual_sol[i] self.ObjVal = self.Obj.getValue() - def optimize(self): + def solve(self): # iterate through the constraints and construct the constraint matrix and the rhs m = len(self.constrs) n = len(self.vars) From 79c484ce561a89e8fcd6c722a68039ae8c2700fb Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 28 Jul 2025 22:11:30 -0700 Subject: [PATCH 04/29] address review, add tests --- .../cuopt/cuopt/linear_programming/problem.py | 361 +++++++++++++++--- .../linear_programming/test_python_API.py | 96 +++++ 2 files changed, 399 insertions(+), 58 deletions(-) create mode 100644 python/cuopt/cuopt/tests/linear_programming/test_python_API.py diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 6ac760b607..cd531b9a66 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -2,19 +2,34 @@ import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings -import enum +from enum import StrEnum, IntEnum import numpy as np -# The type of a variable is either continuous, binary, or integer -CONTINUOUS = 'C' -INTEGER = 'I' +# The type of a variable is either continuous or integer +class vtype(StrEnum): + CONTINUOUS = 'C', + INTEGER = 'I' + +# The sense of a constraint is either LE, GE or EQ +class ctype(StrEnum): + LE = 'L', + GE = 'G', + EQ = 'E' + +# The sense of a model is either MINIMIZE or MAXIMIZE +class sense(IntEnum): + MAXIMIZE = -1, + MINIMIZE = 1 -# Variable objects hold a reference to the problem they were created from as well as info -# about there index, lower bound, upper bound, objective coefficient and variable type. -# You can add and subtract Variables to scalars and other Variables to form LinearExpression objects. -# You can multiply variables by a scalar to form a LinearExpression object class Variable: - def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, vname=''): + """ + cuOpt variable object initialized with details of the variable + such as lower bound, upper bound, type and name. + Variables are always associated with a problem and can be + created using problem.addVariable (See problem class). + """ + + def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=vtype.CONTINUOUS, vname=''): self.problem = problem self.index = index self.LB = lb @@ -24,30 +39,81 @@ def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTI self.ReducedCost = float('nan') self.VariableType = vtype self.VariableName = vname + def getIndex(self): + """ + Get the index position of the variable in the problem. + """ return self.index + def getValue(self): + """ + Returns the Value of the variable computed in current solution. + Defaults to 0 + """ return self.Value + def getObjectiveCoefficient(self): + """ + Returns the objective coefficient of the variable. + """ return self.Obj + def setObjectiveCoefficient(self, val): + """ + Sets the objective cofficient of the variable. + """ self.Obj = val + def setLowerBound(self, val): + """ + Sets the lower bound of the variable. + """ self.LB = val + def getLowerBound(self): + """ + Returns the lower bound of the variable. + """ return self.LB + def setUpperBound(self, val): + """ + Sets the upper bound of the variable. + """ self.UB = val + def getUpperBound(self): + """ + Returns the upper bound of the variable. + """ return self.UB + def setVariableType(self, val): + """ + Sets the variable type of the variable. + Variable types can be either CONTINUOUS or INTEGER. + """ self.VariableType = val + def getVariableType(self): + """ + Returns the type of the variable. + """ return self.VariableType + def setVariableName(self, val): + """ + Sets the name of the variable. + """ self.VariableName = val + def getVariableName(self): + """ + Returns the name of the variable. + """ return self.VariableName + def __add__(self, other): match other: case int() | float(): @@ -59,8 +125,10 @@ def __add__(self, other): return other + self case _: raise ValueError('Cannot add type %s to variable' % type(other).__name__) + def __radd__(self, other): return self + other + def __sub__(self, other): match other: case int() | float(): @@ -72,48 +140,87 @@ def __sub__(self, other): return other * -1.0 + self case _: raise ValueError('Cannot subtract type %s from variable' % type(other).__name__) + def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 + def __mul__(self, other): match other: case int() | float(): return LinearExpression([self], [float(other)], 0.0) case _: raise ValueError('Cannot multiply type %s with variable' % type(other).__name__) + def __rmul__(self, other): return self * other -# LinearExpressions contain a set of variables, the coefficients for the variables, and a constant -# LinearExpressions can be used to create constraints and the objective in the Problem -# LinearExpressions can be added and subtracted with other LinearExpressions and Variables -# LinearExpressions can be multiplied and divided by scalars -# LinearExpressions can be compared with scalars, Variables, and other LinearExpressions to create Constraints class LinearExpression: + """ + LinearExpressions contain a set of variables, the coefficients + for the variables, and a constant. + LinearExpressions can be used to create constraints and the + objective in the Problem. + LinearExpressions can be added and subtracted with other + LinearExpressions and Variables and can also be multiplied and + divided by scalars. + LinearExpressions can be compared with scalars, Variables, and + other LinearExpressions to create Constraints. + """ + def __init__(self, vars, coefficients, constant): self.vars = vars self.coefficients = coefficients self.constant = constant + def getVariables(self): + """ + Returns all the variables in the linear expression. + """ return self.vars + def getVariable(self, i): + """ + Gets Variable at ith index in the linear expression. + """ return self.vars[i] + def getCoefficients(self): + """ + Returns all the coefficients in the linear expression. + """ return self.coefficients + def getCoefficient(self, i): + """ + Gets the coefficient of the variable at ith index of the + linear expression. + """ return self.coefficients[i] + def getConstant(self): + """ + Returns the constant in the linear expression. + """ return self.constant + def zipVarCoefficients(self): return zip(self.vars, self.coefficients) + def getValue(self): + """ + Returns the value of the expression computed with the + current solution. + """ value = 0.0 for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] return value + def __len__(self): return len(self.vars) + def __iadd__(self, other): match other: case int() | float(): @@ -130,6 +237,7 @@ def __iadd__(self, other): return self case _: raise ValueError("Can't add type %s to Linear Expression" % type(other).__name__) + def __add__(self, other): match other: case int() | float(): @@ -139,12 +247,14 @@ def __add__(self, other): coeffs = self.coefficients + [1.0] return LinearExpression(vars, coeffs, self.constant) case LinearExpression(): - vars = self.vars + [other.vars] - coeffs = self.coefficients + [other.coefficients] + vars = self.vars + other.vars + coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant return LinearExpression(vars, coeffs, constant) + def __radd__(self, other): return self + other + def __isub__(self, other): match other: case int() | float(): @@ -162,6 +272,7 @@ def __isub__(self, other): return self case _: raise ValueError("Can't sub type %s from LinearExpression" % type(other).__name__) + def __sub__(self, other): match other: case int() | float(): @@ -171,7 +282,7 @@ def __sub__(self, other): coeffs = self.coefficients + [-1.0] return LinearExpression(vars, coeffs, self.constant) case LinearExpression(): - vars = self.vars + [other.vars] + vars = self.vars + other.vars coeffs = [] for i in self.coefficients: coeffs.append(i) @@ -179,9 +290,11 @@ def __sub__(self, other): coeffs.append[-1.0*i] constant = self.constant - other.constant return LinearExpression(vars, coeffs, constant) + def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 + def __mul__(self, other): match other: case int() | float(): @@ -190,8 +303,10 @@ def __mul__(self, other): return self case _: raise ValueError("Can't multiply type %s by LinearExpresson" % type(other).__name__) + def __rmul__(self, other): return self * other + def __div__(self, other): match other: case int() | float(): @@ -200,38 +315,44 @@ def __div__(self, other): return self case _: raise ValueError("Can't divide LinearExpression by type %s" % type(other).__name__) + def __le__(self, other): match other: case int() | float(): - return Constraint(self, CONSTRAINT_LE, float(other)) + return Constraint(self, ctype.LE, float(other)) case Variable() | LinearExpression(): # expr1 <= expr2 -> expr1 - expr2 <= 0 expr = self - other - return Constraint(expr, CONSTRAINT_LE, 0.0) + return Constraint(expr, ctype.LE, 0.0) + def __ge__(self, other): match other: case int() | float(): - return Constraint(self, CONSTRAINT_GE, float(other)) + return Constraint(self, ctype.GE, float(other)) case Variable() | LinearExpression(): # expr1 >= expr2 -> expr1 - expr2 >= 0 expr = self - other - return Constraint(expr, CONSTRAINT_GE, 0.0) + return Constraint(expr, ctype.GE, 0.0) + def __eq__(self, other): match other: case int() | float(): - return Constraint(self, CONSTRAINT_EQ, float(other)) + return Constraint(self, ctype.EQ, float(other)) case Variable() | LinearExpression(): # expr1 == expr2 -> expr1 - expr2 == 0 expr = self - other - return Constraint(expr, CONSTRAINT_EQ, 0.0) - -# The sense of a constraint is either less than or equal, greater than or equal, or equal -CONSTRAINT_LE = 'L' -CONSTRAINT_GE = 'G' -CONSTRAINT_EQ = 'E' + return Constraint(expr, ctype.EQ, 0.0) # A constraint contains a linear expression, the sense of the constraint, and the right-hand side of the constraint class Constraint: + """ + cuOpt constraint object containing a linear expression, + the sense of the constraint, and the right-hand side of + the constraint. + Constraints are associated with a problem and can be + created using problem.addConstraint (See problem class). + """ + def __init__(self, expr, sense, rhs, name=''): self.vindex_coeff_dict = {} nz = len(expr) @@ -244,39 +365,60 @@ def __init__(self, expr, sense, rhs, name=''): self.RHS = rhs - expr.getConstant() self.ConstraintName = name self.DualValue = float('nan') + def __len__(self): return len(self.vindex_coeff_dict) - def getName(self): - return ConstraintName + + def getConstraintName(self): + """ + Returns the name of the constraint. + """ + return self.ConstraintName + def getSense(self): + """ + Returns the sense of the constraint. + Constraint sense can be LE(<=), GE(>=) or EQ(==). + """ return self.Sense + def getRHS(self): + """ + Returns the right-hand side value of the constraint. + """ return self.RHS + def getCoefficient(self, var): + """ + Returns the coefficient of a variable in the constraint. + """ v_idx = var.index - return vindex_coeff_dict[v_idx] + return self.vindex_coeff_dict[v_idx] + @property def Slack(self): + """ + Returns the constraint Slack in the current solution. + """ lhs = 0.0 for var in self.vars: lhs += var.Value * self.vindex_coeff_dict[var.index] return self.RHS - lhs -# The sense of a problem is either minimize or maximize -MINIMIZE = 0 -MAXIMIZE = 1 -# A Problem defines a Linear Program or Mixed Integer Program -# Variable can be be created by calling addVariable() -# Constraints can be added by calling addConstraint() -# The objective can be set by calling setObjective() -# The problem data is formed when calling optimize() class Problem: + """ + A Problem defines a Linear Program or Mixed Integer Program + Variable can be be created by calling addVariable() + Constraints can be added by calling addConstraint() + The objective can be set by calling setObjective() + The problem data is formed when calling solve() + """ def __init__(self, model_name=''): self.Name = model_name self.vars = [] self.constrs = [] - self.ObjSense = MINIMIZE + self.ObjSense = sense.MINIMIZE self.Obj = None self.ObjConstant = 0.0 self.Status = -1 @@ -297,15 +439,57 @@ def __init__(self, mdict): for key, value in mdict.items(): setattr(self, key, value) - def addVariable(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=CONTINUOUS, name=''): + def addVariable(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=vtype.CONTINUOUS, name=''): + """ + Adds a variable to the problem defined by lower bound, + upper bound, type and name. + + Parameters + ---------- + lb : float + Lower bound of the variable. Defaults to 0. + ub : float + Upper bound of the variable. Defaults to infinity. + vtype : enum + vtype.CONTINUOUS or vtype.INTEGER. Defaults to CONTINUOUS. + name : string + Name of the variable. Optional. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + """ n = len(self.vars) - if vtype == INTEGER or vtype == BINARY: + if vtype == vtype.INTEGER: self.IsMIP = True var = Variable(self, n, lb, ub, obj, vtype, name) self.vars.append(var) return var def addConstraint(self, constr, name=''): + """ + Adds a constraint to the problem defined by constraint object + and name. A constraint is generated using LinearExpression, + Sense and RHS. + + Parameters + ---------- + constr : Constraint + Constructed using LinearExpressions (See Examples) + name : string + Name of the variable. Optional. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> y = problem.addVariable(name="Var2") + >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") + >>> expr = 3*x + y + >>> problem.addConstraint(expr + x == 20, name="Constr2") + """ + n = len(self.constrs) match constr: case Constraint(): @@ -315,7 +499,29 @@ def addConstraint(self, constr, name=''): case _: raise ValueError("addConstraint requires a Constraint object") - def setObjective(self, expr, sense=MINIMIZE): + def setObjective(self, expr, sense=sense.MINIMIZE): + """ + Set the Objective of the problem with an expression that needs to + be MINIMIZED or MAXIMIZED. + + Parameters + ---------- + expr : LinearExpression or Variable or Constant + Objective expression that needs maximization or minimization. + sense : enum + Sets whether the problem is a maximization or a minimization + problem. Values passed can either be sense.MINIMIZE or + sense.MAXIMIZE. Defaults to sense.MINIMIZE. + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> y = problem.addVariable(name="Var2") + >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") + >>> expr = 3*x + y + >>> problem.addConstraint(expr + x == 20, name="Constr2") + >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + """ self.ObjSense = sense match expr: case int() | float(): @@ -335,30 +541,52 @@ def setObjective(self, expr, sense=MINIMIZE): self.Obj = expr def getObjective(self): + """ + Get the Objective expression of the problem. + """ return self.Obj - def getVariabless(self): + def getVariables(self): + """ + Get a list of all the variables in the problem. + """ return self.vars def getConstraints(self): + """ + Get a list of all the Constraints in a problem. + """ return self.constrs @property def NumVariables(self): + """ + Returns number of variables in the problem. + """ return len(self.vars) @property def NumConstraints(self): + """ + Returns number of contraints in the problem. + """ return len(self.constrs) @property def NumNZs(self): + """ + Returns number of non-zeros in the problem. + """ nnz = 0 for constr in self.constrs: nnz += len(constr) return nnz def getCSR(self): + """ + Computes and returns the CSR representation of the + constraint matrix. + """ csr_dict = {'row_pointers' : [0], 'column_indices' : [], 'values' : []} @@ -373,9 +601,9 @@ def post_solve(self, solution): self.SolveTime = solution.get_solve_time() if solution.problem_category == 0: - self.SolutionStats = solution.get_lp_stats() + self.SolutionStats = self.dict_to_object(solution.get_lp_stats()) else: - self.SolutionStats = solution.get_milp_stats() + self.SolutionStats = self.dict_to_object(solution.get_milp_stats()) primal_sol = solution.get_primal_solution() reduced_cost = solution.get_reduced_cost() @@ -392,6 +620,22 @@ def post_solve(self, solution): self.ObjVal = self.Obj.getValue() def solve(self): + """ + Optimizes the LP or MIP problem with the added variables, + constraints and objective. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> y = problem.addVariable(name="Var2") + >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") + >>> expr = 3*x + y + >>> problem.addConstraint(expr + x == 20, name="Constr2") + >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + >>> problem.solve() + """ + # iterate through the constraints and construct the constraint matrix and the rhs m = len(self.constrs) n = len(self.vars) @@ -407,26 +651,27 @@ def solve(self): self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) - self.objective = [] - self.lower_bound, self.upper_bound = [], [] - self.var_type = [] + self.objective = np.zeros(n) + self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) + self.var_type = np.empty(n, dtype='S1') for j in range(n): - self.objective.append(self.vars[j].getObjectiveCoefficient()) - self.var_type.append(self.vars[j].getVariableType()) - self.lower_bound.append(self.vars[j].getLowerBound()) - self.upper_bound.append(self.vars[j].getUpperBound()) + self.objective[j] = self.vars[j].getObjectiveCoefficient() + self.var_type[j] = self.vars[j].getVariableType() + self.lower_bound[j] = self.vars[j].getLowerBound() + self.upper_bound[j] = self.vars[j].getUpperBound() # Initialize datamodel dm = data_model.DataModel() dm.set_csr_constraint_matrix(np.array(self.values), np.array(self.column_indicies), np.array(self.row_pointers)) - dm.set_maximize(self.ObjSense) + if self.ObjSense == -1: + dm.set_maximize(True) dm.set_constraint_bounds(np.array(self.rhs)) dm.set_row_types(np.array(self.row_sense)) - dm.set_objective_coefficients(np.array(self.objective)) - dm.set_variable_lower_bounds(np.array(self.lower_bound)) - dm.set_variable_upper_bounds(np.array(self.upper_bound)) - dm.set_variable_types(np.array(self.var_type)) + dm.set_objective_coefficients(self.objective) + dm.set_variable_lower_bounds(self.lower_bound) + dm.set_variable_upper_bounds(self.upper_bound) + dm.set_variable_types(self.var_type) # Call Solver solution = solver.Solve(dm, self.Settings) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py new file mode 100644 index 0000000000..63ff540c7f --- /dev/null +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import numpy as np +import pytest + +from cuopt.linear_programming.problem import Problem +from cuopt.linear_programming.problem import sense, vtype, ctype + + +def test_model(): + + prob = Problem("Simple MIP") + assert prob.Name == "Simple MIP" + + # Adding Variable + x = prob.addVariable(lb=0, vtype=vtype.INTEGER, name="V_x") + y = prob.addVariable(lb=10, ub=50, vtype=vtype.INTEGER, name="V_y") + + assert x.getVariableName() == "V_x" + assert y.getUpperBound() == 50 + assert y.getLowerBound() == 10 + assert x.getVariableType() == vtype.INTEGER + assert y.getVariableType() == "I" + + # Adding Constraints + prob.addConstraint(2*x + 4*y >= 230, name="C1") + prob.addConstraint(3*x + 2*y <= 190, name="C2") + + expected_name = ["C1", "C2"] + expected_coefficient_x = [2, 3] + expected_coefficient_y = [4, 2] + expected_sense = [ctype.GE, "L"] + expected_rhs = [230, 190] + for i, c in enumerate(prob.getConstraints()): + assert c.getConstraintName() == expected_name[i] + assert c.getSense() == expected_sense[i] + assert c.getRHS() == expected_rhs[i] + assert c.getCoefficient(x) == expected_coefficient_x[i] + assert c.getCoefficient(y) == expected_coefficient_y[i] + + assert prob.NumVariables == 2 + assert prob.NumConstraints == 2 + assert prob.NumNZs == 4 + + # Setting Objective + expr = 5*x + 3*y + prob.setObjective(expr, sense=sense.MAXIMIZE) + + expected_obj_coeff = [5, 3] + assert expr.getVariables() == [x, y] + assert expr.getCoefficients() == expected_obj_coeff + assert prob.ObjSense == sense.MAXIMIZE + assert prob.getObjective() is expr + + # Adding Settings + prob.Settings.set_parameter("time_limit", 60) + + # Solving Problem + prob.solve() + assert prob.Status.name == "Optimal" + + csr = prob.getCSR() + expected_row_pointers = [0, 2, 4] + expected_column_indices = [0, 1, 0, 1] + expected_values = [2.0, 4.0, 3.0, 2.0] + + assert csr.row_pointers == expected_row_pointers + assert csr.column_indices == expected_column_indices + assert csr.values == expected_values + + expected_slack = [-6, 0] + expected_var_values = [36, 41] + + for i, var in enumerate(prob.getVariables()): + assert var.Value == pytest.approx(expected_var_values[i]) + assert var.getObjectiveCoefficient() == expected_obj_coeff[i] + + assert prob.ObjVal == 303 + + for i, c in enumerate(prob.getConstraints()): + assert c.Slack == pytest.approx(expected_slack[i]) From a904552f9503eb0a8349a37eb4b465def8ab290a Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 29 Jul 2025 00:09:21 -0700 Subject: [PATCH 05/29] formatting --- .../cuopt/cuopt/linear_programming/problem.py | 230 +++++++++++++----- .../linear_programming/test_python_API.py | 134 +++++++++- 2 files changed, 294 insertions(+), 70 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index cd531b9a66..9b9e161077 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1,26 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import IntEnum, StrEnum + +import numpy as np + import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings -from enum import StrEnum, IntEnum -import numpy as np # The type of a variable is either continuous or integer -class vtype(StrEnum): - CONTINUOUS = 'C', - INTEGER = 'I' +class VType(StrEnum): + CONTINUOUS = ("C",) + INTEGER = "I" + # The sense of a constraint is either LE, GE or EQ -class ctype(StrEnum): - LE = 'L', - GE = 'G', - EQ = 'E' +class CType(StrEnum): + LE = ("L",) + GE = ("G",) + EQ = "E" + # The sense of a model is either MINIMIZE or MAXIMIZE class sense(IntEnum): - MAXIMIZE = -1, + MAXIMIZE = (-1,) MINIMIZE = 1 + class Variable: """ cuOpt variable object initialized with details of the variable @@ -29,14 +49,23 @@ class Variable: created using problem.addVariable (See problem class). """ - def __init__(self, problem, index, lb=0.0, ub=float('inf'), obj=0.0, vtype=vtype.CONTINUOUS, vname=''): + def __init__( + self, + problem, + index, + lb=0.0, + ub=float("inf"), + obj=0.0, + vtype=VType.CONTINUOUS, + vname="", + ): self.problem = problem self.index = index self.LB = lb self.UB = ub self.Obj = obj self.Value = 0.0 - self.ReducedCost = float('nan') + self.ReducedCost = float("nan") self.VariableType = vtype self.VariableName = vname @@ -124,7 +153,9 @@ def __add__(self, other): case LinearExpression(): return other + self case _: - raise ValueError('Cannot add type %s to variable' % type(other).__name__) + raise ValueError( + "Cannot add type %s to variable" % type(other).__name__ + ) def __radd__(self, other): return self + other @@ -139,7 +170,10 @@ def __sub__(self, other): # self - other -> other * -1.0 + self return other * -1.0 + self case _: - raise ValueError('Cannot subtract type %s from variable' % type(other).__name__) + raise ValueError( + "Cannot subtract type %s from variable" + % type(other).__name__ + ) def __rsub__(self, other): # other - self -> other + self * -1.0 @@ -150,7 +184,10 @@ def __mul__(self, other): case int() | float(): return LinearExpression([self], [float(other)], 0.0) case _: - raise ValueError('Cannot multiply type %s with variable' % type(other).__name__) + raise ValueError( + "Cannot multiply type %s with variable" + % type(other).__name__ + ) def __rmul__(self, other): return self * other @@ -222,31 +259,44 @@ def __len__(self): return len(self.vars) def __iadd__(self, other): + # Compute expr1 += expr2 match other: case int() | float(): + # Update just the constant value self.constant += float(other) return self case Variable(): + # Append just a variable with coefficient 1.0 self.vars.append(other) self.coefficients.append(1.0) return self case LinearExpression(): + # Append all variables, coefficients and constants self.vars.extend(other.vars) self.coefficients.extend(other.coefficients) self.constant += other.constant return self case _: - raise ValueError("Can't add type %s to Linear Expression" % type(other).__name__) + raise ValueError( + "Can't add type %s to Linear Expression" + % type(other).__name__ + ) def __add__(self, other): + # Compute expr3 = expr1 + expr2 match other: case int() | float(): - return LinearExpression(self.vars, self.coefficients, self.constant + float(other)) + # Update just the constant value + return LinearExpression( + self.vars, self.coefficients, self.constant + float(other) + ) case Variable(): + # Append just a variable with coefficient 1.0 vars = self.vars + [other] coeffs = self.coefficients + [1.0] return LinearExpression(vars, coeffs, self.constant) case LinearExpression(): + # Append all variables, coefficients and constants vars = self.vars + other.vars coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant @@ -256,38 +306,51 @@ def __radd__(self, other): return self + other def __isub__(self, other): + # Compute expr1 -= expr2 match other: case int() | float(): + # Update just the constant value self.constant -= float(other) return self case Variable(): + # Append just a variable with coefficient -1.0 self.vars.append(other) self.coefficients.append(-1.0) return self case LinearExpression(): + # Append all variables, coefficients and constants self.vars.extend(other.vars) - for coeff in other.coefficients: # Same Time Complexity as extend O(k), k = nelements to append + for coeff in other.coefficients: self.coefficients.append(-coeff) self.constant -= other.constant return self case _: - raise ValueError("Can't sub type %s from LinearExpression" % type(other).__name__) + raise ValueError( + "Can't sub type %s from LinearExpression" + % type(other).__name__ + ) def __sub__(self, other): + # Compute expr3 = expr1 - expr2 match other: case int() | float(): - return LinearExpression(self.vars, self.coefficients, self.constant - float(other)) + # Update just the constant value + return LinearExpression( + self.vars, self.coefficients, self.constant - float(other) + ) case Variable(): + # Append just a variable with coefficient -1.0 vars = self.vars + [other] coeffs = self.coefficients + [-1.0] return LinearExpression(vars, coeffs, self.constant) case LinearExpression(): + # Append all variables, coefficients and constants vars = self.vars + other.vars coeffs = [] for i in self.coefficients: coeffs.append(i) for i in other.coefficients: - coeffs.append[-1.0*i] + coeffs.append(-1.0 * i) constant = self.constant - other.constant return LinearExpression(vars, coeffs, constant) @@ -295,55 +358,93 @@ def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 - def __mul__(self, other): + def __imul__(self, other): + # Compute expr *= constant match other: case int() | float(): - self.coefficients = [coeff * float(other) for coeff in self.coefficients] + self.coefficients = [ + coeff * float(other) for coeff in self.coefficients + ] self.constant = self.constant * float(other) return self case _: - raise ValueError("Can't multiply type %s by LinearExpresson" % type(other).__name__) + raise ValueError( + "Can't multiply type %s by LinearExpresson" + % type(other).__name__ + ) + + def __mul__(self, other): + # Compute expr2 = expr1 * constant + match other: + case int() | float(): + coeffs = [coeff * float(other) for coeff in self.coefficients] + constant = self.constant * float(other) + return LinearExpression(self.vars, coeffs, constant) + case _: + raise ValueError( + "Can't multiply type %s by LinearExpresson" + % type(other).__name__ + ) def __rmul__(self, other): return self * other - def __div__(self, other): + def __itruediv__(self, other): + # Compute expr /= constant match other: case int() | float(): - self.coefficients = [coeff / float(other) for coeff in self.coefficients] + self.coefficients = [ + coeff / float(other) for coeff in self.coefficients + ] self.constant = self.constant / float(other) return self case _: - raise ValueError("Can't divide LinearExpression by type %s" % type(other).__name__) + raise ValueError( + "Can't divide LinearExpression by type %s" + % type(other).__name__ + ) + + def __truediv__(self, other): + # Compute expr2 = expr1 / constant + match other: + case int() | float(): + coeffs = [coeff / float(other) for coeff in self.coefficients] + constant = self.constant / float(other) + return LinearExpression(self.vars, coeffs, constant) + case _: + raise ValueError( + "Can't divide LinearExpression by type %s" + % type(other).__name__ + ) def __le__(self, other): match other: case int() | float(): - return Constraint(self, ctype.LE, float(other)) + return Constraint(self, CType.LE, float(other)) case Variable() | LinearExpression(): # expr1 <= expr2 -> expr1 - expr2 <= 0 expr = self - other - return Constraint(expr, ctype.LE, 0.0) + return Constraint(expr, CType.LE, 0.0) def __ge__(self, other): match other: case int() | float(): - return Constraint(self, ctype.GE, float(other)) + return Constraint(self, CType.GE, float(other)) case Variable() | LinearExpression(): # expr1 >= expr2 -> expr1 - expr2 >= 0 expr = self - other - return Constraint(expr, ctype.GE, 0.0) + return Constraint(expr, CType.GE, 0.0) def __eq__(self, other): match other: case int() | float(): - return Constraint(self, ctype.EQ, float(other)) + return Constraint(self, CType.EQ, float(other)) case Variable() | LinearExpression(): # expr1 == expr2 -> expr1 - expr2 == 0 expr = self - other - return Constraint(expr, ctype.EQ, 0.0) + return Constraint(expr, CType.EQ, 0.0) + -# A constraint contains a linear expression, the sense of the constraint, and the right-hand side of the constraint class Constraint: """ cuOpt constraint object containing a linear expression, @@ -353,18 +454,23 @@ class Constraint: created using problem.addConstraint (See problem class). """ - def __init__(self, expr, sense, rhs, name=''): + def __init__(self, expr, sense, rhs, name=""): self.vindex_coeff_dict = {} nz = len(expr) self.vars = expr.vars + self.index = -1 for i in range(nz): v_idx = expr.vars[i].index v_coeff = expr.coefficients[i] - self.vindex_coeff_dict[v_idx] = self.vindex_coeff_dict[v_idx] + v_coeff if v_idx in self.vindex_coeff_dict else v_coeff + self.vindex_coeff_dict[v_idx] = ( + self.vindex_coeff_dict[v_idx] + v_coeff + if v_idx in self.vindex_coeff_dict + else v_coeff + ) self.Sense = sense self.RHS = rhs - expr.getConstant() self.ConstraintName = name - self.DualValue = float('nan') + self.DualValue = float("nan") def __len__(self): return len(self.vindex_coeff_dict) @@ -414,7 +520,8 @@ class Problem: The objective can be set by calling setObjective() The problem data is formed when calling solve() """ - def __init__(self, model_name=''): + + def __init__(self, model_name=""): self.Name = model_name self.vars = [] self.constrs = [] @@ -439,7 +546,9 @@ def __init__(self, mdict): for key, value in mdict.items(): setattr(self, key, value) - def addVariable(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=vtype.CONTINUOUS, name=''): + def addVariable( + self, lb=0.0, ub=float("inf"), obj=0.0, vtype=VType.CONTINUOUS, name="" + ): """ Adds a variable to the problem defined by lower bound, upper bound, type and name. @@ -458,16 +567,17 @@ def addVariable(self, lb=0.0, ub=float('inf'), obj=0.0, vtype=vtype.CONTINUOUS, Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER, + name="Var1") """ n = len(self.vars) - if vtype == vtype.INTEGER: + if vtype == VType.INTEGER: self.IsMIP = True var = Variable(self, n, lb, ub, obj, vtype, name) self.vars.append(var) return var - def addConstraint(self, constr, name=''): + def addConstraint(self, constr, name=""): """ Adds a constraint to the problem defined by constraint object and name. A constraint is generated using LinearExpression, @@ -483,7 +593,7 @@ def addConstraint(self, constr, name=''): Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y @@ -512,10 +622,11 @@ def setObjective(self, expr, sense=sense.MINIMIZE): Sets whether the problem is a maximization or a minimization problem. Values passed can either be sense.MINIMIZE or sense.MAXIMIZE. Defaults to sense.MINIMIZE. + Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y @@ -537,7 +648,9 @@ def setObjective(self, expr, sense=sense.MINIMIZE): for var, coeff in expr.zipVarCoefficients(): self.vars[var.getIndex()].setObjectiveCoefficient(coeff) case _: - raise ValueError('Objective must be a LinearExpression or a constant') + raise ValueError( + "Objective must be a LinearExpression or a constant" + ) self.Obj = expr def getObjective(self): @@ -587,13 +700,13 @@ def getCSR(self): Computes and returns the CSR representation of the constraint matrix. """ - csr_dict = {'row_pointers' : [0], - 'column_indices' : [], - 'values' : []} + csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} for constr in self.constrs: - csr_dict['column_indices'].extend(list(constr.vindex_coeff_dict.keys())) - csr_dict['values'].extend(list(constr.vindex_coeff_dict.values())) - csr_dict['row_pointers'].append(len(csr_dict['column_indices'])) + csr_dict["column_indices"].extend( + list(constr.vindex_coeff_dict.keys()) + ) + csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) + csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) return self.dict_to_object(csr_dict) def post_solve(self, solution): @@ -611,7 +724,7 @@ def post_solve(self, solution): for var in self.vars: var.Value = primal_sol[var.index] if not self.IsMIP: - var.ReducedCost = reduced_cost[var.index] + var.ReducedCost = reduced_cost[var.index] if not self.IsMIP: dual_sol = solution.get_dual_solution() if len(dual_sol) > 0: @@ -627,7 +740,7 @@ def solve(self): Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=vtype.INTEGER, name="Var1") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y @@ -636,8 +749,7 @@ def solve(self): >>> problem.solve() """ - # iterate through the constraints and construct the constraint matrix and the rhs - m = len(self.constrs) + # iterate through the constraints and construct the constraint matrix n = len(self.vars) self.row_pointers = [0] self.column_indicies = [] @@ -653,7 +765,7 @@ def solve(self): self.objective = np.zeros(n) self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) - self.var_type = np.empty(n, dtype='S1') + self.var_type = np.empty(n, dtype="S1") for j in range(n): self.objective[j] = self.vars[j].getObjectiveCoefficient() @@ -663,7 +775,11 @@ def solve(self): # Initialize datamodel dm = data_model.DataModel() - dm.set_csr_constraint_matrix(np.array(self.values), np.array(self.column_indicies), np.array(self.row_pointers)) + dm.set_csr_constraint_matrix( + np.array(self.values), + np.array(self.column_indicies), + np.array(self.row_pointers), + ) if self.ObjSense == -1: dm.set_maximize(True) dm.set_constraint_bounds(np.array(self.rhs)) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 63ff540c7f..73c652f9fd 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -13,13 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - -import numpy as np import pytest -from cuopt.linear_programming.problem import Problem -from cuopt.linear_programming.problem import sense, vtype, ctype +from cuopt.linear_programming.problem import CType, Problem, VType, sense def test_model(): @@ -28,23 +24,23 @@ def test_model(): assert prob.Name == "Simple MIP" # Adding Variable - x = prob.addVariable(lb=0, vtype=vtype.INTEGER, name="V_x") - y = prob.addVariable(lb=10, ub=50, vtype=vtype.INTEGER, name="V_y") + x = prob.addVariable(lb=0, vtype=VType.INTEGER, name="V_x") + y = prob.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") assert x.getVariableName() == "V_x" assert y.getUpperBound() == 50 assert y.getLowerBound() == 10 - assert x.getVariableType() == vtype.INTEGER + assert x.getVariableType() == VType.INTEGER assert y.getVariableType() == "I" # Adding Constraints - prob.addConstraint(2*x + 4*y >= 230, name="C1") - prob.addConstraint(3*x + 2*y <= 190, name="C2") + prob.addConstraint(2 * x + 4 * y >= 230, name="C1") + prob.addConstraint(3 * x + 2 * y <= 190, name="C2") expected_name = ["C1", "C2"] expected_coefficient_x = [2, 3] expected_coefficient_y = [4, 2] - expected_sense = [ctype.GE, "L"] + expected_sense = [CType.GE, "L"] expected_rhs = [230, 190] for i, c in enumerate(prob.getConstraints()): assert c.getConstraintName() == expected_name[i] @@ -58,7 +54,7 @@ def test_model(): assert prob.NumNZs == 4 # Setting Objective - expr = 5*x + 3*y + expr = 5 * x + 3 * y prob.setObjective(expr, sense=sense.MAXIMIZE) expected_obj_coeff = [5, 3] @@ -73,6 +69,7 @@ def test_model(): # Solving Problem prob.solve() assert prob.Status.name == "Optimal" + assert prob.SolveTime < 60 csr = prob.getCSR() expected_row_pointers = [0, 2, 4] @@ -80,7 +77,7 @@ def test_model(): expected_values = [2.0, 4.0, 3.0, 2.0] assert csr.row_pointers == expected_row_pointers - assert csr.column_indices == expected_column_indices + assert csr.column_indices == expected_column_indices assert csr.values == expected_values expected_slack = [-6, 0] @@ -94,3 +91,114 @@ def test_model(): for i, c in enumerate(prob.getConstraints()): assert c.Slack == pytest.approx(expected_slack[i]) + + +def test_linear_expression(): + + prob = Problem() + + x = prob.addVariable() + y = prob.addVariable() + z = prob.addVariable() + + expr1 = 2 * x + 5 + 3 * y + expr2 = y - z + 2 * x - 3 + + expr3 = expr1 + expr2 + expr4 = expr1 - expr2 + + # Test expr1 and expr 2 is unchanged + assert expr1.getCoefficients() == [2, 3] + assert expr1.getVariables() == [x, y] + assert expr1.getConstant() == 5 + assert expr2.getCoefficients() == [1, -1, 2] + assert expr2.getVariables() == [y, z, x] + assert expr2.getConstant() == -3 + + # Testing add and sub + assert expr3.getCoefficients() == [2, 3, 1, -1, 2] + assert expr3.getVariables() == [x, y, y, z, x] + assert expr3.getConstant() == 2 + assert expr4.getCoefficients() == [2, 3, -1, 1, -2] + assert expr4.getVariables() == [x, y, y, z, x] + assert expr4.getConstant() == 8 + + expr5 = 8 * y - x - 5 + expr6 = expr5 / 2 + expr7 = expr5 * 2 + + # Test expr5 is unchanged + assert expr5.getCoefficients() == [8, -1] + assert expr5.getVariables() == [y, x] + assert expr5.getConstant() == -5 + + # Test mul and truediv + assert expr6.getCoefficients() == [4, -0.5] + assert expr6.getVariables() == [y, x] + assert expr6.getConstant() == -2.5 + assert expr7.getCoefficients() == [16, -2] + assert expr7.getVariables() == [y, x] + assert expr7.getConstant() == -10 + + expr6 *= 2 + expr7 /= 2 + + # Test imul and itruediv + assert expr6.getCoefficients() == [8, -1] + assert expr6.getVariables() == [y, x] + assert expr6.getConstant() == -5 + assert expr7.getCoefficients() == [8, -1] + assert expr7.getVariables() == [y, x] + assert expr7.getConstant() == -5 + + +def test_constraint_matrix(): + + prob = Problem() + + a = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="a") + b = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="b") + c = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="c") + d = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="d") + e = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="e") + f = prob.addVariable(lb=0, ub=float("inf"), vtype="C", name="f") + + # 2*a + 3*e + 1 + 4*d - 2*e + f - 8 <= 90 i.e. 2a + e + 4d + f <= 97 + prob.addConstraint(2 * a + 3 * e + 1 + 4 * d - 2 * e + f - 8 <= 90, "C1") + # d + 5*c - a - 4*d - 2 + 5*b - 20 >= 10 i.e. -3d + 5c - a + 5b >= 32 + prob.addConstraint(d + 5 * c - a - 4 * d - 2 + 5 * b - 20 >= 10, "C2") + # 7*f + 3 - 2*b + c == 3*f - 61 + 8*e i.e. 4f - 2b + c - 8e == -64 + prob.addConstraint(7 * f + 3 - 2 * b + c == 3 * f - 61 + 8 * e, "C3") + + sense = [] + rhs = [] + for c in prob.getConstraints(): + sense.append(c.Sense) + rhs.append(c.RHS) + + csr = prob.getCSR() + + exp_row_pointers = [0, 4, 8, 12] + exp_column_indices = [0, 4, 3, 5, 2, 3, 0, 1, 5, 1, 2, 4] + exp_values = [ + 2.0, + 1.0, + 4.0, + 1.0, + 5.0, + -3.0, + -1.0, + 5.0, + 4.0, + -2.0, + 1.0, + -8.0, + ] + exp_sense = ["L", "G", "E"] + exp_rhs = [97, 32, -64] + + assert csr.row_pointers == exp_row_pointers + assert csr.column_indices == exp_column_indices + assert csr.values == exp_values + assert sense == exp_sense + assert rhs == exp_rhs From 4877396ec08cce971d493023df38d7e6df5623c8 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 29 Jul 2025 09:01:39 -0700 Subject: [PATCH 06/29] fix String Enum --- .../cuopt/cuopt/linear_programming/__init__.py | 2 +- .../cuopt/cuopt/linear_programming/problem.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index f49060ca08..7941ad9110 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -15,9 +15,9 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel +from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve -from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solver_settings import ( PDLPSolverMode, SolverMethod, diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 9b9e161077..4603d13ae7 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import IntEnum, StrEnum +from enum import Enum import numpy as np @@ -23,21 +23,21 @@ # The type of a variable is either continuous or integer -class VType(StrEnum): - CONTINUOUS = ("C",) +class VType(str, Enum): + CONTINUOUS = "C" INTEGER = "I" # The sense of a constraint is either LE, GE or EQ -class CType(StrEnum): - LE = ("L",) - GE = ("G",) +class CType(str, Enum): + LE = "L" + GE = "G" EQ = "E" # The sense of a model is either MINIMIZE or MAXIMIZE -class sense(IntEnum): - MAXIMIZE = (-1,) +class sense(int, Enum): + MAXIMIZE = -1, MINIMIZE = 1 @@ -783,7 +783,7 @@ def solve(self): if self.ObjSense == -1: dm.set_maximize(True) dm.set_constraint_bounds(np.array(self.rhs)) - dm.set_row_types(np.array(self.row_sense)) + dm.set_row_types(np.array(self.row_sense, dtype="S1")) dm.set_objective_coefficients(self.objective) dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) From bcd82b6c9575bc27cb04c98f36a3fd13cdd79bd7 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 16:23:52 -0400 Subject: [PATCH 07/29] Update python/cuopt/cuopt/tests/linear_programming/test_python_API.py fix copyright date Co-authored-by: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> --- python/cuopt/cuopt/tests/linear_programming/test_python_API.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 73c652f9fd..1e5f5822b3 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); From e503101b94e5ac9a9f89002aa27bb298d67229d4 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 16:24:36 -0400 Subject: [PATCH 08/29] Update python/cuopt/cuopt/linear_programming/problem.py Fix copyright date Co-authored-by: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> --- python/cuopt/cuopt/linear_programming/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 4603d13ae7..d120b86441 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); From 51ad041dd2e6cf3ef326f3b0b1c947f65929547c Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Jul 2025 17:29:24 -0500 Subject: [PATCH 09/29] Add docs and examples --- docs/cuopt/source/cuopt-python/index.rst | 11 +- .../source/cuopt-python/lp-milp/index.rst | 14 + .../cuopt-python/lp-milp/lp-milp-api.rst | 43 +++ .../cuopt-python/lp-milp/lp-milp-examples.rst | 247 ++++++++++++++++++ docs/cuopt/source/introduction.rst | 3 +- docs/cuopt/source/lp-features.rst | 2 + docs/cuopt/source/milp-features.rst | 2 + .../cuopt/cuopt/linear_programming/problem.py | 14 +- 8 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 docs/cuopt/source/cuopt-python/lp-milp/index.rst create mode 100644 docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst create mode 100644 docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst diff --git a/docs/cuopt/source/cuopt-python/index.rst b/docs/cuopt/source/cuopt-python/index.rst index d54d1f8355..7a412804ab 100644 --- a/docs/cuopt/source/cuopt-python/index.rst +++ b/docs/cuopt/source/cuopt-python/index.rst @@ -21,4 +21,13 @@ This section contains details on the cuOpt Python package. :name: Routing Optimization :titlesonly: - Routing Optimization \ No newline at end of file + Routing Optimization + + +.. toctree:: + :maxdepth: 3 + :caption: Linear Programming and Mixed Integer Linear Programming + :name: LP and MILP API + :titlesonly: + + Linear Programming and Mixed Integer Linear Programming \ No newline at end of file diff --git a/docs/cuopt/source/cuopt-python/lp-milp/index.rst b/docs/cuopt/source/cuopt-python/lp-milp/index.rst new file mode 100644 index 0000000000..0d60ccc411 --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-milp/index.rst @@ -0,0 +1,14 @@ +======================================================= +Linear Programming and Mixed Integer Linear Programming +======================================================= + +This section contains details on the cuOpt linear programming and mixed integer linear programming Python API. + +.. toctree:: + :maxdepth: 3 + :caption: LP and MILP + :name: LP and MILP + :titlesonly: + + lp-milp-api.rst + lp-milp-examples.rst \ No newline at end of file diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst new file mode 100644 index 0000000000..62a2cbeaa5 --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst @@ -0,0 +1,43 @@ +========================= +LP and MILP API Reference +========================= + +.. autoclass:: cuopt.linear_programming.problem.VType + :members: + :member-order: bysource + :undoc-members: + :exclude-members: capitalize, casefold, center, count, encode, endswith, expandtabs, find, format, format_map, index, isalnum, isalpha, isascii, isdecimal, isdigit, isidentifier, islower, isnumeric, isprintable, isspace, istitle, isupper, join, ljust, lower, lstrip, maketrans, partition, removeprefix, removesuffix, replace, rfind, rindex, rjust, rpartition, rsplit, rstrip, split, splitlines, startswith, strip, swapcase, title, translate, upper, zfill + +.. autoclass:: cuopt.linear_programming.problem.CType + :members: + :member-order: bysource + :undoc-members: + :exclude-members: capitalize, casefold, center, count, encode, endswith, expandtabs, find, format, format_map, index, isalnum, isalpha, isascii, isdecimal, isdigit, isidentifier, islower, isnumeric, isprintable, isspace, istitle, isupper, join, ljust, lower, lstrip, maketrans, partition, removeprefix, removesuffix, replace, rfind, rindex, rjust, rpartition, rsplit, rstrip, split, splitlines, startswith, strip, swapcase, title, translate, upper, zfill + +.. autoclass:: cuopt.linear_programming.problem.sense + :members: + :member-order: bysource + :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes + :no-inherited-members: + +.. autoclass:: cuopt.linear_programming.problem.Problem + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: cuopt.linear_programming.problem.Variable + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: cuopt.linear_programming.problem.Constraint + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: cuopt.linear_programming.problem.LinearExpression + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst new file mode 100644 index 0000000000..481d176c37 --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -0,0 +1,247 @@ +==================== +LP and MILP Examples +==================== + +This section contains examples of how to use the cuOpt linear programming and mixed integer linear programming Python API. + +.. note:: + + The examples in this section are not exhaustive. They are provided to help you get started with the cuOpt linear programming and mixed integer linear programming Python API. For more examples, please refer to the `cuopt-examples GitHub repository `_. + + +Simple Linear Programming Example +--------------------------------- + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, VType, CType, sense + + # Create a new problem + prob = Problem("Simple LP") + + # Add variables + x = prob.addVariable(lb=0, vtype=VType.CONTINUOUS, name="x") + y = prob.addVariable(lb=0, vtype=VType.CONTINUOUS, name="y") + + # Add constraints + prob.addConstraint(x + y <= 10, name="c1") + prob.addConstraint(x - y >= 0, name="c2") + + # Set objective function + prob.setObjective(x + y, sense=sense.MAXIMIZE) + + # Solve the problem + prob.solve() + + # Check solution status + if prob.Status.name == "Optimal": + print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") + print(f"Objective value = {prob.ObjVal}") + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.00 seconds + x = 5.0 + y = 5.0 + Objective value = 10.0 + +Mixed Integer Linear Programming Example +---------------------------------------- + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, VType, CType, sense + + # Create a new MIP problem + prob = Problem("Simple MIP") + + # Add integer variables with bounds + x = prob.addVariable(lb=0, vtype=VType.INTEGER, name="V_x") + y = prob.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") + + # Verify variable properties + print(f"Variable x name: {x.getVariableName()}") + print(f"Variable y upper bound: {y.getUpperBound()}") + print(f"Variable y lower bound: {y.getLowerBound()}") + print(f"Variable x type: {x.getVariableType()}") + print(f"Variable y type: {y.getVariableType()}") # Returns "I" for integer + + # Add constraints + prob.addConstraint(2 * x + 4 * y >= 230, name="C1") + prob.addConstraint(3 * x + 2 * y <= 190, name="C2") + + # Verify constraint properties + expected_name = ["C1", "C2"] + expected_coefficient_x = [2, 3] + expected_coefficient_y = [4, 2] + expected_sense = [CType.GE, "L"] # GE = Greater Equal, L = Less Equal + expected_rhs = [230, 190] + + for i, c in enumerate(prob.getConstraints()): + print(f"Constraint {c.getConstraintName()}:") + print(f" Sense: {c.getSense()}") + print(f" RHS: {c.getRHS()}") + print(f" Coefficient of x: {c.getCoefficient(x)}") + print(f" Coefficient of y: {c.getCoefficient(y)}") + + # Check problem statistics + print(f"Number of variables: {prob.NumVariables}") + print(f"Number of constraints: {prob.NumConstraints}") + print(f"Number of non-zeros: {prob.NumNZs}") + + # Set objective function + expr = 5 * x + 3 * y + prob.setObjective(expr, sense=sense.MAXIMIZE) + + # Verify objective properties + expected_obj_coeff = [5, 3] + print(f"Objective variables: {expr.getVariables()}") + print(f"Objective coefficients: {expr.getCoefficients()}") + print(f"Objective sense: {prob.ObjSense}") + print(f"Objective expression: {prob.getObjective()}") + + # Configure solver settings + prob.Settings.set_parameter("time_limit", 60) + + # Solve the problem + prob.solve() + + # Check solution status and results + if prob.Status.name == "Optimal": + print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") + print(f"Objective value = {prob.getObjectiveValue()}") + else: + print(f"Problem status: {prob.Status.name}") + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.00 seconds + x = 5.0 + y = 5.0 + Objective value = 10.0 + +Advanced Example: Production Planning +------------------------------------- + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, VType, CType, sense + + # Production planning problem + prob = Problem("Production Planning") + + # Decision variables: production quantities + # x1 = units of product A + # x2 = units of product B + x1 = prob.addVariable(lb=0, vtype=VType.INTEGER, name="Product_A") + x2 = prob.addVariable(lb=0, vtype=VType.INTEGER, name="Product_B") + + # Resource constraints + # Machine time: 2 hours per unit of A, 1 hour per unit of B, max 100 hours + prob.addConstraint(2 * x1 + x2 <= 100, name="Machine_Time") + + # Labor: 1 hour per unit of A, 3 hours per unit of B, max 120 hours + prob.addConstraint(x1 + 3 * x2 <= 120, name="Labor_Hours") + + # Material: 4 units per unit of A, 2 units per unit of B, max 200 units + prob.addConstraint(4 * x1 + 2 * x2 <= 200, name="Material") + + # Demand constraints + prob.addConstraint(x1 >= 10, name="Min_Demand_A") + prob.addConstraint(x2 >= 15, name="Min_Demand_B") + + # Objective: maximize profit + # Profit: $50 per unit of A, $30 per unit of B + prob.setObjective(50 * x1 + 30 * x2, sense=sense.MAXIMIZE) + + # Solve with time limit + prob.Settings.set_parameter("time_limit", 30) + prob.solve() + + # Display results + if prob.Status.name == "Optimal": + print("=== Production Planning Solution ===") + print(f"Status: {prob.Status.name}") + print(f"Solve time: {prob.SolveTime:.2f} seconds") + print(f"Product A production: {x1.getValue()} units") + print(f"Product B production: {x2.getValue()} units") + print(f"Total profit: ${prob.ObjVal:.2f}") + + # Check constraint satisfaction + print("\n=== Constraint Analysis ===") + for constraint in prob.getConstraints(): + print(f"{constraint.getConstraintName()}: {constraint.getSense()} {constraint.getRHS()}") + else: + print(f"Problem not solved optimally. Status: {prob.Status.name}") + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.00 seconds + x = 5.0 + y = 5.0 + Objective value = 10.0 + +Working with Expressions and Constraints +---------------------------------------- + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, VType, CType, sense + + prob = Problem("Expression Example") + + # Create variables + x = prob.addVariable(lb=0, name="x") + y = prob.addVariable(lb=0, name="y") + z = prob.addVariable(lb=0, name="z") + + # Create complex expressions + expr1 = 2 * x + 3 * y - z + expr2 = x + y + z + + # Add constraints using expressions + prob.addConstraint(expr1 <= 100, name="Complex_Constraint_1") + prob.addConstraint(expr2 >= 20, name="Complex_Constraint_2") + + # Add constraint with different senses + prob.addConstraint(x + y == 50, name="Equality_Constraint") # Equality + prob.addConstraint(x <= 30, name="Upper_Bound") # Less than + prob.addConstraint(y >= 10, name="Lower_Bound") # Greater than + + # Set objective + prob.setObjective(expr1 + expr2, sense=sense.MAXIMIZE) + + # Solve + prob.solve() + + if prob.Status.name == "Optimal": + print("=== Expression Example Results ===") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") + print(f"z = {z.getValue()}") + print(f"Objective value = {prob.ObjVal}") + + # Show constraint details + print("\n=== Constraint Details ===") + for constraint in prob.getConstraints(): + print(f"{constraint.getConstraintName()}: {constraint.getSense()} {constraint.getRHS()}") + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.00 seconds + x = 5.0 + y = 5.0 + z = 5.0 + Objective value = 10.0 \ No newline at end of file diff --git a/docs/cuopt/source/introduction.rst b/docs/cuopt/source/introduction.rst index 6714467278..aaf164198f 100644 --- a/docs/cuopt/source/introduction.rst +++ b/docs/cuopt/source/introduction.rst @@ -112,8 +112,7 @@ cuOpt supports the following APIs: - cuOpt is written in C++ and includes a native C++ API. However, we do not provide documentation for the C++ API at this time. We anticipate that the C++ API will change significantly in the future. Use it at your own risk. - Python support - :doc:`Routing (TSP, VRP, and PDP) - Python ` - - Linear Programming (LP) and Mixed Integer Linear Programming (MILP) - - cuOpt includes a Python API that is used as the backend of the cuOpt server. However, we do not provide documentation for the Python API at this time. We suggest using cuOpt server to access cuOpt via Python. We anticipate that the Python API will change significantly in the future. Use it at your own risk. + - :doc:`Linear Programming (LP) and Mixed Integer Linear Programming (MILP) - Python ` - Server support - :doc:`Linear Programming (LP) - Server ` - :doc:`Mixed Integer Linear Programming (MILP) - Server ` diff --git a/docs/cuopt/source/lp-features.rst b/docs/cuopt/source/lp-features.rst index f3861ffacd..25b8da751c 100644 --- a/docs/cuopt/source/lp-features.rst +++ b/docs/cuopt/source/lp-features.rst @@ -16,6 +16,8 @@ The LP solver can be accessed in the following ways: - **C API**: A native C API that provides direct low-level access to cuOpt's LP capabilities, enabling integration into any application or system that can interface with C. +- **Python SDK**: A Python package that provides direct access to cuOpt's LP capabilities through a simple, intuitive API. This allows for seamless integration into Python applications and workflows. For more information, see :doc:`cuopt-python/quick-start`. + - **As a Self-Hosted Service**: cuOpt's LP solver can be deployed as a in your own infrastructure, enabling you to maintain full control while integrating it into your existing systems. Each option provide the same powerful linear optimization capabilities while offering flexibility in deployment and integration. diff --git a/docs/cuopt/source/milp-features.rst b/docs/cuopt/source/milp-features.rst index 814207a1c0..88254181b6 100644 --- a/docs/cuopt/source/milp-features.rst +++ b/docs/cuopt/source/milp-features.rst @@ -16,6 +16,8 @@ The MILP solver can be accessed in the following ways: - **C API**: A native C API that provides direct low-level access to cuOpt's MILP solver, enabling integration into any application or system that can interface with C. +- **Python SDK**: A Python package that provides direct access to cuOpt's MILP capabilities through a simple, intuitive API. This allows for seamless integration into Python applications and workflows. For more information, see :doc:`cuopt-python/quick-start`. + - **As a Self-Hosted Service**: cuOpt's MILP solver can be deployed in your own infrastructure, enabling you to maintain full control while integrating it into your existing systems. Each option provide the same powerful mixed-integer linear optimization capabilities while offering flexibility in deployment and integration. diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 4603d13ae7..657f83a7f0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -22,22 +22,28 @@ import cuopt.linear_programming.solver_settings as solver_settings -# The type of a variable is either continuous or integer class VType(str, Enum): + """ + The type of a variable is either continuous or integer. + """ CONTINUOUS = "C" INTEGER = "I" -# The sense of a constraint is either LE, GE or EQ class CType(str, Enum): + """ + The sense of a constraint is either LE, GE or EQ. + """ LE = "L" GE = "G" EQ = "E" -# The sense of a model is either MINIMIZE or MAXIMIZE class sense(int, Enum): - MAXIMIZE = -1, + """ + The sense of a model is either MINIMIZE or MAXIMIZE. + """ + MAXIMIZE = -1 MINIMIZE = 1 From a31527268d3a1f590358148cfd8af415b3b3afa2 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 29 Jul 2025 21:11:11 -0700 Subject: [PATCH 10/29] update problem.py --- python/cuopt/cuopt/linear_programming/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 4603d13ae7..b12d9c59d2 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -37,7 +37,7 @@ class CType(str, Enum): # The sense of a model is either MINIMIZE or MAXIMIZE class sense(int, Enum): - MAXIMIZE = -1, + MAXIMIZE = -1 MINIMIZE = 1 From 13864b7b8f33373820ffb421691d28c1f70429b9 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 29 Jul 2025 23:09:55 -0700 Subject: [PATCH 11/29] separate solver settings, add incumbent --- .../cuopt/cuopt/linear_programming/problem.py | 18 ++++- .../linear_programming/test_python_API.py | 75 ++++++++++++++++++- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index d7ec5ce119..6f672348de 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -26,6 +26,7 @@ class VType(str, Enum): """ The type of a variable is either continuous or integer. """ + CONTINUOUS = "C" INTEGER = "I" @@ -34,6 +35,7 @@ class CType(str, Enum): """ The sense of a constraint is either LE, GE or EQ. """ + LE = "L" GE = "G" EQ = "E" @@ -43,6 +45,7 @@ class sense(int, Enum): """ The sense of a model is either MINIMIZE or MAXIMIZE. """ + MAXIMIZE = -1 MINIMIZE = 1 @@ -259,7 +262,7 @@ def getValue(self): value = 0.0 for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] - return value + return value + self.constant def __len__(self): return len(self.vars) @@ -715,6 +718,15 @@ def getCSR(self): csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) return self.dict_to_object(csr_dict) + def get_incumbent_values(self, solution, vars): + """ + Extract incumbent values of the vars from a problem solution. + """ + values = [] + for var in vars: + values.append(solution[var.index]) + return values + def post_solve(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() @@ -738,7 +750,7 @@ def post_solve(self, solution): constr.DualValue = dual_sol[i] self.ObjVal = self.Obj.getValue() - def solve(self): + def solve(self, settings=solver_settings.SolverSettings()): """ Optimizes the LP or MIP problem with the added variables, constraints and objective. @@ -796,7 +808,7 @@ def solve(self): dm.set_variable_types(self.var_type) # Call Solver - solution = solver.Solve(dm, self.Settings) + solution = solver.Solve(dm, settings) # Post Solve self.post_solve(solution) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 1e5f5822b3..83caabdc7a 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -15,6 +15,11 @@ import pytest +from cuopt.linear_programming import SolverSettings +from cuopt.linear_programming.internals import ( + GetSolutionCallback, + SetSolutionCallback, +) from cuopt.linear_programming.problem import CType, Problem, VType, sense @@ -63,13 +68,14 @@ def test_model(): assert prob.ObjSense == sense.MAXIMIZE assert prob.getObjective() is expr - # Adding Settings - prob.Settings.set_parameter("time_limit", 60) + # Initialize Settings + settings = SolverSettings() + settings.set_parameter("time_limit", 5) # Solving Problem - prob.solve() + prob.solve(settings) assert prob.Status.name == "Optimal" - assert prob.SolveTime < 60 + assert prob.SolveTime < 5 csr = prob.getCSR() expected_row_pointers = [0, 2, 4] @@ -202,3 +208,64 @@ def test_constraint_matrix(): assert csr.values == exp_values assert sense == exp_sense assert rhs == exp_rhs + + +def test_incumbent_solutions(): + + # Callback for incumbent solution + class CustomGetSolutionCallback(GetSolutionCallback): + def __init__(self): + super().__init__() + self.n_callbacks = 0 + self.solutions = [] + + def get_solution(self, solution, solution_cost): + + self.n_callbacks += 1 + assert len(solution) > 0 + assert len(solution_cost) == 1 + + self.solutions.append( + { + "solution": solution.copy_to_host(), + "cost": solution_cost.copy_to_host()[0], + } + ) + + class CustomSetSolutionCallback(SetSolutionCallback): + def __init__(self, get_callback): + super().__init__() + self.n_callbacks = 0 + self.get_callback = get_callback + + def set_solution(self, solution, solution_cost): + self.n_callbacks += 1 + if self.get_callback.solutions: + solution[:] = self.get_callback.solutions[-1]["solution"] + solution_cost[0] = float( + self.get_callback.solutions[-1]["cost"] + ) + + prob = Problem() + x = prob.addVariable(vtype=VType.INTEGER) + y = prob.addVariable(vtype=VType.INTEGER) + prob.addConstraint(2 * x + 4 * y >= 230) + prob.addConstraint(3 * x + 2 * y <= 190) + prob.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) + + # callback = CustomGetSolutionCallback() + get_callback = CustomGetSolutionCallback() + set_callback = CustomSetSolutionCallback(get_callback) + settings = SolverSettings() + settings.set_mip_callback(get_callback) + settings.set_mip_callback(set_callback) + settings.set_parameter("time_limit", 0.01) + + prob.solve(settings) + print(prob.SolveTime) + # assert prob.Status.name == "FeasibleFound" + print(prob.Status.name) + print(get_callback.n_callbacks) + # assert get_callback.n_callbacks > 0 + # values = prob.get_incumbent_values(callback.solution, [x, y]) + # print(values) From 9df40796bace6ea2f2ece00d4218e36cbfffcc6c Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 11:03:40 -0500 Subject: [PATCH 12/29] update example --- .../cuopt-python/lp-milp/lp-milp-examples.rst | 184 +++++++----------- 1 file changed, 72 insertions(+), 112 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 481d176c37..931155497b 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -17,36 +17,36 @@ Simple Linear Programming Example from cuopt.linear_programming.problem import Problem, VType, CType, sense # Create a new problem - prob = Problem("Simple LP") + problem = Problem("Simple LP") # Add variables - x = prob.addVariable(lb=0, vtype=VType.CONTINUOUS, name="x") - y = prob.addVariable(lb=0, vtype=VType.CONTINUOUS, name="y") + x = problem.addVariable(lb=0, vtype=VType.CONTINUOUS, name="x") + y = problem.addVariable(lb=0, vtype=VType.CONTINUOUS, name="y") # Add constraints - prob.addConstraint(x + y <= 10, name="c1") - prob.addConstraint(x - y >= 0, name="c2") + problem.addConstraint(x + y <= 10, name="c1") + problem.addConstraint(x - y >= 0, name="c2") # Set objective function - prob.setObjective(x + y, sense=sense.MAXIMIZE) + problem.setObjective(x + y, sense=sense.MAXIMIZE) # Solve the problem - prob.solve() + problem.solve() # Check solution status - if prob.Status.name == "Optimal": - print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + if problem.Status.name == "Optimal": + print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") - print(f"Objective value = {prob.ObjVal}") + print(f"Objective value = {problem.ObjVal}") The response is as follows: .. code-block:: text - Optimal solution found in 0.00 seconds - x = 5.0 - y = 5.0 + Optimal solution found in 0.01 seconds + x = 10.0 + y = 0.0 Objective value = 10.0 Mixed Integer Linear Programming Example @@ -57,76 +57,43 @@ Mixed Integer Linear Programming Example from cuopt.linear_programming.problem import Problem, VType, CType, sense # Create a new MIP problem - prob = Problem("Simple MIP") + problem = Problem("Simple MIP") # Add integer variables with bounds - x = prob.addVariable(lb=0, vtype=VType.INTEGER, name="V_x") - y = prob.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") - - # Verify variable properties - print(f"Variable x name: {x.getVariableName()}") - print(f"Variable y upper bound: {y.getUpperBound()}") - print(f"Variable y lower bound: {y.getLowerBound()}") - print(f"Variable x type: {x.getVariableType()}") - print(f"Variable y type: {y.getVariableType()}") # Returns "I" for integer + x = problem.addVariable(vtype=VType.INTEGER, name="V_x") + y = problem.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") # Add constraints - prob.addConstraint(2 * x + 4 * y >= 230, name="C1") - prob.addConstraint(3 * x + 2 * y <= 190, name="C2") - - # Verify constraint properties - expected_name = ["C1", "C2"] - expected_coefficient_x = [2, 3] - expected_coefficient_y = [4, 2] - expected_sense = [CType.GE, "L"] # GE = Greater Equal, L = Less Equal - expected_rhs = [230, 190] - - for i, c in enumerate(prob.getConstraints()): - print(f"Constraint {c.getConstraintName()}:") - print(f" Sense: {c.getSense()}") - print(f" RHS: {c.getRHS()}") - print(f" Coefficient of x: {c.getCoefficient(x)}") - print(f" Coefficient of y: {c.getCoefficient(y)}") - - # Check problem statistics - print(f"Number of variables: {prob.NumVariables}") - print(f"Number of constraints: {prob.NumConstraints}") - print(f"Number of non-zeros: {prob.NumNZs}") + problem.addConstraint(2 * x + 4 * y >= 230, name="C1") + problem.addConstraint(3 * x + 2 * y <= 190, name="C2") # Set objective function - expr = 5 * x + 3 * y - prob.setObjective(expr, sense=sense.MAXIMIZE) - - # Verify objective properties - expected_obj_coeff = [5, 3] - print(f"Objective variables: {expr.getVariables()}") - print(f"Objective coefficients: {expr.getCoefficients()}") - print(f"Objective sense: {prob.ObjSense}") - print(f"Objective expression: {prob.getObjective()}") + problem.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) # Configure solver settings - prob.Settings.set_parameter("time_limit", 60) + problem.Settings.set_parameter("time_limit", 60) # Solve the problem - prob.solve() + problem.solve() # Check solution status and results - if prob.Status.name == "Optimal": - print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + if problem.Status.name == "Optimal": + print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") - print(f"Objective value = {prob.getObjectiveValue()}") + print(f"Objective value = {problem.ObjVal}") else: - print(f"Problem status: {prob.Status.name}") + print(f"Problem status: {problem.Status.name}") The response is as follows: .. code-block:: text Optimal solution found in 0.00 seconds - x = 5.0 - y = 5.0 - Objective value = 10.0 + x = 36.0 + y = 40.99999999999999 + Objective value = 303.0 + Advanced Example: Production Planning ------------------------------------- @@ -136,60 +103,55 @@ Advanced Example: Production Planning from cuopt.linear_programming.problem import Problem, VType, CType, sense # Production planning problem - prob = Problem("Production Planning") + problem = Problem("Production Planning") # Decision variables: production quantities # x1 = units of product A # x2 = units of product B - x1 = prob.addVariable(lb=0, vtype=VType.INTEGER, name="Product_A") - x2 = prob.addVariable(lb=0, vtype=VType.INTEGER, name="Product_B") + x1 = problem.addVariable(lb=10, vtype=VType.INTEGER, name="Product_A") + x2 = problem.addVariable(lb=15, vtype=VType.INTEGER, name="Product_B") # Resource constraints # Machine time: 2 hours per unit of A, 1 hour per unit of B, max 100 hours - prob.addConstraint(2 * x1 + x2 <= 100, name="Machine_Time") + problem.addConstraint(2 * x1 + x2 <= 100, name="Machine_Time") # Labor: 1 hour per unit of A, 3 hours per unit of B, max 120 hours - prob.addConstraint(x1 + 3 * x2 <= 120, name="Labor_Hours") + problem.addConstraint(x1 + 3 * x2 <= 120, name="Labor_Hours") # Material: 4 units per unit of A, 2 units per unit of B, max 200 units - prob.addConstraint(4 * x1 + 2 * x2 <= 200, name="Material") - - # Demand constraints - prob.addConstraint(x1 >= 10, name="Min_Demand_A") - prob.addConstraint(x2 >= 15, name="Min_Demand_B") + problem.addConstraint(4 * x1 + 2 * x2 <= 200, name="Material") # Objective: maximize profit # Profit: $50 per unit of A, $30 per unit of B - prob.setObjective(50 * x1 + 30 * x2, sense=sense.MAXIMIZE) + problem.setObjective(50 * x1 + 30 * x2, sense=sense.MAXIMIZE) # Solve with time limit - prob.Settings.set_parameter("time_limit", 30) - prob.solve() + problem.Settings.set_parameter("time_limit", 30) + problem.solve() # Display results - if prob.Status.name == "Optimal": + if problem.Status.name == "Optimal": print("=== Production Planning Solution ===") - print(f"Status: {prob.Status.name}") - print(f"Solve time: {prob.SolveTime:.2f} seconds") + print(f"Status: {problem.Status.name}") + print(f"Solve time: {problem.SolveTime:.2f} seconds") print(f"Product A production: {x1.getValue()} units") print(f"Product B production: {x2.getValue()} units") - print(f"Total profit: ${prob.ObjVal:.2f}") + print(f"Total profit: ${problem.ObjVal:.2f}") - # Check constraint satisfaction - print("\n=== Constraint Analysis ===") - for constraint in prob.getConstraints(): - print(f"{constraint.getConstraintName()}: {constraint.getSense()} {constraint.getRHS()}") else: - print(f"Problem not solved optimally. Status: {prob.Status.name}") + print(f"Problem not solved optimally. Status: {problem.Status.name}") The response is as follows: .. code-block:: text - Optimal solution found in 0.00 seconds - x = 5.0 - y = 5.0 - Objective value = 10.0 + === Production Planning Solution === + + Status: Optimal + Solve time: 0.09 seconds + Product A production: 36.0 units + Product B production: 28.000000000000004 units + Total profit: $2640.00 Working with Expressions and Constraints ---------------------------------------- @@ -198,50 +160,48 @@ Working with Expressions and Constraints from cuopt.linear_programming.problem import Problem, VType, CType, sense - prob = Problem("Expression Example") + problem = Problem("Expression Example") # Create variables - x = prob.addVariable(lb=0, name="x") - y = prob.addVariable(lb=0, name="y") - z = prob.addVariable(lb=0, name="z") + x = problem.addVariable(lb=0, name="x") + y = problem.addVariable(lb=0, name="y") + z = problem.addVariable(lb=0, name="z") # Create complex expressions expr1 = 2 * x + 3 * y - z expr2 = x + y + z # Add constraints using expressions - prob.addConstraint(expr1 <= 100, name="Complex_Constraint_1") - prob.addConstraint(expr2 >= 20, name="Complex_Constraint_2") + problem.addConstraint(expr1 <= 100, name="Complex_Constraint_1") + problem.addConstraint(expr2 >= 20, name="Complex_Constraint_2") # Add constraint with different senses - prob.addConstraint(x + y == 50, name="Equality_Constraint") # Equality - prob.addConstraint(x <= 30, name="Upper_Bound") # Less than - prob.addConstraint(y >= 10, name="Lower_Bound") # Greater than + problem.addConstraint(x + y == 50, name="Equality_Constraint") + problem.addConstraint(1 * x <= 30, name="Upper_Bound_X") + problem.addConstraint(1 * y >= 10, name="Lower_Bound_Y") + problem.addConstraint(1 * z <= 100, name="Upper_Bound_Z") # Set objective - prob.setObjective(expr1 + expr2, sense=sense.MAXIMIZE) + problem.setObjective(x + 2 * y + 3 * z, sense=sense.MAXIMIZE) + + problem.Settings.set_parameter("time_limit", 20) + + problem.solve() - # Solve - prob.solve() - if prob.Status.name == "Optimal": + if problem.Status.name == "Optimal": print("=== Expression Example Results ===") print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") print(f"z = {z.getValue()}") - print(f"Objective value = {prob.ObjVal}") + print(f"Objective value = {problem.ObjVal}") - # Show constraint details - print("\n=== Constraint Details ===") - for constraint in prob.getConstraints(): - print(f"{constraint.getConstraintName()}: {constraint.getSense()} {constraint.getRHS()}") - The response is as follows: .. code-block:: text - Optimal solution found in 0.00 seconds - x = 5.0 - y = 5.0 - z = 5.0 - Objective value = 10.0 \ No newline at end of file + === Expression Example Results === + x = 0.0 + y = 50.0 + z = 99.99999999999999 + Objective value = 399.99999999999994 From 40abc690100be0db839a3e7d65794dd6982ef4ce Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 12:17:49 -0500 Subject: [PATCH 13/29] update solver setting --- .../cuopt-python/lp-milp/lp-milp-examples.rst | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 931155497b..1eff3aeff3 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -15,6 +15,7 @@ Simple Linear Programming Example .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.settings import SolverSettings # Create a new problem problem = Problem("Simple LP") @@ -30,8 +31,12 @@ Simple Linear Programming Example # Set objective function problem.setObjective(x + y, sense=sense.MAXIMIZE) + # Configure solver settings + settings = SolverSettings() + settings.set_parameter("time_limit", 60) + # Solve the problem - problem.solve() + problem.solve(settings) # Check solution status if problem.Status.name == "Optimal": @@ -55,6 +60,7 @@ Mixed Integer Linear Programming Example .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.settings import SolverSettings # Create a new MIP problem problem = Problem("Simple MIP") @@ -71,10 +77,11 @@ Mixed Integer Linear Programming Example problem.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) # Configure solver settings - problem.Settings.set_parameter("time_limit", 60) + settings = SolverSettings() + settings.set_parameter("time_limit", 60) # Solve the problem - problem.solve() + problem.solve(settings) # Check solution status and results if problem.Status.name == "Optimal": @@ -101,6 +108,7 @@ Advanced Example: Production Planning .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.settings import SolverSettings # Production planning problem problem = Problem("Production Planning") @@ -126,8 +134,9 @@ Advanced Example: Production Planning problem.setObjective(50 * x1 + 30 * x2, sense=sense.MAXIMIZE) # Solve with time limit - problem.Settings.set_parameter("time_limit", 30) - problem.solve() + settings = SolverSettings() + settings.set_parameter("time_limit", 30) + problem.solve(settings) # Display results if problem.Status.name == "Optimal": @@ -159,6 +168,7 @@ Working with Expressions and Constraints .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.settings import SolverSettings problem = Problem("Expression Example") @@ -184,9 +194,10 @@ Working with Expressions and Constraints # Set objective problem.setObjective(x + 2 * y + 3 * z, sense=sense.MAXIMIZE) - problem.Settings.set_parameter("time_limit", 20) + settings = SolverSettings() + settings.set_parameter("time_limit", 20) - problem.solve() + problem.solve(settings) if problem.Status.name == "Optimal": From 67a3ff76c983b8a911fb30bfc2a7af8fe53d7dfc Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 12:27:13 -0500 Subject: [PATCH 14/29] update cupy deps --- dependencies.yaml | 19 +++++++------------ python/cuopt/pyproject.toml | 3 ++- python/cuopt_server/pyproject.toml | 1 + 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/dependencies.yaml b/dependencies.yaml index 3aa6c94601..743faf3fa5 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -371,17 +371,6 @@ dependencies: # This index is needed for cubinlinker, ptxcompiler. - --extra-index-url=https://pypi.nvidia.com - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple - specific: - - output_types: [requirements, pyproject] - matrices: - - matrix: - cuda: "12.*" - cuda_suffixed: "true" - packages: - - cupy-cuda12x - - matrix: null - packages: - - cupy-cuda12x test_python_cuopt_server: common: @@ -537,7 +526,13 @@ dependencies: - output_types: conda packages: - cupy>=12.0.0 - + specific: + - output_types: [requirements, pyproject] + matrices: + - matrix: {cuda: "12.*"} + packages: &cupy_packages_cu12 + - cupy-cuda12x>=12.0.0 + - {matrix: null, packages: *cupy_packages_cu12} depends_on_rapids_logger: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 309fa478dc..bb507b7fa9 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -33,7 +33,7 @@ requires-python = ">=3.10" dependencies = [ "cudf==25.8.*,>=0.0.0a0", "cuopt-mps-parser==25.8.*,>=0.0.0a0", - "cupy-cuda12x", + "cupy-cuda12x>=12.0.0", "cuvs==25.8.*,>=0.0.0a0", "libcuopt==25.8.*,>=0.0.0a0", "numba-cuda>=0.14.0", @@ -151,6 +151,7 @@ matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=3.30.4", "cuopt-mps-parser==25.8.*,>=0.0.0a0", + "cupy-cuda12x>=12.0.0", "cython>=3.0.3", "libcuopt==25.8.*,>=0.0.0a0", "ninja", diff --git a/python/cuopt_server/pyproject.toml b/python/cuopt_server/pyproject.toml index ed8f9b8f26..4697cba9ad 100644 --- a/python/cuopt_server/pyproject.toml +++ b/python/cuopt_server/pyproject.toml @@ -33,6 +33,7 @@ license = { text = "Apache-2.0" } requires-python = ">=3.10" dependencies = [ "cuopt==25.8.*,>=0.0.0a0", + "cupy-cuda12x>=12.0.0", "fastapi", "httpx", "jsonref==1.1.0", From 0e0d515fbdf1b9c6bae43e16e8fd39968884ad0d Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 13:07:06 -0500 Subject: [PATCH 15/29] update doc --- .../cuopt/cuopt/linear_programming/problem.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 6f672348de..9e91c98988 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -527,7 +527,23 @@ class Problem: Variable can be be created by calling addVariable() Constraints can be added by calling addConstraint() The objective can be set by calling setObjective() - The problem data is formed when calling solve() + The problem data is formed when calling solve(). + + Parameters + ---------- + model_name : str, optional + Name of the model. Default is an empty string. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) + >>> y = problem.addVariable(name="Var2") + >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") + >>> expr = 3*x + y + >>> problem.addConstraint(expr + x == 20, name="Constr2") + >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + >>> problem.solve() """ def __init__(self, model_name=""): From 330eb917536e0b04e97c162d6d2a3ca0da48653e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 14:01:25 -0500 Subject: [PATCH 16/29] Add attributes and remove other non userfacing functions --- .../cuopt-python/lp-milp/lp-milp-api.rst | 19 +-------- .../cuopt/cuopt/linear_programming/problem.py | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst index 62a2cbeaa5..1996ff1035 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst @@ -23,21 +23,4 @@ LP and MILP API Reference .. autoclass:: cuopt.linear_programming.problem.Problem :members: :undoc-members: - :show-inheritance: - -.. autoclass:: cuopt.linear_programming.problem.Variable - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: cuopt.linear_programming.problem.Constraint - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: cuopt.linear_programming.problem.LinearExpression - :members: - :undoc-members: - :show-inheritance: - - + :show-inheritance: \ No newline at end of file diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 9e91c98988..4a1be67024 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -534,6 +534,47 @@ class Problem: model_name : str, optional Name of the model. Default is an empty string. + Attributes + ---------- + Name : str + Name of the model. + vars : list + List of variables in the problem. + constrs : list + List of constraints in the problem. + ObjSense : sense + Objective sense (MINIMIZE or MAXIMIZE). + Obj : object + The objective function. + ObjConstant : float + Constant term in the objective. + Status : int + Status of the problem after solving. + IsMIP : bool + Indicates if the problem is a Mixed Integer Program. + rhs : array-like + Right-hand side values for constraints. + row_sense : array-like + Senses for each constraint row. + row_pointers : array-like + Row pointers for constraint matrix. + column_indicies : array-like + Column indices for constraint matrix. + values : array-like + Values for constraint matrix. + lower_bound : array-like + Lower bounds for variables. + upper_bound : array-like + Upper bounds for variables. + var_type : array-like + Types of variables (continuous, integer, etc.). + SolveTime : float + Time taken to solve the problem. + SolutionStats : dict + Solution statistics. + ObjVal : float + Objective value of the problem. + Examples -------- >>> problem = problem.Problem("MIP_model") @@ -555,7 +596,6 @@ def __init__(self, model_name=""): self.ObjConstant = 0.0 self.Status = -1 self.IsMIP = False - self.Settings = solver_settings.SolverSettings() self.rhs = None self.row_sense = None From 064c2149d9981612eb166c87d71b84858cb63fd2 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 14:59:17 -0500 Subject: [PATCH 17/29] update docs and apply incumbent patch --- .../cuopt-python/lp-milp/lp-milp-examples.rst | 93 +++++++++++++++++++ .../cuopt/linear_programming/solver/solver.py | 13 ++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 1eff3aeff3..efba606e9c 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -216,3 +216,96 @@ The response is as follows: y = 50.0 z = 99.99999999999999 Objective value = 399.99999999999994 + +Working with Incumbent Solutions +-------------------------------- + +Incumbent solutions are intermediate feasible solutions found during the MIP solving process. They represent the best integer-feasible solution discovered so far and can be accessed through callback functions. + +.. note:: + Incumbent solutions are only available for Mixed Integer Programming (MIP) problems, not for pure Linear Programming (LP) problems. + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, VType, sense + from cuopt.linear_programming.solver_settings import SolverSettings + from cuopt.linear_programming.internals import GetSolutionCallback, SetSolutionCallback + + # Create a callback class to receive incumbent solutions + class IncumbentCallback(GetSolutionCallback): + def __init__(self): + super().__init__() + self.solutions = [] + self.n_callbacks = 0 + + def get_solution(self, solution, solution_cost): + """ + Called whenever the solver finds a new incumbent solution. + + Parameters + ---------- + solution : array-like + The variable values of the incumbent solution + solution_cost : array-like + The objective value of the incumbent solution + """ + self.n_callbacks += 1 + + # Store the incumbent solution + incumbent = { + "solution": solution.copy_to_host(), + "cost": solution_cost.copy_to_host()[0], + "iteration": self.n_callbacks + } + self.solutions.append(incumbent) + + print(f"Incumbent {self.n_callbacks}: {incumbent['solution']}, cost: {incumbent['cost']:.2f}") + + # Create a more complex MIP problem that will generate multiple incumbents + problem = Problem("Incumbent Example") + + # Add integer variables + x = problem.addVariable(vtype=VType.INTEGER) + y = problem.addVariable(vtype=VType.INTEGER) + + # Add constraints to create a problem that will generate multiple incumbents + problem.addConstraint(2 * x + 4 * y >= 230) + problem.addConstraint(3 * x + 2 * y <= 190) + + # Set objective to maximize + problem.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) + + # Configure solver settings with callback + settings = SolverSettings() + # Set the incumbent callback + incumbent_callback = IncumbentCallback() + settings.set_mip_callback(incumbent_callback) + settings.set_parameter("time_limit", 30) # Allow enough time to find multiple incumbents + + # Solve the problem + problem.solve(settings) + + # Display final results + print(f"\n=== Final Results ===") + print(f"Problem status: {problem.Status.name}") + print(f"Solve time: {problem.SolveTime:.2f} seconds") + print(f"Final solution: x={x.getValue()}, y={y.getValue()}") + print(f"Final objective value: {problem.ObjVal:.2f}") + +The response is as follows: + +.. code-block:: text + Optimal solution found. + Incumbent 1: [ 0. 58.], cost: 174.00 + Incumbent 2: [36. 41.], cost: 303.00 + Generated fast solution in 0.158467 seconds with objective 303.000000 + Consuming B&B solutions, solution queue size 2 + Solution objective: 303.000000 , relative_mip_gap 0.000000 solution_bound 303.000000 presolve_time 0.043211 total_solve_time 0.160270 max constraint violation 0.000000 max int violation 0.000000 max var bounds violation 0.000000 nodes 4 simplex_iterations 3 + + === Final Results === + Problem status: Optimal + Solve time: 0.16 seconds + Final solution: x=36.0, y=40.99999999999999 + Final objective value: 303.00 + + diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 4543cd523f..6ba4083a96 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -84,10 +84,17 @@ def Solve(data_model, solver_settings=None): def is_mip(var_types): if len(var_types) == 0: return False - elif "I" in var_types: - return True - return False + # Check if all types are the same (fast check) + if len(set(map(type, var_types))) == 1: + # Homogeneous - use appropriate check + if isinstance(var_types[0], bytes): + return b'I' in var_types + else: + return "I" in var_types + else: + # Mixed types - fallback to comprehensive check + return any(vt == "I" or vt == b'I' for vt in var_types) return solver_wrapper.Solve( data_model, From 4172aeb4d643612c473a4e8c2ff19a96ede38f34 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Jul 2025 15:12:44 -0500 Subject: [PATCH 18/29] fix doc --- .../source/cuopt-python/lp-milp/lp-milp-examples.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index efba606e9c..70b8ab5ffa 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -15,7 +15,7 @@ Simple Linear Programming Example .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense - from cuopt.linear_programming.settings import SolverSettings + from cuopt.linear_programming.solver_settings import SolverSettings # Create a new problem problem = Problem("Simple LP") @@ -60,7 +60,7 @@ Mixed Integer Linear Programming Example .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense - from cuopt.linear_programming.settings import SolverSettings + from cuopt.linear_programming.solver_settings import SolverSettings # Create a new MIP problem problem = Problem("Simple MIP") @@ -108,7 +108,7 @@ Advanced Example: Production Planning .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense - from cuopt.linear_programming.settings import SolverSettings + from cuopt.linear_programming.solver_settings import SolverSettings # Production planning problem problem = Problem("Production Planning") @@ -168,7 +168,7 @@ Working with Expressions and Constraints .. code-block:: python from cuopt.linear_programming.problem import Problem, VType, CType, sense - from cuopt.linear_programming.settings import SolverSettings + from cuopt.linear_programming.solver_settings import SolverSettings problem = Problem("Expression Example") @@ -295,6 +295,7 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol The response is as follows: .. code-block:: text + Optimal solution found. Incumbent 1: [ 0. 58.], cost: 174.00 Incumbent 2: [36. 41.], cost: 303.00 From 498987f77c91684bd3c64517d410827bb6583de6 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 30 Jul 2025 13:23:44 -0700 Subject: [PATCH 19/29] update tests, update isMIP in solver --- .../cuopt/cuopt/linear_programming/problem.py | 36 +++++++++++++++++ .../cuopt/linear_programming/solver/solver.py | 14 +++++-- .../linear_programming/test_python_API.py | 39 +++++++++++++------ 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 6f672348de..08a1104ff0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -201,6 +201,42 @@ def __mul__(self, other): def __rmul__(self, other): return self * other + def __le__(self, other): + match other: + case int() | float(): + expr = LinearExpression([self], [1.0], 0.0) + return Constraint(expr, CType.LE, float(other)) + case Variable() | LinearExpression(): + # var1 <= var2 -> var1 - var2 <= 0 + expr = self - other + return Constraint(expr, CType.LE, 0.0) + case _: + raise ValueError("Unsupported operation") + + def __ge__(self, other): + match other: + case int() | float(): + expr = LinearExpression([self], [1.0], 0.0) + return Constraint(expr, CType.GE, float(other)) + case Variable() | LinearExpression(): + # var1 >= var2 -> var1 - var2 >= 0 + expr = self - other + return Constraint(expr, CType.GE, 0.0) + case _: + raise ValueError("Unsupported operation") + + def __eq__(self, other): + match other: + case int() | float(): + expr = LinearExpression([self], [1.0], 0.0) + return Constraint(expr, CType.EQ, float(other)) + case Variable() | LinearExpression(): + # var1 == var2 -> var1 - var2 == 0 + expr = self - other + return Constraint(expr, CType.EQ, 0.0) + case _: + raise ValueError("Unsupported operation") + class LinearExpression: """ diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 24812e70c9..8a0ed54566 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -84,10 +84,16 @@ def Solve(data_model, solver_settings=None, log_file=""): def is_mip(var_types): if len(var_types) == 0: return False - elif "I" in var_types: - return True - - return False + # Check if all types are the same (fast check) + if len(set(map(type, var_types))) == 1: + # Homogeneous - use appropriate check + if isinstance(var_types[0], bytes): + return b"I" in var_types + else: + return "I" in var_types + else: + # Mixed types - fallback to comprehensive check + return any(vt == "I" or vt == b"I" for vt in var_types) return solver_wrapper.Solve( data_model, diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 83caabdc7a..7c60188045 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -175,6 +175,12 @@ def test_constraint_matrix(): prob.addConstraint(d + 5 * c - a - 4 * d - 2 + 5 * b - 20 >= 10, "C2") # 7*f + 3 - 2*b + c == 3*f - 61 + 8*e i.e. 4f - 2b + c - 8e == -64 prob.addConstraint(7 * f + 3 - 2 * b + c == 3 * f - 61 + 8 * e, "C3") + # a <= 5 + prob.addConstraint(a <= 5, "C4") + # d >= 7*f - b - 27 i.e. d - 7*f + b >= -27 + prob.addConstraint(d >= 7 * f - b - 27, "C5") + # c == e i.e. c - e == 0 + prob.addConstraint(c == e, "C6") sense = [] rhs = [] @@ -184,8 +190,8 @@ def test_constraint_matrix(): csr = prob.getCSR() - exp_row_pointers = [0, 4, 8, 12] - exp_column_indices = [0, 4, 3, 5, 2, 3, 0, 1, 5, 1, 2, 4] + exp_row_pointers = [0, 4, 8, 12, 13, 16, 18] + exp_column_indices = [0, 4, 3, 5, 2, 3, 0, 1, 5, 1, 2, 4, 0, 5, 1, 3, 2, 4] exp_values = [ 2.0, 1.0, @@ -199,9 +205,15 @@ def test_constraint_matrix(): -2.0, 1.0, -8.0, + 1.0, + -7.0, + 1.0, + 1.0, + 1.0, + -1.0, ] - exp_sense = ["L", "G", "E"] - exp_rhs = [97, 32, -64] + exp_sense = ["L", "G", "E", "L", "G", "E"] + exp_rhs = [97, 32, -64, 5, -27, 0] assert csr.row_pointers == exp_row_pointers assert csr.column_indices == exp_column_indices @@ -253,7 +265,6 @@ def set_solution(self, solution, solution_cost): prob.addConstraint(3 * x + 2 * y <= 190) prob.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) - # callback = CustomGetSolutionCallback() get_callback = CustomGetSolutionCallback() set_callback = CustomSetSolutionCallback(get_callback) settings = SolverSettings() @@ -262,10 +273,14 @@ def set_solution(self, solution, solution_cost): settings.set_parameter("time_limit", 0.01) prob.solve(settings) - print(prob.SolveTime) - # assert prob.Status.name == "FeasibleFound" - print(prob.Status.name) - print(get_callback.n_callbacks) - # assert get_callback.n_callbacks > 0 - # values = prob.get_incumbent_values(callback.solution, [x, y]) - # print(values) + + assert prob.Status.name == "FeasibleFound" + assert get_callback.n_callbacks > 0 + + for sol in get_callback.solutions: + x_val = sol["solution"][0] + y_val = sol["solution"][1] + cost = sol["cost"] + assert 2 * x_val + 4 * y_val >= 230 + assert 3 * x_val + 2 * y_val <= 190 + assert 5 * x_val + 3 * y_val == cost From a27fb8059250925795638057056f21c8136988e6 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 30 Jul 2025 13:27:14 -0700 Subject: [PATCH 20/29] format --- python/cuopt/cuopt/linear_programming/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 05b462eebf..f327bc301d 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -576,7 +576,7 @@ class Problem: Name of the model. vars : list List of variables in the problem. - constrs : list + constrs : list List of constraints in the problem. ObjSense : sense Objective sense (MINIMIZE or MAXIMIZE). From 105e913122d64e71f4d33a00b48a71e22831e471 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 30 Jul 2025 14:56:39 -0700 Subject: [PATCH 21/29] revert cuda-cupy changes and update attributes --- .../cuopt/cuopt/linear_programming/problem.py | 20 ------------------- python/cuopt/pyproject.toml | 3 +-- python/cuopt_server/pyproject.toml | 1 - 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index f327bc301d..469dc2951e 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -574,10 +574,6 @@ class Problem: ---------- Name : str Name of the model. - vars : list - List of variables in the problem. - constrs : list - List of constraints in the problem. ObjSense : sense Objective sense (MINIMIZE or MAXIMIZE). Obj : object @@ -588,22 +584,6 @@ class Problem: Status of the problem after solving. IsMIP : bool Indicates if the problem is a Mixed Integer Program. - rhs : array-like - Right-hand side values for constraints. - row_sense : array-like - Senses for each constraint row. - row_pointers : array-like - Row pointers for constraint matrix. - column_indicies : array-like - Column indices for constraint matrix. - values : array-like - Values for constraint matrix. - lower_bound : array-like - Lower bounds for variables. - upper_bound : array-like - Upper bounds for variables. - var_type : array-like - Types of variables (continuous, integer, etc.). SolveTime : float Time taken to solve the problem. SolutionStats : dict diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index bb507b7fa9..309fa478dc 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -33,7 +33,7 @@ requires-python = ">=3.10" dependencies = [ "cudf==25.8.*,>=0.0.0a0", "cuopt-mps-parser==25.8.*,>=0.0.0a0", - "cupy-cuda12x>=12.0.0", + "cupy-cuda12x", "cuvs==25.8.*,>=0.0.0a0", "libcuopt==25.8.*,>=0.0.0a0", "numba-cuda>=0.14.0", @@ -151,7 +151,6 @@ matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=3.30.4", "cuopt-mps-parser==25.8.*,>=0.0.0a0", - "cupy-cuda12x>=12.0.0", "cython>=3.0.3", "libcuopt==25.8.*,>=0.0.0a0", "ninja", diff --git a/python/cuopt_server/pyproject.toml b/python/cuopt_server/pyproject.toml index 4697cba9ad..ed8f9b8f26 100644 --- a/python/cuopt_server/pyproject.toml +++ b/python/cuopt_server/pyproject.toml @@ -33,7 +33,6 @@ license = { text = "Apache-2.0" } requires-python = ">=3.10" dependencies = [ "cuopt==25.8.*,>=0.0.0a0", - "cupy-cuda12x>=12.0.0", "fastapi", "httpx", "jsonref==1.1.0", From 7562bbe6ba440220ad1b1bdeaacd08a359fda256 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 30 Jul 2025 14:58:06 -0700 Subject: [PATCH 22/29] dependencies.yaml update --- dependencies.yaml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/dependencies.yaml b/dependencies.yaml index 743faf3fa5..b382a9a0ec 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -371,6 +371,17 @@ dependencies: # This index is needed for cubinlinker, ptxcompiler. - --extra-index-url=https://pypi.nvidia.com - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple + specific: + - output_types: [requirements, pyproject] + matrices: + - matrix: + cuda: "12.*" + cuda_suffixed: "true" + packages: + - cupy-cuda12x + - matrix: null + packages: + - cupy-cuda12x test_python_cuopt_server: common: @@ -526,13 +537,6 @@ dependencies: - output_types: conda packages: - cupy>=12.0.0 - specific: - - output_types: [requirements, pyproject] - matrices: - - matrix: {cuda: "12.*"} - packages: &cupy_packages_cu12 - - cupy-cuda12x>=12.0.0 - - {matrix: null, packages: *cupy_packages_cu12} depends_on_rapids_logger: common: - output_types: [conda, requirements, pyproject] From f05c7b12df7fe67a7449335c3f8c7fafe988b82b Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 30 Jul 2025 23:53:05 -0700 Subject: [PATCH 23/29] update tests, docs and address reviews --- .../cuopt-python/lp-milp/lp-milp-api.rst | 20 ++- .../cuopt/cuopt/linear_programming/problem.py | 131 +++++++++++++----- .../linear_programming/test_python_API.py | 62 +++++++-- 3 files changed, 165 insertions(+), 48 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst index 1996ff1035..072769c84d 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst @@ -20,7 +20,25 @@ LP and MILP API Reference :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes :no-inherited-members: +.. autoclass:: cuopt.linear_programming.problem.Variable + :members: + :undoc-members: + :show-inheritance: + :exclude-members: + +.. autoclass:: cuopt.linear_programming.problem.LinearExpression + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: cuopt.linear_programming.problem.Constraint + :members: + :undoc-members: + :show-inheritance: + :exclude-members: compute_slack + .. autoclass:: cuopt.linear_programming.problem.Problem :members: :undoc-members: - :show-inheritance: \ No newline at end of file + :show-inheritance: + :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 469dc2951e..d9e092f646 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -31,6 +31,10 @@ class VType(str, Enum): INTEGER = "I" +CONTINUOUS = VType.CONTINUOUS +INTEGER = VType.INTEGER + + class CType(str, Enum): """ The sense of a constraint is either LE, GE or EQ. @@ -41,6 +45,11 @@ class CType(str, Enum): EQ = "E" +LE = CType.LE +GE = CType.GE +EQ = CType.EQ + + class sense(int, Enum): """ The sense of a model is either MINIMIZE or MAXIMIZE. @@ -50,30 +59,48 @@ class sense(int, Enum): MINIMIZE = 1 +MAXIMIZE = sense.MAXIMIZE +MINIMIZE = sense.MINIMIZE + + class Variable: """ cuOpt variable object initialized with details of the variable such as lower bound, upper bound, type and name. Variables are always associated with a problem and can be created using problem.addVariable (See problem class). + + Attributes + ---------- + VariableName : str + Name of the Variable. + VariableType : CONTINUOUS or INTEGER + Variable type. + LB : float + Lower Bound of the Variable. + UB : float + Upper Bound of the Variable. + Obj : float + Coefficient of the variable in the Objective function. + Value : float + Value of the variable after solving. + ReducedCost : float + Reduced Cost after solving an LP problem. """ def __init__( self, - problem, - index, lb=0.0, ub=float("inf"), obj=0.0, vtype=VType.CONTINUOUS, vname="", ): - self.problem = problem - self.index = index + self.index = -1 self.LB = lb self.UB = ub self.Obj = obj - self.Value = 0.0 + self.Value = float("nan") self.ReducedCost = float("nan") self.VariableType = vtype self.VariableName = vname @@ -497,6 +524,19 @@ class Constraint: the constraint. Constraints are associated with a problem and can be created using problem.addConstraint (See problem class). + + Attributes + ---------- + ConstraintName : str + Name of the constraint. + Sense : LE, GE or EQ + Row sense. LE for >=, GE for <= or EQ for == . + RHS : float + Constraint right-hand side value. + Slack : float + Computed LHS - RHS with current solution. + DualValue : float + Constraint dual value in the current solution. """ def __init__(self, expr, sense, rhs, name=""): @@ -516,6 +556,7 @@ def __init__(self, expr, sense, rhs, name=""): self.RHS = rhs - expr.getConstant() self.ConstraintName = name self.DualValue = float("nan") + self.Slack = float("nan") def __len__(self): return len(self.vindex_coeff_dict) @@ -546,11 +587,8 @@ def getCoefficient(self, var): v_idx = var.index return self.vindex_coeff_dict[v_idx] - @property - def Slack(self): - """ - Returns the constraint Slack in the current solution. - """ + def compute_slack(self): + # Computes the constraint Slack in the current solution. lhs = 0.0 for var in self.vars: lhs += var.Value * self.vindex_coeff_dict[var.index] @@ -576,20 +614,22 @@ class Problem: Name of the model. ObjSense : sense Objective sense (MINIMIZE or MAXIMIZE). - Obj : object - The objective function. ObjConstant : float Constant term in the objective. Status : int Status of the problem after solving. - IsMIP : bool - Indicates if the problem is a Mixed Integer Program. SolveTime : float Time taken to solve the problem. - SolutionStats : dict - Solution statistics. - ObjVal : float + SolutionStats : object + Solution statistics for LP or MIP problem. + ObjValue : float Objective value of the problem. + NumVariables : int + Number of Variables in the problem. + NumConstraints : int + Number of constraints in the problem. + NumNZs : int + Number of non-zeros in the problem. Examples -------- @@ -611,8 +651,9 @@ def __init__(self, model_name=""): self.Obj = None self.ObjConstant = 0.0 self.Status = -1 - self.IsMIP = False + self.ObjValue = float("nan") + self.solved = False self.rhs = None self.row_sense = None self.row_pointers = None @@ -627,6 +668,19 @@ def __init__(self, mdict): for key, value in mdict.items(): setattr(self, key, value) + def reset_solved_values(self): + # Resets all post solve values + for var in self.vars: + var.Value = float("nan") + var.ReducedCost = float("nan") + + for constr in self.constrs: + constr.Slack = float("nan") + constr.DualValue = float("nan") + + self.ObjValue = float("nan") + self.solved = False + def addVariable( self, lb=0.0, ub=float("inf"), obj=0.0, vtype=VType.CONTINUOUS, name="" ): @@ -651,10 +705,11 @@ def addVariable( >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER, name="Var1") """ + if self.solved: + self.reset_solved_values() # Reset all solved values n = len(self.vars) - if vtype == VType.INTEGER: - self.IsMIP = True - var = Variable(self, n, lb, ub, obj, vtype, name) + var = Variable(lb, ub, obj, vtype, name) + var.index = n self.vars.append(var) return var @@ -680,7 +735,8 @@ def addConstraint(self, constr, name=""): >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") """ - + if self.solved: + self.reset_solved_values() # Reset all solved values n = len(self.constrs) match constr: case Constraint(): @@ -714,6 +770,8 @@ def setObjective(self, expr, sense=sense.MINIMIZE): >>> problem.addConstraint(expr + x == 20, name="Constr2") >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) """ + if self.solved: + self.reset_solved_values() # Reset all solved values self.ObjSense = sense match expr: case int() | float(): @@ -754,23 +812,17 @@ def getConstraints(self): @property def NumVariables(self): - """ - Returns number of variables in the problem. - """ + # Returns number of variables in the problem return len(self.vars) @property def NumConstraints(self): - """ - Returns number of contraints in the problem. - """ + # Returns number of contraints in the problem. return len(self.constrs) @property def NumNZs(self): - """ - Returns number of non-zeros in the problem. - """ + # Returns number of non-zeros in the problem. nnz = 0 for constr in self.constrs: nnz += len(constr) @@ -803,9 +855,11 @@ def post_solve(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() + IsMIP = False if solution.problem_category == 0: self.SolutionStats = self.dict_to_object(solution.get_lp_stats()) else: + IsMIP = True self.SolutionStats = self.dict_to_object(solution.get_milp_stats()) primal_sol = solution.get_primal_solution() @@ -813,14 +867,17 @@ def post_solve(self, solution): if len(primal_sol) > 0: for var in self.vars: var.Value = primal_sol[var.index] - if not self.IsMIP: + if not IsMIP: var.ReducedCost = reduced_cost[var.index] - if not self.IsMIP: + dual_sol = None + if not IsMIP: dual_sol = solution.get_dual_solution() - if len(dual_sol) > 0: - for i, constr in enumerate(self.constrs): - constr.DualValue = dual_sol[i] - self.ObjVal = self.Obj.getValue() + for i, constr in enumerate(self.constrs): + if dual_sol is not None: + constr.DualValue = dual_sol[i] + constr.Slack = constr.compute_slack() + self.ObjValue = self.Obj.getValue() + self.solved = True def solve(self, settings=solver_settings.SolverSettings()): """ diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 7c60188045..30f0f71159 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -13,14 +13,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math + import pytest from cuopt.linear_programming import SolverSettings -from cuopt.linear_programming.internals import ( - GetSolutionCallback, - SetSolutionCallback, +from cuopt.linear_programming.problem import ( + CONTINUOUS, + INTEGER, + MAXIMIZE, + CType, + Problem, + VType, + sense, ) -from cuopt.linear_programming.problem import CType, Problem, VType, sense + +# from cuopt.linear_programming.internals import ( +# GetSolutionCallback, +# SetSolutionCallback, +# ) def test_model(): @@ -30,7 +41,7 @@ def test_model(): # Adding Variable x = prob.addVariable(lb=0, vtype=VType.INTEGER, name="V_x") - y = prob.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") + y = prob.addVariable(lb=10, ub=50, vtype=INTEGER, name="V_y") assert x.getVariableName() == "V_x" assert y.getUpperBound() == 50 @@ -40,7 +51,7 @@ def test_model(): # Adding Constraints prob.addConstraint(2 * x + 4 * y >= 230, name="C1") - prob.addConstraint(3 * x + 2 * y <= 190, name="C2") + prob.addConstraint(3 * x + 2 * y + 10 <= 200, name="C2") expected_name = ["C1", "C2"] expected_coefficient_x = [2, 3] @@ -59,12 +70,13 @@ def test_model(): assert prob.NumNZs == 4 # Setting Objective - expr = 5 * x + 3 * y - prob.setObjective(expr, sense=sense.MAXIMIZE) + expr = 5 * x + 3 * y + 50 + prob.setObjective(expr, sense=MAXIMIZE) expected_obj_coeff = [5, 3] assert expr.getVariables() == [x, y] assert expr.getCoefficients() == expected_obj_coeff + assert expr.getConstant() == 50 assert prob.ObjSense == sense.MAXIMIZE assert prob.getObjective() is expr @@ -72,8 +84,10 @@ def test_model(): settings = SolverSettings() settings.set_parameter("time_limit", 5) + assert not prob.solved # Solving Problem prob.solve(settings) + assert prob.solved assert prob.Status.name == "Optimal" assert prob.SolveTime < 5 @@ -93,11 +107,38 @@ def test_model(): assert var.Value == pytest.approx(expected_var_values[i]) assert var.getObjectiveCoefficient() == expected_obj_coeff[i] - assert prob.ObjVal == 303 + assert prob.ObjValue == 353 for i, c in enumerate(prob.getConstraints()): assert c.Slack == pytest.approx(expected_slack[i]) + assert hasattr(prob.SolutionStats, "mip_gap") + + # Change Objective + prob.setObjective(expr + 20, sense.MINIMIZE) + assert not prob.solved + + # Check if values reset + for i, var in enumerate(prob.getVariables()): + assert math.isnan(var.Value) and math.isnan(var.ReducedCost) + for i, c in enumerate(prob.getConstraints()): + assert math.isnan(c.Slack) and math.isnan(c.DualValue) + + # Change Problem to LP + x.VariableType = VType.CONTINUOUS + y.VariableType = CONTINUOUS + y.UB = 45.5 + + prob.solve(settings) + assert prob.solved + assert prob.Status.name == "Optimal" + assert hasattr(prob.SolutionStats, "primal_residual") + + assert x.getValue() == 24 + assert y.getValue() == pytest.approx(45.5) + + assert prob.ObjValue == pytest.approx(5 * x.Value + 3 * y.Value + 70) + def test_linear_expression(): @@ -222,7 +263,7 @@ def test_constraint_matrix(): assert rhs == exp_rhs -def test_incumbent_solutions(): +"""def test_incumbent_solutions(): # Callback for incumbent solution class CustomGetSolutionCallback(GetSolutionCallback): @@ -284,3 +325,4 @@ def set_solution(self, solution, solution_cost): assert 2 * x_val + 4 * y_val >= 230 assert 3 * x_val + 2 * y_val <= 190 assert 5 * x_val + 3 * y_val == cost +""" From ae5e373b05e7de658e8855616c098b643e51899c Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 10:30:25 -0700 Subject: [PATCH 24/29] update docs --- .../cuopt-python/lp-milp/lp-milp-api.rst | 14 ++--- .../cuopt-python/lp-milp/lp-milp-examples.rst | 36 +++++------ .../cuopt/cuopt/linear_programming/problem.py | 60 ++++++++++++++++--- .../linear_programming/test_python_API.py | 1 + 4 files changed, 78 insertions(+), 33 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst index 072769c84d..4c7ecd40ad 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst @@ -20,25 +20,25 @@ LP and MILP API Reference :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes :no-inherited-members: -.. autoclass:: cuopt.linear_programming.problem.Variable +.. autoclass:: cuopt.linear_programming.problem.Problem :members: :undoc-members: :show-inheritance: - :exclude-members: + :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints -.. autoclass:: cuopt.linear_programming.problem.LinearExpression +.. autoclass:: cuopt.linear_programming.problem.Variable :members: :undoc-members: :show-inheritance: + :exclude-members: -.. autoclass:: cuopt.linear_programming.problem.Constraint +.. autoclass:: cuopt.linear_programming.problem.LinearExpression :members: :undoc-members: :show-inheritance: - :exclude-members: compute_slack -.. autoclass:: cuopt.linear_programming.problem.Problem +.. autoclass:: cuopt.linear_programming.problem.Constraint :members: :undoc-members: :show-inheritance: - :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints + :exclude-members: compute_slack diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 70b8ab5ffa..037bc43331 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -14,22 +14,22 @@ Simple Linear Programming Example .. code-block:: python - from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE from cuopt.linear_programming.solver_settings import SolverSettings # Create a new problem problem = Problem("Simple LP") # Add variables - x = problem.addVariable(lb=0, vtype=VType.CONTINUOUS, name="x") - y = problem.addVariable(lb=0, vtype=VType.CONTINUOUS, name="y") + x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") + y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") # Add constraints problem.addConstraint(x + y <= 10, name="c1") problem.addConstraint(x - y >= 0, name="c2") # Set objective function - problem.setObjective(x + y, sense=sense.MAXIMIZE) + problem.setObjective(x + y, sense=MAXIMIZE) # Configure solver settings settings = SolverSettings() @@ -59,22 +59,22 @@ Mixed Integer Linear Programming Example .. code-block:: python - from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE from cuopt.linear_programming.solver_settings import SolverSettings # Create a new MIP problem problem = Problem("Simple MIP") # Add integer variables with bounds - x = problem.addVariable(vtype=VType.INTEGER, name="V_x") - y = problem.addVariable(lb=10, ub=50, vtype=VType.INTEGER, name="V_y") + x = problem.addVariable(vtype=INTEGER, name="V_x") + y = problem.addVariable(lb=10, ub=50, vtype=INTEGER, name="V_y") # Add constraints problem.addConstraint(2 * x + 4 * y >= 230, name="C1") problem.addConstraint(3 * x + 2 * y <= 190, name="C2") # Set objective function - problem.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) + problem.setObjective(5 * x + 3 * y, sense=MAXIMIZE) # Configure solver settings settings = SolverSettings() @@ -107,7 +107,7 @@ Advanced Example: Production Planning .. code-block:: python - from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE from cuopt.linear_programming.solver_settings import SolverSettings # Production planning problem @@ -116,8 +116,8 @@ Advanced Example: Production Planning # Decision variables: production quantities # x1 = units of product A # x2 = units of product B - x1 = problem.addVariable(lb=10, vtype=VType.INTEGER, name="Product_A") - x2 = problem.addVariable(lb=15, vtype=VType.INTEGER, name="Product_B") + x1 = problem.addVariable(lb=10, vtype=INTEGER, name="Product_A") + x2 = problem.addVariable(lb=15, vtype=INTEGER, name="Product_B") # Resource constraints # Machine time: 2 hours per unit of A, 1 hour per unit of B, max 100 hours @@ -131,7 +131,7 @@ Advanced Example: Production Planning # Objective: maximize profit # Profit: $50 per unit of A, $30 per unit of B - problem.setObjective(50 * x1 + 30 * x2, sense=sense.MAXIMIZE) + problem.setObjective(50 * x1 + 30 * x2, sense=MAXIMIZE) # Solve with time limit settings = SolverSettings() @@ -167,7 +167,7 @@ Working with Expressions and Constraints .. code-block:: python - from cuopt.linear_programming.problem import Problem, VType, CType, sense + from cuopt.linear_programming.problem import Problem, MAXIMIZE from cuopt.linear_programming.solver_settings import SolverSettings problem = Problem("Expression Example") @@ -192,7 +192,7 @@ Working with Expressions and Constraints problem.addConstraint(1 * z <= 100, name="Upper_Bound_Z") # Set objective - problem.setObjective(x + 2 * y + 3 * z, sense=sense.MAXIMIZE) + problem.setObjective(x + 2 * y + 3 * z, sense=MAXIMIZE) settings = SolverSettings() settings.set_parameter("time_limit", 20) @@ -227,7 +227,7 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol .. code-block:: python - from cuopt.linear_programming.problem import Problem, VType, sense + from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE from cuopt.linear_programming.solver_settings import SolverSettings from cuopt.linear_programming.internals import GetSolutionCallback, SetSolutionCallback @@ -265,15 +265,15 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol problem = Problem("Incumbent Example") # Add integer variables - x = problem.addVariable(vtype=VType.INTEGER) - y = problem.addVariable(vtype=VType.INTEGER) + x = problem.addVariable(vtype=INTEGER) + y = problem.addVariable(vtype=INTEGER) # Add constraints to create a problem that will generate multiple incumbents problem.addConstraint(2 * x + 4 * y >= 230) problem.addConstraint(3 * x + 2 * y <= 190) # Set objective to maximize - problem.setObjective(5 * x + 3 * y, sense=sense.MAXIMIZE) + problem.setObjective(5 * x + 3 * y, sense=MAXIMIZE) # Configure solver settings with callback settings = SolverSettings() diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index d9e092f646..d5479a86b6 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -25,6 +25,9 @@ class VType(str, Enum): """ The type of a variable is either continuous or integer. + Variable Types can be directly used as a constant. + CONTINUOUS is VType.CONTINUOUS + INTEGER is VType.INTEGER """ CONTINUOUS = "C" @@ -38,6 +41,10 @@ class VType(str, Enum): class CType(str, Enum): """ The sense of a constraint is either LE, GE or EQ. + Constraint Sense Types can be directly used as a constant. + LE is CType.LE + GE is CType.GE + EQ is CType EQ """ LE = "L" @@ -53,6 +60,9 @@ class CType(str, Enum): class sense(int, Enum): """ The sense of a model is either MINIMIZE or MAXIMIZE. + Model objective sense can be directly used as a constant. + MINIMIZE is sense.MINIMIZE + MAXIMIZE is sense.MAXIMIZE """ MAXIMIZE = -1 @@ -70,6 +80,19 @@ class Variable: Variables are always associated with a problem and can be created using problem.addVariable (See problem class). + Parameters + ---------- + lb : float + Lower bound of the variable. Defaults to 0. + ub : float + Upper bound of the variable. Defaults to infinity. + vtype : enum + CONTINUOUS or INTEGER. Defaults to CONTINUOUS. + obj : float + Coefficient of the Variable in the objective. + name : str + Name of the variable. Optional. + Attributes ---------- VariableName : str @@ -276,6 +299,15 @@ class LinearExpression: divided by scalars. LinearExpressions can be compared with scalars, Variables, and other LinearExpressions to create Constraints. + + Parameters + ---------- + vars : List + List of Variables in the linear expression. + coefficients : List + List of coefficients corresponding to the variables. + constant : float + Constant of the linear expression. """ def __init__(self, vars, coefficients, constant): @@ -525,6 +557,18 @@ class Constraint: Constraints are associated with a problem and can be created using problem.addConstraint (See problem class). + Parameters + ---------- + expr : LinearExpression + Linear expression corresponding to a problem. + sense : enum + Sense of the constraint. Either LE for <=, + GE for >= or EQ for == . + rhs : float + Constraint right-hand side value. + name : str, Optional + Name of the constraint. Optional. + Attributes ---------- ConstraintName : str @@ -634,12 +678,12 @@ class Problem: Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") - >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + >>> problem.setObjective(x + y, sense=MAXIMIZE) >>> problem.solve() """ @@ -702,7 +746,7 @@ def addVariable( Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER, + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=INTEGER, name="Var1") """ if self.solved: @@ -729,7 +773,7 @@ def addConstraint(self, constr, name=""): Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y @@ -763,12 +807,12 @@ def setObjective(self, expr, sense=sense.MINIMIZE): Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") - >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + >>> problem.setObjective(x + y, sense=MAXIMIZE) """ if self.solved: self.reset_solved_values() # Reset all solved values @@ -887,12 +931,12 @@ def solve(self, settings=solver_settings.SolverSettings()): Examples -------- >>> problem = problem.Problem("MIP_model") - >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=VType.INTEGER) + >>> x = problem.addVariable(lb=-2.0, ub=8.0, vtype=INTEGER) >>> y = problem.addVariable(name="Var2") >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") - >>> problem.setObjective(x + y, sense=sense.MAXIMIZE) + >>> problem.setObjective(x + y, sense=MAXIMIZE) >>> problem.solve() """ diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 30f0f71159..d7559c00f0 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -48,6 +48,7 @@ def test_model(): assert y.getLowerBound() == 10 assert x.getVariableType() == VType.INTEGER assert y.getVariableType() == "I" + assert [x.getIndex(), y.getIndex()] == [0, 1] # Adding Constraints prob.addConstraint(2 * x + 4 * y >= 230, name="C1") From 2c0a1abd8f20c28b264f812d6c89e8af175410a2 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 10:40:53 -0700 Subject: [PATCH 25/29] more doc changes --- .../cuopt/cuopt/linear_programming/problem.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index d5479a86b6..7490c7f930 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -116,7 +116,7 @@ def __init__( lb=0.0, ub=float("inf"), obj=0.0, - vtype=VType.CONTINUOUS, + vtype=CONTINUOUS, vname="", ): self.index = -1 @@ -255,11 +255,11 @@ def __le__(self, other): match other: case int() | float(): expr = LinearExpression([self], [1.0], 0.0) - return Constraint(expr, CType.LE, float(other)) + return Constraint(expr, LE, float(other)) case Variable() | LinearExpression(): # var1 <= var2 -> var1 - var2 <= 0 expr = self - other - return Constraint(expr, CType.LE, 0.0) + return Constraint(expr, LE, 0.0) case _: raise ValueError("Unsupported operation") @@ -267,11 +267,11 @@ def __ge__(self, other): match other: case int() | float(): expr = LinearExpression([self], [1.0], 0.0) - return Constraint(expr, CType.GE, float(other)) + return Constraint(expr, GE, float(other)) case Variable() | LinearExpression(): # var1 >= var2 -> var1 - var2 >= 0 expr = self - other - return Constraint(expr, CType.GE, 0.0) + return Constraint(expr, GE, 0.0) case _: raise ValueError("Unsupported operation") @@ -279,11 +279,11 @@ def __eq__(self, other): match other: case int() | float(): expr = LinearExpression([self], [1.0], 0.0) - return Constraint(expr, CType.EQ, float(other)) + return Constraint(expr, EQ, float(other)) case Variable() | LinearExpression(): # var1 == var2 -> var1 - var2 == 0 expr = self - other - return Constraint(expr, CType.EQ, 0.0) + return Constraint(expr, EQ, 0.0) case _: raise ValueError("Unsupported operation") @@ -524,29 +524,29 @@ def __truediv__(self, other): def __le__(self, other): match other: case int() | float(): - return Constraint(self, CType.LE, float(other)) + return Constraint(self, LE, float(other)) case Variable() | LinearExpression(): # expr1 <= expr2 -> expr1 - expr2 <= 0 expr = self - other - return Constraint(expr, CType.LE, 0.0) + return Constraint(expr, LE, 0.0) def __ge__(self, other): match other: case int() | float(): - return Constraint(self, CType.GE, float(other)) + return Constraint(self, GE, float(other)) case Variable() | LinearExpression(): # expr1 >= expr2 -> expr1 - expr2 >= 0 expr = self - other - return Constraint(expr, CType.GE, 0.0) + return Constraint(expr, GE, 0.0) def __eq__(self, other): match other: case int() | float(): - return Constraint(self, CType.EQ, float(other)) + return Constraint(self, EQ, float(other)) case Variable() | LinearExpression(): # expr1 == expr2 -> expr1 - expr2 == 0 expr = self - other - return Constraint(expr, CType.EQ, 0.0) + return Constraint(expr, EQ, 0.0) class Constraint: @@ -691,7 +691,7 @@ def __init__(self, model_name=""): self.Name = model_name self.vars = [] self.constrs = [] - self.ObjSense = sense.MINIMIZE + self.ObjSense = MINIMIZE self.Obj = None self.ObjConstant = 0.0 self.Status = -1 @@ -726,7 +726,7 @@ def reset_solved_values(self): self.solved = False def addVariable( - self, lb=0.0, ub=float("inf"), obj=0.0, vtype=VType.CONTINUOUS, name="" + self, lb=0.0, ub=float("inf"), obj=0.0, vtype=CONTINUOUS, name="" ): """ Adds a variable to the problem defined by lower bound, @@ -790,7 +790,7 @@ def addConstraint(self, constr, name=""): case _: raise ValueError("addConstraint requires a Constraint object") - def setObjective(self, expr, sense=sense.MINIMIZE): + def setObjective(self, expr, sense=MINIMIZE): """ Set the Objective of the problem with an expression that needs to be MINIMIZED or MAXIMIZED. @@ -801,8 +801,8 @@ def setObjective(self, expr, sense=sense.MINIMIZE): Objective expression that needs maximization or minimization. sense : enum Sets whether the problem is a maximization or a minimization - problem. Values passed can either be sense.MINIMIZE or - sense.MAXIMIZE. Defaults to sense.MINIMIZE. + problem. Values passed can either be MINIMIZE or MAXIMIZE. + Defaults to MINIMIZE. Examples -------- From 41077fc2e2aea39bb2e7452990fea72a89351f75 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 13:07:08 -0700 Subject: [PATCH 26/29] increase timelimit on incumbent test --- .../tests/linear_programming/test_python_API.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index d7559c00f0..38964803e0 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -28,10 +28,10 @@ sense, ) -# from cuopt.linear_programming.internals import ( -# GetSolutionCallback, -# SetSolutionCallback, -# ) +from cuopt.linear_programming.internals import ( + GetSolutionCallback, + SetSolutionCallback, +) def test_model(): @@ -264,7 +264,7 @@ def test_constraint_matrix(): assert rhs == exp_rhs -"""def test_incumbent_solutions(): +def test_incumbent_solutions(): # Callback for incumbent solution class CustomGetSolutionCallback(GetSolutionCallback): @@ -312,11 +312,10 @@ def set_solution(self, solution, solution_cost): settings = SolverSettings() settings.set_mip_callback(get_callback) settings.set_mip_callback(set_callback) - settings.set_parameter("time_limit", 0.01) + settings.set_parameter("time_limit", 1) prob.solve(settings) - assert prob.Status.name == "FeasibleFound" assert get_callback.n_callbacks > 0 for sol in get_callback.solutions: @@ -326,4 +325,3 @@ def set_solution(self, solution, solution_cost): assert 2 * x_val + 4 * y_val >= 230 assert 3 * x_val + 2 * y_val <= 190 assert 5 * x_val + 3 * y_val == cost -""" From f5c127d1b7c2a2e30a669112bf33403323a22dab Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 13:09:07 -0700 Subject: [PATCH 27/29] formatting --- .../cuopt/tests/linear_programming/test_python_API.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 38964803e0..138add201b 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -18,6 +18,10 @@ import pytest from cuopt.linear_programming import SolverSettings +from cuopt.linear_programming.internals import ( + GetSolutionCallback, + SetSolutionCallback, +) from cuopt.linear_programming.problem import ( CONTINUOUS, INTEGER, @@ -28,11 +32,6 @@ sense, ) -from cuopt.linear_programming.internals import ( - GetSolutionCallback, - SetSolutionCallback, -) - def test_model(): From a8a6515c4aa0812d6a1199258799134ee81f55c2 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 14:45:02 -0700 Subject: [PATCH 28/29] update docs, tests and add back IsMIP --- .../source/cuopt-python/lp-milp/lp-milp-api.rst | 2 +- .../cuopt-python/lp-milp/lp-milp-examples.rst | 14 +++++++------- python/cuopt/cuopt/linear_programming/problem.py | 10 ++++++++++ .../tests/linear_programming/test_python_API.py | 2 ++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst index 4c7ecd40ad..ea6b0ff792 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-api.rst @@ -24,7 +24,7 @@ LP and MILP API Reference :members: :undoc-members: :show-inheritance: - :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints + :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP .. autoclass:: cuopt.linear_programming.problem.Variable :members: diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 037bc43331..2a201ee353 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -43,7 +43,7 @@ Simple Linear Programming Example print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") - print(f"Objective value = {problem.ObjVal}") + print(f"Objective value = {problem.ObjValue}") The response is as follows: @@ -88,7 +88,7 @@ Mixed Integer Linear Programming Example print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") - print(f"Objective value = {problem.ObjVal}") + print(f"Objective value = {problem.ObjValue}") else: print(f"Problem status: {problem.Status.name}") @@ -145,7 +145,7 @@ Advanced Example: Production Planning print(f"Solve time: {problem.SolveTime:.2f} seconds") print(f"Product A production: {x1.getValue()} units") print(f"Product B production: {x2.getValue()} units") - print(f"Total profit: ${problem.ObjVal:.2f}") + print(f"Total profit: ${problem.ObjValue:.2f}") else: print(f"Problem not solved optimally. Status: {problem.Status.name}") @@ -205,7 +205,7 @@ Working with Expressions and Constraints print(f"x = {x.getValue()}") print(f"y = {y.getValue()}") print(f"z = {z.getValue()}") - print(f"Objective value = {problem.ObjVal}") + print(f"Objective value = {problem.ObjValue}") The response is as follows: @@ -228,7 +228,7 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol .. code-block:: python from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE - from cuopt.linear_programming.solver_settings import SolverSettings + from cuopt.linear_programming.solver_settings import SolverSettings, CUOPT_TIME_LIMIT from cuopt.linear_programming.internals import GetSolutionCallback, SetSolutionCallback # Create a callback class to receive incumbent solutions @@ -280,7 +280,7 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol # Set the incumbent callback incumbent_callback = IncumbentCallback() settings.set_mip_callback(incumbent_callback) - settings.set_parameter("time_limit", 30) # Allow enough time to find multiple incumbents + settings.set_parameter(CUOPT_TIME_LIMIT, 30) # Allow enough time to find multiple incumbents # Solve the problem problem.solve(settings) @@ -290,7 +290,7 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol print(f"Problem status: {problem.Status.name}") print(f"Solve time: {problem.SolveTime:.2f} seconds") print(f"Final solution: x={x.getValue()}, y={y.getValue()}") - print(f"Final objective value: {problem.ObjVal:.2f}") + print(f"Final objective value: {problem.ObjValue:.2f}") The response is as follows: diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 7490c7f930..1a14e17cf1 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -668,6 +668,8 @@ class Problem: Solution statistics for LP or MIP problem. ObjValue : float Objective value of the problem. + IsMIP : bool + Indicates if the problem is a Mixed Integer Program. NumVariables : int Number of Variables in the problem. NumConstraints : int @@ -872,6 +874,14 @@ def NumNZs(self): nnz += len(constr) return nnz + @property + def IsMIP(self): + # Returns if the problem is a MIP problem. + for var in self.vars: + if var.VariableType == "I": + return True + return False + def getCSR(self): """ Computes and returns the CSR representation of the diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 138add201b..132920a865 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -48,6 +48,7 @@ def test_model(): assert x.getVariableType() == VType.INTEGER assert y.getVariableType() == "I" assert [x.getIndex(), y.getIndex()] == [0, 1] + assert prob.IsMIP # Adding Constraints prob.addConstraint(2 * x + 4 * y >= 230, name="C1") @@ -128,6 +129,7 @@ def test_model(): x.VariableType = VType.CONTINUOUS y.VariableType = CONTINUOUS y.UB = 45.5 + assert not prob.IsMIP prob.solve(settings) assert prob.solved From 1967a3dd7d2a3b53427ca84734e664ac9710af64 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 31 Jul 2025 15:14:49 -0700 Subject: [PATCH 29/29] update incumbent example --- docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst index 2a201ee353..98ef2d75d0 100644 --- a/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst @@ -228,7 +228,8 @@ Incumbent solutions are intermediate feasible solutions found during the MIP sol .. code-block:: python from cuopt.linear_programming.problem import Problem, INTEGER, MAXIMIZE - from cuopt.linear_programming.solver_settings import SolverSettings, CUOPT_TIME_LIMIT + from cuopt.linear_programming.solver_settings import SolverSettings + from cuopt.linear_programming.solver.solver_parameters import CUOPT_TIME_LIMIT from cuopt.linear_programming.internals import GetSolutionCallback, SetSolutionCallback # Create a callback class to receive incumbent solutions