Skip to content

Commit 0486e4f

Browse files
Merge remote-tracking branch 'upstream/master' into model-annotations-1
2 parents e127b14 + 9547994 commit 0486e4f

6 files changed

Lines changed: 208 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
## Unreleased
44
### Added
5+
- Added `addConsCumulative()` for SCIP cumulative constraints (#1222)
56
- Added type annotations to most methods on the `Model` class
67
### Fixed
78
### Changed
9+
- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class (#1204)
810
- Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API
911
### Removed
1012

src/pyscipopt/expr.pxi

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,27 @@ cdef class ExprLike:
257257

258258
return NotImplemented
259259

260+
def __radd__(self, other, /):
261+
return self + other
262+
263+
def __sub__(self, other, /):
264+
return self + (-other)
265+
266+
def __rsub__(self, other, /):
267+
return (-self) + other
268+
269+
def __rmul__(self, other, /):
270+
return self * other
271+
272+
def __rtruediv__(self, other, /) -> GenExpr:
273+
return buildGenExprObj(other) / self
274+
275+
def __richcmp__(self, other, int op):
276+
return _expr_richcmp(self, other, op)
277+
278+
def __neg__(self, /) -> Union[Expr, GenExpr]:
279+
return self * -1.0
280+
260281
def __abs__(self) -> GenExpr:
261282
return UnaryExpr(Operator.fabs, buildGenExprObj(self))
262283

@@ -358,11 +379,10 @@ cdef class Expr(ExprLike):
358379
return 1.0 / other * self
359380
return buildGenExprObj(self) / other
360381

361-
def __rtruediv__(self, other):
362-
''' other / self '''
382+
def __rtruediv__(self, other, /) -> GenExpr:
363383
if not _is_expr_compatible(other):
364384
return NotImplemented
365-
return buildGenExprObj(other) / self
385+
return super().__rtruediv__(other)
366386

367387
def __pow__(self, other, modulo):
368388
if float(other).is_integer() and other >= 0:
@@ -387,25 +407,6 @@ cdef class Expr(ExprLike):
387407
raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base)
388408
return (self * Constant(base).log()).exp()
389409

390-
def __neg__(self):
391-
return Expr({v:-c for v,c in self.terms.items()})
392-
393-
def __sub__(self, other):
394-
return self + (-other)
395-
396-
def __radd__(self, other):
397-
return self.__add__(other)
398-
399-
def __rmul__(self, other):
400-
return self.__mul__(other)
401-
402-
def __rsub__(self, other):
403-
return -1.0 * self + other
404-
405-
def __richcmp__(self, other, int op):
406-
'''turn it into a constraint'''
407-
return _expr_richcmp(self, other, op)
408-
409410
def normalize(self):
410411
'''remove terms with coefficient of 0'''
411412
self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0}
@@ -464,7 +465,6 @@ cdef class ExprCons:
464465
if not self._rhs is None:
465466
self._rhs -= c
466467

467-
468468
def __richcmp__(self, other, op):
469469
'''turn it into a constraint'''
470470
if not _is_number(other):
@@ -690,30 +690,10 @@ cdef class GenExpr(ExprLike):
690690
raise ZeroDivisionError("cannot divide by 0")
691691
return self * divisor**(-1)
692692

693-
def __rtruediv__(self, other):
694-
''' other / self '''
693+
def __rtruediv__(self, other, /) -> GenExpr:
695694
if not _is_genexpr_compatible(other):
696695
return NotImplemented
697-
return buildGenExprObj(other) / self
698-
699-
def __neg__(self):
700-
return -1.0 * self
701-
702-
def __sub__(self, other):
703-
return self + (-other)
704-
705-
def __radd__(self, other):
706-
return self.__add__(other)
707-
708-
def __rmul__(self, other):
709-
return self.__mul__(other)
710-
711-
def __rsub__(self, other):
712-
return -1.0 * self + other
713-
714-
def __richcmp__(self, other, int op):
715-
'''turn it into a constraint'''
716-
return _expr_richcmp(self, other, op)
696+
return super().__rtruediv__(other)
717697

718698
def degree(self):
719699
'''Note: none of these expressions should be polynomial'''

