Skip to content

Commit a56aee7

Browse files
committed
Merge remote-tracking branch 'upstream/master' into Term/ptr
2 parents d8c98c0 + 75ccba9 commit a56aee7

12 files changed

Lines changed: 211 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
## Unreleased
44
### Added
5+
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
6+
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
57
### Fixed
8+
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
9+
- Used `getIndex()` instead of `ptr()` for sorting nonlinear expression terms to avoid nondeterministic behavior
610
### Changed
711
- Speed up `Term.__eq__` via the C-level API
812
### Removed
913
- Removed `Term.ptrtuple` to optimize `Term` memory usage
14+
- Removed outdated warning about Make build system incompatibility
1015

1116
## 6.1.0 - 2026.01.31
1217
### 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/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pyscipopt.scip import LP as LP
2929
from pyscipopt.scip import IISfinder as IISfinder
3030
from pyscipopt.scip import PY_SCIP_LPPARAM as SCIP_LPPARAM
31+
from pyscipopt.scip import PY_SCIP_BASESTAT as SCIP_BASESTAT
3132
from pyscipopt.scip import readStatistics as readStatistics
3233
from pyscipopt.scip import Expr as Expr
3334
from pyscipopt.scip import MatrixExpr as MatrixExpr

src/pyscipopt/expr.pxi

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

109109
def __init__(self, *vartuple: Variable):
110-
self.vartuple = tuple(sorted(vartuple, key=hash))
110+
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.getIndex()))
111111
self.hashval = <Py_ssize_t>hash(self.vartuple)
112112

113113
def __getitem__(self, idx):
@@ -153,7 +153,7 @@ cdef class Term:
153153
while i < n1 and j < n2:
154154
var1 = <Variable>PyTuple_GET_ITEM(self.vartuple, i)
155155
var2 = <Variable>PyTuple_GET_ITEM(other.vartuple, j)
156-
if hash(var1) <= hash(var2):
156+
if var1.ptr() <= var2.ptr():
157157
vartuple[k] = var1
158158
i += 1
159159
else:

src/pyscipopt/lp.pxi

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,66 @@ cdef class LP:
531531

532532
return binds
533533

534+
def getBase(self):
535+
"""Returns the basis status of columns and rows.
536+
537+
Status values are defined in SCIP_BASESTAT: LOWER, BASIC, UPPER, ZERO.
538+
539+
Returns
540+
-------
541+
tuple of (list of int, list of int)
542+
Column basis statuses and row basis statuses.
543+
544+
"""
545+
cdef int ncols = self.ncols()
546+
cdef int nrows = self.nrows()
547+
cdef int* c_cstat = <int*> malloc(ncols * sizeof(int))
548+
cdef int* c_rstat = <int*> malloc(nrows * sizeof(int))
549+
cdef int i
550+
551+
PY_SCIP_CALL(SCIPlpiGetBase(self.lpi, c_cstat, c_rstat))
552+
553+
cstat = [c_cstat[i] for i in range(ncols)]
554+
rstat = [c_rstat[i] for i in range(nrows)]
555+
556+
free(c_rstat)
557+
free(c_cstat)
558+
559+
return cstat, rstat
560+
561+
def setBase(self, cstat, rstat):
562+
"""Sets the basis status of columns and rows.
563+
564+
Status values are defined in SCIP_BASESTAT: LOWER, BASIC, UPPER, ZERO.
565+
566+
Parameters
567+
----------
568+
cstat : list of int
569+
Column basis statuses (length must equal ncols).
570+
rstat : list of int
571+
Row basis statuses (length must equal nrows).
572+
573+
"""
574+
cdef int ncols = self.ncols()
575+
cdef int nrows = self.nrows()
576+
if len(cstat) != ncols:
577+
raise ValueError(f"cstat has length {len(cstat)}, expected {ncols}")
578+
if len(rstat) != nrows:
579+
raise ValueError(f"rstat has length {len(rstat)}, expected {nrows}")
580+
cdef int* c_cstat = <int*> malloc(ncols * sizeof(int))
581+
cdef int* c_rstat = <int*> malloc(nrows * sizeof(int))
582+
cdef int i
583+
584+
for i in range(ncols):
585+
c_cstat[i] = cstat[i]
586+
for i in range(nrows):
587+
c_rstat[i] = rstat[i]
588+
589+
PY_SCIP_CALL(SCIPlpiSetBase(self.lpi, c_cstat, c_rstat))
590+
591+
free(c_rstat)
592+
free(c_cstat)
593+
534594
# Parameter Methods
535595

536596
def setIntParam(self, param, value):

src/pyscipopt/scip.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,8 @@ cdef extern from "scip/scip.h":
15321532
SCIP_RETCODE SCIPlpiGetPrimalRay(SCIP_LPI* lpi, SCIP_Real* ray)
15331533
SCIP_RETCODE SCIPlpiGetDualfarkas(SCIP_LPI* lpi, SCIP_Real* dualfarkas)
15341534
SCIP_RETCODE SCIPlpiGetBasisInd(SCIP_LPI* lpi, int* bind)
1535+
SCIP_RETCODE SCIPlpiGetBase(SCIP_LPI* lpi, int* cstat, int* rstat)
1536+
SCIP_RETCODE SCIPlpiSetBase(SCIP_LPI* lpi, const int* cstat, const int* rstat)
15351537
SCIP_RETCODE SCIPlpiGetRealSolQuality(SCIP_LPI* lpi, SCIP_LPSOLQUALITY qualityindicator, SCIP_Real* quality)
15361538
SCIP_RETCODE SCIPlpiGetIntpar(SCIP_LPI* lpi, SCIP_LPPARAM type, int* ival)
15371539
SCIP_RETCODE SCIPlpiGetRealpar(SCIP_LPI* lpi, SCIP_LPPARAM type, SCIP_Real* dval)
@@ -1822,6 +1824,11 @@ cdef extern from "blockmemshell/memory.h":
18221824
void BMScheckEmptyMemory()
18231825
long long BMSgetMemoryUsed()
18241826

