From 84c88deffe66b92d797a5884588cdfe3cb223754 Mon Sep 17 00:00:00 2001 From: jackthepunished Date: Tue, 18 Aug 2026 09:49:22 +0300 Subject: [PATCH] Track all row variables in Constraint.vars Quadratic rows stored only the variables of their linear terms, and updateConstraint did not record variables it introduced, so mapping a row's column indices back to Variables (compute_slack) could KeyError. Signed-off-by: jackthepunished --- .../cuopt/cuopt/linear_programming/problem.py | 13 ++++++- .../linear_programming/test_python_API.py | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 49885acf52..b9cf0096d2 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1341,7 +1341,12 @@ def __init__(self, expr, sense, rhs, name=""): self.rhs_value = rhs_value self.RHS = rhs_value self.vindex_coeff_dict = {} - self.vars = expr.vars + # expr.vars holds only the linear terms; id() because Variable + # overrides __eq__ and is unhashable. + seen = {} + for var in (*expr.vars, *expr.qvars1, *expr.qvars2, *expr.qvars): + seen.setdefault(id(var), var) + self.vars = list(seen.values()) return self.is_quadratic = False @@ -1760,9 +1765,15 @@ def updateConstraint(self, constr, coeffs=None, rhs=None): ) if isinstance(coeffs, dict): coeffs = coeffs.items() + new_vars = [] for var, coeff in coeffs: idx = var.index + if idx not in constr.vindex_coeff_dict: + new_vars.append(var) constr.vindex_coeff_dict[idx] = coeff + if new_vars: + # constr.vars aliases the expression's list; rebind it. + constr.vars = constr.vars + new_vars if rhs is not None: constr.RHS = rhs else: 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 964809553f..076ec80eb4 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -160,6 +160,43 @@ def test_constraint_duplicate_terms_slack(): assert c.compute_slack() == pytest.approx(6.0) +def test_updateConstraint_tracks_new_variables(): + """Variables added via updateConstraint end up in Constraint.vars.""" + prob = Problem() + x1 = prob.addVariable(name="x1") + x2 = prob.addVariable(name="x2") + c = prob.addConstraint(2 * x1 <= 10) + assert [v.index for v in c.vars] == [0] + + prob.updateConstraint(c, coeffs=[(x2, 4.0)]) + assert [v.index for v in c.vars] == [0, 1] + x1.Value = 1.0 + x2.Value = 2.0 + assert c.compute_slack() == pytest.approx(0.0) + + +def test_updateConstraint_does_not_mutate_expression(): + """The expression a constraint was built from is left unchanged.""" + prob = Problem() + a = prob.addVariable(name="a") + b = prob.addVariable(name="b") + expr = 2 * a + c = prob.addConstraint(expr <= 10) + prob.updateConstraint(c, coeffs=[(b, 1.0)]) + assert len(expr.vars) == 1 + assert len(expr.coefficients) == 1 + + +def test_constraint_vars_includes_quadratic_only_variables(): + """Variables that appear only in quadratic terms are in Constraint.vars.""" + prob = Problem() + x = prob.addVariable(name="x") + y = prob.addVariable(name="y") + c = prob.addConstraint(x * x + 2 * x * y <= 4) + assert c.is_quadratic + assert [v.index for v in c.vars] == [0, 1] + + def test_semi_continuous_variable(): prob = Problem("Semi-continuous") x = prob.addVariable(lb=5.0, ub=10.0, vtype=SEMI_CONTINUOUS, name="x")