From d355d03718e66e0ad92a2c0caa4f98752bfb4064 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 16 Oct 2026 11:11:26 +0100 Subject: [PATCH 1/3] Add addConsLinear() and skip np.ndim in _VarArray --- CHANGELOG.md | 2 + src/pyscipopt/scip.pxi | 91 +++++++++++++++++++++++++++++++++++++++--- src/pyscipopt/scip.pyi | 18 +++++++++ tests/test_cons.py | 27 +++++++++++++ 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbec1d025..f5d7f268c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ ### Added - Added more type annotations and deprecation markers to the stubs. - Added tests for `getObjVal()` and `getVal()` +- Added `addConsLinear()` to add linear constraints from variables and coefficients without building an expression ### Fixed - Fixed incorrect `getObjVal()` result when called before `getBestSol()` (e.g., in event handlers). ### Changed - Replaced `format()` with f-strings in examples +- Methods taking a list of variables (e.g. `addConsKnapsack()`) no longer convert it to a NumPy array to check its dimension ### Removed ## 6.3.0 - 2026.10.11 diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4e7022974..08fe0e3e7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2669,10 +2669,7 @@ cdef class _VarArray: if isinstance(vars, Variable): vars = [vars] - elif isinstance(vars, (list, tuple, MatrixVariable)): - if (ndim := np.ndim(vars)) != 1: - raise ValueError(f"Expected a 1D array, but got a {ndim}D array.") - else: + elif not isinstance(vars, (list, tuple, MatrixVariable)): raise TypeError(f"Expected Variable or list of Variable, got {type(vars)}.") self.size = len(vars) @@ -2680,6 +2677,8 @@ cdef class _VarArray: self.ptr = malloc(self.size * sizeof(SCIP_VAR*)) for i, var in enumerate(vars): if not isinstance(var, Variable): + if (ndim := np.ndim(vars)) != 1: + raise ValueError(f"Expected a 1D array, but got a {ndim}D array.") raise TypeError(f"Expected Variable, got {type(var)}.") self.ptr[i] = (var).scip_var @@ -7411,7 +7410,89 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) return pycons - + + def addConsLinear(self, vars, vals, lhs=None, rhs=None, name="", + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=False, modifiable=False, dynamic=False, + removable=False, stickingatnode=False): + """ + Add a linear constraint lhs <= sum(vals[i] * vars[i]) <= rhs directly from + variables and coefficients, without building an expression. Faster than addCons + for building large models. + + Parameters + ---------- + vars : list of Variable + variables in the constraint + vals : list of float + coefficients of the variables + lhs : float or None, optional + left-hand side, -infinity if None (Default value = None) + rhs : float or None, optional + right-hand side, infinity if None (Default value = None) + name : str, optional + name of the constraint, generic name if empty (Default value = "") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created linear constraint + + """ + cdef int nvars = len(vars) + cdef int i + cdef SCIP_Real* vals_array + cdef SCIP_CONS* scip_cons + cdef _VarArray wrapper + + assert nvars == len(vals), "Number of variables and values must be the same." + + if lhs is None: + lhs = -SCIPinfinity(self._scip) + if rhs is None: + rhs = SCIPinfinity(self._scip) + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + + wrapper = _VarArray(vars) + vals_array = malloc(nvars * sizeof(SCIP_Real)) + for i in range(nvars): + vals_array[i] = vals[i] + + PY_SCIP_CALL(SCIPcreateConsLinear( + self._scip, &scip_cons, str_conversion(name), nvars, wrapper.ptr, vals_array, + lhs, rhs, initial, separate, enforce, check, propagate, local, modifiable, + dynamic, removable, stickingatnode)) + + free(vals_array) + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + pyCons = self._getOrCreateCons(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + return pyCons + def addConsKnapsack(self, vars, weights, capacity, name="", initial=True, separate=True, enforce=True, check=True, modifiable=False, propagate=True, local=False, dynamic=False, diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index a29999dc1..4bbdab70b 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -800,6 +800,24 @@ class Model: removable: bool = False, stickingatnode: bool = False, ) -> Constraint: ... + def addConsLinear( + self, + vars: Sequence[Variable], + vals: Sequence[float], + lhs: float | None = None, + rhs: float | None = None, + name: str = "", + initial: bool = True, + separate: bool = True, + enforce: bool = True, + check: bool = True, + propagate: bool = True, + local: bool = False, + modifiable: bool = False, + dynamic: bool = False, + removable: bool = False, + stickingatnode: bool = False, + ) -> Constraint: ... def addConsKnapsack( self, vars: Sequence[Variable], diff --git a/tests/test_cons.py b/tests/test_cons.py index e29f46773..f8fe59a86 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -433,6 +433,33 @@ def test_cons_cumulative(): assert m.isEQ(m.getVal(start3), 0) assert m.isEQ(m.getObjVal(), 3) +def test_addConsLinear(): + m = Model() + x = m.addVar("x", lb=0, ub=2, obj=-1) + y = m.addVar("y", lb=0, ub=4, obj=-1) + z = m.addVar("z", lb=0, ub=5, obj=2) + + c1 = m.addConsLinear([x, y], [2, 1], rhs=5, name="c1") + c2 = m.addConsLinear((x, z), (1.0, 4.0), lhs=1, rhs=5) + c3 = m.addConsLinear([y], [1], lhs=1) + assert c1.getConshdlrName() == "linear" + assert c1.name == "c1" + assert m.getValsLinear(c1) == {'x': 2, 'y': 1} + assert m.getValsLinear(c2) == {'x': 1, 'z': 4} + assert (m.getLhs(c1), m.getRhs(c1)) == (-m.infinity(), 5) + assert (m.getLhs(c2), m.getRhs(c2)) == (1, 5) + assert (m.getLhs(c3), m.getRhs(c3)) == (1, m.infinity()) + assert m.getConss() == [c1, c2, c3] + + m.optimize() + assert m.getObjVal() == -4.25 + assert m.getActivity(c1, m.getBestSol()) == 5 + + with pytest.raises(AssertionError): + m.addConsLinear([x, y], [1]) + with pytest.raises(TypeError): + m.addConsLinear([x, 1], [1, 1]) + def test_getValsLinear(): m = Model() x = m.addVar("x", lb=0, ub=2, obj=-1) From 03bf2f7a4433990f2aaae94bb423df538c080acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:19:53 +0100 Subject: [PATCH 2/3] Update src/pyscipopt/scip.pxi Co-authored-by: 40% --- src/pyscipopt/scip.pxi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 08fe0e3e7..dd0d123d2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -7466,7 +7466,8 @@ cdef class Model: cdef SCIP_CONS* scip_cons cdef _VarArray wrapper - assert nvars == len(vals), "Number of variables and values must be the same." + if nvars != len(vals): + raise ValueError("Number of variables and values must be the same.") if lhs is None: lhs = -SCIPinfinity(self._scip) From c9c54d4f34ae90ea2df6d3fec38283d6b7411f7c Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sun, 18 Oct 2026 21:20:20 +0100 Subject: [PATCH 3/3] Expect ValueError in test_addConsLinear --- tests/test_cons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cons.py b/tests/test_cons.py index f8fe59a86..f7e3d7d36 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -455,7 +455,7 @@ def test_addConsLinear(): assert m.getObjVal() == -4.25 assert m.getActivity(c1, m.getBestSol()) == 5 - with pytest.raises(AssertionError): + with pytest.raises(ValueError): m.addConsLinear([x, y], [1]) with pytest.raises(TypeError): m.addConsLinear([x, 1], [1, 1])