Skip to content

Commit 4f200bf

Browse files
Merge branch 'master' into expr/__mul__
2 parents ea65e91 + ffb41fb commit 4f200bf

9 files changed

Lines changed: 109 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
## Unreleased
44
### Added
5+
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
56
### Fixed
7+
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
8+
- Used getIndex() instead of ptr() for sorting nonlinear expression terms to avoid nondeterministic behavior
69
### Changed
710
- Speed up `constant * Expr` via C-level API
811
### Removed
12+
- Removed outdated warning about Make build system incompatibility
913

1014
## 6.1.0 - 2026.01.31
1115
### Added

INSTALL.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ Suite](https://www.scipopt.org/). Please, make sure that your SCIP installation
1818

1919
**Note that the latest PySCIPOpt version is usually only compatible with the latest major release of the SCIP Optimization Suite. See the table on the README.md page for details.**
2020

21-
If you install SCIP yourself and are not using the installer packages, you need to [install the
22-
SCIP Optimization Suite using CMake](https://www.scipopt.org/doc/html/md_INSTALL.php#CMAKE).
23-
The Makefile system is not compatible with PySCIPOpt!
24-
2521
If installing SCIP from source or using PyPI with a python and operating system that is not mentioned above, and SCIP is not installed in the global path,
2622
you need to specify the install location using the environment variable
2723
`SCIPOPTDIR`:

docs/build.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ To download SCIP please either use the pre-built SCIP Optimization Suite availab
4444
* - 3.2
4545
- 1.0
4646

47-
.. note:: If you install SCIP yourself and are not using the pre-built packages,
48-
you need to install the SCIP Optimization Suite using CMake.
49-
The Makefile system is not compatible with PySCIPOpt!
50-
5147
Download Source Code
5248
======================
5349

src/pyscipopt/expr.pxi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ cdef class Term:
108108
cdef Py_ssize_t hashval
109109

110110
def __init__(self, *vartuple: Variable):
111-
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr()))
111+
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.getIndex()))
112112
self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
113113
self.hashval = <Py_ssize_t>hash(self.ptrtuple)
114114

@@ -138,7 +138,7 @@ cdef class Term:
138138
while i < n1 and j < n2:
139139
var1 = <Variable>PyTuple_GET_ITEM(self.vartuple, i)
140140
var2 = <Variable>PyTuple_GET_ITEM(other.vartuple, j)
141-
if var1.ptr() <= var2.ptr():
141+
if var1.getIndex() <= var2.getIndex():
142142
vartuple[k] = var1
143143
i += 1
144144
else:

src/pyscipopt/scip.pxd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,11 @@ cdef extern from "blockmemshell/memory.h":
18221822
void BMScheckEmptyMemory()
18231823
long long BMSgetMemoryUsed()
18241824

1825+
cdef extern from "scip/scip_mem.h":
1826+
SCIP_Longint SCIPgetMemUsed(SCIP* scip)
1827+
SCIP_Longint SCIPgetMemTotal(SCIP* scip)
1828+
SCIP_Longint SCIPgetMemExternEstim(SCIP* scip)
1829+
18251830
cdef extern from "scip/scip_expr.h":
18261831
SCIP_RETCODE SCIPcreateExpr(SCIP* scip,
18271832
SCIP_EXPR** expr,

src/pyscipopt/scip.pxi

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11268,7 +11268,6 @@ cdef class Model:
1126811268
else:
1126911269
raise Warning("event handler not found")
1127011270

11271-
Py_INCREF(self)
1127211271
PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL))
1127311272

1127411273
def dropEvent(self, eventtype, Eventhdlr eventhdlr):
@@ -11288,7 +11287,6 @@ cdef class Model:
1128811287
else:
1128911288
raise Warning("event handler not found")
1129011289

11291-
Py_DECREF(self)
1129211290
PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1))
1129311291

1129411292
def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr):
@@ -11467,6 +11465,41 @@ cdef class Model:
1146711465

1146811466
locale.setlocale(locale.LC_NUMERIC,user_locale)
1146911467

