Skip to content

Commit 7e81977

Browse files
committed
Merge remote-tracking branch 'origin/master' into realtime-trace-jsonl
2 parents c7d36b9 + 660db29 commit 7e81977

3 files changed

Lines changed: 108 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,35 @@
44
### Added
55
- Added automated script for generating type stubs
66
- 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()
11-
- getSolVal() supports MatrixExpr now
12-
- Added realtime_trace_jsonl recipe for real-time optimization progress tracking with JSONL streaming output
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()`
11+
- `getSolVal()` supports `MatrixExpr` now
12+
- Added `realtime_trace_jsonl` recipe for real-time optimization progress tracking with JSONL streaming output
1313
### Fixed
14-
- getBestSol() now returns None for infeasible problems instead of a Solution with NULL pointer
14+
- `getBestSol()` now returns `None` for infeasible problems instead of a `Solution` with `NULL` pointer
1515
- all fundamental callbacks now raise an error if not implemented
16-
- Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr.
17-
- Updated IIS result in PyiisfinderExec()
18-
- Model.getVal now supports GenExpr type
19-
- Fixed lotsizing_lazy example
20-
- Fixed incorrect getVal() result when _bestSol.sol was outdated
21-
- Fixed segmentation fault when using Variable or Constraint objects after freeTransform() or Model destruction
16+
- Fixed the type of `MatrixExpr.sum(axis=...)` result from `MatrixVariable` to `MatrixExpr`.
17+
- Updated IIS result in `PyiisfinderExec()`
18+
- `Model.getVal` now supports `GenExpr` type
19+
- Fixed `lotsizing_lazy` example
20+
- Fixed incorrect `getVal()` result when `_bestSol.sol` was outdated
21+
- Fixed segmentation fault when using `Variable` or `Constraint` objects after `freeTransform()` or `Model` destruction
22+
- `getTermsQuadratic()` now correctly returns all linear terms
2223
### Changed
23-
- changed default value of enablepricing flag to True
24-
- Speed up MatrixExpr.sum(axis=...) via quicksum
25-
- Speed up MatrixExpr.add.reduce via quicksum
26-
- Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr
27-
- Speed up Expr * Expr via C-level API and Term * Term
28-
- 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.
29-
- Rename from `Term.__add__` to `Term.__mul__`, due to this method only working with Expr * Expr.
30-
- MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs
31-
- Set `__array_priority__` for MatrixExpr and MatrixExprCons
32-
- changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint
24+
- changed default value of `enablepricing` flag to `True`
25+
- Speed up `MatrixExpr.sum(axis=...)` via `quicksum`
26+
- Speed up `MatrixExpr.add.reduce` via `quicksum`
27+
- Speed up `np.ndarray(..., dtype=np.float64) @ MatrixExpr`
28+
- Speed up `Expr * Expr` via C-level API and `Term * Term`
29+
- 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.
30+
- Rename from `Term.__add__` to `Term.__mul__`, due to this method only working with `Expr * Expr`.
31+
- `MatrixExpr` and `MatrixExprCons` use `__array_ufunc__` protocol to control all `numpy.ufunc` inputs and outputs
32+
- Set `__array_priority__` for `MatrixExpr` and `MatrixExprCons`
33+
- changed `addConsNode()` and `addConsLocal()` to mirror `addCons()` and accept `ExprCons` instead of `Constraint`
3334
- Improved `chgReoptObjective()` performance
34-
- Return itself for abs to UnaryExpr(Operator.fabs)
35+
- Return itself for `abs` to `UnaryExpr(Operator.fabs)`
3536
### Removed
3637

3738
## 6.0.0 - 2025.11.28
@@ -116,7 +117,7 @@
116117
- Stopped tests from running in draft PRs
117118
### Removed
118119

119-
## 5.4.1 - 2024.02.24
120+
## 5.4.1 - 2025.02.24
120121
### Added
121122
- Added option to get Lhs, Rhs of nonlinear constraints
122123
- Added cutoffNode and test

src/pyscipopt/scip.pxi

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8309,8 +8309,16 @@ cdef class Model:
83098309
Returns
83108310
-------
83118311
bilinterms : list of tuple
8312+
Triples ``(var1, var2, coef)`` for terms of the form
8313+
``coef * var1 * var2`` with ``var1 != var2``.
83128314
quadterms : list of tuple
8315+
Triples ``(var, sqrcoef, lincoef)`` for variables that appear in
8316+
quadratic or bilinear terms. ``sqrcoef`` is the coefficient of
8317+
``var**2``, and ``lincoef`` is the linear coefficient of ``var``
8318+
if it also appears linearly.
83138319
linterms : list of tuple
8320+
Pairs ``(var, coef)`` for purely linear variables, i.e.,
8321+
variables that do not participate in any quadratic or bilinear term.
83148322
83158323
"""
83168324
cdef SCIP_EXPR* expr
@@ -8329,6 +8337,7 @@ cdef class Model:
83298337
cdef int nbilinterms
83308338

83318339
# quadratic terms
8340+
cdef SCIP_EXPR* quadexpr
83328341
cdef SCIP_EXPR* sqrexpr
83338342
cdef SCIP_Real sqrcoef
83348343
cdef int nquadterms
@@ -8341,33 +8350,49 @@ cdef class Model:
83418350
assert self.checkQuadraticNonlinear(cons), "constraint is not quadratic"
83428351

83438352
expr = SCIPgetExprNonlinear(cons.scip_cons)
8344-
SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs, &nquadterms, &nbilinterms, NULL, NULL)
8353+
SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs,
8354+
&nquadterms, &nbilinterms, NULL, NULL)
83458355

83468356
linterms = []
83478357
bilinterms = []
8348-
quadterms = []
83498358

8359+
# Purely linear terms (variables not in any quadratic/bilinear term)
83508360
for termidx in range(nlinvars):
83518361
var = self._getOrCreateVar(SCIPgetVarExprVar(linexprs[termidx]))
83528362
linterms.append((var, lincoefs[termidx]))
83538363

8364+
# Collect quadratic terms in a dict so we can merge entries for the same variable.
8365+
quaddict = {} # var.ptr() -> [var, sqrcoef, lincoef]
8366+
83548367
for termidx in range(nbilinterms):
83558368
SCIPexprGetQuadraticBilinTerm(expr, termidx, &bilinterm1, &bilinterm2, &bilincoef, NULL, NULL)
83568369
scipvar1 = SCIPgetVarExprVar(bilinterm1)
83578370
scipvar2 = SCIPgetVarExprVar(bilinterm2)
83588371
var1 = self._getOrCreateVar(scipvar1)
83598372
var2 = self._getOrCreateVar(scipvar2)
83608373
if scipvar1 != scipvar2:
8361-
bilinterms.append((var1,var2,bilincoef))
8374+
bilinterms.append((var1, var2, bilincoef))
83628375
else:
8363-
quadterms.append((var1,bilincoef,0.0))
8364-
8376+
# Squared term reported as bilinear var*var
8377+
key = var1.ptr()
8378+
if key in quaddict:
8379+
quaddict[key][1] += bilincoef
8380+
else: # TODO: SCIP handles expr like x**2 appropriately, but PySCIPOpt requires this. Need to investigate why.
8381+
quaddict[key] = [var1, bilincoef, 0.0]
8382+
8383+
# Also collect linear coefficients from the quadratic terms
83658384
for termidx in range(nquadterms):
8366-
SCIPexprGetQuadraticQuadTerm(expr, termidx, NULL, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr)
8367-
if sqrexpr == NULL:
8368-
continue
8369-
var = self._getOrCreateVar(SCIPgetVarExprVar(sqrexpr))
8370-
quadterms.append((var,sqrcoef,lincoef))
8385+
SCIPexprGetQuadraticQuadTerm(expr, termidx, &quadexpr, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr)
8386+
scipvar1 = SCIPgetVarExprVar(quadexpr)
8387+
var = self._getOrCreateVar(scipvar1)
8388+
key = var.ptr()
8389+
if key in quaddict:
8390+
quaddict[key][1] += sqrcoef
8391+
quaddict[key][2] += lincoef
8392+
else:
8393+
quaddict[key] = [var, sqrcoef, lincoef]
8394+
8395+
quadterms = [tuple(entry) for entry in quaddict.values()]
83718396

83728397
return (bilinterms, quadterms, linterms)
83738398

tests/test_nonlinear.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,53 @@ def test_quad_coeffs():
288288
assert linterms[0][0].name == z.name
289289
assert linterms[0][1] == 4
290290

291+
292+
def test_quad_coeffs_mixed_linear_and_quadratic():
293+
294+
scip = Model()
295+
296+
var1 = scip.addVar(name="var1", vtype='C', lb=None)
297+
var2 = scip.addVar(name="var2", vtype='C')
298+
var3 = scip.addVar(name="var3", vtype='B')
299+
var4 = scip.addVar(name="var4", vtype='B')
300+
301+
cons = scip.addCons(
302+
8 * var4
303+
+ 4 * var3
304+
- 5 * var2
305+
+ 6 * var3 ** 2
306+
- 3 * var1 ** 2
307+
+ 2 * var2 * var1
308+
+ 7 * var1 * var3
309+
== -2
310+
)
311+
312+
bilinterms, quadterms, linterms = scip.getTermsQuadratic(cons)
313+
314+
# linterms contains only purely linear variables (not in any quadratic/bilinear term)
315+
lin_only = {v.name: c for (v, c) in linterms}
316+
assert lin_only["var4"] == 8
317+
assert len(linterms) == 1 # only var4 is purely linear
318+
319+
# quadterms contains all variables that appear in quadratic/bilinear terms,
320+
# with both their squared coefficient and linear coefficient
321+
quad_dict = {v.name: (sqrcoef, lincoef) for v, sqrcoef, lincoef in quadterms}
322+
assert quad_dict["var3"] == (6.0, 4.0) # 6*var3^2 + 4*var3
323+
assert quad_dict["var1"] == (-3.0, 0.0) # -3*var1^2, no linear term
324+
assert quad_dict["var2"] == (0.0, -5.0) # -5*var2, no squared term
325+
326+
# Verify we can reconstruct all linear coefficients by combining linterms and quadterms
327+
full_lin = {}
328+
for v, c in linterms:
329+
full_lin[v.name] = full_lin.get(v.name, 0.0) + c
330+
for v, _, lincoef in quadterms:
331+
if lincoef != 0.0:
332+
full_lin[v.name] = full_lin.get(v.name, 0.0) + lincoef
333+
334+
assert full_lin["var4"] == 8
335+
assert full_lin["var3"] == 4
336+
assert full_lin["var2"] == -5
337+
291338
def test_addExprNonLinear():
292339
m = Model()
293340
x = m.addVar("x", lb=0, ub=1, obj=10)

0 commit comments

Comments
 (0)