Skip to content

Commit 51a2183

Browse files
committed
Merge remote-tracking branch 'upstream/master' into expr/notimplemented
2 parents 9c1cc6c + 75ccba9 commit 51a2183

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,10 +2,15 @@
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
- Return NotImplemented for `Expr` and `GenExpr` operators, if they can't handle input types in the calculation
812
### Removed
13+
- Removed outdated warning about Make build system incompatibility
914

1015
## 6.1.0 - 2026.01.31
1116
### 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
@@ -89,7 +89,7 @@ cdef class Term:
8989
cdef Py_ssize_t hashval
9090

9191
def __init__(self, *vartuple: Variable):
92-
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr()))
92+
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.getIndex()))
9393
self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
9494
self.hashval = <Py_ssize_t>hash(self.ptrtuple)
9595

@@ -119,7 +119,7 @@ cdef class Term:
119119
while i < n1 and j < n2:
120120
var1 = <Variable>PyTuple_GET_ITEM(self.vartuple, i)
121121
var2 = <Variable>PyTuple_GET_ITEM(other.vartuple, j)
122-
if var1.ptr() <= var2.ptr():
122+
if var1.getIndex() <= var2.getIndex():
123123
vartuple[k] = var1
124124
i += 1
125125
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
@@ -11267,7 +11273,6 @@ cdef class Model:
1126711273
else:
1126811274
raise Warning("event handler not found")
1126911275

11270-
Py_INCREF(self)
1127111276
PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL))
1127211277

1127311278
def dropEvent(self, eventtype, Eventhdlr eventhdlr):
@@ -11287,7 +11292,6 @@ cdef class Model:
1128711292
else:
1128811293
raise Warning("event handler not found")
1128911294

11290-
Py_DECREF(self)
1129111295
PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1))
1129211296

1129311297
def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr):
@@ -11466,6 +11470,41 @@ cdef class Model:
1146611470

1146711471
locale.setlocale(locale.LC_NUMERIC,user_locale)
1146811472

11473+
def getMemUsed(self):
11474+
"""
11475+
Gets the total number of bytes used in block and buffer memory.
11476+
11477+
Returns
11478+
-------
11479+
int
11480+
11481+
"""
11482+
return SCIPgetMemUsed(self._scip)
11483+
11484+
def getMemTotal(self):
11485+
"""
11486+
Gets the total number of bytes in block and buffer memory
11487+
(i.e., total allocated, including unused).
11488+
11489+
Returns
11490+
-------
11491+
int
11492+
11493+
"""
11494+
return SCIPgetMemTotal(self._scip)
11495+
11496+
def getMemExternEstim(self):
11497+
"""
11498+
Gets the estimated number of bytes used by external software,
11499+
e.g., the LP solver.
11500+
11501+
Returns
11502+
-------
11503+
int
11504+
11505+
"""
11506+
return SCIPgetMemExternEstim(self._scip)
11507+
1146911508
def getNLPs(self):
1147011509
"""
1147111510
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
@@ -466,6 +466,7 @@ class LP:
466466
def delCols(self, firstcol: Incomplete, lastcol: Incomplete) -> Incomplete: ...
467467
def delRows(self, firstrow: Incomplete, lastrow: Incomplete) -> Incomplete: ...
468468
def getActivity(self) -> Incomplete: ...
469+
def getBase(self) -> Incomplete: ...
469470
def getBasisInds(self) -> Incomplete: ...
470471
def getBounds(
471472
self, firstcol: Incomplete = ..., lastcol: Incomplete = ...
@@ -490,6 +491,7 @@ class LP:
490491
def ncols(self) -> Incomplete: ...
491492
def nrows(self) -> Incomplete: ...
492493
def readLP(self, filename: Incomplete) -> Incomplete: ...
494+
def setBase(self, cstat: Incomplete, rstat: Incomplete) -> Incomplete: ...
493495
def setIntParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
494496
def setRealParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
495497
def solve(self, dual: Incomplete = ...) -> Incomplete: ...
@@ -1152,6 +1154,9 @@ class Model:
11521154
def getNLPIterations(self) -> Incomplete: ...
11531155
def getNLPRows(self) -> Incomplete: ...
11541156
def getNLPs(self) -> Incomplete: ...
1157+
def getMemUsed(self) -> int: ...
1158+
def getMemTotal(self) -> int: ...
1159+
def getMemExternEstim(self) -> int: ...
11551160
def getNLeaves(self) -> Incomplete: ...
11561161
def getNLimSolsFound(self) -> Incomplete: ...
11571162
def getNNlRows(self) -> Incomplete: ...
@@ -1797,6 +1802,13 @@ class PY_SCIP_LOCKTYPE:
17971802
MODEL: ClassVar[int] = ...
17981803
def __init__(self) -> None: ...
17991804

1805+
class PY_SCIP_BASESTAT:
1806+
LOWER: ClassVar[int] = ...
1807+
BASIC: ClassVar[int] = ...
1808+
UPPER: ClassVar[int] = ...
1809+
ZERO: ClassVar[int] = ...
1810+
def __init__(self) -> None: ...
1811+
18001812
class PY_SCIP_LPPARAM:
18011813
BARRIERCONVTOL: ClassVar[int] = ...
18021814
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)