1827+
cdef extern from "scip/scip_mem.h":
1828+
SCIP_Longint SCIPgetMemUsed(SCIP* scip)
1829+
SCIP_Longint SCIPgetMemTotal(SCIP* scip)
1830+
SCIP_Longint SCIPgetMemExternEstim(SCIP* scip)
1831+
18251832
cdef extern from "scip/scip_expr.h":
18261833
SCIP_RETCODE SCIPcreateExpr(SCIP* scip,
18271834
SCIP_EXPR** expr,

src/pyscipopt/scip.pxi

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ cdef class PY_SCIP_LPPARAM:
118118
POLISHING = SCIP_LPPAR_POLISHING
119119
REFACTOR = SCIP_LPPAR_REFACTOR
120120

121+
cdef class PY_SCIP_BASESTAT:
122+
LOWER = SCIP_BASESTAT_LOWER
123+
BASIC = SCIP_BASESTAT_BASIC
124+
UPPER = SCIP_BASESTAT_UPPER
125+
ZERO = SCIP_BASESTAT_ZERO
126+
121127
cdef class PY_SCIP_PARAMEMPHASIS:
122128
DEFAULT = SCIP_PARAMEMPHASIS_DEFAULT
123129
CPSOLVER = SCIP_PARAMEMPHASIS_CPSOLVER
@@ -11273,7 +11279,6 @@ cdef class Model:
1127311279
else:
1127411280
raise Warning("event handler not found")
1127511281

11276-
Py_INCREF(self)
1127711282
PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL))
1127811283

1127911284
def dropEvent(self, eventtype, Eventhdlr eventhdlr):
@@ -11293,7 +11298,6 @@ cdef class Model:
1129311298
else:
1129411299
raise Warning("event handler not found")
1129511300

11296-
Py_DECREF(self)
1129711301
PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1))
1129811302

1129911303
def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr):
@@ -11472,6 +11476,41 @@ cdef class Model:
1147211476

1147311477
locale.setlocale(locale.LC_NUMERIC,user_locale)
1147411478

11479+
def getMemUsed(self):
11480+
"""
11481+
Gets the total number of bytes used in block and buffer memory.
11482+
11483+
Returns
11484+
-------
11485+
int
11486+
11487+
"""
11488+
return SCIPgetMemUsed(self._scip)
11489+
11490+
def getMemTotal(self):
11491+
"""
11492+
Gets the total number of bytes in block and buffer memory
11493+
(i.e., total allocated, including unused).
11494+
11495+
Returns
11496+
-------
11497+
int
11498+
11499+
"""
11500+
return SCIPgetMemTotal(self._scip)
11501+
11502+
def getMemExternEstim(self):
11503+
"""
11504+
Gets the estimated number of bytes used by external software,
11505+
e.g., the LP solver.
11506+
11507+
Returns
11508+
-------
11509+
int
11510+
11511+
"""
11512+
return SCIPgetMemExternEstim(self._scip)
11513+
1147511514
def getNLPs(self):
1147611515
"""
1147711516
Gets total number of LPs solved so far.

src/pyscipopt/scip.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ class LP:
467467
def delCols(self, firstcol: Incomplete, lastcol: Incomplete) -> Incomplete: ...
468468
def delRows(self, firstrow: Incomplete, lastrow: Incomplete) -> Incomplete: ...
469469
def getActivity(self) -> Incomplete: ...
470+
def getBase(self) -> Incomplete: ...
470471
def getBasisInds(self) -> Incomplete: ...
471472
def getBounds(
472473
self, firstcol: Incomplete = ..., lastcol: Incomplete = ...
@@ -491,6 +492,7 @@ class LP:
491492
def ncols(self) -> Incomplete: ...
492493
def nrows(self) -> Incomplete: ...
493494
def readLP(self, filename: Incomplete) -> Incomplete: ...
495+
def setBase(self, cstat: Incomplete, rstat: Incomplete) -> Incomplete: ...
494496
def setIntParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
495497
def setRealParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
496498
def solve(self, dual: Incomplete = ...) -> Incomplete: ...
@@ -1153,6 +1155,9 @@ class Model:
11531155
def getNLPIterations(self) -> Incomplete: ...
11541156
def getNLPRows(self) -> Incomplete: ...
11551157
def getNLPs(self) -> Incomplete: ...
1158+
def getMemUsed(self) -> int: ...
1159+
def getMemTotal(self) -> int: ...
1160+
def getMemExternEstim(self) -> int: ...
11561161
def getNLeaves(self) -> Incomplete: ...
11571162
def getNLimSolsFound(self) -> Incomplete: ...
11581163
def getNNlRows(self) -> Incomplete: ...
@@ -1798,6 +1803,13 @@ class PY_SCIP_LOCKTYPE:
17981803
MODEL: ClassVar[int] = ...
17991804
def __init__(self) -> None: ...
18001805

1806+
class PY_SCIP_BASESTAT:
1807+
LOWER: ClassVar[int] = ...
1808+
BASIC: ClassVar[int] = ...
1809+
UPPER: ClassVar[int] = ...
1810+
ZERO: ClassVar[int] = ...
1811+
def __init__(self) -> None: ...
1812+
18011813
class PY_SCIP_LPPARAM:
18021814
BARRIERCONVTOL: ClassVar[int] = ...
18031815
CONDITIONLIMIT: ClassVar[int] = ...

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"

0 commit comments

Comments
 (0)