Skip to content

Commit 212f89b

Browse files
Merge branch 'master' into release-automation
2 parents 6f59439 + ec0568f commit 212f89b

7 files changed

Lines changed: 129 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
## Unreleased
44
### Added
5-
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`)
5+
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`, `np.negative`)
6+
- `Expr` and `GenExpr` support NumPy binary functions (`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.true_divide`, `np.power`, `np.less_equal`, `np.greater_equal`, `np.equal`)
67
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
78
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
9+
- Added `isReoptEnabled()` and raising error if not enabled upon calling `reoptSolve()`
810
### Fixed
911
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
1012
- Used `getIndex()` instead of `ptr()` for sorting nonlinear expression terms to avoid nondeterministic behavior

src/pyscipopt/expr.pxi

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,32 @@ cdef class ExprLike:
246246
)
247247

248248
if method == "__call__":
249-
if ufunc is np.absolute:
249+
if arrays := [a for a in args if type(a) is np.ndarray]:
250+
if any(a.dtype.kind not in "fiub" for a in arrays):
251+
return NotImplemented
252+
# If the np.ndarray is of numeric type, all arguments are converted to
253+
# MatrixExpr or MatrixGenExpr and then the ufunc is applied.
254+
return ufunc(*[_ensure_matrix(a) for a in args], **kwargs)
255+
256+
if ufunc is np.add:
257+
return args[0] + args[1]
258+
elif ufunc is np.subtract:
259+
return args[0] - args[1]
260+
elif ufunc is np.multiply:
261+
return args[0] * args[1]
262+
elif ufunc in {np.divide, np.true_divide}:
263+
return args[0] / args[1]
264+
elif ufunc is np.power:
265+
return args[0] ** args[1]
266+
elif ufunc is np.negative:
267+
return -args[0]
268+
elif ufunc is np.less_equal:
269+
return args[0] <= args[1]
270+
elif ufunc is np.greater_equal:
271+
return args[0] >= args[1]
272+
elif ufunc is np.equal:
273+
return args[0] == args[1]
274+
elif ufunc is np.absolute:
250275
return args[0].__abs__()
251276
elif ufunc is np.exp:
252277
return args[0].exp()
@@ -1031,6 +1056,12 @@ cdef inline object _wrap_ufunc(object x, object ufunc):
10311056
return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res
10321057
return ufunc(_to_const(x))
10331058

1059+
cdef inline object _ensure_matrix(object arg):
1060+
if type(arg) is np.ndarray:
1061+
return arg.view(MatrixExpr)
1062+
matrix = MatrixExpr if isinstance(arg, Expr) else MatrixGenExpr
1063+
return np.array(arg, dtype=object).view(matrix)
1064+
10341065

