Skip to content
Open
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
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
16 changes: 13 additions & 3 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from collections.abc import Iterable, Mapping, Sequence
from typing import Any, AnyStr, ClassVar, Literal, SupportsFloat, Union, overload
from typing import Any, AnyStr, ClassVar, Literal, SupportsFloat, overload

import numpy as np
from _typeshed import Incomplete
Expand Down 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 Expand Up @@ -1329,7 +1339,7 @@ class Model:
def getSolObjVal(self, sol: Solution | None, original: bool = True) -> float: ...
def getSolTime(self, sol: Solution) -> float: ...
@overload
def getSolVal(self, sol: Solution | None, expr: Union[Expr, GenExpr]) -> float: ...
def getSolVal(self, sol: Solution | None, expr: Expr | GenExpr) -> float: ...
@overload
def getSolVal(self, sol: Solution | None, expr: MatrixExpr) -> np.ndarray: ...
def getSols(self) -> list[Solution]: ...
Expand Down Expand Up @@ -1370,7 +1380,7 @@ class Model:
def getTransformedVar(self, var: Variable) -> Variable: ...
def getTreesizeEstimation(self) -> float: ...
@overload
def getVal(self, expr: Union[Expr, GenExpr]) -> float: ...
def getVal(self, expr: Expr | GenExpr) -> float: ...
@overload
def getVal(self, expr: MatrixExpr) -> np.ndarray: ...
def getValsLinear(self, cons: Constraint) -> dict[str, float]: ...
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