Skip to content

Commit 0d4a6c2

Browse files
committed
Merge branch 'master' into expr/notimplemented
2 parents 4d84056 + 08d5c05 commit 0d4a6c2

9 files changed

Lines changed: 191 additions & 81 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,44 @@
22

33
## Unreleased
44
### Added
5+
### Fixed
6+
### Changed
7+
- Return NotImplemented for `Expr` and `GenExpr` operators, if they can't handle input types in the calculation
8+
### Removed
9+
10+
## 6.1.0 - 2026.01.31
11+
### Added
12+
- Support for SCIP 10.0.1
513
- Added automated script for generating type stubs
614
- Include parameter names in type stubs
7-
- Added pre-commit hook for automatic stub regeneration (see .pre-commit-config.yaml)
8-
- Wrapped isObjIntegral() and test
9-
- Added structured_optimization_trace recipe for structured optimization progress tracking
10-
- Added methods: getPrimalDualIntegral()
15+
- Added pre-commit hook for automatic stub regeneration (see `.pre-commit-config.yaml`)
16+
- Wrapped `isObjIntegral()` and test
17+
- Added `structured_optimization_trace` recipe for structured optimization progress tracking
18+
- Added methods: `getPrimalDualIntegral()`
19+
- `getSolVal()` supports `MatrixExpr` now
1120
### Fixed
12-
- getBestSol() now returns None for infeasible problems instead of a Solution with NULL pointer
21+
- `getBestSol()` now returns `None` for infeasible problems instead of a `Solution` with `NULL` pointer
1322
- all fundamental callbacks now raise an error if not implemented
14-
- Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr.
15-
- Updated IIS result in PyiisfinderExec()
16-
- Model.getVal now supports GenExpr type
17-
- Fixed lotsizing_lazy example
18-
- Fixed incorrect getVal() result when _bestSol.sol was outdated
19-
- Fixed segmentation fault when using Variable or Constraint objects after freeTransform() or Model destruction
23+
- Fixed the type of `MatrixExpr.sum(axis=...)` result from `MatrixVariable` to `MatrixExpr`.
24+
- Updated IIS result in `PyiisfinderExec()`
25+
- `Model.getVal` now supports `GenExpr` type
26+
- Fixed `lotsizing_lazy` example
27+
- Fixed incorrect `getVal()` result when `_bestSol.sol` was outdated
28+
- Fixed segmentation fault when using `Variable` or `Constraint` objects after `freeTransform()` or `Model` destruction
29+
- `getTermsQuadratic()` now correctly returns all linear terms
2030
### Changed
21-
- changed default value of enablepricing flag to True
22-
- Speed up MatrixExpr.sum(axis=...) via quicksum
23-
- Speed up MatrixExpr.add.reduce via quicksum
24-
- Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr
25-
- Speed up Expr * Expr via C-level API and Term * Term
26-
- Speed up Term * Term via a $O(n)$ sort algorithm instead of Python $O(n\log(n))$ sorted function. `Term.__mul__` requires that Term.vartuple is sorted.
27-
- Rename from `Term.__add__` to `Term.__mul__`, due to this method only working with Expr * Expr.
28-
- MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs
29-
- Set `__array_priority__` for MatrixExpr and MatrixExprCons
30-
- changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint
31+
- changed default value of `enablepricing` flag to `True`
32+
- Speed up `MatrixExpr.sum(axis=...)` via `quicksum`
33+
- Speed up `MatrixExpr.add.reduce` via `quicksum`
34+
- Speed up `np.ndarray(..., dtype=np.float64) @ MatrixExpr`
35+
- Speed up `Expr * Expr` via C-level API and `Term * Term`
36+
- Speed up `Term * Term` via a $O(n)$ sort algorithm instead of Python $O(n\log(n))$ sorted function. `Term.__mul__` requires that `Term.vartuple` is sorted.
37+
- Rename from `Term.__add__` to `Term.__mul__`, due to this method only working with `Expr * Expr`.
38+
- `MatrixExpr` and `MatrixExprCons` use `__array_ufunc__` protocol to control all `numpy.ufunc` inputs and outputs
39+
- Set `__array_priority__` for `MatrixExpr` and `MatrixExprCons`
40+
- changed `addConsNode()` and `addConsLocal()` to mirror `addCons()` and accept `ExprCons` instead of `Constraint`
3141
- Improved `chgReoptObjective()` performance
32-
- Return NotImplemented for Expr and GenExpr operators, if they can't handle input types in the calculation
33-
- Return itself for abs to UnaryExpr(Operator.fabs)
42+
- Return itself for `abs` to `UnaryExpr(Operator.fabs)`
3443
### Removed
3544

