Skip to content
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## Unreleased
### Added
- Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests
- Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()`, `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`,\
`getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()`, `getUpperbound()`, `getNObjlimLeaves()` with tests
- Added `addConsCumulative()` for SCIP cumulative constraints (#1222)
- `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr`
- Added type annotations to most methods on the `Model` class
Expand Down
10 changes: 10 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ cdef extern from "scip/scip.h":
SCIP_Real SCIPgetSolvingTime(SCIP* scip)
SCIP_Real SCIPgetReadingTime(SCIP* scip)
SCIP_Real SCIPgetPresolvingTime(SCIP* scip)
SCIP_Real SCIPgetDeterministicTime(SCIP* scip)
SCIP_STAGE SCIPgetStage(SCIP* scip)
SCIP_RETCODE SCIPsetProbName(SCIP* scip, char* name)
const char* SCIPgetProbName(SCIP* scip)
Expand Down Expand Up @@ -749,6 +750,7 @@ cdef extern from "scip/scip.h":
SCIP_RETCODE SCIPpresolve(SCIP* scip)

# Node Methods
SCIP_NODE* SCIPgetFocusNode(SCIP* scip)
SCIP_NODE* SCIPgetCurrentNode(SCIP* scip)
SCIP_NODE* SCIPnodeGetParent(SCIP_NODE* node)
SCIP_Longint SCIPnodeGetNumber(SCIP_NODE* node)
Expand Down Expand Up @@ -969,6 +971,7 @@ cdef extern from "scip/scip.h":
SCIP_RETCODE SCIPprintBestTransSol(SCIP* scip, FILE* outfile, SCIP_Bool printzeros)
SCIP_RETCODE SCIPprintSol(SCIP* scip, SCIP_SOL* sol, FILE* outfile, SCIP_Bool printzeros)
SCIP_RETCODE SCIPprintTransSol(SCIP* scip, SCIP_SOL* sol, FILE* outfile, SCIP_Bool printzeros)
SCIP_Real SCIPgetFirstPrimalBound(SCIP* scip)
SCIP_Real SCIPgetPrimalbound(SCIP* scip)
SCIP_Real SCIPgetGap(SCIP* scip)
int SCIPgetDepth(SCIP* scip)
Expand Down Expand Up @@ -1480,12 +1483,19 @@ cdef extern from "scip/scip.h":
SCIP_Longint SCIPgetNTotalNodes(SCIP* scip)
SCIP_Longint SCIPgetNFeasibleLeaves(SCIP* scip)
SCIP_Longint SCIPgetNInfeasibleLeaves(SCIP* scip)
SCIP_Longint SCIPgetNObjlimLeaves(SCIP* scip)
SCIP_Longint SCIPgetNLPs(SCIP* scip)
SCIP_Longint SCIPgetNLPIterations(SCIP* scip)
int SCIPgetNSepaRounds(SCIP* scip)
SCIP_Real SCIPgetAvgLowerbound(SCIP* scip)
SCIP_Real SCIPgetAvgDualbound(SCIP* scip)
SCIP_Real SCIPgetLowerbound(SCIP* scip)
SCIP_Real SCIPgetLowerboundRoot(SCIP* scip)
SCIP_Real SCIPgetCutoffbound(SCIP* scip)
SCIP_Real SCIPgetUpperbound(SCIP* scip)
int SCIPgetMaxDepth(SCIP* scip)
int SCIPgetMaxTotalDepth(SCIP* scip)
SCIP_Longint SCIPgetNBacktracks(SCIP* scip)
int SCIPgetPlungeDepth(SCIP* scip)
SCIP_Longint SCIPgetNNodeLPIterations(SCIP* scip)
SCIP_Longint SCIPgetNStrongbranchLPIterations(SCIP* scip)
Expand Down
110 changes: 110 additions & 0 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,28 @@ cdef class Model:
"""
return SCIPgetPresolvingTime(self._scip)

def getDeterministicTime(self):
"""
Computes a deterministic measure of time from statistics.

Returns
-------
float

"""
return SCIPgetDeterministicTime(self._scip)

def getFirstPrimalBound(self):
"""
Gets the primal bound of the very first solution.

Returns
-------
float

"""
return SCIPgetFirstPrimalBound(self._scip)

def getNLPIterations(self):
"""
Returns the total number of LP iterations so far.
Expand Down Expand Up @@ -3373,6 +3395,17 @@ cdef class Model:
"""
return SCIPgetNInfeasibleLeaves(self._scip)

def getNObjlimLeaves(self):
"""
Gets number of processed leaf nodes that hit LP objective limit.

Returns
-------
int

"""
return SCIPgetNObjlimLeaves(self._scip)

def getNLeaves(self):
"""
Gets number of leaves in the tree.
Expand Down Expand Up @@ -3417,6 +3450,18 @@ cdef class Model:
"""
return SCIPgetNSiblings(self._scip)

def getFocusNode(self):
"""
Gets focus node in the tree.
If we are in probing/diving mode this method returns the node in the tree where the probing/diving mode was started.

Returns
-------
Node

"""
return Node.create(SCIPgetFocusNode(self._scip))

def getCurrentNode(self):
"""
Retrieve current node.
Expand Down Expand Up @@ -3462,6 +3507,28 @@ cdef class Model:
"""
return SCIPgetMaxDepth(self._scip)

def getMaxTotalDepth(self):
"""
Gets maximal depth of all processed nodes over all branch and bound runs.

Returns
-------
int

"""
return SCIPgetMaxTotalDepth(self._scip)

def getNBacktracks(self):
"""
Gets total number of backtracks, i.e. number of times, the new node was selected from the leaves queue.

Returns
-------
int

"""
return SCIPgetNBacktracks(self._scip)

def getPlungeDepth(self):
"""
Gets current plunging depth (successive selections of child/sibling nodes).
Expand All @@ -3473,6 +3540,28 @@ cdef class Model:
"""
return SCIPgetPlungeDepth(self._scip)

def getAvgLowerbound(self):
"""
Gets average lower (dual) bound of all unprocessed nodes in transformed problem.

Returns
-------
float

"""
return SCIPgetAvgLowerbound(self._scip)

def getAvgDualbound(self):
"""
Gets average dual bound of all unprocessed nodes for original problem.

Returns
-------
float

"""
return SCIPgetAvgDualbound(self._scip)

def getLowerbound(self):
"""
Gets global lower (dual) bound of the transformed problem.
Expand All @@ -3495,6 +3584,16 @@ cdef class Model:
"""
return SCIPgetCutoffbound(self._scip)

def getUpperbound(self):
"""
Gets global upper (primal) bound in transformed problem (objective value of best solution or user objective limit).

Returns
-------
float
"""
return SCIPgetUpperbound(self._scip)

def getNNodeLPIterations(self):
"""
Gets number of LP iterations used for solving node relaxations so far.
Expand Down Expand Up @@ -11506,6 +11605,17 @@ cdef class Model:
"""
return SCIPgetDualboundRoot(self._scip)

def getLowerboundRoot(self):
"""
Gets lower (dual) bound in transformed problem of the root node.

Returns
-------
float

"""
return SCIPgetLowerboundRoot(self._scip)

def writeName(self, Variable var):
"""
Write the name of the variable to the std out.
Expand Down
10 changes: 10 additions & 0 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,8 @@ class Model:
list[list[float]],
dict[str, dict[str, int]],
]: ...
def getAvgLowerbound(self) -> float: ...
def getAvgDualbound(self) -> float: ...
def getBranchScoreMultiple(self, var: Variable, gains: list[float]) -> float: ...
def getCapacityKnapsack(self, cons: Constraint) -> int: ...
def getChildren(self) -> list[Node]: ...
Expand All @@ -1205,17 +1207,22 @@ class Model:
def getConsVals(self, constraint: Constraint) -> list[float] | None: ...
def getConsVars(self, constraint: Constraint) -> list[Variable] | None: ...
def getConss(self, transformed: bool = True) -> list[Constraint]: ...
def getFocusNode(self) -> Node | None: ...
def getCurrentNode(self) -> Node | None: ...
def getCutEfficacy(self, cut: Row, sol: Solution | None = None) -> float: ...
def getCutLPSolCutoffDistance(self, cut: Row, sol: Solution) -> float: ...
def getCutoffbound(self) -> float: ...
def getUpperbound(self) -> float: ...
def getDepth(self) -> int: ...
def getDeterministicTime(self) -> float: ...
def getFirstPrimalBound(self) -> float: ...
def getDualMultiplier(self, cons: Constraint) -> float: ...
def getDualSolVal(
self, cons: Constraint, boundconstraint: bool = False
) -> float: ...
def getDualbound(self) -> float: ...
def getDualboundRoot(self) -> float: ...
def getLowerboundRoot(self) -> float: ...
def getDualfarkasKnapsack(self, cons: Constraint) -> float: ...
def getDualfarkasLinear(self, cons: Constraint) -> float: ...
def getDualsolKnapsack(self, cons: Constraint) -> float: ...
Expand All @@ -1240,6 +1247,8 @@ class Model:
def getLowerbound(self) -> float: ...
def getMajorVersion(self) -> int: ...
def getMaxDepth(self) -> int: ...
def getMaxTotalDepth(self) -> int: ...
def getNBacktracks(self) -> int: ...
def getMinorVersion(self) -> int: ...
def getNBestSolsFound(self) -> int: ...
def getNBinVars(self) -> int: ...
Expand All @@ -1252,6 +1261,7 @@ class Model:
def getNFeasibleLeaves(self) -> int: ...
def getNImplVars(self) -> int: ...
def getNInfeasibleLeaves(self) -> int: ...
def getNObjlimLeaves(self) -> int: ...
def getNIntVars(self) -> int: ...
def getNLPBranchCands(self) -> int: ...
def getNLPCols(self) -> int: ...
Expand Down
84 changes: 83 additions & 1 deletion tests/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from helpers.utils import random_mip_1
from json import load
import pytest
import numpy as np


@pytest.fixture
def optimized_model():
model = random_mip_1(small=True) # Using small=True for speed across tests
model = random_mip_1(small=True, node_lim=2400) # Using small=True for speed across tests
model.optimize()
return model

Expand All @@ -21,6 +22,12 @@ def test_statistics_json(optimized_model):
os.remove("statistics.json")


def test_getNSolsFound(optimized_model):
sols = optimized_model.getNSolsFound()

assert sols >= 1


def test_getPrimalDualIntegral(optimized_model):
primal_dual_integral = optimized_model.getPrimalDualIntegral()

Expand All @@ -41,9 +48,84 @@ def test_getNReoptRuns(optimized_model):
assert n_reopt_runs >= 0


def test_getNObjlimLeaves(optimized_model):
n_objlim_leaves = optimized_model.getNObjlimLeaves()

assert isinstance(n_objlim_leaves, int)


def test_addNNodes(optimized_model):
initial_n_nodes = optimized_model.getNTotalNodes()
optimized_model.addNNodes(5)
new_n_nodes = optimized_model.getNTotalNodes()

assert new_n_nodes == initial_n_nodes + 5


def test_getMaxTotalDepth(optimized_model):
max_total_depth = optimized_model.getMaxTotalDepth()
total_depth = optimized_model.getMaxDepth()

assert isinstance(max_total_depth, int)
assert max_total_depth >= 0
assert max_total_depth >= total_depth


def test_getNBacktracks(optimized_model):
n_backtracks = optimized_model.getNBacktracks()

assert isinstance(n_backtracks, int)
assert n_backtracks >= 0


def test_getAvgLowerbound(optimized_model):
avg_lowerbound = optimized_model.getAvgLowerbound()
leaves, children, siblings = optimized_model.getOpenNodes()
open_nodes = leaves + children + siblings
manual_avg_lowerbound = 0.0
if len(open_nodes) > 0:
manual_avg_lowerbound = np.mean(
[node.getLowerbound() for node in open_nodes] + [optimized_model.getFocusNode().getLowerbound()]
)

assert isinstance(avg_lowerbound, float)
assert manual_avg_lowerbound == pytest.approx(avg_lowerbound)


def test_getAvgDualbound(optimized_model):
avg_dualbound = optimized_model.getAvgDualbound()
avg_lowerbound = optimized_model.getAvgLowerbound()

assert isinstance(avg_dualbound, float)
assert avg_dualbound == pytest.approx(avg_lowerbound) or avg_dualbound == pytest.approx(-avg_lowerbound)


def test_getDeterministicTime(optimized_model):
det_time = optimized_model.getDeterministicTime()

assert isinstance(det_time, float)
assert det_time >= 0.0


def test_getUpperbound(optimized_model):
upperbound = optimized_model.getUpperbound()
lowerbound = optimized_model.getLowerbound()

assert isinstance(upperbound, float)
assert upperbound >= lowerbound


def test_getFirstPrimalBound(optimized_model):
first_primal = optimized_model.getFirstPrimalBound()
upperbound = optimized_model.getUpperbound()

assert isinstance(first_primal, float)
assert first_primal >= upperbound


def test_getLowerboundRoot(optimized_model):
lowerbound_root = optimized_model.getLowerboundRoot()
lowerbound = optimized_model.getLowerbound()

assert isinstance(lowerbound_root, float)
assert lowerbound_root <= lowerbound
Loading