Skip to content

Commit 95f8261

Browse files
getTermsQuadractic correctly returns linear terms (#1132)
* fix getTermsQuadratic * test and changelog * add getVarFarkasCoef (no test) * add back deleted changelog * copilot suggestions * correct handling of purely linear and bilinear terms * add stub * Apply suggestion from @Joao-Dionisio * Remove getVarFarkasCoef * Apply suggestions from code review Co-authored-by: DominikKamp <[email protected]> * Apply suggestion from @Joao-Dionisio --------- Co-authored-by: DominikKamp <[email protected]>
1 parent ef78d29 commit 95f8261

File tree

3 files changed

+83
-10
lines changed

3 files changed

+83
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- Fixed lotsizing_lazy example
1919
- Fixed incorrect getVal() result when _bestSol.sol was outdated
2020
- Fixed segmentation fault when using Variable or Constraint objects after freeTransform() or Model destruction
21+
- getTermsQuadratic() now correctly returns all linear terms
2122
### Changed
2223
- changed default value of enablepricing flag to True
2324
- Speed up MatrixExpr.sum(axis=...) via quicksum

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)