11468+
def getMemUsed(self):
11469+
"""
11470+
Gets the total number of bytes used in block and buffer memory.
11471+
11472+
Returns
11473+
-------
11474+
int
11475+
11476+
"""
11477+
return SCIPgetMemUsed(self._scip)
11478+
11479+
def getMemTotal(self):
11480+
"""
11481+
Gets the total number of bytes in block and buffer memory
11482+
(i.e., total allocated, including unused).
11483+
11484+
Returns
11485+
-------
11486+
int
11487+
11488+
"""
11489+
return SCIPgetMemTotal(self._scip)
11490+
11491+
def getMemExternEstim(self):
11492+
"""
11493+
Gets the estimated number of bytes used by external software,
11494+
e.g., the LP solver.
11495+
11496+
Returns
11497+
-------
11498+
int
11499+
11500+
"""
11501+
return SCIPgetMemExternEstim(self._scip)
11502+
1147011503
def getNLPs(self):
1147111504
"""
1147211505
Gets total number of LPs solved so far.

src/pyscipopt/scip.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,9 @@ class Model:
11531153
def getNLPIterations(self) -> Incomplete: ...
11541154
def getNLPRows(self) -> Incomplete: ...
11551155
def getNLPs(self) -> Incomplete: ...
1156+
def getMemUsed(self) -> int: ...
1157+
def getMemTotal(self) -> int: ...
1158+
def getMemExternEstim(self) -> int: ...
11561159
def getNLeaves(self) -> Incomplete: ...
11571160
def getNLimSolsFound(self) -> Incomplete: ...
11581161
def getNNlRows(self) -> Incomplete: ...

tests/test_event.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import pytest, random
1+
import pytest, weakref, gc, random
22

33
from pyscipopt import Model, Eventhdlr, SCIP_RESULT, SCIP_EVENTTYPE, SCIP_PARAMSETTING, quicksum
44

@@ -189,4 +189,42 @@ def eventexec(self, event):
189189
m.includeEventhdlr(ev, "var_event", "event handler for var events")
190190

191191
with pytest.raises(Exception):
192-
m.optimize()
192+
m.optimize()
193+
194+
def test_catchEvent_does_not_leak_model():
195+
"""catchEvent should not artificially increment the Model's reference count.
196+
197+
Previously, catchEvent called Py_INCREF(self) on the Model, and dropEvent
198+
called Py_DECREF(self). In practice these calls are often unbalanced — event
199+
handlers commonly catch events without a matching drop (e.g. calling
200+
catchEvent in eventinit but omitting dropEvent in eventexit). Each unmatched
201+
catchEvent permanently inflated the Model's refcount, preventing garbage
202+
collection.
203+
"""
204+
205+
class SimpleEvent(Eventhdlr):
206+
def eventinit(self):
207+
self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self)
208+
209+
def eventexit(self):
210+
pass # intentionally no dropEvent, which is bad practice
211+
212+
def eventexec(self, event):
213+
pass
214+
215+
m = Model()
216+
m.hideOutput()
217+
ev = SimpleEvent()
218+
m.includeEventhdlr(ev, "simple", "test event handler")
219+
m.addVar("x", obj=1, vtype="I")
220+
m.optimize()
221+
222+
ref = weakref.ref(m)
223+
224+
del ev
225+
gc.collect()
226+
assert ref() is not None, "Model was garbage collected — event handler absorbed a reference"
227+
228+
del m
229+
gc.collect()
230+
assert ref() is None, "Model was not garbage collected — catchEvent likely leaked a reference"

tests/test_model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,3 +642,23 @@ def test_getSolVal():
642642
m.getVal("not_a_var")
643643
with pytest.raises(TypeError):
644644
m.getSolVal(sol, "not_a_var")
645+
646+
647+
def test_memory_methods():
648+
m = Model()
649+
650+
# Memory values should be non-negative even on an empty model
651+
assert m.getMemUsed() >= 0
652+
assert m.getMemTotal() >= 0
653+
assert m.getMemExternEstim() >= 0
654+
655+
# Total allocated should be at least as much as actively used
656+
assert m.getMemTotal() >= m.getMemUsed()
657+
658+
# After adding variables and solving, memory usage should increase
659+
x = m.addVar("x", vtype="C", obj=1.0)
660+
m.addCons(x >= 0)
661+
m.optimize()
662+
663+
assert m.getMemUsed() > 0
664+
assert m.getMemTotal() > 0

0 commit comments

Comments
 (0)