diff --git a/CHANGELOG.md b/CHANGELOG.md index 0113dae47..e5984a320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 3a126f4dc..64bc5dc16 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index cc2722af9..7ab979f91 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -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. @@ -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. @@ -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. @@ -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). @@ -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. @@ -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. @@ -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. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 455bc9b36..41aa032ab 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -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 @@ -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]: ... @@ -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: ... @@ -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: ... @@ -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: ... @@ -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]: ... @@ -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]: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index fef2f4027..8f16fa399 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -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 @@ -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() @@ -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