From ddd2fcda4acb378ecd2c2437d832b341a6e03c0d Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 29 Sep 2025 15:06:41 -0700 Subject: [PATCH 1/2] update warmstart, docs, tests and delta functions --- .../cuopt-python/lp-milp/lp-milp-api.rst | 43 ++-- .../cuopt-python/lp-milp/lp-milp-examples.rst | 75 +++++++ .../cuopt/cuopt/linear_programming/problem.py | 201 ++++++++++++++++-- .../solver_settings/solver_settings.py | 16 +- .../linear_programming/test_python_API.py | 81 ++++++- 5 files changed, 359 insertions(+), 57 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 ea6b0ff79..c9e9cc786 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 @@ -2,43 +2,44 @@ LP and MILP API Reference ========================= -.. autoclass:: cuopt.linear_programming.problem.VType +.. autoclass:: cuopt.linear_programming.problem.Problem :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 + :exclude-members: reset_solved_values, populate_solution, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP -.. autoclass:: cuopt.linear_programming.problem.CType +.. autoclass:: cuopt.linear_programming.problem.Variable :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 + :exclude-members: -.. autoclass:: cuopt.linear_programming.problem.sense +.. autoclass:: cuopt.linear_programming.problem.LinearExpression :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: + :undoc-members: -.. autoclass:: cuopt.linear_programming.problem.Problem +.. autoclass:: cuopt.linear_programming.problem.Constraint + :members: + :undoc-members: + :exclude-members: compute_slack + +.. autoclass:: cuopt.linear_programming.solver_settings.SolverSettings :members: :undoc-members: - :show-inheritance: - :exclude-members: reset_solved_values, post_solve, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP + :exclude-members: to_base_type, toDict -.. autoclass:: cuopt.linear_programming.problem.Variable +.. autoclass:: cuopt.linear_programming.problem.VType :members: + :member-order: bysource :undoc-members: - :show-inheritance: - :exclude-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.LinearExpression +.. autoclass:: cuopt.linear_programming.problem.CType :members: + :member-order: bysource :undoc-members: - :show-inheritance: + :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.Constraint +.. autoclass:: cuopt.linear_programming.problem.sense :members: + :member-order: bysource :undoc-members: - :show-inheritance: - :exclude-members: compute_slack + :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 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 7a3cfe55b..129ce3e15 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 @@ -309,3 +309,78 @@ The response is as follows: Solve time: 0.16 seconds Final solution: x=36.0, y=40.99999999999999 Final objective value: 303.00 + +Working with PDLP Warmstart Data +-------------------------------- + +Warmstart data allows to restart PDLP with a previous solution context. This should be used when you solve a new problem which is similar to the previous one. + +.. note:: + Warmstart data is only available for Linear Programming (LP) problems, not for Mixed Integer Programming (MIP) problems. + +.. code-block:: python + + from cuopt.linear_programming.problem import Problem, CONTINUOUS, MAXIMIZE + from cuopt.linear_programming.solver.solver_parameters import CUOPT_METHOD + from cuopt.linear_programming.solver_settings import SolverSettings, SolverMethod + + # Create a new problem + problem = Problem("Simple LP") + + # Add variables + x = problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") + y = problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") + + # Add constraints + problem.addConstraint(4*x + 10*y <= 130, name="c1") + problem.addConstraint(8*x - 3*y >= 40, name="c2") + + # Set objective function + problem.setObjective(2*x + y, sense=MAXIMIZE) + + # Configure solver settings + settings = SolverSettings() + settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) + + # Solve the problem + problem.solve(settings) + + # Get the warmstart data + warmstart_data = problem.get_pdlp_warm_start_data() + + print(warmstart_data.current_primal_solution) + # Create a new problem + new_problem = Problem("Warmstart LP") + + # Add variables + x = new_problem.addVariable(lb=0, vtype=CONTINUOUS, name="x") + y = new_problem.addVariable(lb=0, vtype=CONTINUOUS, name="y") + + # Add constraints + new_problem.addConstraint(4*x + 10*y <= 100, name="c1") + new_problem.addConstraint(8*x - 3*y >= 50, name="c2") + + # Set objective function + new_problem.setObjective(2*x + y, sense=MAXIMIZE) + + # Configure solver settings + settings.set_pdlp_warm_start_data(warmstart_data) + + # Solve the problem + new_problem.solve(settings) + + # Check solution status + if new_problem.Status.name == "Optimal": + print(f"Optimal solution found in {new_problem.SolveTime:.2f} seconds") + print(f"x = {x.getValue()}") + print(f"y = {y.getValue()}") + print(f"Objective value = {new_problem.ObjValue}") + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.01 seconds + x = 25.000000000639382 + y = 0.0 + Objective value = 50.000000001278764 diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 9ad77ca58..f81d1f624 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -46,7 +46,7 @@ class CType(str, Enum): Constraint Sense Types can be directly used as a constant. LE is CType.LE GE is CType.GE - EQ is CType EQ + EQ is CType.EQ """ LE = "L" @@ -80,7 +80,7 @@ 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). + created using :py:meth:`Problem.addVariable`. Parameters ---------- @@ -557,7 +557,7 @@ class Constraint: 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). + created using :py:meth:`Problem.addConstraint`. Parameters ---------- @@ -696,10 +696,10 @@ def __init__(self, model_name=""): self.vars = [] self.constrs = [] self.ObjSense = MINIMIZE - self.Obj = None self.ObjConstant = 0.0 self.Status = -1 self.ObjValue = float("nan") + self.warmstart_data = None self.model = None self.solved = False @@ -791,7 +791,10 @@ def _to_data_model(self): ) self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) - self.row_names.append(constr.ConstraintName) + constr_name = constr.ConstraintName + if constr_name == "": + constr_name = "R" + str(constr.index) + self.row_names.append(constr_name) self.constraint_csr_matrix = csr_dict else: @@ -809,7 +812,10 @@ def _to_data_model(self): self.var_type[j] = self.vars[j].getVariableType() self.lower_bound[j] = self.vars[j].getLowerBound() self.upper_bound[j] = self.vars[j].getUpperBound() - self.var_names.append(self.vars[j].VariableName) + var_name = self.vars[j].VariableName + if var_name == "": + var_name = "C" + str(self.vars[j].index) + self.var_names.append(var_name) # Initialize datamodel dm = data_model.DataModel() @@ -833,6 +839,14 @@ def _to_data_model(self): self.model = dm + def update(self): + """ + Update the problem. This is mandatory if attributes of + existing Variables, Constraints or Objective has been + modified. + """ + self.reset_solved_values() + def reset_solved_values(self): # Resets all post solve values for var in self.vars: @@ -846,6 +860,7 @@ def reset_solved_values(self): self.model = None self.constraint_csr_matrix = None self.ObjValue = float("nan") + self.warmstart_data = None self.solved = False def addVariable( @@ -861,11 +876,16 @@ def addVariable( Lower bound of the variable. Defaults to 0. ub : float Upper bound of the variable. Defaults to infinity. - vtype : enum + vtype : enum :py:class:`VType` vtype.CONTINUOUS or vtype.INTEGER. Defaults to CONTINUOUS. name : string Name of the variable. Optional. + Returns + ------- + variable : :py:class:`Variable` + Variable object added to the problem. + Examples -------- >>> problem = problem.Problem("MIP_model") @@ -888,7 +908,7 @@ def addConstraint(self, constr, name=""): Parameters ---------- - constr : Constraint + constr : :py:class:`Constraint` Constructed using LinearExpressions (See Examples) name : string Name of the variable. Optional. @@ -912,6 +932,43 @@ def addConstraint(self, constr, name=""): self.constrs.append(constr) case _: raise ValueError("addConstraint requires a Constraint object") + return constr + + def updateConstraint(self, constr, coeffs=[], rhs=None): + """ + Updates a previously added constraint. Values that can be updated are + constraint coefficients and RHS. + + Parameters + ---------- + constr : :py:class:`Constraint` + Constraint to be updated. + coeffs : List[Tuple[:py:class:`Variable`, coefficient]] + List of Tuples containing variable and corresponding coefficient. + Optional. + rhs : int|float + New RHS value for the constraint. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=0.0, vtype=INTEGER) + >>> y = problem.addVariable(lb=0.0, vtype=INTEGER) + >>> c1 = problem.addConstraint(2 * x + y <= 7, name="c1") + >>> c2 = problem.addConstraint(x + y <= 5, name="c2") + >>> problem.updateConstraint(c1, coeffs=[(x, 1)], rhs=10) + """ + self.reset_solved_values() + if isinstance(constr, Constraint): + if isinstance(coeffs, dict): + coeffs = coeffs.items() + for var, coeff in coeffs: + idx = var.index + constr.vindex_coeff_dict[idx] = coeff + if rhs: + constr.RHS = rhs + else: + raise ValueError("Object to update must be a Constraint") def setObjective(self, expr, sense=MINIMIZE): """ @@ -920,9 +977,9 @@ def setObjective(self, expr, sense=MINIMIZE): Parameters ---------- - expr : LinearExpression or Variable or Constant + expr : :py:class:`LinearExpression` or :py:class:`Variable` or Constant Objective expression that needs maximization or minimization. - sense : enum + sense : enum :py:class:`sense` Sets whether the problem is a maximization or a minimization problem. Values passed can either be MINIMIZE or MAXIMIZE. Defaults to MINIMIZE. @@ -951,14 +1008,81 @@ def setObjective(self, expr, sense=MINIMIZE): if var.getIndex() == expr.getIndex(): var.setObjectiveCoefficient(1.0) case LinearExpression(): + for var in self.vars: + var.setObjectiveCoefficient(0.0) for var, coeff in expr.zipVarCoefficients(): - self.vars[var.getIndex()].setObjectiveCoefficient(coeff) + c_val = self.vars[var.getIndex()].getObjectiveCoefficient() + sum_coeff = coeff + c_val + self.vars[var.getIndex()].setObjectiveCoefficient( + sum_coeff + ) self.ObjConstant = expr.getConstant() case _: raise ValueError( "Objective must be a LinearExpression or a constant" ) - self.Obj = expr + + def updateObjective(self, coeffs=[], constant=None, sense=None): + """ + Updates the objective of the problem. Values that can be updated are + objective coefficients, constant and sense. + + Parameters + ---------- + coeffs : List[Tuple[:py:class:`Variable`, coefficient]] + List of Tuples containing variable and corresponding coefficient. + Optional. + constant : int|float + New Objective constant for the problem. Optional. + sense : enum :py:class:`sense` + Sets the objective sense to either maximize or minimize. Optional. + + Examples + -------- + >>> problem = problem.Problem("MIP_model") + >>> x = problem.addVariable(lb=0.0, vtype=INTEGER) + >>> y = problem.addVariable(lb=0.0, vtype=INTEGER) + >>> problem.setObjective(4*x + y + 4, MAXIMIZE) + >>> problem.updateObjective(coeffs=[(x1, 1.0), (x2, 3.0)], constant=5, + sense=MINIMIZE) + """ + self.reset_solved_values() + if isinstance(coeffs, dict): + coeffs = coeffs.items() + for var, coeff in coeffs: + var.setObjectiveCoefficient(coeff) + if constant: + self.ObjConstant = constant + if sense: + self.ObjSense = sense + + 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 get_pdlp_warm_start_data(self): + """ + Note: Applicable to only LP. + Allows to retrieve the warm start data from the PDLP solver + once the problem is solved. + This data can be used to warmstart the next PDLP solve by setting it + in :py:meth:`cuopt.linear_programming.solver_settings.SolverSettings.set_pdlp_warm_start_data` # noqa + + Examples + -------- + >>> problem = problem.Problem.readMPS("LP.mps") + >>> problem.solve() + >>> warmstart_data = problem.get_pdlp_warm_start_data() + >>> settings.set_pdlp_warm_start_data(warmstart_data) + >>> updated_problem = problem.Problem.readMPS("updated_LP.mps") + >>> updated_problem.solve(settings) + """ + return self.warmstart_data def getObjective(self): """ @@ -972,16 +1096,36 @@ def getVariables(self): """ return self.vars + def getVariable(self, identifier): + """ + Get a Variable by its index or name. + """ + for v in self.vars: + if v.index == identifier or v.VariableName == identifier: + return v + def getConstraints(self): """ Get a list of all the Constraints in a problem. """ return self.constrs + def getConstraint(self, identifier): + """ + Get a Constraint by its index or name. + """ + for c in self.constrs: + if c.index == identifier or c.ConstraintName == identifier: + return c + @classmethod def readMPS(cls, mps_file): """ - Initiliaze a problem from an MPS file. + Initiliaze a problem from an `MPS `__ file. # noqa + + Examples + -------- + >>> problem = problem.Problem.readMPS("model.mps") """ problem = cls() data_model = cuopt_mps_parser.ParseMps(mps_file) @@ -990,6 +1134,13 @@ def readMPS(cls, mps_file): return problem def writeMPS(self, mps_file): + """ + Write the problem into an `MPS `__ file. # noqa + + Examples + -------- + >>> problem.writeMPS("model.mps") + """ if self.model is None: self._to_data_model() self.model.writeMPS(mps_file) @@ -1020,6 +1171,14 @@ def IsMIP(self): return True return False + @property + def Obj(self): + # Returns objective expression of the problem. + coeffs = [] + for var in self.vars: + coeffs.append(var.getObjectiveCoefficient()) + return LinearExpression(self.vars, coeffs, self.ObjConstant) + def getCSR(self): """ Computes and returns the CSR representation of the @@ -1037,18 +1196,15 @@ def getCSR(self): self.constraint_csr_matrix = csr_dict 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 relax(self): """ Relax a MIP problem into an LP problem and return the relaxed model. + The relaxed model has all variable types set to CONTINUOUS. + + Examples + -------- + >>> mip_problem = problem.Problem.readMPS("MIP.mps") + >>> lp_problem = problem.relax() """ self.reset_solved_values() relaxed_problem = copy.deepcopy(self) @@ -1060,6 +1216,7 @@ def relax(self): def populate_solution(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() + self.warmstart_data = solution.get_pdlp_warm_start_data() IsMIP = False if solution.problem_category == 0: diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 9f429e655..8fc5596f6 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -205,6 +205,7 @@ def set_optimality_tolerance(self, eps_optimal): ---------- eps_optimal : float64 Tolerance to optimality + Notes ----- Default value is 1e-4. @@ -228,7 +229,8 @@ def set_pdlp_warm_start_data(self, pdlp_warm_start_data): Parameters ---------- pdlp_warm_start_data : PDLPWarmStartData - PDLP warm start data. + PDLP warm start data obtained from a previous solve. + Refer :py:meth:`cuopt.linear_programming.problem.Problem.get_pdlp_warm_start_data` # noqa Notes ----- @@ -239,11 +241,7 @@ def set_pdlp_warm_start_data(self, pdlp_warm_start_data): Examples -------- - >>> solution = solver.Solve(first_problem, settings) - >>> settings.set_pdlp_warm_start_data( - >>> solution.get_pdlp_warm_start_data() - >>> ) - >>> solution = solver.Solve(second_problem, settings) + >>> settings.set_pdlp_warm_start_data(pdlp_warm_start_data) """ self.pdlp_warm_start_data = pdlp_warm_start_data @@ -315,11 +313,7 @@ def get_pdlp_warm_start_data(self): Returns ------- - pdlp_warm_start_data - - Notes - ----- - ... + pdlp_warm_start_data: """ return self.pdlp_warm_start_data 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 1cc4993d2..1f0ade10e 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -14,6 +14,7 @@ # limitations under the License. import math +import os import pytest @@ -32,6 +33,16 @@ VType, sense, ) +from cuopt.linear_programming.solver.solver_parameters import ( + CUOPT_INFEASIBILITY_DETECTION, + CUOPT_PDLP_SOLVER_MODE, +) +from cuopt.linear_programming.solver_settings import PDLPSolverMode + +RAPIDS_DATASET_ROOT_DIR = os.getenv("RAPIDS_DATASET_ROOT_DIR") +if RAPIDS_DATASET_ROOT_DIR is None: + RAPIDS_DATASET_ROOT_DIR = os.getcwd() + RAPIDS_DATASET_ROOT_DIR = os.path.join(RAPIDS_DATASET_ROOT_DIR, "datasets") def test_model(): @@ -80,7 +91,9 @@ def test_model(): assert expr.getCoefficients() == expected_obj_coeff assert expr.getConstant() == 50 assert prob.ObjSense == sense.MAXIMIZE - assert prob.getObjective() is expr + assert prob.getObjective().vars == [x, y] + assert prob.getObjective().coefficients == [5, 3] + assert prob.getObjective().constant == prob.ObjConstant # Initialize Settings settings = SolverSettings() @@ -284,8 +297,8 @@ def test_read_write_mps_and_relaxation(): # Constraints (5 total) m.addConstraint(x1 + x2 + x3 <= 10, name="c1") m.addConstraint(2 * x1 + x3 - x4 >= 3, name="c2") - m.addConstraint(x2 + 3 * x5 == 7, name="c3") - m.addConstraint(x4 + x5 <= 8, name="c4") + m.addConstraint(x2 + 3 * x5 == 7) + m.addConstraint(x4 + x5 <= 8) m.addConstraint(x1 + x2 + x3 + x4 + x5 >= 5, name="c5") # Write MPS @@ -374,3 +387,65 @@ 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 + + +def test_warm_start(): + file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/a2864/a2864.mps" + problem = Problem.readMPS(file_path) + + settings = SolverSettings() + settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Stable2) + settings.set_optimality_tolerance(1e-3) + settings.set_parameter(CUOPT_INFEASIBILITY_DETECTION, False) + + problem.solve(settings) + iterations_first_solve = problem.SolutionStats.nb_iterations + + settings.set_optimality_tolerance(1e-2) + problem.solve(settings) + iterations_second_solve = problem.SolutionStats.nb_iterations + + settings.set_optimality_tolerance(1e-3) + warmstart_data = problem.get_pdlp_warm_start_data() + settings.set_pdlp_warm_start_data(warmstart_data) + problem.solve(settings) + + iterations_third_solve = problem.SolutionStats.nb_iterations + + assert ( + iterations_third_solve + iterations_second_solve + == iterations_first_solve + ) + + +def test_problem_update(): + prob = Problem() + x1 = prob.addVariable(vtype=INTEGER, lb=0, name="x1") + x2 = prob.addVariable(vtype=INTEGER, lb=0, name="x2") + + prob.addConstraint(2 * x1 + x2 <= 7, name="c1") + prob.addConstraint(x1 + x2 <= 5, name="c2") + + prob.setObjective(4 * x1 + 5 * x2 + 4 - 4 * x2, MAXIMIZE) + prob.solve() + + assert prob.ObjValue == pytest.approx(17) + + prob.updateObjective(coeffs=[(x1, 1.0), (x2, 3.0)]) + prob.solve() + assert prob.ObjValue == pytest.approx(19) + + c1 = prob.getConstraint("c1") + c2 = prob.getConstraint(1) + + prob.updateConstraint(c1, coeffs=[(x1, 1)], rhs=10) + prob.updateConstraint(c2, rhs=10) + prob.solve() + assert prob.ObjValue == pytest.approx(34) + + assert prob.getVariable("x1").Value == pytest.approx(0) + assert prob.getVariable("x2").Value == pytest.approx(10) + + prob.updateObjective(constant=5, sense=MINIMIZE) + prob.solve() + assert prob.ObjValue == pytest.approx(5) From 48d6b342882bb86a3af911265e685bc75165a4b9 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:21:48 -0500 Subject: [PATCH 2/2] Update docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst Co-authored-by: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> --- docs/cuopt/source/cuopt-python/lp-milp/lp-milp-examples.rst | 2 +- 1 file changed, 1 insertion(+), 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 129ce3e15..663adda8a 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 @@ -316,7 +316,7 @@ Working with PDLP Warmstart Data Warmstart data allows to restart PDLP with a previous solution context. This should be used when you solve a new problem which is similar to the previous one. .. note:: - Warmstart data is only available for Linear Programming (LP) problems, not for Mixed Integer Programming (MIP) problems. + Warmstart data is only available for Linear Programming (LP) problems, not for Mixed Integer Linear Programming (MILP) problems. .. code-block:: python