diff --git a/CHANGELOG.md b/CHANGELOG.md index 7464e9a8f..2f7124710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added `addConsCumulative()` for SCIP cumulative constraints (#1222) ### Fixed ### Changed - Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index af933fe19..9ff2979e7 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1636,6 +1636,26 @@ cdef extern from "scip/cons_knapsack.h": int SCIPgetNVarsKnapsack(SCIP* scip, SCIP_CONS* cons) SCIP_Longint* SCIPgetWeightsKnapsack(SCIP* scip, SCIP_CONS* cons) +cdef extern from "scip/cons_cumulative.h": + SCIP_RETCODE SCIPcreateConsCumulative(SCIP* scip, + SCIP_CONS** cons, + const char* name, + int nvars, + SCIP_VAR** vars, + int* durations, + int* demands, + int capacity, + SCIP_Bool initial, + SCIP_Bool separate, + SCIP_Bool enforce, + SCIP_Bool check, + SCIP_Bool propagate, + SCIP_Bool local, + SCIP_Bool modifiable, + SCIP_Bool dynamic, + SCIP_Bool removable, + SCIP_Bool stickingatnode) + cdef extern from "scip/cons_nonlinear.h": SCIP_EXPR* SCIPgetExprNonlinear(SCIP_CONS* cons) SCIP_RETCODE SCIPcreateConsNonlinear(SCIP* scip, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 15cf9812a..45f0ccf9e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2385,6 +2385,19 @@ cdef class Constraint: constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype == 'knapsack' + def isCumulative(self): + """ + Returns True if constraint is a cumulative constraint. + Cumulative is typically used in scheduling applications. + + Returns + ------- + bool + + """ + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') + return constype == 'cumulative' + def isLinearType(self): """ Returns True if constraint can be represented as a linear constraint. @@ -7206,6 +7219,96 @@ cdef class Model: return pyCons + def addConsCumulative(self, vars, durations, demands, capacity, name="", + initial=True, separate=True, enforce=True, check=True, + modifiable=False, propagate=True, local=False, dynamic=False, + removable=False, stickingatnode=False): + """ + Add a cumulative constraint. + + A cumulative constraint models resource-constrained scheduling: given jobs + with start times, durations, and demands on a shared resource of fixed + capacity, it ensures that at every time point t the total demand of all + jobs active at t does not exceed the capacity. Job j is active at t if + start[j] <= t < start[j] + duration[j]. + + The start times are given as integer variables in `vars`. The `durations`, + `demands`, and `capacity` arguments must be fixed integers. End times are + implicit (start + duration); post separate constraints if explicit end + variables are needed. + + If you simply want the jobs to not overlap, set all durations to '1'. + + Parameters + ---------- + vars : list of Variable + list of integer variables corresponding to job start times + durations : list of int + list of durations, one for each job + demands : list of int + list of demands, one for each job + capacity : int + available cumulative capacity at any time point + name : str, optional + name of the constraint (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) + 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 cumulative constraint + """ + + cdef int nvars = len(vars) + cdef int i + cdef int* durations_array = malloc(nvars * sizeof(int)) + cdef int* demands_array = malloc(nvars * sizeof(int)) + cdef SCIP_CONS* scip_cons + cdef _VarArray wrapper + + assert nvars == len(durations) == len(demands), "Number of variables, durations, and demands must be the same." + + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + + wrapper = _VarArray(vars) + for i in range(nvars): + durations_array[i] = durations[i] + demands_array[i] = demands[i] + + PY_SCIP_CALL(SCIPcreateConsCumulative( + self._scip, &scip_cons, str_conversion(name), nvars, wrapper.ptr, durations_array, + demands_array, capacity, initial, separate, enforce, check, propagate, local, modifiable, + dynamic, removable, stickingatnode)) + + free(durations_array) + free(demands_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 addConsSOS1(self, vars, weights=None, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index ee82c00d9..4b4fe6d16 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -243,6 +243,7 @@ class Constraint: def getConshdlrName(self) -> Incomplete: ... def isActive(self) -> Incomplete: ... def isChecked(self) -> Incomplete: ... + def isCumulative(self) -> Incomplete: ... def isDynamic(self) -> Incomplete: ... def isEnforced(self) -> Incomplete: ... def isInitial(self) -> Incomplete: ... @@ -649,6 +650,24 @@ class Model: def addConsCoeff( self, cons: Incomplete, var: Incomplete, coeff: Incomplete ) -> Incomplete: ... + def addConsCumulative( + self, + vars: Incomplete, + durations: Incomplete, + demands: Incomplete, + capacity: Incomplete, + name: Incomplete = ..., + initial: Incomplete = ..., + separate: Incomplete = ..., + enforce: Incomplete = ..., + check: Incomplete = ..., + modifiable: Incomplete = ..., + propagate: Incomplete = ..., + local: Incomplete = ..., + dynamic: Incomplete = ..., + removable: Incomplete = ..., + stickingatnode: Incomplete = ..., + ) -> Incomplete: ... def addConsDisjunction( self, conss: Incomplete, diff --git a/tests/test_cons.py b/tests/test_cons.py index 623adf0d9..243014427 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -351,6 +351,39 @@ def test_cons_knapsack(): assert m.getDualsolKnapsack(knapsack_cons) == 0 assert m.getDualfarkasKnapsack(knapsack_cons) == 0 +def test_cons_cumulative(): + """Three jobs on a resource with capacity 3 must not overlap in demand. + + Job 1: duration 3, demand 2 + Job 2: duration 2, demand 3 + Job 3: duration 2, demand 1 + + Jobs 2 and 3 cannot run together (demand 3+1 > 3). Minimizing the sum of + start times yields start1=0, start2=3, start3=0. + """ + m = Model() + start1 = m.addVar("start1", vtype="I", lb=0, ub=10, obj=1) + start2 = m.addVar("start2", vtype="I", lb=0, ub=10, obj=1) + start3 = m.addVar("start3", vtype="I", lb=0, ub=10, obj=1) + durations = [3, 2, 2] + demands = [2, 3, 1] + capacity = 3 + + cumulative_cons = m.addConsCumulative([start1, start2, start3], durations, demands, capacity) + assert cumulative_cons.getConshdlrName() == "cumulative" + assert cumulative_cons.isCumulative() + + assert m.getConsNVars(cumulative_cons) == 3 + assert m.getConsVars(cumulative_cons) == [start1, start2, start3] + + m.setObjective(start1 + start2 + start3, "minimize") + m.optimize() + + assert m.isEQ(m.getVal(start1), 0) + assert m.isEQ(m.getVal(start2), 3) + assert m.isEQ(m.getVal(start3), 0) + assert m.isEQ(m.getObjVal(), 3) + def test_getValsLinear(): m = Model() x = m.addVar("x", lb=0, ub=2, obj=-1)