Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
### 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
- Dual and Farkas getters for linear, knapsack, and logicor constraints now return zero when presolving has removed them.
- Fixed incorrect `getObjVal()` result when called before `getBestSol()` (e.g., in event handlers).
- Fixed `getDualsolKnapsack()` and `getDualfarkasKnapsack()` returning 0 for original constraints, and the handler check in `getVarsAnd()` that could never fail
### Changed
- Replaced `format()` with f-strings in examples
- Made expression fields read-only to prevent accidental runtime modification
- 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
Expand Down
92 changes: 87 additions & 5 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -2669,17 +2669,16 @@ 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)
if self.size:
self.ptr = <SCIP_VAR**> 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] = (<Variable>var).scip_var

Expand Down Expand Up @@ -7419,7 +7418,90 @@ 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

if nvars != len(vals):
raise ValueError("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 = <SCIP_Real*> malloc(nvars * sizeof(SCIP_Real))
for i in range(nvars):
vals_array[i] = <SCIP_Real>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,
Expand Down
18 changes: 18 additions & 0 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,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],
Expand Down
27 changes: 27 additions & 0 deletions tests/test_cons.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,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(ValueError):
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)
Expand Down
Loading