10351066
def expr_to_nodes(expr):
10361067
'''transforms tree to an array of nodes. each node is an operator and the position of the

src/pyscipopt/scip.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,7 @@ cdef extern from "scip/scip.h":
15501550
SCIP_RETCODE SCIPfreeReoptSolve(SCIP* scip)
15511551
SCIP_RETCODE SCIPchgReoptObjective(SCIP* scip, SCIP_OBJSENSE objsense, SCIP_VAR** vars, SCIP_Real* coefs, int nvars)
15521552
SCIP_RETCODE SCIPenableReoptimization(SCIP* scip, SCIP_Bool enable)
1553+
SCIP_Bool SCIPisReoptEnabled(SCIP* scip)
15531554

15541555
BMS_BLKMEM* SCIPblkmem(SCIP* scip)
15551556

src/pyscipopt/scip.pxi

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3973,6 +3973,17 @@ cdef class Model:
39733973
"""
39743974
PY_SCIP_CALL(SCIPenableReoptimization(self._scip, enable))
39753975

3976+
def isReoptEnabled(self):
3977+
"""
3978+
Returns whether reoptimization is enabled.
3979+
3980+
Returns
3981+
-------
3982+
bool
3983+
3984+
"""
3985+
return SCIPisReoptEnabled(self._scip)
3986+
39763987
def lpiGetIterations(self):
39773988
"""
39783989
Get the iteration count of the last solved LP.
@@ -11950,6 +11961,10 @@ cdef class Model:
1195011961
def freeReoptSolve(self):
1195111962
"""Frees all solution process data and prepares for reoptimization."""
1195211963

11964+
if not SCIPisReoptEnabled(self._scip):
11965+
raise ValueError("freeReoptSolve requires reoptimization to be enabled. "
11966+
"Call enableReoptimization() before solving.")
11967+
1195311968
if self.getStage() not in [SCIP_STAGE_INIT,
1195411969
SCIP_STAGE_PROBLEM,
1195511970
SCIP_STAGE_TRANSFORMED,

src/pyscipopt/scip.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,7 @@ class Model:
14541454
def isObjChangedProbing(self) -> Incomplete: ...
14551455
def isObjIntegral(self) -> Incomplete: ...
14561456
def isPositive(self, val: Incomplete) -> Incomplete: ...
1457+
def isReoptEnabled(self) -> bool: ...
14571458
def isZero(self, value: Incomplete) -> Incomplete: ...
14581459
def lpiGetIterations(self) -> Incomplete: ...
14591460
def markDoNotAggrVar(self, var: Incomplete) -> Incomplete: ...

tests/test_expr.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def test_getVal_with_GenExpr():
222222
m.getVal(1 / z)
223223

224224

225-
def test_unary(model):
225+
def test_unary_ufunc(model):
226226
m, x, y, z = model
227227

228228
res = "abs(sum(0.0,prod(1.0,x)))"
@@ -276,6 +276,57 @@ def test_unary(model):
276276
# forbid modifying Variable/Expr/GenExpr in-place via out parameter
277277
np.sin(x, out=np.array([0]))
278278

279+
# test np.negative
280+
assert str(np.negative(x)) == "Expr({Term(x): -1.0})"
281+
282+
283+
def test_binary_ufunc(model):
284+
m, x, y, z = model
285+
286+
# test np.add
287+
assert str(np.add(x, 1)) == "Expr({Term(x): 1.0, Term(): 1.0})"
288+
assert str(np.add(1, x)) == "Expr({Term(x): 1.0, Term(): 1.0})"
289+
a = np.array([1])
290+
assert str(np.add(x, a)) == "[Expr({Term(x): 1.0, Term(): 1.0})]"
291+
assert str(np.add(a, x)) == "[Expr({Term(x): 1.0, Term(): 1.0})]"
292+
293+
# test np.subtract
294+
assert str(np.subtract(x, 1)) == "Expr({Term(x): 1.0, Term(): -1.0})"
295+
assert str(np.subtract(1, x)) == "Expr({Term(x): -1.0, Term(): 1.0})"
296+
assert str(np.subtract(x, a)) == "[Expr({Term(x): 1.0, Term(): -1.0})]"
297+
assert str(np.subtract(a, x)) == "[Expr({Term(x): -1.0, Term(): 1.0})]"
298+
299+
# test np.multiply
300+
a = np.array([2])
301+
assert str(np.multiply(x, 2)) == "Expr({Term(x): 2.0})"
302+
assert str(np.multiply(2, x)) == "Expr({Term(x): 2.0})"
303+
assert str(np.multiply(x, a)) == "[Expr({Term(x): 2.0})]"
304+
assert str(np.multiply(a, x)) == "[Expr({Term(x): 2.0})]"
305+
306+
# test np.divide
307+
assert str(np.divide(x, 2)) == "Expr({Term(x): 0.5})"
308+
assert str(np.divide(2, x)) == "prod(2.0,**(sum(0.0,prod(1.0,x)),-1))"
309+
assert str(np.divide(x, a)) == "[Expr({Term(x): 0.5})]"
310+
assert str(np.divide(a, x)) == "[prod(2.0,**(sum(0.0,prod(1.0,x)),-1))]"
311+
312+
# test np.power
313+
assert str(np.power(x, 2)) == "Expr({Term(x, x): 1.0})"
314+
assert str(np.power(2, x)) == "exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))"
315+
assert str(np.power(x, a)) == "[Expr({Term(x, x): 1.0})]"
316+
assert str(np.power(a, x)) == "[exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))]"
317+
318+
# test np.less_equal
319+
assert str(np.less_equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]"
320+
assert str(np.less_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, None)]"
321+
322+
# test np.equal
323+
assert str(np.equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, 2.0)]"
324+
assert str(np.equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, 2.0)]"
325+
326+
# test np.greater_equal
327+
assert str(np.greater_equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, None)]"
328+
assert str(np.greater_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]"
329+
279330

280331
def test_mul():
281332
m = Model()

tests/test_reopt.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ def test_reopt_many_variables_sparse_objective(self):
7373
self.assertAlmostEqual(m.getVal(vars[50]), 0.0)
7474
self.assertAlmostEqual(m.getVal(vars[99]), 0.0)
7575

76+
def test_freeReoptSolve_requires_reoptimization_enabled(self):
77+
"""freeReoptSolve must raise ValueError instead of crashing when
78+
reoptimization was not enabled (see issue #624)."""
79+
m = Model()
80+
m.hideOutput()
81+
self.assertFalse(m.isReoptEnabled())
82+
83+
x = m.addVar(name="x", vtype="I", ub=10)
84+
m.addCons(x >= 3)
85+
m.setObjective(x)
86+
m.optimize()
87+
88+
with self.assertRaises(ValueError):
89+
m.freeReoptSolve()
90+
91+
m2 = Model()
92+
m2.enableReoptimization()
93+
self.assertTrue(m2.isReoptEnabled())
94+
m2.hideOutput()
95+
y = m2.addVar(name="y", vtype="I", ub=10)
96+
m2.addCons(y >= 3)
97+
m2.setObjective(y)
98+
m2.optimize()
99+
m2.freeReoptSolve()
100+
76101
def test_reopt_zero_objective(self):
77102
"""Test reoptimization with zero objective (no variables, all coefficients zero)."""
78103
m = Model()

0 commit comments

Comments
 (0)