3645
## 6.0.0 - 2025.11.28
@@ -115,7 +124,7 @@
115124
- Stopped tests from running in draft PRs
116125
### Removed
117126

118-
## 5.4.1 - 2024.02.24
127+
## 5.4.1 - 2025.02.24
119128
### Added
120129
- Added option to get Lhs, Rhs of nonlinear constraints
121130
- Added cutoffNode and test

docs/build.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ To download SCIP please either use the pre-built SCIP Optimization Suite availab
2121

2222
* - SCIP
2323
- PySCIPOpt
24+
* - 10.0.1
25+
- 6.1
2426
* - 10.0.0
2527
- 6.0
2628
* - 9.2

pyproject.toml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,15 @@ authors = [
1111
dependencies = ['numpy >=1.16.0']
1212
requires-python = ">=3.8"
1313
readme = "README.md"
14-
license = {text = "MIT License"}
1514
classifiers = [
1615
"Development Status :: 4 - Beta",
1716
"Intended Audience :: Education",
1817
"Intended Audience :: Science/Research",
19-
"License :: OSI Approved :: MIT License",
2018
"Programming Language :: Cython",
2119
"Programming Language :: Python :: 3",
2220
"Topic :: Scientific/Engineering :: Mathematics",
2321
]
24-
dynamic = ["version"]
22+
dynamic = ["version", "license"]
2523

2624
[project.urls]
2725
Homepage = "https://github.com/SCIP-Interfaces/PySCIPOpt"
@@ -51,9 +49,9 @@ AARCH=$(uname -m)
5149
echo "------"
5250
echo $AARCH
5351
if [[ $AARCH == "aarch64" ]]; then
54-
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.10.0/libscip-linux-arm.zip -O scip.zip
52+
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.11.0/libscip-linux-arm.zip -O scip.zip
5553
else
56-
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.10.0/libscip-linux.zip -O scip.zip
54+
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.11.0/libscip-linux.zip -O scip.zip
5755
fi
5856
unzip scip.zip
5957
mv scip_install scip
@@ -67,10 +65,10 @@ before-all = '''
6765
#!/bin/bash
6866
brew install wget zlib gcc
6967
if [[ $CIBW_ARCHS == *"arm"* ]]; then
70-
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.10.0/libscip-macos-arm.zip -O scip.zip
68+
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.11.0/libscip-macos-arm.zip -O scip.zip
7169
export MACOSX_DEPLOYMENT_TARGET=14.0
7270
else
73-
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.10.0/libscip-macos-intel.zip -O scip.zip
71+
wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.11.0/libscip-macos-intel.zip -O scip.zip
7472
export MACOSX_DEPLOYMENT_TARGET=14.0
7573
fi
7674
unzip scip.zip
@@ -96,7 +94,7 @@ repair-wheel-command = '''
9694
skip="pp* cp36* cp37*"
9795
before-all = [
9896
"choco install 7zip wget",
99-
"wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.10.0/libscip-windows.zip -O scip.zip",
97+
"wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.11.0/libscip-windows.zip -O scip.zip",
10098
"\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"",
10199
"mv .\\scip-test\\scip_install .\\test",
102100
"mv .\\test .\\scip"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133

134134
setup(
135135
name="PySCIPOpt",
136-
version="6.0.0",
136+
version="6.1.0",
137137
description="Python interface and modeling environment for SCIP",
138138
long_description=long_description,
139139
long_description_content_type="text/markdown",

src/pyscipopt/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__: str = '6.0.0'
1+
__version__: str = '6.1.0'

src/pyscipopt/scip.pxi

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,16 @@ cdef class Solution:
10981098
sol.scip = scip
10991099
return sol
11001100

1101-
def __getitem__(self, expr: Union[Expr, MatrixExpr]):
1101+
def __getitem__(
1102+
self,
1103+
expr: Union[Expr, GenExpr, MatrixExpr],
1104+
) -> Union[float, np.ndarray]:
1105+
if not isinstance(expr, (Expr, GenExpr, MatrixExpr)):
1106+
raise TypeError(
1107+
"Argument 'expr' has incorrect type, expected 'Expr', 'GenExpr', or "
1108+
f"'MatrixExpr', got {type(expr).__name__!r}"
1109+
)
1110+
11021111
self._checkStage("SCIPgetSolVal")
11031112
return expr._evaluate(self)
11041113

@@ -8299,8 +8308,16 @@ cdef class Model:
82998308
Returns
83008309
-------
83018310
bilinterms : list of tuple
8311+
Triples ``(var1, var2, coef)`` for terms of the form
8312+
``coef * var1 * var2`` with ``var1 != var2``.
83028313
quadterms : list of tuple
8314+
Triples ``(var, sqrcoef, lincoef)`` for variables that appear in
8315+
quadratic or bilinear terms. ``sqrcoef`` is the coefficient of
8316+
``var**2``, and ``lincoef`` is the linear coefficient of ``var``
8317+
if it also appears linearly.
83038318
linterms : list of tuple
8319+
Pairs ``(var, coef)`` for purely linear variables, i.e.,
8320+
variables that do not participate in any quadratic or bilinear term.
83048321
83058322
"""
83068323
cdef SCIP_EXPR* expr
@@ -8319,6 +8336,7 @@ cdef class Model:
83198336
cdef int nbilinterms
83208337

83218338
# quadratic terms
8339+
cdef SCIP_EXPR* quadexpr
83228340
cdef SCIP_EXPR* sqrexpr
83238341
cdef SCIP_Real sqrcoef
83248342
cdef int nquadterms
@@ -8331,33 +8349,49 @@ cdef class Model:
83318349
assert self.checkQuadraticNonlinear(cons), "constraint is not quadratic"
83328350

83338351
expr = SCIPgetExprNonlinear(cons.scip_cons)
8334-
SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs, &nquadterms, &nbilinterms, NULL, NULL)
8352+
SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs,
8353+
&nquadterms, &nbilinterms, NULL, NULL)
83358354

83368355
linterms = []
83378356
bilinterms = []
8338-
quadterms = []
83398357

8358+
# Purely linear terms (variables not in any quadratic/bilinear term)
83408359
for termidx in range(nlinvars):
83418360
var = self._getOrCreateVar(SCIPgetVarExprVar(linexprs[termidx]))
83428361
linterms.append((var, lincoefs[termidx]))
83438362

8363+
# Collect quadratic terms in a dict so we can merge entries for the same variable.
8364+
quaddict = {} # var.ptr() -> [var, sqrcoef, lincoef]
8365+
83448366
for termidx in range(nbilinterms):
83458367
SCIPexprGetQuadraticBilinTerm(expr, termidx, &bilinterm1, &bilinterm2, &bilincoef, NULL, NULL)
83468368
scipvar1 = SCIPgetVarExprVar(bilinterm1)
83478369
scipvar2 = SCIPgetVarExprVar(bilinterm2)
83488370
var1 = self._getOrCreateVar(scipvar1)
83498371
var2 = self._getOrCreateVar(scipvar2)
83508372
if scipvar1 != scipvar2:
8351-
bilinterms.append((var1,var2,bilincoef))
8373+
bilinterms.append((var1, var2, bilincoef))
83528374
else:
8353-
quadterms.append((var1,bilincoef,0.0))
8354-
8375+
# Squared term reported as bilinear var*var
8376+
key = var1.ptr()
8377+
if key in quaddict:
8378+
quaddict[key][1] += bilincoef
8379+
else: # TODO: SCIP handles expr like x**2 appropriately, but PySCIPOpt requires this. Need to investigate why.
8380+
quaddict[key] = [var1, bilincoef, 0.0]
8381+
8382+
# Also collect linear coefficients from the quadratic terms
83558383
for termidx in range(nquadterms):
8356-
SCIPexprGetQuadraticQuadTerm(expr, termidx, NULL, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr)
8357-
if sqrexpr == NULL:
8358-
continue
8359-
var = self._getOrCreateVar(SCIPgetVarExprVar(sqrexpr))
8360-
quadterms.append((var,sqrcoef,lincoef))
8384+
SCIPexprGetQuadraticQuadTerm(expr, termidx, &quadexpr, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr)
8385+
scipvar1 = SCIPgetVarExprVar(quadexpr)
8386+
var = self._getOrCreateVar(scipvar1)
8387+
key = var.ptr()
8388+
if key in quaddict:
8389+
quaddict[key][1] += sqrcoef
8390+
quaddict[key][2] += lincoef
8391+
else:
8392+
quaddict[key] = [var, sqrcoef, lincoef]
8393+
8394+
quadterms = [tuple(entry) for entry in quaddict.values()]
83618395

83628396
return (bilinterms, quadterms, linterms)
83638397

@@ -10967,75 +11001,63 @@ cdef class Model:
1096711001
def getSolVal(
1096811002
self,
1096911003
Solution sol,
10970-
expr: Union[Expr, GenExpr],
11004+
expr: Union[Expr, GenExpr, MatrixExpr],
1097111005
) -> Union[float, np.ndarray]:
1097211006
"""
10973-
Retrieve value of given variable or expression in the given solution or in
10974-
the LP/pseudo solution if sol == None
11007+
Retrieve value of given variable or expression in the given solution.
1097511008

1097611009
Parameters
1097711010
----------
1097811011
sol : Solution
10979-
expr : Expr
10980-
polynomial expression to query the value of
11012+
Solution to query the value from. If None, the current LP/pseudo solution is
11013+
used.
11014+
11015+
expr : Expr, GenExpr, MatrixExpr
11016+
Expression to query the value of.
1098111017

1098211018
Returns
1098311019
-------
10984-
float
11020+
float or np.ndarray
1098511021

1098611022
Notes
1098711023
-----
1098811024
A variable is also an expression.
1098911025

1099011026
"""
10991-
if not isinstance(expr, (Expr, GenExpr)):
10992-
raise TypeError(
10993-
"Argument 'expr' has incorrect type (expected 'Expr' or 'GenExpr', "
10994-
f"got {type(expr)})"
10995-
)
1099611027
# no need to create a NULL solution wrapper in case we have a variable
1099711028
return (sol or Solution.create(self._scip, NULL))[expr]
1099811029

10999-
def getVal(self, expr: Union[Expr, GenExpr, MatrixExpr] ):
11030+
def getVal(self, expr: Union[Expr, GenExpr, MatrixExpr]) -> Union[float, np.ndarray]:
1100011031
"""
1100111032
Retrieve the value of the given variable or expression in the best known solution.
1100211033
Can only be called after solving is completed.
1100311034

1100411035
Parameters
1100511036
----------
1100611037
expr : Expr, GenExpr or MatrixExpr
11038+
Expression to query the value of.
1100711039

1100811040
Returns
1100911041
-------
11010-
float
11042+
float or np.ndarray
1101111043

1101211044
Notes
1101311045
-----
1101411046
A variable is also an expression.
1101511047

1101611048
"""
11017-
cdef SCIP_SOL* current_best_sol
11018-
11019-
stage_check = SCIPgetStage(self._scip) not in [SCIP_STAGE_INIT, SCIP_STAGE_FREE]
11020-
if not stage_check:
11049+
if SCIPgetStage(self._scip) in {SCIP_STAGE_INIT, SCIP_STAGE_FREE}:
1102111050
raise Warning("Method cannot be called in stage ", self.getStage())
1102211051

1102311052
# Ensure _bestSol is up-to-date (cheap pointer comparison)
11024-
current_best_sol = SCIPgetBestSol(self._scip)
11053+
cdef SCIP_SOL* current_best_sol = SCIPgetBestSol(self._scip)
1102511054
if self._bestSol is None or self._bestSol.sol != current_best_sol:
1102611055
self._bestSol = Solution.create(self._scip, current_best_sol)
1102711056

1102811057
if self._bestSol.sol == NULL and SCIPgetStage(self._scip) != SCIP_STAGE_SOLVING:
1102911058
raise Warning("No solution available")
1103011059

11031-
if isinstance(expr, MatrixExpr):
11032-
result = np.empty(expr.shape, dtype=float)
11033-
for idx in np.ndindex(result.shape):
11034-
result[idx] = self.getSolVal(self._bestSol, expr[idx])
11035-
else:
11036-
result = self.getSolVal(self._bestSol, expr)
11037-
11038-
return result
11060+
return self._bestSol[expr]
1103911061

1104011062
def hasPrimalRay(self):
1104111063
"""

0 commit comments

Comments
 (0)