From 05aabf6ca55ae13eb91849957d3019819d111fc6 Mon Sep 17 00:00:00 2001 From: nu Date: Thu, 23 Jul 2026 19:45:41 +0200 Subject: [PATCH 01/11] add getMaxTotalDepth method --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 9 +++++++++ 5 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0113dae47..ef1d5d669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added methods: `getMaxTotalDepth` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 3a126f4dc..f85bd35e4 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1486,6 +1486,7 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetLowerbound(SCIP* scip) SCIP_Real SCIPgetCutoffbound(SCIP* scip) int SCIPgetMaxDepth(SCIP* scip) + int SCIPgetMaxTotalDepth(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..02391270b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3462,6 +3462,17 @@ 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 getPlungeDepth(self): """ Gets current plunging depth (successive selections of child/sibling nodes). diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 455bc9b36..638f6bcdc 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1240,6 +1240,7 @@ class Model: def getLowerbound(self) -> float: ... def getMajorVersion(self) -> int: ... def getMaxDepth(self) -> int: ... + def getMaxTotalDepth(self) -> int: ... def getMinorVersion(self) -> int: ... def getNBestSolsFound(self) -> int: ... def getNBinVars(self) -> int: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index fef2f4027..422517e61 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -47,3 +47,12 @@ def test_addNNodes(optimized_model): 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 From 0c279a9099400401c1f181e10cdcd507e21fe5f9 Mon Sep 17 00:00:00 2001 From: nu Date: Thu, 23 Jul 2026 22:02:09 +0200 Subject: [PATCH 02/11] add getNBacktracks and getFocusNode method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 2 ++ src/pyscipopt/scip.pxi | 23 +++++++++++++++++++++++ src/pyscipopt/scip.pyi | 2 ++ tests/test_statistics.py | 7 +++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1d5d669..e793f6a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getMaxTotalDepth` with tests +- Added methods: `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index f85bd35e4..77a65dea4 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -749,6 +749,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) @@ -1487,6 +1488,7 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetCutoffbound(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 02391270b..8d0085827 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3417,6 +3417,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. @@ -3473,6 +3485,17 @@ cdef class Model: """ 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). diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 638f6bcdc..19eaf0c0e 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1205,6 +1205,7 @@ 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: ... @@ -1241,6 +1242,7 @@ class Model: 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: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 422517e61..37a7f4408 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -56,3 +56,10 @@ def test_getMaxTotalDepth(optimized_model): 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 From eeec10179418f11c055461cde1723006bc553e47 Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 11:57:27 +0200 Subject: [PATCH 03/11] add getAvgLowerbound method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e793f6a66..cb687f82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()` with tests +- Added methods: `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 77a65dea4..84406a508 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1484,6 +1484,7 @@ cdef extern from "scip/scip.h": SCIP_Longint SCIPgetNLPs(SCIP* scip) SCIP_Longint SCIPgetNLPIterations(SCIP* scip) int SCIPgetNSepaRounds(SCIP* scip) + SCIP_Real SCIPgetAvgLowerbound(SCIP* scip) SCIP_Real SCIPgetLowerbound(SCIP* scip) SCIP_Real SCIPgetCutoffbound(SCIP* scip) int SCIPgetMaxDepth(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 8d0085827..9f0a2d5b0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3507,6 +3507,17 @@ 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 getLowerbound(self): """ Gets global lower (dual) bound of the transformed problem. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 19eaf0c0e..da2ff0001 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1196,6 +1196,7 @@ class Model: list[list[float]], dict[str, dict[str, int]], ]: ... + def getAvgLowerbound(self) -> float: ... def getBranchScoreMultiple(self, var: Variable, gains: list[float]) -> float: ... def getCapacityKnapsack(self, cons: Constraint) -> int: ... def getChildren(self) -> list[Node]: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 37a7f4408..6298f2cf6 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -2,6 +2,7 @@ from helpers.utils import random_mip_1 from json import load import pytest +import numpy as np @pytest.fixture @@ -63,3 +64,17 @@ def test_getNBacktracks(optimized_model): 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 np.isclose(manual_avg_lowerbound, avg_lowerbound) From 84ad583f83899e34a308d89fdb4fd1554eef2ca0 Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 12:33:14 +0200 Subject: [PATCH 04/11] add getAvgDualbound method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 14 +++++++++++--- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb687f82b..969447cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests +- Added methods: `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 84406a508..a8308ba3c 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1485,6 +1485,7 @@ cdef extern from "scip/scip.h": 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 SCIPgetCutoffbound(SCIP* scip) int SCIPgetMaxDepth(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9f0a2d5b0..0ad454258 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3518,6 +3518,17 @@ cdef class Model: """ 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. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index da2ff0001..e8004def7 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1197,6 +1197,7 @@ class Model: 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]: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 6298f2cf6..e67197334 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -73,8 +73,16 @@ def test_getAvgLowerbound(optimized_model): 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()] + [node.getLowerbound() for node in open_nodes] + [optimized_model.getFocusNode().getLowerbound()] ) - + assert isinstance(avg_lowerbound, float) - assert np.isclose(manual_avg_lowerbound, avg_lowerbound) + 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) From b62bbc10076996fae4e6952319d398f0baa193f1 Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 12:35:32 +0200 Subject: [PATCH 05/11] add getDeterministicTime method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 7 +++++++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 969447cb3..2fb71db9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests +- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index a8308ba3c..a4c258f6e 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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 0ad454258..b988f7725 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3285,6 +3285,17 @@ 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 getNLPIterations(self): """ Returns the total number of LP iterations so far. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index e8004def7..92345faf3 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1213,6 +1213,7 @@ class Model: def getCutLPSolCutoffDistance(self, cut: Row, sol: Solution) -> float: ... def getCutoffbound(self) -> float: ... def getDepth(self) -> int: ... + def getDeterministicTime(self) -> float: ... def getDualMultiplier(self, cons: Constraint) -> float: ... def getDualSolVal( self, cons: Constraint, boundconstraint: bool = False diff --git a/tests/test_statistics.py b/tests/test_statistics.py index e67197334..5dc417020 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -86,3 +86,10 @@ def test_getAvgDualbound(optimized_model): 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 From 719b8f923ab59a70bb4aecaeb9a08663e2e74634 Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 12:54:47 +0200 Subject: [PATCH 06/11] add getFirstPrimalBound method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 8 +++++++- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb71db9c..762e90005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()` with tests +- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index a4c258f6e..ed8b3b41a 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -971,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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index b988f7725..5953ab457 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3296,6 +3296,17 @@ cdef class Model: """ 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. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 92345faf3..41bd2ab74 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1214,6 +1214,7 @@ class Model: def getCutoffbound(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 diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 5dc417020..682098b15 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -75,7 +75,7 @@ def test_getAvgLowerbound(optimized_model): 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) @@ -93,3 +93,9 @@ def test_getDeterministicTime(optimized_model): assert isinstance(det_time, float) assert det_time >= 0.0 + + +def test_getFirstPrimalBound(optimized_model): + first_primal = optimized_model.getFirstPrimalBound() + + assert isinstance(first_primal, float) From 98969dd5d860060b7ffa5b4a6f7a1525fb23a45d Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 13:19:55 +0200 Subject: [PATCH 07/11] add getLowerboundRoot method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 8 ++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762e90005..7b2e6265a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()` with tests +- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index ed8b3b41a..77625d3e8 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1489,6 +1489,7 @@ cdef extern from "scip/scip.h": 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) int SCIPgetMaxDepth(SCIP* scip) int SCIPgetMaxTotalDepth(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5953ab457..f7969ba5e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -11584,6 +11584,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 41bd2ab74..69406abc5 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1221,6 +1221,7 @@ class Model: ) -> 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: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 682098b15..108242cd6 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -99,3 +99,11 @@ def test_getFirstPrimalBound(optimized_model): first_primal = optimized_model.getFirstPrimalBound() assert isinstance(first_primal, float) + + +def test_getLowerboundRoot(optimized_model): + lowerbound_root = optimized_model.getLowerboundRoot() + lowerbound = optimized_model.getLowerbound() + + assert isinstance(lowerbound_root, float) + assert lowerbound_root <= lowerbound From cb2dbd9e2cb45c1608e27063ebf80801a717078f Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 14:37:02 +0200 Subject: [PATCH 08/11] add getUpperbound method --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 10 ++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 12 +++++++++++- 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2e6265a..d6b2cf93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()` with tests +- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()`, `getUpperbound()` with tests - Added methods: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()` with tests - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 77625d3e8..fc2c04ce8 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1491,6 +1491,7 @@ cdef extern from "scip/scip.h": 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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f7969ba5e..a70313890 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3573,6 +3573,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. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 69406abc5..8c618122d 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1212,6 +1212,7 @@ class Model: 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: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 108242cd6..1cb7a203b 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -95,10 +95,20 @@ def test_getDeterministicTime(optimized_model): 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): From 691f134fdebf8acf21099f50eb464eb7b4043e2a Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 14:47:21 +0200 Subject: [PATCH 09/11] add getNObjlimLeaves method --- CHANGELOG.md | 4 ++-- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 11 +++++++++++ src/pyscipopt/scip.pyi | 1 + tests/test_statistics.py | 6 ++++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b2cf93d..e5984a320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## Unreleased ### Added -- Added methods: `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()`, `getUpperbound()` with tests -- 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 fc2c04ce8..64bc5dc16 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1483,6 +1483,7 @@ 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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a70313890..7ab979f91 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3395,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. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 8c618122d..4e259ff67 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1261,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: ... diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 1cb7a203b..7059a8fc2 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -42,6 +42,12 @@ 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) From c68166004305c067684aedaf1d9dd3f34eb100e3 Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 21:11:28 +0200 Subject: [PATCH 10/11] change Union syntax to X | Y --- src/pyscipopt/scip.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 4e259ff67..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 @@ -1339,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]: ... @@ -1380,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]: ... From 7c739933d8654124640d0cd9250f8232d67ba6af Mon Sep 17 00:00:00 2001 From: nu Date: Sat, 25 Jul 2026 21:30:09 +0200 Subject: [PATCH 11/11] increase optimized_model's node_lim to get primal solutions --- tests/test_statistics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 7059a8fc2..8f16fa399 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -7,7 +7,7 @@ @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 @@ -22,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()