Skip to content

Commit 3c4c30f

Browse files
Merge branch 'master' into expr/unary
2 parents 92851c8 + 8b1d1f5 commit 3c4c30f

9 files changed

Lines changed: 117 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
## Unreleased
44
### Added
55
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`)
6+
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
67
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
78
### Fixed
89
- 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
10+
- Used `getIndex()` instead of `ptr()` for sorting nonlinear expression terms to avoid nondeterministic behavior
1011
### Changed
12+
- Speed up `constant * Expr` via C-level API
1113
### Removed
1214
- Removed outdated warning about Make build system incompatibility
1315

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: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,22 +337,24 @@ cdef class Expr(ExprLike):
337337
cdef PyObject *v2_ptr = NULL
338338
cdef PyObject *old_v_ptr = NULL
339339
cdef Term child
340-
cdef double prod_v
340+
cdef double coef
341341

342342
if _is_number(other):
343-
f = float(other)
344-
return Expr({v:f*c for v,c in self.terms.items()})
343+
coef = float(other)
344+
while PyDict_Next(self.terms, &pos1, &k1_ptr, &v1_ptr):
345+
res[<Term>k1_ptr] = <double>(<object>v1_ptr) * coef
346+
return Expr(res)
345347

346348
elif isinstance(other, Expr):
347349
while PyDict_Next(self.terms, &pos1, &k1_ptr, &v1_ptr):
348350
pos2 = <Py_ssize_t>0
349351
while PyDict_Next(other.terms, &pos2, &k2_ptr, &v2_ptr):
350352
child = (<Term>k1_ptr) * (<Term>k2_ptr)
351-
prod_v = (<double>(<object>v1_ptr)) * (<double>(<object>v2_ptr))
353+
coef = (<double>(<object>v1_ptr)) * (<double>(<object>v2_ptr))
352354
if (old_v_ptr := PyDict_GetItem(res, child)) != NULL:
353-
res[child] = <double>(<object>old_v_ptr) + prod_v
355+
res[child] = <double>(<object>old_v_ptr) + coef
354356
else:
355-
res[child] = prod_v
357+
res[child] = coef
356358
return Expr(res)
357359

358360
elif isinstance(other, GenExpr):

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: 2 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)

src/pyscipopt/scip.pxi

Lines changed: 6 additions & 0 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

src/pyscipopt/scip.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ class LP:
481481
def delCols(self, firstcol: Incomplete, lastcol: Incomplete) -> Incomplete: ...
482482
def delRows(self, firstrow: Incomplete, lastrow: Incomplete) -> Incomplete: ...
483483
def getActivity(self) -> Incomplete: ...
484+
def getBase(self) -> Incomplete: ...
484485
def getBasisInds(self) -> Incomplete: ...
485486
def getBounds(
486487
self, firstcol: Incomplete = ..., lastcol: Incomplete = ...
@@ -505,6 +506,7 @@ class LP:
505506
def ncols(self) -> Incomplete: ...
506507
def nrows(self) -> Incomplete: ...
507508
def readLP(self, filename: Incomplete) -> Incomplete: ...
509+
def setBase(self, cstat: Incomplete, rstat: Incomplete) -> Incomplete: ...
508510
def setIntParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
509511
def setRealParam(self, param: Incomplete, value: Incomplete) -> Incomplete: ...
510512
def solve(self, dual: Incomplete = ...) -> Incomplete: ...
@@ -1815,6 +1817,13 @@ class PY_SCIP_LOCKTYPE:
18151817
MODEL: ClassVar[int] = ...
18161818
def __init__(self) -> None: ...
18171819

1820+
class PY_SCIP_BASESTAT:
1821+
LOWER: ClassVar[int] = ...
1822+
BASIC: ClassVar[int] = ...
1823+
UPPER: ClassVar[int] = ...
1824+
ZERO: ClassVar[int] = ...
1825+
def __init__(self) -> None: ...
1826+
18181827
class PY_SCIP_LPPARAM:
18191828
BARRIERCONVTOL: ClassVar[int] = ...
18201829
CONDITIONLIMIT: ClassVar[int] = ...

tests/test_expr.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ def test_mul():
278278
x = m.addVar(name="x")
279279
y = m.addVar(name="y")
280280

281+
# test Expr * number
282+
assert str((x + y) * 2.0) == "Expr({Term(x): 2.0, Term(y): 2.0})"
283+
assert str(2.0 * (x + y)) == "Expr({Term(x): 2.0, Term(y): 2.0})"
284+
285+
# test Expr * Expr
281286
assert str(Expr({CONST: 1.0}) * x) == "Expr({Term(x): 1.0})"
282287
assert str(y * Expr({CONST: -1.0})) == "Expr({Term(y): -1.0})"
283288
assert str((x - x) * y) == "Expr({Term(x, y): 0.0})"

tests/test_lp.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pyscipopt import LP
22
from pyscipopt import SCIP_LPPARAM
3+
from pyscipopt import SCIP_BASESTAT
34

45
def test_lp():
56
# create LP instance, minimizing by default
@@ -89,3 +90,25 @@ def test_lp():
8990

9091
assert round(myLP.getObjVal() == solval)
9192
assert round(5.0 == solval)
93+
94+
# test basis get/set
95+
binds = myLP.getBasisInds()
96+
assert len(binds) == myLP.nrows()
97+
98+
cstat, rstat = myLP.getBase()
99+
assert len(cstat) == myLP.ncols()
100+
assert len(rstat) == myLP.nrows()
101+
assert all(s in (SCIP_BASESTAT.LOWER, SCIP_BASESTAT.BASIC,
102+
SCIP_BASESTAT.UPPER, SCIP_BASESTAT.ZERO) for s in cstat)
103+
assert all(s in (SCIP_BASESTAT.LOWER, SCIP_BASESTAT.BASIC,
104+
SCIP_BASESTAT.UPPER) for s in rstat)
105+
106+
# set the same basis back and re-solve
107+
myLP.setBase(cstat, rstat)
108+
solval2 = myLP.solve()
109+
assert round(solval2, 10) == round(solval, 10)
110+
111+
# verify basis is preserved after set
112+
cstat2, rstat2 = myLP.getBase()
113+
assert cstat2 == cstat
114+
assert rstat2 == rstat

0 commit comments

Comments
 (0)