src/pyscipopt/scip.pxd

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,26 @@ cdef extern from "scip/cons_knapsack.h":
16361636
int SCIPgetNVarsKnapsack(SCIP* scip, SCIP_CONS* cons)
16371637
SCIP_Longint* SCIPgetWeightsKnapsack(SCIP* scip, SCIP_CONS* cons)
16381638

1639+
cdef extern from "scip/cons_cumulative.h":
1640+
SCIP_RETCODE SCIPcreateConsCumulative(SCIP* scip,
1641+
SCIP_CONS** cons,
1642+
const char* name,
1643+
int nvars,
1644+
SCIP_VAR** vars,
1645+
int* durations,
1646+
int* demands,
1647+
int capacity,
1648+
SCIP_Bool initial,
1649+
SCIP_Bool separate,
1650+
SCIP_Bool enforce,
1651+
SCIP_Bool check,
1652+
SCIP_Bool propagate,
1653+
SCIP_Bool local,
1654+
SCIP_Bool modifiable,
1655+
SCIP_Bool dynamic,
1656+
SCIP_Bool removable,
1657+
SCIP_Bool stickingatnode)
1658+
16391659
cdef extern from "scip/cons_nonlinear.h":
16401660
SCIP_EXPR* SCIPgetExprNonlinear(SCIP_CONS* cons)
16411661
SCIP_RETCODE SCIPcreateConsNonlinear(SCIP* scip,

src/pyscipopt/scip.pxi

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,6 +2385,19 @@ cdef class Constraint:
23852385
constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8')
23862386
return constype == 'knapsack'
23872387

2388+
def isCumulative(self):
2389+
"""
2390+
Returns True if constraint is a cumulative constraint.
2391+
Cumulative is typically used in scheduling applications.
2392+
2393+
Returns
2394+
-------
2395+
bool
2396+
2397+
"""
2398+
constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8')
2399+
return constype == 'cumulative'
2400+
23882401
def isLinearType(self):
23892402
"""
23902403
Returns True if constraint can be represented as a linear constraint.
@@ -7206,6 +7219,96 @@ cdef class Model:
72067219

72077220
return pyCons
72087221

7222+
def addConsCumulative(self, vars, durations, demands, capacity, name="",
7223+
initial=True, separate=True, enforce=True, check=True,
7224+
modifiable=False, propagate=True, local=False, dynamic=False,
7225+
removable=False, stickingatnode=False):
7226+
"""
7227+
Add a cumulative constraint.
7228+
7229+
A cumulative constraint models resource-constrained scheduling: given jobs
7230+
with start times, durations, and demands on a shared resource of fixed
7231+
capacity, it ensures that at every time point t the total demand of all
7232+
jobs active at t does not exceed the capacity. Job j is active at t if
7233+
start[j] <= t < start[j] + duration[j].
7234+
7235+
The start times are given as integer variables in `vars`. The `durations`,
7236+
`demands`, and `capacity` arguments must be fixed integers. End times are
7237+
implicit (start + duration); post separate constraints if explicit end
7238+
variables are needed.
7239+
7240+
If you simply want the jobs to not overlap, set all durations to '1'.
7241+
7242+
Parameters
7243+
----------
7244+
vars : list of Variable
7245+
list of integer variables corresponding to job start times
7246+
durations : list of int
7247+
list of durations, one for each job
7248+
demands : list of int
7249+
list of demands, one for each job
7250+
capacity : int
7251+
available cumulative capacity at any time point
7252+
name : str, optional
7253+
name of the constraint (Default value = "")
7254+
initial : bool, optional
7255+
should the LP relaxation of constraint be in the initial LP? (Default value = True)
7256+
separate : bool, optional
7257+
should the constraint be separated during LP processing? (Default value = True)
7258+
enforce : bool, optional
7259+
should the constraint be enforced during node processing? (Default value = True)
7260+
check : bool, optional
7261+
should the constraint be checked for feasibility? (Default value = True)
7262+
propagate : bool, optional
7263+
should the constraint be propagated during node processing? (Default value = True)
7264+
local : bool, optional
7265+
is the constraint only valid locally? (Default value = False)
7266+
dynamic : bool, optional
7267+
is the constraint subject to aging? (Default value = False)
7268+
removable : bool, optional
7269+
should the relaxation be removed from the LP due to aging or cleanup? (Default value = False)
7270+
stickingatnode : bool, optional
7271+
should the constraint always be kept at the node where it was added,
7272+
even if it may be moved to a more global node? (Default value = False)
7273+
7274+
Returns
7275+
-------
7276+
Constraint
7277+
The newly created cumulative constraint
7278+
"""
7279+
7280+
cdef int nvars = len(vars)
7281+
cdef int i
7282+
cdef int* durations_array = <int*> malloc(nvars * sizeof(int))
7283+
cdef int* demands_array = <int*> malloc(nvars * sizeof(int))
7284+
cdef SCIP_CONS* scip_cons
7285+
cdef _VarArray wrapper
7286+
7287+
assert nvars == len(durations) == len(demands), "Number of variables, durations, and demands must be the same."
7288+
7289+
if name == '':
7290+
name = 'c'+str(SCIPgetNConss(self._scip)+1)
7291+
7292+
wrapper = _VarArray(vars)
7293+
for i in range(nvars):
7294+
durations_array[i] = <int>durations[i]
7295+
demands_array[i] = <int>demands[i]
7296+
7297+
PY_SCIP_CALL(SCIPcreateConsCumulative(
7298+
self._scip, &scip_cons, str_conversion(name), nvars, wrapper.ptr, durations_array,
7299+
demands_array, capacity, initial, separate, enforce, check, propagate, local, modifiable,
7300+
dynamic, removable, stickingatnode))
7301+
7302+
free(durations_array)
7303+
free(demands_array)
7304+
7305+
PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons))
7306+
7307+
pyCons = self._getOrCreateCons(scip_cons)
7308+
PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons))
7309+
7310+
return pyCons
7311+
72097312
def addConsSOS1(self, vars, weights=None, name="",
72107313
initial=True, separate=True, enforce=True, check=True,
72117314
propagate=True, local=False, dynamic=False,

src/pyscipopt/scip.pyi

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ class Constraint:
245245
def getConshdlrName(self) -> Incomplete: ...
246246
def isActive(self) -> Incomplete: ...
247247
def isChecked(self) -> Incomplete: ...
248+
def isCumulative(self) -> Incomplete: ...
248249
def isDynamic(self) -> Incomplete: ...
249250
def isEnforced(self) -> Incomplete: ...
250251
def isInitial(self) -> Incomplete: ...
@@ -333,6 +334,12 @@ class ExprLike:
333334
*args: Incomplete,
334335
**kwargs: Incomplete,
335336
) -> Incomplete: ...
337+
def __radd__(self, other: object, /) -> Incomplete: ...
338+
def __sub__(self, other: object, /) -> Incomplete: ...
339+
def __rsub__(self, other: object, /) -> Incomplete: ...
340+
def __rmul__(self, other: object, /) -> Incomplete: ...
341+
def __rtruediv__(self, other: object, /) -> GenExpr: ...
342+
def __neg__(self, /) -> Union[Expr, GenExpr]: ...
336343
def __abs__(self) -> GenExpr: ...
337344
def exp(self) -> GenExpr: ...
338345
def log(self) -> GenExpr: ...
@@ -346,7 +353,6 @@ class Expr(ExprLike):
346353
def __init__(self, terms: Incomplete = ...) -> None: ...
347354
def degree(self) -> Incomplete: ...
348355
def normalize(self) -> Incomplete: ...
349-
def __abs__(self) -> GenExpr: ...
350356
def __add__(self, other: Incomplete, /) -> Incomplete: ...
351357
def __eq__(self, other: object, /) -> bool: ...
352358
def __ge__(self, other: object, /) -> bool: ...
@@ -358,14 +364,8 @@ class Expr(ExprLike):
358364
def __lt__(self, other: object, /) -> bool: ...
359365
def __mul__(self, other: Incomplete, /) -> Incomplete: ...
360366
def __ne__(self, other: object, /) -> bool: ...
361-
def __neg__(self) -> Incomplete: ...
362367
def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ...
363-
def __radd__(self, other: Incomplete, /) -> Incomplete: ...
364-
def __rmul__(self, other: Incomplete, /) -> Incomplete: ...
365368
def __rpow__(self, other: Incomplete, /) -> Incomplete: ...
366-
def __rsub__(self, other: Incomplete, /) -> Incomplete: ...
367-
def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ...
368-
def __sub__(self, other: Incomplete, /) -> Incomplete: ...
369369
def __truediv__(self, other: Incomplete, /) -> Incomplete: ...
370370

371371
@disjoint_base
@@ -392,7 +392,6 @@ class GenExpr(ExprLike):
392392
def __init__(self) -> None: ...
393393
def degree(self) -> Incomplete: ...
394394
def getOp(self) -> Incomplete: ...
395-
def __abs__(self) -> GenExpr: ...
396395
def __add__(self, other: Incomplete, /) -> Incomplete: ...
397396
def __eq__(self, other: object, /) -> bool: ...
398397
def __ge__(self, other: object, /) -> bool: ...
@@ -401,14 +400,8 @@ class GenExpr(ExprLike):
401400
def __lt__(self, other: object, /) -> bool: ...
402401
def __mul__(self, other: Incomplete, /) -> Incomplete: ...
403402
def __ne__(self, other: object, /) -> bool: ...
404-
def __neg__(self) -> Incomplete: ...
405403
def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ...
406-
def __radd__(self, other: Incomplete, /) -> Incomplete: ...
407-
def __rmul__(self, other: Incomplete, /) -> Incomplete: ...
408404
def __rpow__(self, other: Incomplete, /) -> Incomplete: ...
409-
def __rsub__(self, other: Incomplete, /) -> Incomplete: ...
410-
def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ...
411-
def __sub__(self, other: Incomplete, /) -> Incomplete: ...
412405
def __truediv__(self, other: Incomplete, /) -> Incomplete: ...
413406

414407
@disjoint_base
@@ -643,6 +636,24 @@ class Model:
643636
stickingatnode: bool = False,
644637
) -> Constraint: ...
645638
def addConsCoeff(self, cons: Constraint, var: Variable, coeff: float) -> None: ...
639+
def addConsCumulative(
640+
self,
641+
vars: Sequence[Variable],
642+
durations: Sequence[int],
643+
demands: Sequence[int],
644+
capacity: int,
645+
name: str = "",
646+
initial: bool = True,
647+
separate: bool = True,
648+
enforce: bool = True,
649+
check: bool = True,
650+
modifiable: bool = False,
651+
propagate: bool = True,
652+
local: bool = False,
653+
dynamic: bool = False,
654+
removable: bool = False,
655+
stickingatnode: bool = False,
656+
) -> Constraint: ...
646657
def addConsDisjunction(
647658
self,
648659
conss: Iterable[Incomplete],

tests/test_cons.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,39 @@ def test_cons_knapsack():
351351
assert m.getDualsolKnapsack(knapsack_cons) == 0
352352
assert m.getDualfarkasKnapsack(knapsack_cons) == 0
353353

354+
def test_cons_cumulative():
355+
"""Three jobs on a resource with capacity 3 must not overlap in demand.
356+
357+
Job 1: duration 3, demand 2
358+
Job 2: duration 2, demand 3
359+
Job 3: duration 2, demand 1
360+
361+
Jobs 2 and 3 cannot run together (demand 3+1 > 3). Minimizing the sum of
362+
start times yields start1=0, start2=3, start3=0.
363+
"""
364+
m = Model()
365+
start1 = m.addVar("start1", vtype="I", lb=0, ub=10, obj=1)
366+
start2 = m.addVar("start2", vtype="I", lb=0, ub=10, obj=1)
367+
start3 = m.addVar("start3", vtype="I", lb=0, ub=10, obj=1)
368+
durations = [3, 2, 2]
369+
demands = [2, 3, 1]
370+
capacity = 3
371+
372+
cumulative_cons = m.addConsCumulative([start1, start2, start3], durations, demands, capacity)
373+
assert cumulative_cons.getConshdlrName() == "cumulative"
374+
assert cumulative_cons.isCumulative()
375+
376+
assert m.getConsNVars(cumulative_cons) == 3
377+
assert m.getConsVars(cumulative_cons) == [start1, start2, start3]
378+
379+
m.setObjective(start1 + start2 + start3, "minimize")
380+
m.optimize()
381+
382+
assert m.isEQ(m.getVal(start1), 0)
383+
assert m.isEQ(m.getVal(start2), 3)
384+
assert m.isEQ(m.getVal(start3), 0)
385+
assert m.isEQ(m.getObjVal(), 3)
386+
354387
def test_getValsLinear():
355388
m = Model()
356389
x = m.addVar("x", lb=0, ub=2, obj=-1)

0 commit comments

Comments
 (0)