diff --git a/CHANGELOG.md b/CHANGELOG.md index 6948ab2c7..c39281a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Wrapped `SCIPcaptureVar()`, `SCIPreleaseVar()`, `SCIPvarGetNUses()`, `SCIPcaptureCons()`, `SCIPreleaseCons()`, and `SCIPconsGetNUses()` ### 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..f3e672563 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -818,8 +818,9 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPmarkDoNotAggrVar(SCIP* scip, SCIP_VAR* var) SCIP_RETCODE SCIPmarkDoNotMultaggrVar(SCIP* scip, SCIP_VAR* var) SCIP_RETCODE SCIPcaptureVar(SCIP* scip, SCIP_VAR* var) - SCIP_RETCODE SCIPaddPricedVar(SCIP* scip, SCIP_VAR* var, SCIP_Real score) SCIP_RETCODE SCIPreleaseVar(SCIP* scip, SCIP_VAR** var) + int SCIPvarGetNUses(SCIP_VAR* var) + SCIP_RETCODE SCIPaddPricedVar(SCIP* scip, SCIP_VAR* var, SCIP_Real score) SCIP_RETCODE SCIPtransformVar(SCIP* scip, SCIP_VAR* var, SCIP_VAR** transvar) SCIP_RETCODE SCIPgetTransformedVar(SCIP* scip, SCIP_VAR* var, SCIP_VAR** transvar) SCIP_RETCODE SCIPaddVarLocks(SCIP* scip, SCIP_VAR* var, int nlocksdown, int nlocksup) @@ -913,6 +914,7 @@ cdef extern from "scip/scip.h": # Constraint Methods SCIP_RETCODE SCIPcaptureCons(SCIP* scip, SCIP_CONS* cons) SCIP_RETCODE SCIPreleaseCons(SCIP* scip, SCIP_CONS** cons) + int SCIPconsGetNUses(SCIP_CONS* cons) SCIP_RETCODE SCIPtransformCons(SCIP* scip, SCIP_CONS* cons, SCIP_CONS** transcons) SCIP_RETCODE SCIPgetTransformedCons(SCIP* scip, SCIP_CONS* cons, SCIP_CONS** transcons) SCIP_RETCODE SCIPgetConsVars(SCIP* scip, SCIP_CONS* cons, SCIP_VAR** vars, int varssize, SCIP_Bool* success) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index eed5339b0..0a8d90d83 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -4636,6 +4636,63 @@ cdef class Model: var.scip_var = NULL return deleted + def captureVar(self, Variable var): + """ + Increase the usage counter of a variable. Must be paired with a later releaseVar. + + Parameters + ---------- + var : Variable + variable to capture + + """ + PY_SCIP_CALL(SCIPcaptureVar(self._scip, var.scip_var)) + + def releaseVar(self, Variable var): + """ + Decrease the usage counter of a variable. + + Unlike the underlying ``SCIPreleaseVar``, this wrapper refuses to + release the last reference. It must be paired with a prior + captureVar call. This guarantees the variable is never freed via + this method and the wrapper's pointer stays valid. + + Parameters + ---------- + var : Variable + variable to release + + Raises + ------ + Exception + if releasing would free the variable (no matching captureVar). + + """ + cdef SCIP_VAR* scip_var = var.scip_var + if SCIPvarGetNUses(scip_var) <= 1: + raise Exception( + "releaseVar would free the variable; must be paired with " + "a prior captureVar call." + ) + PY_SCIP_CALL(SCIPreleaseVar(self._scip, &scip_var)) + + def getVarNUses(self, Variable var): + """ + Get the number of times the variable is currently captured. + + Parameters + ---------- + var : Variable + variable to query + + Returns + ------- + int + the current usage count. + + """ + return SCIPvarGetNUses(var.scip_var) + def aggregateVars(self, Variable varx, Variable vary, coefx=1.0, coefy=-1.0, rhs=0.0): """ Aggregate two variables by adding an aggregation constraint. @@ -8568,7 +8625,64 @@ cdef class Model: """ PY_SCIP_CALL(SCIPdelConsLocal(self._scip, cons.scip_cons)) - + + def captureCons(self, Constraint cons): + """ + Increase the usage counter of a constraint. Must be paired with a later releaseCons. + + Parameters + ---------- + cons : Constraint + constraint to capture + + """ + PY_SCIP_CALL(SCIPcaptureCons(self._scip, cons.scip_cons)) + + def releaseCons(self, Constraint cons): + """ + Decrease the usage counter of a constraint. + + Unlike the underlying ``SCIPreleaseCons``, this wrapper refuses to + release the last reference. It must be paired with a prior + captureCons call. This guarantees the constraint is never freed via + this method and the wrapper's pointer stays valid. + + Parameters + ---------- + cons : Constraint + constraint to release + + Raises + ------ + Exception + if releasing would free the constraint (no matching captureCons). + + """ + cdef SCIP_CONS* scip_cons = cons.scip_cons + if SCIPconsGetNUses(scip_cons) <= 1: + raise Exception( + "releaseCons would free the constraint; must be paired with " + "a prior captureCons call." + ) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + def getConsNUses(self, Constraint cons): + """ + Get the number of times the constraint is currently captured. + + Parameters + ---------- + cons : Constraint + constraint to query + + Returns + ------- + int + the current usage count. + + """ + return SCIPconsGetNUses(cons.scip_cons) + def getValsLinear(self, Constraint cons): """ Retrieve the coefficients of a linear constraint diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 2fbc976f4..40478d9ac 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -922,6 +922,8 @@ class Model: def calcNodeselPriority( self, variable: Incomplete, branchdir: Incomplete, targetvalue: Incomplete ) -> Incomplete: ... + def captureCons(self, cons: Incomplete) -> Incomplete: ... + def captureVar(self, var: Incomplete) -> Incomplete: ... def catchEvent( self, eventtype: Incomplete, eventhdlr: Incomplete ) -> Incomplete: ... @@ -1113,6 +1115,7 @@ class Model: def getChildren(self) -> Incomplete: ... def getColRedCost(self, col: Incomplete) -> Incomplete: ... def getCondition(self, exact: Incomplete = ...) -> Incomplete: ... + def getConsNUses(self, cons: Incomplete) -> Incomplete: ... def getConsNVars(self, constraint: Incomplete) -> Incomplete: ... def getConsVals(self, constraint: Incomplete) -> Incomplete: ... def getConsVars(self, constraint: Incomplete) -> Incomplete: ... @@ -1257,6 +1260,7 @@ class Model: def getValsLinear(self, cons: Incomplete) -> Incomplete: ... def getVarDict(self, transformed: Incomplete = ...) -> Incomplete: ... def getVarLbDive(self, var: Incomplete) -> Incomplete: ... + def getVarNUses(self, var: Incomplete) -> Incomplete: ... def getVarPseudocost( self, var: Incomplete, branchdir: Incomplete ) -> Incomplete: ... @@ -1487,7 +1491,9 @@ class Model: def readSolFile(self, filename: Incomplete) -> Incomplete: ... def redirectOutput(self) -> Incomplete: ... def relax(self) -> Incomplete: ... + def releaseCons(self, cons: Incomplete) -> Incomplete: ... def releaseRow(self, row: Incomplete) -> Incomplete: ... + def releaseVar(self, var: Incomplete) -> Incomplete: ... def repropagateNode(self, node: Incomplete) -> Incomplete: ... def resetParam(self, name: Incomplete) -> Incomplete: ... def resetParams(self) -> Incomplete: ... diff --git a/tests/test_cons.py b/tests/test_cons.py index 623adf0d9..6fa592e57 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -368,3 +368,20 @@ def test_getValsLinear(): @pytest.mark.skip(reason="TODO: test getRowLinear()") def test_getRowLinear(): assert True + + +def test_captureCons_releaseCons(): + m = Model() + x = m.addVar("x") + c = m.addCons(x <= 1) + + # Pair capture+release: problem still holds its own capture, so this is safe. + assert m.getConsNUses(c) == 1 + m.captureCons(c) + assert m.getConsNUses(c) == 2 + m.releaseCons(c) + assert m.getConsNUses(c) == 1 + + # Model continues to function — problem's capture survived. + m.optimize() + assert m.getStatus() == "optimal" diff --git a/tests/test_vars.py b/tests/test_vars.py index 43f71586d..917a2daaa 100644 --- a/tests/test_vars.py +++ b/tests/test_vars.py @@ -223,4 +223,19 @@ def test_adjustedVarUb(): # For continuous variables, values should generally stay the same x_cont = m.addVar(vtype='C', lb=-10.0, ub=10.0, name="x_cont") assert m.adjustedVarUb(x_cont, 5.5) == 5.5 - assert m.adjustedVarUb(x_cont, -3.2) == -3.2 \ No newline at end of file + assert m.adjustedVarUb(x_cont, -3.2) == -3.2 + + +def test_captureVar_releaseVar(): + m = Model() + x = m.addVar("x") + + # Pair capture+release: problem still holds its own capture, so this is safe. + assert m.getVarNUses(x) == 1 + m.captureVar(x) + assert m.getVarNUses(x) == 2 + m.releaseVar(x) + assert m.getVarNUses(x) == 1 + + m.optimize() + assert m.getStatus() == "optimal" \ No newline at end of file