Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion python/cuopt/cuopt/linear_programming/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

@Iroy30 Iroy30 Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to use id() here? We could perhaps update this part to hash index

            self.vars = {}
            # Variables may appear only in quadratic terms, so the linear
            # terms of the expression are not enough to cover the row.
            for var in (*expr.vars, *expr.qvars1, *expr.qvars2, *expr.qvars):
                self.vars[var.index] = var

where self.vars is now a dict and can be directly used in compute_slack() rather than reformulating it.

You would need to make the list to dict changes for self.vars in the following lines when expression is Linear, in updateConstraint which would then just add to the dict, plus update the tests.

seen.setdefault(id(var), var)
self.vars = list(seen.values())
return

self.is_quadratic = False
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions python/cuopt/cuopt/tests/linear_programming/test_python_API.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down