Skip to content

Commit fb3a4bd

Browse files
Merge branch 'master' into ExprLike
2 parents aa6ae86 + 2e5d929 commit fb3a4bd

5 files changed

Lines changed: 176 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- Added `addConsCumulative()` for SCIP cumulative constraints (#1222)
56
### Fixed
67
### Changed
78
<<<<<<< ExprLike

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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ class Constraint:
243243
def getConshdlrName(self) -> Incomplete: ...
244244
def isActive(self) -> Incomplete: ...
245245
def isChecked(self) -> Incomplete: ...
246+
def isCumulative(self) -> Incomplete: ...
246247
def isDynamic(self) -> Incomplete: ...
247248
def isEnforced(self) -> Incomplete: ...
248249
def isInitial(self) -> Incomplete: ...
@@ -641,6 +642,24 @@ class Model:
641642
def addConsCoeff(
642643
self, cons: Incomplete, var: Incomplete, coeff: Incomplete
643644
) -> Incomplete: ...
645+
def addConsCumulative(
646+
self,
647+
vars: Incomplete,
648+
durations: Incomplete,
649+
demands: Incomplete,
650+
capacity: Incomplete,
651+
name: Incomplete = ...,
652+
initial: Incomplete = ...,
653+
separate: Incomplete = ...,
654+
enforce: Incomplete = ...,
655+
check: Incomplete = ...,
656+
modifiable: Incomplete = ...,
657+
propagate: Incomplete = ...,
658+
local: Incomplete = ...,
659+
dynamic: Incomplete = ...,
660+
removable: Incomplete = ...,
661+
stickingatnode: Incomplete = ...,
662+
) -> Incomplete: ...
644663
def addConsDisjunction(
645664
self,
646665
conss: 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)