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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Added `getCountedSols()` to retrieve the solutions collected by `count()` (#906)
- Added docstrings to the bundled stubs in release wheels for Python 3.10 and later.
- Added more type annotations and deprecation markers to the stubs.
- Added tests for `getObjVal()` and `getVal()`
Expand Down
6 changes: 6 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ cdef extern from "scip/scip.h":
ctypedef struct SCIP_DIVESET:
pass

ctypedef struct SCIP_SPARSESOL:
pass

ctypedef struct SCIP_HASHMAP:
pass

Expand Down Expand Up @@ -2102,6 +2105,9 @@ cdef extern from "scip/cons_countsols.h":
SCIP_RETCODE SCIPcount(SCIP* scip)
SCIP_RETCODE SCIPsetParamsCountsols(SCIP* scip)
SCIP_Longint SCIPgetNCountedSols(SCIP* scip, SCIP_Bool* valid)
void SCIPgetCountedSparseSols(SCIP* scip, SCIP_VAR*** vars, int* nvars, SCIP_SPARSESOL*** sols, int* nsols)
void SCIPsparseSolGetFirstSol(SCIP_SPARSESOL* sparsesol, SCIP_Longint* sol, int nvars)
SCIP_Bool SCIPsparseSolGetNextSol(SCIP_SPARSESOL* sparsesol, SCIP_Longint* sol, int nvars)

cdef extern from "scip/paramset.h":

Expand Down
50 changes: 50 additions & 0 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -12654,6 +12654,56 @@ cdef class Model:
print('total number of solutions found is not valid!')
return nsols

def getCountedSols(self):
"""
Get the feasible solutions collected by `count()`.

Requires `constraints/countsols/collect` to be True and presolving to be off
before calling `count()`, and a problem without continuous variables.

Returns
-------
list of dict
Each solution maps the variable names to their values.

"""
cdef SCIP_VAR** vars
cdef SCIP_SPARSESOL** sparsesols
cdef SCIP_Longint* vals
cdef SCIP_Bool valid
cdef int nvars
cdef int nsparsesols
cdef int i
cdef int s

if not self.getParam("constraints/countsols/collect"):
raise Warning("set constraints/countsols/collect to True before calling count()")
if self.getParam("presolving/maxrounds") != 0:
raise Warning("turn presolving off before calling count()")
if SCIPgetNContVars(self._scip) > 0:
raise Warning("counted solutions are only available for problems without continuous variables")

SCIPgetNCountedSols(self._scip, &valid)
if not valid:
raise Warning("number of counted solutions does not fit into a long integer, cannot collect them")

SCIPgetCountedSparseSols(self._scip, &vars, &nvars, &sparsesols, &nsparsesols)
# vars are transformed, strip the "t_" prefix to get the original names
names = [bytes(SCIPvarGetName(vars[i])).decode("utf-8")[2:] for i in range(nvars)]
Comment thread
Joao-Dionisio marked this conversation as resolved.

vals = <SCIP_Longint*> malloc(nvars * sizeof(SCIP_Longint))
sols = []
for s in range(nsparsesols):
SCIPsparseSolGetFirstSol(sparsesols[s], vals, nvars)
while True:
sols.append({names[i]: vals[i] for i in range(nvars)})
if not SCIPsparseSolGetNextSol(sparsesols[s], vals, nvars):
break

free(vals)

return sols

def setParamsCountsols(self):
"""Sets SCIP parameters such that a valid counting process is possible."""
PY_SCIP_CALL(SCIPsetParamsCountsols(self._scip))
Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,7 @@ class Model:
def getConsVars(self, constraint: Constraint) -> list[Variable] | None: ...
def getConss(self, transformed: bool = True) -> list[Constraint]: ...
def getFocusNode(self) -> Node | None: ...
def getCountedSols(self) -> list[dict[str, int]]: ...
def getCurrentNode(self) -> Node | None: ...
def getCutEfficacy(self, cut: Row, sol: Solution | None = None) -> float: ...
def getCutLPSolCutoffDistance(self, cut: Row, sol: Solution) -> float: ...
Expand Down
28 changes: 28 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,31 @@ def test_memory_methods():

assert m.getMemUsed() > 0
assert m.getMemTotal() > 0


def test_getCountedSols():
m = Model()
m.hideOutput()
m.setParam("constraints/countsols/collect", True)
m.setPresolve(SCIP_PARAMSETTING.OFF)
x = m.addVar("x", vtype="I", lb=-2, ub=3)
y = m.addVar("y", vtype="I", lb=0, ub=4)
z = m.addVar("z", vtype="I", lb=0, ub=10)
w = m.addVar("w", vtype="B")
m.addCons(z == 2*x + 3)
m.addCons(x + y <= 4)
m.addCons(w + y >= 1)

assert m.getCountedSols() == []

m.count()
sols = m.getCountedSols()
expected = {(a, b, c, d) for a, b, c, d in itertools.product(range(-2, 4), range(5), range(11), range(2))
if c == 2*a + 3 and a + b <= 4 and d + b >= 1}
assert len(sols) == m.getNCountedSols() == len(expected)
assert all(isinstance(sol, dict) and set(sol) == {"x", "y", "z", "w"} for sol in sols)
assert all(type(val) is int for sol in sols for val in sol.values())
assert {tuple(sol[v.name] for v in (x, y, z, w)) for sol in sols} == expected

m.freeTransform()
assert m.getCountedSols() == []
Loading