diff --git a/CHANGELOG.md b/CHANGELOG.md index a4758d982..43b74bc66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added getVarFarkasCoef() (untested) - Added automated script for generating type stubs - Include parameter names in type stubs - Added pre-commit hook for automatic stub regeneration (see .pre-commit-config.yaml) @@ -17,6 +18,7 @@ - Fixed lotsizing_lazy example - Fixed incorrect getVal() result when _bestSol.sol was outdated - Fixed segmentation fault when using Variable or Constraint objects after freeTransform() or Model destruction +- getTermsQuadratic() now correctly returns all linear terms ### Changed - changed default value of enablepricing flag to True - Speed up MatrixExpr.sum(axis=...) via quicksum diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 2e78296e3..c8f8f30b7 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1014,6 +1014,7 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetDualboundRoot(SCIP* scip) SCIP_Real SCIPgetVarRedcost(SCIP* scip, SCIP_VAR* var) SCIP_RETCODE SCIPgetDualSolVal(SCIP* scip, SCIP_CONS* cons, SCIP_Real* dualsolval, SCIP_Bool* boundconstraint) + SCIP_Real SCIPgetVarFarkasCoef(SCIP* scip, SCIP_VAR* var) # Reader plugin SCIP_RETCODE SCIPincludeReader(SCIP* scip, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e48f63211..4bc7f1936 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -8300,8 +8300,16 @@ cdef class Model: Returns ------- bilinterms : list of tuple + Triples ``(var1, var2, coef)`` for terms of the form + ``coef * var1 * var2`` with ``var1 != var2``. quadterms : list of tuple + Triples ``(var, sqrcoef, lincoef)`` for variables that appear in + quadratic or bilinear terms. ``sqrcoef`` is the coefficient of + ``var**2``, and ``lincoef`` is the linear coefficient of ``var`` + if it also appears linearly. linterms : list of tuple + Pairs ``(var, coef)`` for purely linear variables, i.e., + variables that do not participate in any quadratic or bilinear term. """ cdef SCIP_EXPR* expr @@ -8320,6 +8328,7 @@ cdef class Model: cdef int nbilinterms # quadratic terms + cdef SCIP_EXPR* quadexpr cdef SCIP_EXPR* sqrexpr cdef SCIP_Real sqrcoef cdef int nquadterms @@ -8332,16 +8341,20 @@ cdef class Model: assert self.checkQuadraticNonlinear(cons), "constraint is not quadratic" expr = SCIPgetExprNonlinear(cons.scip_cons) - SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs, &nquadterms, &nbilinterms, NULL, NULL) + SCIPexprGetQuadraticData(expr, NULL, &nlinvars, &linexprs, &lincoefs, + &nquadterms, &nbilinterms, NULL, NULL) linterms = [] bilinterms = [] - quadterms = [] + # Purely linear terms (variables not in any quadratic/bilinear term) for termidx in range(nlinvars): var = self._getOrCreateVar(SCIPgetVarExprVar(linexprs[termidx])) linterms.append((var, lincoefs[termidx])) + # Collect quadratic terms in a dict so we can merge entries for the same variable. + quaddict = {} # var.ptr() -> [var, sqrcoef, lincoef] + for termidx in range(nbilinterms): SCIPexprGetQuadraticBilinTerm(expr, termidx, &bilinterm1, &bilinterm2, &bilincoef, NULL, NULL) scipvar1 = SCIPgetVarExprVar(bilinterm1) @@ -8349,16 +8362,28 @@ cdef class Model: var1 = self._getOrCreateVar(scipvar1) var2 = self._getOrCreateVar(scipvar2) if scipvar1 != scipvar2: - bilinterms.append((var1,var2,bilincoef)) + bilinterms.append((var1, var2, bilincoef)) else: - quadterms.append((var1,bilincoef,0.0)) + # Squared term reported as bilinear var*var + key = var1.ptr() + if key in quaddict: + quaddict[key][1] += bilincoef + else: + quaddict[key] = [var1, bilincoef, 0.0] + # Also collect linear coefficients from the quadratic terms for termidx in range(nquadterms): - SCIPexprGetQuadraticQuadTerm(expr, termidx, NULL, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr) - if sqrexpr == NULL: - continue - var = self._getOrCreateVar(SCIPgetVarExprVar(sqrexpr)) - quadterms.append((var,sqrcoef,lincoef)) + SCIPexprGetQuadraticQuadTerm(expr, termidx, &quadexpr, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr) + scipvar1 = SCIPgetVarExprVar(quadexpr) + var = self._getOrCreateVar(scipvar1) + key = var.ptr() + if key in quaddict: + quaddict[key][1] += sqrcoef + quaddict[key][2] += lincoef + else: + quaddict[key] = [var, sqrcoef, lincoef] + + quadterms = [(entry[0], entry[1], entry[2]) for entry in quaddict.values()] return (bilinterms, quadterms, linterms) @@ -8706,6 +8731,31 @@ cdef class Model: return _dualsol + def getVarFarkasCoef(self, Variable var): + """ + Returns the Farkas coefficient of the variable in the current node's LP relaxation; the current node has to have an infeasible LP. + + Parameters + ---------- + var : Variable + variable to get the farkas coefficient of + + Returns + ------- + float + + """ + assert SCIPgetStatus(self._scip) == SCIP_STATUS_INFEASIBLE, "Method can only be called with an infeasible model." + + farkas_coef = None + try: + farkas_coef = SCIPgetVarFarkasCoef(self._scip, var.scip_var) + if self.getObjectiveSense() == "maximize": + farkas_coef = -farkas_coef + except Exception: + raise Warning("no farkas coefficient available for variable " + var.name) + return farkas_coef + def optimize(self): """Optimize the problem.""" PY_SCIP_CALL(SCIPsolve(self._scip)) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 097235b78..88e13c735 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1238,6 +1238,7 @@ class Model: self, var: Incomplete, solVal: Incomplete ) -> Incomplete: ... def getVarRedcost(self, var: Incomplete) -> Incomplete: ... + def getVarFarkasCoef(self, var: Incomplete) -> Incomplete: ... def getVarStrongbranch( self, var: Incomplete, diff --git a/tests/test_nonlinear.py b/tests/test_nonlinear.py index 383532f2e..55fe6900c 100644 --- a/tests/test_nonlinear.py +++ b/tests/test_nonlinear.py @@ -288,6 +288,53 @@ def test_quad_coeffs(): assert linterms[0][0].name == z.name assert linterms[0][1] == 4 + +def test_quad_coeffs_mixed_linear_and_quadratic(): + + scip = Model() + + var1 = scip.addVar(name="var1", vtype='C', lb=None) + var2 = scip.addVar(name="var2", vtype='C') + var3 = scip.addVar(name="var3", vtype='B') + var4 = scip.addVar(name="var4", vtype='B') + + cons = scip.addCons( + 8 * var4 + + 4 * var3 + - 5 * var2 + + 6 * var3 ** 2 + - 3 * var1 ** 2 + + 2 * var2 * var1 + + 7 * var1 * var3 + == -2 + ) + + bilinterms, quadterms, linterms = scip.getTermsQuadratic(cons) + + # linterms contains only purely linear variables (not in any quadratic/bilinear term) + lin_only = {v.name: c for (v, c) in linterms} + assert lin_only["var4"] == 8 + assert len(linterms) == 1 # only var4 is purely linear + + # quadterms contains all variables that appear in quadratic/bilinear terms, + # with both their squared coefficient and linear coefficient + quad_dict = {v.name: (sqrcoef, lincoef) for v, sqrcoef, lincoef in quadterms} + assert quad_dict["var3"] == (6.0, 4.0) # 6*var3^2 + 4*var3 + assert quad_dict["var1"] == (-3.0, 0.0) # -3*var1^2, no linear term + assert quad_dict["var2"] == (0.0, -5.0) # -5*var2, no squared term + + # Verify we can reconstruct all linear coefficients by combining linterms and quadterms + full_lin = {} + for v, c in linterms: + full_lin[v.name] = full_lin.get(v.name, 0.0) + c + for v, _, lincoef in quadterms: + if lincoef != 0.0: + full_lin[v.name] = full_lin.get(v.name, 0.0) + lincoef + + assert full_lin["var4"] == 8 + assert full_lin["var3"] == 4 + assert full_lin["var2"] == -5 + def test_addExprNonLinear(): m = Model() x = m.addVar("x", lb=0, ub=1, obj=10)