From 80a97c213a4cbd1c3a9f58e7bf4f164e8f73e3e9 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Fri, 29 Mar 2024 13:49:25 -0500 Subject: [PATCH 001/135] approximation of linear, quadratic disjunction constraints --- src/pyscipopt/scip.pxd | 17 +++++++ src/pyscipopt/scip.pxi | 102 +++++++++++++++++++++++++++++++++++++- tests/test_cons.py | 17 +++++++ tests/test_disjunction.py | 2 + 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/test_disjunction.py diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 7c9b182f0..b60d70b05 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1493,6 +1493,23 @@ cdef extern from "scip/cons_sos2.h": SCIP_CONS* cons, SCIP_VAR* var) +cdef extern from "scip/cons_disjunction.h": + SCIP_RETCODE SCIPcreateConsDisjunction(SCIP *scip, + SCIP_CONS **cons, + const char *name, + int nconss, + SCIP_CONS **conss, + SCIP_CONS *relaxcons, + SCIP_Bool initial, + SCIP_Bool enforce, + SCIP_Bool check, + SCIP_Bool local, + SCIP_Bool modifiable, + SCIP_Bool dynamic) + SCIP_RETCODE SCIPaddConsElemDisjunction(SCIP *scip, + SCIP_CONS *cons, + SCIP_CONS *addcons) + cdef extern from "scip/cons_and.h": SCIP_RETCODE SCIPcreateConsAnd(SCIP* scip, SCIP_CONS** cons, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 2499f173b..92cbf1aea 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2122,7 +2122,7 @@ cdef class Model: return self._addGenNonlinearCons(cons, **kwargs) else: return self._addNonlinearCons(cons, **kwargs) - + def addConss(self, conss, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, @@ -2188,6 +2188,106 @@ cdef class Model: return constraints + def addConsDisjunction(self, conss, name = '', initial = True, + relaxcons = None, enforce=True, check =True, + local=False, modifiable = False, dynamic = False): + + def ensure_iterable(elem, length): + if isinstance(elem, Iterable): + return elem + else: + return list(repeat(elem, length)) + assert isinstance(conss, Iterable), "Given constraint list is not iterable" + + conss = list(conss) + n_conss = len(conss) + assert n_conss >= 2, "Given constraint list contains fewer than 2 items!" + + cdef SCIP_CONS* disj_cons + + cdef SCIP_CONS* scip_cons + + cdef SCIP_EXPR* scip_expr + + PY_SCIP_CALL(SCIPcreateConsDisjunction( + self._scip, &disj_cons, str_conversion(name), 0, &scip_cons, NULL, + initial, enforce, check, local, modifiable, dynamic + )) + + #PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + for i, cons in enumerate(conss): + deg = cons.expr.degree() + assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ + + kwargs = dict(name='c'+str(SCIPgetNConss(self._scip)+1),initial=True,separate=True, + enforce=True, check=True, propagate=True, local=False, + modifiable=False, dynamic=False, removable=False, + stickingatnode=False) + + kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs + kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs + terms = cons.expr.terms + + if deg <= 1: + nvars = len(terms.items()) + vars_array = malloc(nvars * sizeof(SCIP_VAR*)) + coeffs_array = malloc(nvars * sizeof(SCIP_Real)) + + for i, (key, coeff) in enumerate(terms.items()): + vars_array[i] = (key[0]).scip_var + coeffs_array[i] = coeff + + PY_SCIP_CALL(SCIPcreateConsLinear( + self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array, + kwargs['lhs'], kwargs['rhs'], kwargs['initial'], + kwargs['separate'], kwargs['enforce'], kwargs['check'], + kwargs['propagate'], kwargs['local'], kwargs['modifiable'], + kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode'])) + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons, scip_cons)) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + free(vars_array) + free(coeffs_array) + elif deg <=2: + PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( + self._scip, &scip_cons, str_conversion(kwargs['name']), + 0, NULL, NULL, # linear + 0, NULL, NULL, NULL, # quadratc + kwargs['lhs'], kwargs['rhs'], + kwargs['initial'], kwargs['separate'], kwargs['enforce'], + kwargs['check'], kwargs['propagate'], kwargs['local'], + kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) + for v, c in terms.items(): + if len(v) == 1: # linear + var = v[0] + PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, var.scip_var, c)) + else: # quadratic + assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) + + varexprs = malloc(2 * sizeof(SCIP_EXPR*)) + var1, var2 = v[0], v[1] + PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], var1.scip_var, NULL, NULL) ) + PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], var2.scip_var, NULL, NULL) ) + PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scip_expr, 2, varexprs, 1.0, NULL, NULL) ) + + PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, scip_expr, c) ) + + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons, scip_cons)) + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scip_expr) ) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) + free(varexprs) + else: + raise NotImplementedError("Only Linear Expressions are currently supported") + + + PY_SCIP_CALL(SCIPaddCons(self._scip, disj_cons)) + PyCons = Constraint.create(disj_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &disj_cons)) + return PyCons + + def getConsNVars(self, Constraint constraint): """ Gets number of variables in a constraint. diff --git a/tests/test_cons.py b/tests/test_cons.py index c3f1f1978..7c0cd952e 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -131,6 +131,23 @@ def test_cons_indicator_fail(): m.setSolVal(sol, binvar, 0) assert m.checkSol(sol) # solution should be feasible +def test_addConsDisjunction(): + m = Model() + + x1 = m.addVar(vtype="C", lb=-1, ub=1) + x2 = m.addVar(vtype="C", lb=-3, ub=3) + o = m.addVar(vtype="C") + + c = m.addConsDisjunction([x1 <= 0, x2 <= 0]) + m.addCons(o <= x1 + x2) + + m.setObjective(o, "maximize") + m.optimize() + + assert m.isEQ(m.getVal(x1), 0.0) + assert m.isEQ(m.getVal(x2), 3.0) + assert m.isEQ(m.getVal(o), 3.0) + #assert c.getConshdlrName() == "disjunction" def test_addConsCardinality(): m = Model() diff --git a/tests/test_disjunction.py b/tests/test_disjunction.py new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/tests/test_disjunction.py @@ -0,0 +1,2 @@ + + From a7f8082986503df8581d2709ba103c558c5dc106 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Tue, 2 Apr 2024 13:32:42 -0500 Subject: [PATCH 002/135] linear, quadratic disjunctions --- src/pyscipopt/scip.pxi | 123 +++++++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 59ab237a6..61e135502 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2076,6 +2076,106 @@ cdef class Model: PY_SCIP_CALL( SCIPseparateSol(self._scip, NULL if sol is None else sol.sol, pretendroot, allowlocal, onlydelayed, &delayed, &cutoff) ) return delayed, cutoff + def _createConsLinear(self, ExprCons lincons, **kwargs): + assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ + + assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() + terms = lincons.expr.terms + + cdef SCIP_CONS* scip_cons + + cdef int nvars = len(terms.items()) + + vars_array = malloc(nvars * sizeof(SCIP_VAR*)) + coeffs_array = malloc(nvars * sizeof(SCIP_Real)) + + for i, (key, coeff) in enumerate(terms.items()): + vars_array[i] = (key[0]).scip_var + coeffs_array[i] = coeff + + PY_SCIP_CALL(SCIPcreateConsLinear( + self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array, + kwargs['lhs'], kwargs['rhs'], kwargs['initial'], + kwargs['separate'], kwargs['enforce'], kwargs['check'], + kwargs['propagate'], kwargs['local'], kwargs['modifiable'], + kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode'])) + + PyCons = Constraint.create(scip_cons) + + free(vars_array) + free(coeffs_array) + return PyCons + + def _createConsQuadratic(self, ExprCons quadcons, **kwargs): + terms = quadcons.expr.terms + assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() + + cdef SCIP_CONS* scip_cons + cdef SCIP_EXPR* prodexpr + PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( + self._scip, &scip_cons, str_conversion(kwargs['name']), + 0, NULL, NULL, # linear + 0, NULL, NULL, NULL, # quadratc + kwargs['lhs'], kwargs['rhs'], + kwargs['initial'], kwargs['separate'], kwargs['enforce'], + kwargs['check'], kwargs['propagate'], kwargs['local'], + kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) + + for v, c in terms.items(): + if len(v) == 1: # linear + var = v[0] + PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, var.scip_var, c)) + else: # quadratic + assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) + + varexprs = malloc(2 * sizeof(SCIP_EXPR*)) + var1, var2 = v[0], v[1] + PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], var1.scip_var, NULL, NULL) ) + PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], var2.scip_var, NULL, NULL) ) + PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL) ) + + PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c) ) + + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &prodexpr) ) + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) + free(varexprs) + + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + PyCons = Constraint.create(scip_cons) + + return PyCons + + def createExprCons(self, cons, name='', initial=True, separate=True, + enforce=True, check=True, propagate=True, local=False, + modifiable=False, dynamic=False, removable=False, + stickingatnode=False): + + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + + kwargs = dict(name=name, initial=initial, separate=separate, + enforce=enforce, check=check, + propagate=propagate, local=local, + modifiable=modifiable, dynamic=dynamic, + removable=removable, + stickingatnode=stickingatnode) + kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs + kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs + + deg = cons.expr.degree() + if deg <= 1: + return self._createConsLinear(cons, **kwargs) + elif deg <= 2: + return self._createConsQuadratic(cons, **kwargs) + elif deg == float('inf'): # general nonlinear + raise NotImplementedError("General Nonlinear Constraint Not yet Implemented") + #return self._addGenNonlinearCons(cons, **kwargs) + else: + #return self._addNonlinearCons(cons, **kwargs) + raise NotImplementedError("Polynomial Nonlinear Constraint Not yet Implemented") + # Constraint functions def addCons(self, cons, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, @@ -2100,28 +2200,18 @@ cdef class Model: """ assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ - # replace empty name with generic one - if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1) - kwargs = dict(name=name, initial=initial, separate=separate, enforce=enforce, check=check, propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, stickingatnode=stickingatnode) - kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs - kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs + # replace empty name with generic one + pycons = self.createExprCons(cons, **kwargs) + PY_SCIP_CALL(SCIPaddCons(self._scip, (pycons).scip_cons)) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((pycons).scip_cons))) + return pycons - deg = cons.expr.degree() - if deg <= 1: - return self._addLinCons(cons, **kwargs) - elif deg <= 2: - return self._addQuadCons(cons, **kwargs) - elif deg == float('inf'): # general nonlinear - return self._addGenNonlinearCons(cons, **kwargs) - else: - return self._addNonlinearCons(cons, **kwargs) def addConss(self, conss, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, @@ -2187,7 +2277,7 @@ cdef class Model: ) return constraints - + def addConsDisjunction(self, conss, name = '', initial = True, relaxcons = None, enforce=True, check =True, local=False, modifiable = False, dynamic = False): @@ -2248,6 +2338,7 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) free(vars_array) free(coeffs_array) + elif deg <=2: PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( self._scip, &scip_cons, str_conversion(kwargs['name']), From fd03b476eea070c73bf737895bcbc72d02864ee5 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Tue, 2 Apr 2024 13:54:43 -0500 Subject: [PATCH 003/135] temp general nonlinear --- src/pyscipopt/scip.pxi | 196 +++++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 74 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 61e135502..4a1731b40 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2141,9 +2141,128 @@ cdef class Model: PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) free(varexprs) + PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + return PyCons + + def _createConsGenNonlinear(self, cons, **kwargs): + cdef SCIP_EXPR** childrenexpr + cdef SCIP_EXPR** scipexprs + cdef SCIP_CONS* scip_cons + cdef int nchildren + + # get arrays from python's expression tree + expr = cons.expr + nodes = expr_to_nodes(expr) + + # in nodes we have a list of tuples: each tuple is of the form + # (operator, [indices]) where indices are the indices of the tuples + # that are the children of this operator. This is sorted, + # so we are going to do is: + # loop over the nodes and create the expression of each + # Note1: when the operator is Operator.const, [indices] stores the value + # Note2: we need to compute the number of variable operators to find out + # how many variables are there. + nvars = 0 + for node in nodes: + if node[0] == Operator.varidx: + nvars += 1 + vars = malloc(nvars * sizeof(SCIP_VAR*)) + + varpos = 0 + scipexprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) + for i,node in enumerate(nodes): + opidx = node[0] + if opidx == Operator.varidx: + assert len(node[1]) == 1 + pyvar = node[1][0] # for vars we store the actual var! + PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &scipexprs[i], (pyvar).scip_var, NULL, NULL) ) + vars[varpos] = (pyvar).scip_var + varpos += 1 + continue + if opidx == Operator.const: + assert len(node[1]) == 1 + value = node[1][0] + PY_SCIP_CALL( SCIPcreateExprValue(self._scip, &scipexprs[i], value, NULL, NULL) ) + continue + if opidx == Operator.add: + nchildren = len(node[1]) + childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) + coefs = malloc(nchildren * sizeof(SCIP_Real)) + for c, pos in enumerate(node[1]): + childrenexpr[c] = scipexprs[pos] + coefs[c] = 1 + PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &scipexprs[i], nchildren, childrenexpr, coefs, 0, NULL, NULL)) + free(coefs) + free(childrenexpr) + continue + if opidx == Operator.prod: + nchildren = len(node[1]) + childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) + for c, pos in enumerate(node[1]): + childrenexpr[c] = scipexprs[pos] + PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scipexprs[i], nchildren, childrenexpr, 1, NULL, NULL) ) + free(childrenexpr) + continue + if opidx == Operator.power: + # the second child is the exponent which is a const + valuenode = nodes[node[1][1]] + assert valuenode[0] == Operator.const + exponent = valuenode[1][0] + PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], exponent, NULL, NULL )) + continue + if opidx == Operator.exp: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprExp(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) + continue + if opidx == Operator.log: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprLog(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) + continue + if opidx == Operator.sqrt: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], 0.5, NULL, NULL) ) + continue + if opidx == Operator.sin: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprSin(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) + continue + if opidx == Operator.cos: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprCos(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) + continue + if opidx == Operator.fabs: + assert len(node[1]) == 1 + PY_SCIP_CALL( SCIPcreateExprAbs(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) + continue + # default: + raise NotImplementedError + assert varpos == nvars + + # create nonlinear constraint for the expression root + PY_SCIP_CALL( SCIPcreateConsNonlinear( + self._scip, + &scip_cons, + str_conversion(kwargs['name']), + scipexprs[len(nodes) - 1], + kwargs['lhs'], + kwargs['rhs'], + kwargs['initial'], + kwargs['separate'], + kwargs['enforce'], + kwargs['check'], + kwargs['propagate'], + kwargs['local'], + kwargs['modifiable'], + kwargs['dynamic'], + kwargs['removable']) ) PyCons = Constraint.create(scip_cons) + for i in range(len(nodes)): + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scipexprs[i]) ) + + # free more memory + free(scipexprs) + free(vars) return PyCons @@ -2170,7 +2289,8 @@ cdef class Model: elif deg <= 2: return self._createConsQuadratic(cons, **kwargs) elif deg == float('inf'): # general nonlinear - raise NotImplementedError("General Nonlinear Constraint Not yet Implemented") + return self._createConsGenNonlinear(cons, **kwargs) + # raise NotImplementedError("General Nonlinear Constraint Not yet Implemented") #return self._addGenNonlinearCons(cons, **kwargs) else: #return self._addNonlinearCons(cons, **kwargs) @@ -2428,79 +2548,7 @@ cdef class Model: def printCons(self, Constraint constraint): return PY_SCIP_CALL(SCIPprintCons(self._scip, constraint.scip_cons, NULL)) - def _addLinCons(self, ExprCons lincons, **kwargs): - assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ - - assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() - terms = lincons.expr.terms - - cdef SCIP_CONS* scip_cons - - cdef int nvars = len(terms.items()) - - vars_array = malloc(nvars * sizeof(SCIP_VAR*)) - coeffs_array = malloc(nvars * sizeof(SCIP_Real)) - - for i, (key, coeff) in enumerate(terms.items()): - vars_array[i] = (key[0]).scip_var - coeffs_array[i] = coeff - - PY_SCIP_CALL(SCIPcreateConsLinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array, - kwargs['lhs'], kwargs['rhs'], kwargs['initial'], - kwargs['separate'], kwargs['enforce'], kwargs['check'], - kwargs['propagate'], kwargs['local'], kwargs['modifiable'], - kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode'])) - PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) - PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - - free(vars_array) - free(coeffs_array) - - return PyCons - - def _addQuadCons(self, ExprCons quadcons, **kwargs): - terms = quadcons.expr.terms - assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() - - cdef SCIP_CONS* scip_cons - cdef SCIP_EXPR* prodexpr - PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), - 0, NULL, NULL, # linear - 0, NULL, NULL, NULL, # quadratc - kwargs['lhs'], kwargs['rhs'], - kwargs['initial'], kwargs['separate'], kwargs['enforce'], - kwargs['check'], kwargs['propagate'], kwargs['local'], - kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) - - for v, c in terms.items(): - if len(v) == 1: # linear - var = v[0] - PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, var.scip_var, c)) - else: # quadratic - assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) - - varexprs = malloc(2 * sizeof(SCIP_EXPR*)) - var1, var2 = v[0], v[1] - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], var1.scip_var, NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], var2.scip_var, NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL) ) - - PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c) ) - - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &prodexpr) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) - free(varexprs) - - - PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) - PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - return PyCons def _addNonlinearCons(self, ExprCons cons, **kwargs): cdef SCIP_EXPR* expr From 640e3e084e585e1b66e505678931f9cd49f4dd34 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Tue, 2 Apr 2024 15:02:38 -0500 Subject: [PATCH 004/135] working polynomial nonlinear --- src/pyscipopt/scip.pxi | 247 ++++++++++------------------------------- 1 file changed, 59 insertions(+), 188 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4a1731b40..9baa2904c 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2145,6 +2145,61 @@ cdef class Model: return PyCons + def _createConsNonlinear(self, cons, **kwargs): + cdef SCIP_EXPR* expr + cdef SCIP_EXPR** varexprs + cdef SCIP_EXPR** monomials + cdef int* idxs + cdef SCIP_CONS* scip_cons + + terms = cons.expr.terms + + # collect variables + variables = {var.ptr():var for term in terms for var in term} + variables = list(variables.values()) + varindex = {var.ptr():idx for (idx,var) in enumerate(variables)} + + # create monomials for terms + monomials = malloc(len(terms) * sizeof(SCIP_EXPR*)) + termcoefs = malloc(len(terms) * sizeof(SCIP_Real)) + for i, (term, coef) in enumerate(terms.items()): + termvars = malloc(len(term) * sizeof(SCIP_VAR*)) + for j, var in enumerate(term): + termvars[j] = (var).scip_var + PY_SCIP_CALL( SCIPcreateExprMonomial(self._scip, &monomials[i], len(term), termvars, NULL, NULL, NULL) ) + termcoefs[i] = coef + free(termvars) + + # create polynomial from monomials + PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL)) + + # create nonlinear constraint for expr + PY_SCIP_CALL( SCIPcreateConsNonlinear( + self._scip, + &scip_cons, + str_conversion(kwargs['name']), + expr, + kwargs['lhs'], + kwargs['rhs'], + kwargs['initial'], + kwargs['separate'], + kwargs['enforce'], + kwargs['check'], + kwargs['propagate'], + kwargs['local'], + kwargs['modifiable'], + kwargs['dynamic'], + kwargs['removable']) ) + + PyCons = Constraint.create(scip_cons) + + PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &expr) ) + for i in range(len(terms)): + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i])) + free(monomials) + free(termcoefs) + return PyCons + def _createConsGenNonlinear(self, cons, **kwargs): cdef SCIP_EXPR** childrenexpr cdef SCIP_EXPR** scipexprs @@ -2290,11 +2345,8 @@ cdef class Model: return self._createConsQuadratic(cons, **kwargs) elif deg == float('inf'): # general nonlinear return self._createConsGenNonlinear(cons, **kwargs) - # raise NotImplementedError("General Nonlinear Constraint Not yet Implemented") - #return self._addGenNonlinearCons(cons, **kwargs) else: - #return self._addNonlinearCons(cons, **kwargs) - raise NotImplementedError("Polynomial Nonlinear Constraint Not yet Implemented") + return self._createConsNonlinear(cons, **kwargs) # Constraint functions def addCons(self, cons, name='', initial=True, separate=True, @@ -2514,7 +2566,7 @@ cdef class Model: conshdlr = SCIPconsGetHdlr(constraint.scip_cons) conshdrlname = SCIPconshdlrGetName(conshdlr) raise TypeError("The constraint handler %s does not have this functionality." % conshdrlname) - + return nvars def getConsVars(self, Constraint constraint): @@ -2530,7 +2582,7 @@ cdef class Model: cdef SCIP_VAR** _vars = malloc(_nvars * sizeof(SCIP_VAR*)) SCIPgetConsVars(self._scip, constraint.scip_cons, _vars, _nvars*sizeof(SCIP_VAR*), &success) - + vars = [] for i in range(_nvars): ptr = (_vars[i]) @@ -2544,191 +2596,10 @@ cdef class Model: self._modelvars[ptr] = var vars.append(var) return vars - + def printCons(self, Constraint constraint): return PY_SCIP_CALL(SCIPprintCons(self._scip, constraint.scip_cons, NULL)) - - - def _addNonlinearCons(self, ExprCons cons, **kwargs): - cdef SCIP_EXPR* expr - cdef SCIP_EXPR** varexprs - cdef SCIP_EXPR** monomials - cdef int* idxs - cdef SCIP_CONS* scip_cons - - terms = cons.expr.terms - - # collect variables - variables = {var.ptr():var for term in terms for var in term} - variables = list(variables.values()) - varindex = {var.ptr():idx for (idx,var) in enumerate(variables)} - - # create monomials for terms - monomials = malloc(len(terms) * sizeof(SCIP_EXPR*)) - termcoefs = malloc(len(terms) * sizeof(SCIP_Real)) - for i, (term, coef) in enumerate(terms.items()): - termvars = malloc(len(term) * sizeof(SCIP_VAR*)) - for j, var in enumerate(term): - termvars[j] = (var).scip_var - PY_SCIP_CALL( SCIPcreateExprMonomial(self._scip, &monomials[i], len(term), termvars, NULL, NULL, NULL) ) - termcoefs[i] = coef - free(termvars) - - # create polynomial from monomials - PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL)) - - # create nonlinear constraint for expr - PY_SCIP_CALL( SCIPcreateConsNonlinear( - self._scip, - &scip_cons, - str_conversion(kwargs['name']), - expr, - kwargs['lhs'], - kwargs['rhs'], - kwargs['initial'], - kwargs['separate'], - kwargs['enforce'], - kwargs['check'], - kwargs['propagate'], - kwargs['local'], - kwargs['modifiable'], - kwargs['dynamic'], - kwargs['removable']) ) - PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) - PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &expr) ) - for i in range(len(terms)): - PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i])) - free(monomials) - free(termcoefs) - return PyCons - - - def _addGenNonlinearCons(self, ExprCons cons, **kwargs): - cdef SCIP_EXPR** childrenexpr - cdef SCIP_EXPR** scipexprs - cdef SCIP_CONS* scip_cons - cdef int nchildren - - # get arrays from python's expression tree - expr = cons.expr - nodes = expr_to_nodes(expr) - - # in nodes we have a list of tuples: each tuple is of the form - # (operator, [indices]) where indices are the indices of the tuples - # that are the children of this operator. This is sorted, - # so we are going to do is: - # loop over the nodes and create the expression of each - # Note1: when the operator is Operator.const, [indices] stores the value - # Note2: we need to compute the number of variable operators to find out - # how many variables are there. - nvars = 0 - for node in nodes: - if node[0] == Operator.varidx: - nvars += 1 - vars = malloc(nvars * sizeof(SCIP_VAR*)) - - varpos = 0 - scipexprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) - for i,node in enumerate(nodes): - opidx = node[0] - if opidx == Operator.varidx: - assert len(node[1]) == 1 - pyvar = node[1][0] # for vars we store the actual var! - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &scipexprs[i], (pyvar).scip_var, NULL, NULL) ) - vars[varpos] = (pyvar).scip_var - varpos += 1 - continue - if opidx == Operator.const: - assert len(node[1]) == 1 - value = node[1][0] - PY_SCIP_CALL( SCIPcreateExprValue(self._scip, &scipexprs[i], value, NULL, NULL) ) - continue - if opidx == Operator.add: - nchildren = len(node[1]) - childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) - coefs = malloc(nchildren * sizeof(SCIP_Real)) - for c, pos in enumerate(node[1]): - childrenexpr[c] = scipexprs[pos] - coefs[c] = 1 - PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &scipexprs[i], nchildren, childrenexpr, coefs, 0, NULL, NULL)) - free(coefs) - free(childrenexpr) - continue - if opidx == Operator.prod: - nchildren = len(node[1]) - childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) - for c, pos in enumerate(node[1]): - childrenexpr[c] = scipexprs[pos] - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scipexprs[i], nchildren, childrenexpr, 1, NULL, NULL) ) - free(childrenexpr) - continue - if opidx == Operator.power: - # the second child is the exponent which is a const - valuenode = nodes[node[1][1]] - assert valuenode[0] == Operator.const - exponent = valuenode[1][0] - PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], exponent, NULL, NULL )) - continue - if opidx == Operator.exp: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprExp(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - if opidx == Operator.log: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprLog(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - if opidx == Operator.sqrt: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], 0.5, NULL, NULL) ) - continue - if opidx == Operator.sin: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprSin(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) - continue - if opidx == Operator.cos: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprCos(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) - continue - if opidx == Operator.fabs: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprAbs(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - # default: - raise NotImplementedError - assert varpos == nvars - - # create nonlinear constraint for the expression root - PY_SCIP_CALL( SCIPcreateConsNonlinear( - self._scip, - &scip_cons, - str_conversion(kwargs['name']), - scipexprs[len(nodes) - 1], - kwargs['lhs'], - kwargs['rhs'], - kwargs['initial'], - kwargs['separate'], - kwargs['enforce'], - kwargs['check'], - kwargs['propagate'], - kwargs['local'], - kwargs['modifiable'], - kwargs['dynamic'], - kwargs['removable']) ) - PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) - PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - for i in range(len(nodes)): - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scipexprs[i]) ) - - # free more memory - free(scipexprs) - free(vars) - - return PyCons - # TODO Find a better way to retrieve a scip expression from a python expression. Consider making GenExpr include Expr, to avoid using Union. See PR #760. from typing import Union def addExprNonlinear(self, Constraint cons, expr: Union[Expr,GenExpr], float coef): From c659b858fde062f34665782b0455300196758ed3 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 08:48:45 -0500 Subject: [PATCH 005/135] add disjunctive constraint with exprcons --- src/pyscipopt/scip.pxi | 88 ++++++++---------------------------------- 1 file changed, 17 insertions(+), 71 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9baa2904c..ff12b31ad 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2449,11 +2449,11 @@ cdef class Model: ) return constraints - + def addConsDisjunction(self, conss, name = '', initial = True, relaxcons = None, enforce=True, check =True, local=False, modifiable = False, dynamic = False): - + def ensure_iterable(elem, length): if isinstance(elem, Iterable): return elem @@ -2463,12 +2463,11 @@ cdef class Model: conss = list(conss) n_conss = len(conss) - assert n_conss >= 2, "Given constraint list contains fewer than 2 items!" cdef SCIP_CONS* disj_cons cdef SCIP_CONS* scip_cons - + cdef SCIP_EXPR* scip_expr PY_SCIP_CALL(SCIPcreateConsDisjunction( @@ -2476,80 +2475,27 @@ cdef class Model: initial, enforce, check, local, modifiable, dynamic )) - #PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + # TODO add constraints to disjunction for i, cons in enumerate(conss): - deg = cons.expr.degree() - assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ - - kwargs = dict(name='c'+str(SCIPgetNConss(self._scip)+1),initial=True,separate=True, - enforce=True, check=True, propagate=True, local=False, - modifiable=False, dynamic=False, removable=False, - stickingatnode=False) - - kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs - kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs - terms = cons.expr.terms - - if deg <= 1: - nvars = len(terms.items()) - vars_array = malloc(nvars * sizeof(SCIP_VAR*)) - coeffs_array = malloc(nvars * sizeof(SCIP_Real)) - - for i, (key, coeff) in enumerate(terms.items()): - vars_array[i] = (key[0]).scip_var - coeffs_array[i] = coeff - - PY_SCIP_CALL(SCIPcreateConsLinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array, - kwargs['lhs'], kwargs['rhs'], kwargs['initial'], - kwargs['separate'], kwargs['enforce'], kwargs['check'], - kwargs['propagate'], kwargs['local'], kwargs['modifiable'], - kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode'])) - PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons, scip_cons)) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - free(vars_array) - free(coeffs_array) - - elif deg <=2: - PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), - 0, NULL, NULL, # linear - 0, NULL, NULL, NULL, # quadratc - kwargs['lhs'], kwargs['rhs'], - kwargs['initial'], kwargs['separate'], kwargs['enforce'], - kwargs['check'], kwargs['propagate'], kwargs['local'], - kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) - for v, c in terms.items(): - if len(v) == 1: # linear - var = v[0] - PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, var.scip_var, c)) - else: # quadratic - assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) - - varexprs = malloc(2 * sizeof(SCIP_EXPR*)) - var1, var2 = v[0], v[1] - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], var1.scip_var, NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], var2.scip_var, NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scip_expr, 2, varexprs, 1.0, NULL, NULL) ) - - PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, scip_expr, c) ) - - PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons, scip_cons)) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scip_expr) ) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) - free(varexprs) - else: - raise NotImplementedError("Only Linear Expressions are currently supported") - - + pycons = self.createExprCons(cons, name=name, initial = initial, + enforce=enforce, check=check, + local=local, modifiable=modifiable, dynamic=dynamic + ) + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip,disj_cons, (pycons).scip_cons)) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(pycons).scip_cons)) PY_SCIP_CALL(SCIPaddCons(self._scip, disj_cons)) PyCons = Constraint.create(disj_cons) PY_SCIP_CALL(SCIPreleaseCons(self._scip, &disj_cons)) return PyCons + def addConsElemDisjunction(self, Constraint disj_cons, Constraint cons): + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons.scip_cons, cons.scip_cons)) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((disj_cons).scip_cons))) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((cons).scip_cons))) + + return disj_cons + def getConsNVars(self, Constraint constraint): """ From 5b94da1810253f1a788b99948563ae1215ec9007 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 09:01:15 -0500 Subject: [PATCH 006/135] fix addConsElemDisjunction cons release --- src/pyscipopt/scip.pxi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index ff12b31ad..1a8983415 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2490,8 +2490,7 @@ cdef class Model: return PyCons def addConsElemDisjunction(self, Constraint disj_cons, Constraint cons): - PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, disj_cons.scip_cons, cons.scip_cons)) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((disj_cons).scip_cons))) + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, (disj_cons).scip_cons, (cons).scip_cons)) PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((cons).scip_cons))) return disj_cons From 749c87c97b8fec3d0a3bd3c466998abb22290040 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 10:10:54 -0500 Subject: [PATCH 007/135] add tests for disjunction constraint, fix constraint free placement --- src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 20 ++++++++++---- tests/test_cons.py | 62 ++++++++++++++++++++++++++++++------------ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index da9c9e560..4ccae1a70 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1507,6 +1507,7 @@ cdef extern from "scip/cons_disjunction.h": SCIP_Bool local, SCIP_Bool modifiable, SCIP_Bool dynamic) + SCIP_RETCODE SCIPaddConsElemDisjunction(SCIP *scip, SCIP_CONS *cons, SCIP_CONS *addcons) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1a8983415..9dd767940 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2372,19 +2372,27 @@ cdef class Model: """ assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ + cdef SCIP_CONS* scip_cons + kwargs = dict(name=name, initial=initial, separate=separate, enforce=enforce, check=check, propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, stickingatnode=stickingatnode) - # replace empty name with generic one - pycons = self.createExprCons(cons, **kwargs) - PY_SCIP_CALL(SCIPaddCons(self._scip, (pycons).scip_cons)) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((pycons).scip_cons))) + # we have to pass this back to a SCIP_CONS* + # object to create a new python constraint & handle constraint release + # correctly. Otherwise, segfaults when trying to query information + # about the created constraint later. + pycons_initial = self.createExprCons(cons, **kwargs) + scip_cons = (pycons_initial).scip_cons + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + pycons = Constraint.create(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + return pycons - def addConss(self, conss, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, @@ -2638,7 +2646,6 @@ cdef class Model: PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) return Constraint.create(scip_cons) - def addConsSOS2(self, vars, weights=None, name="SOS2cons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, @@ -3458,6 +3465,7 @@ cdef class Model: self.optimize() else: PY_SCIP_CALL(SCIPsolveConcurrent(self._scip)) + print("solveconcurrent") self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) def presolve(self): diff --git a/tests/test_cons.py b/tests/test_cons.py index 7c0cd952e..86046f554 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -11,6 +11,7 @@ def test_getConsNVars(): x[i] = m.addVar("%i" % i) c = m.addCons(quicksum(x[i] for i in x) <= 10) + print(c.__hash__()) assert m.getConsNVars(c) == n_vars m.optimize() @@ -117,7 +118,9 @@ def test_cons_indicator(): assert c.getConshdlrName() == "indicator" -@pytest.mark.xfail(reason="addConsIndicator doesn't behave as expected when binary variable is False. See Issue #717.") +@pytest.mark.xfail( + reason="addConsIndicator doesn't behave as expected when binary variable is False. See Issue #717." +) def test_cons_indicator_fail(): m = Model() binvar = m.addVar(vtype="B") @@ -131,23 +134,6 @@ def test_cons_indicator_fail(): m.setSolVal(sol, binvar, 0) assert m.checkSol(sol) # solution should be feasible -def test_addConsDisjunction(): - m = Model() - - x1 = m.addVar(vtype="C", lb=-1, ub=1) - x2 = m.addVar(vtype="C", lb=-3, ub=3) - o = m.addVar(vtype="C") - - c = m.addConsDisjunction([x1 <= 0, x2 <= 0]) - m.addCons(o <= x1 + x2) - - m.setObjective(o, "maximize") - m.optimize() - - assert m.isEQ(m.getVal(x1), 0.0) - assert m.isEQ(m.getVal(x2), 3.0) - assert m.isEQ(m.getVal(o), 3.0) - #assert c.getConshdlrName() == "disjunction" def test_addConsCardinality(): m = Model() @@ -170,6 +156,42 @@ def test_printCons(): m.printCons(c) +def test_addConsElemDisjunction(): + m = Model() + x = m.addVar(vtype="c", lb=-10, ub=2) + y = m.addVar(vtype="c", lb=-10, ub=5) + o = m.addVar(vtype="c") + + m.addCons(o <= (x + y)) + disj_cons = m.addConsDisjunction([]) + c1 = m.createExprCons(x <= 1) + c2 = m.createExprCons(x <= 0) + c3 = m.createExprCons(y <= 0) + m.addConsElemDisjunction(disj_cons, c1) + disj_cons = m.addConsElemDisjunction(disj_cons, c2) + disj_cons = m.addConsElemDisjunction(disj_cons, c3) + m.setObjective(o, "maximize") + m.optimize() + assert m.isEQ(m.getVal(x), 1) + assert m.isEQ(m.getVal(y), 5) + assert m.isEQ(m.getVal(o), 6) + + +def test_addConsDisjunction_expr_init(): + m = Model() + x = m.addVar(vtype="c", lb=-10, ub=2) + y = m.addVar(vtype="c", lb=-10, ub=5) + o = m.addVar(vtype="c") + + m.addCons(o <= (x + y)) + m.addConsDisjunction([x <= 1, x <= 0, y <= 0]) + m.setObjective(o, "maximize") + m.optimize() + assert m.isEQ(m.getVal(x), 1) + assert m.isEQ(m.getVal(y), 5) + assert m.isEQ(m.getVal(o), 6) + + @pytest.mark.skip(reason="TODO: test getValsLinear()") def test_getValsLinear(): assert True @@ -178,3 +200,7 @@ def test_getValsLinear(): @pytest.mark.skip(reason="TODO: test getRowLinear()") def test_getRowLinear(): assert True + + +if __name__ == "__main__": + test_getConsNVars() From 1f0a5b0387a51db97bcad14ed97dd2aa8b1a44d7 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 11:00:47 -0500 Subject: [PATCH 008/135] add documentation to new methods --- src/pyscipopt/scip.pxi | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index fb3aeae2e..5e142b5ef 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2338,7 +2338,25 @@ cdef class Model: enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): + """Create a linear or nonlinear constraint without adding it to the SCIP problem. This is useful for creating disjunction constraints + without also enforcing the individual constituents. Currently, this can only be used as an argument to `.addConsElemDisjunction`. To add + an individual linear/nonlinear constraint, prefer `.addCons()`. + :param cons: constraint object + :param name: the name of the constraint, generic name if empty (Default value = '') + :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) + :param separate: should the constraint be separated during LP processing? (Default value = True) + :param enforce: should the constraint be enforced during node processing? (Default value = True) + :param check: should the constraint be checked for feasibility? (Default value = True) + :param propagate: should the constraint be propagated during node processing? (Default value = True) + :param local: is the constraint only valid locally? (Default value = False) + :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) + :param dynamic: is the constraint subject to aging? (Default value = False) + :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + :return The created @ref scip#Constraint "Constraint" object. + + """ if name == '': name = 'c'+str(SCIPgetNConss(self._scip)+1) @@ -2474,7 +2492,19 @@ cdef class Model: def addConsDisjunction(self, conss, name = '', initial = True, relaxcons = None, enforce=True, check =True, local=False, modifiable = False, dynamic = False): + """Add a disjunction constraint. + :param Iterable[Constraint] conss: An iterable of constraint objects to be included initially in the disjunction. Currently, these must be expressions. + :param name: the name of the disjunction constraint. + :param initial: should the LP relaxation of disjunction constraint be in the initial LP? (Default value = True) + :param relaxcons: a conjunction constraint containing the linear relaxation of the disjunction constraint, or None. (Default value = None) + :param enforce: should the constraint be enforced during node processing? (Default value = True) + :param check: should the constraint be checked for feasibility? (Default value = True) + :param local: is the constraint only valid locally? (Default value = False) + :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) + :param dynamic: is the constraint subject to aging? (Default value = False) + :return The added @ref scip#Constraint "Constraint" object. + """ def ensure_iterable(elem, length): if isinstance(elem, Iterable): return elem @@ -2513,7 +2543,12 @@ cdef class Model: def addConsElemDisjunction(self, Constraint disj_cons, Constraint cons): PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, (disj_cons).scip_cons, (cons).scip_cons)) PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((cons).scip_cons))) - + """Appends a constraint to a disjunction. + + :param Constraint disj_cons: the disjunction constraint to append to. + :param Constraint cons: the Constraint to append + :return The disjunction constraint with added @ref scip#Constraint object. + """ return disj_cons From 49c9159dafa2b886baa79e714788eb0eeef29e04 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 11:05:05 -0500 Subject: [PATCH 009/135] removed unused test_disjunction.py --- tests/test_disjunction.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 tests/test_disjunction.py diff --git a/tests/test_disjunction.py b/tests/test_disjunction.py deleted file mode 100644 index 139597f9c..000000000 --- a/tests/test_disjunction.py +++ /dev/null @@ -1,2 +0,0 @@ - - From 7781721aa489530e763808c92c641c7f099623c6 Mon Sep 17 00:00:00 2001 From: Connor Duncan Date: Wed, 3 Apr 2024 11:07:34 -0500 Subject: [PATCH 010/135] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8df01ce..2e745c3ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased ### Added +- Added methods for creating expression constraints without adding to problem +- Added methods for creating/adding/appending disjunction constraints - Added recipe for nonlinear objective functions - Added method for adding piecewise linear constraints - Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation From ddfe540418c47c1d5df825bdff97c79fdee9e2be Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 3 May 2024 14:24:01 +0200 Subject: [PATCH 011/135] Rename createExprCons --- src/pyscipopt/scip.pxi | 6 +++--- tests/test_cons.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5e142b5ef..8729da3d4 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2334,7 +2334,7 @@ cdef class Model: return PyCons - def createExprCons(self, cons, name='', initial=True, separate=True, + def createCons(self, cons, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -2415,7 +2415,7 @@ cdef class Model: # object to create a new python constraint & handle constraint release # correctly. Otherwise, segfaults when trying to query information # about the created constraint later. - pycons_initial = self.createExprCons(cons, **kwargs) + pycons_initial = self.createCons(cons, **kwargs) scip_cons = (pycons_initial).scip_cons PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) @@ -2529,7 +2529,7 @@ cdef class Model: # TODO add constraints to disjunction for i, cons in enumerate(conss): - pycons = self.createExprCons(cons, name=name, initial = initial, + pycons = self.createCons(cons, name=name, initial = initial, enforce=enforce, check=check, local=local, modifiable=modifiable, dynamic=dynamic ) diff --git a/tests/test_cons.py b/tests/test_cons.py index 86046f554..b6396ce96 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -164,9 +164,9 @@ def test_addConsElemDisjunction(): m.addCons(o <= (x + y)) disj_cons = m.addConsDisjunction([]) - c1 = m.createExprCons(x <= 1) - c2 = m.createExprCons(x <= 0) - c3 = m.createExprCons(y <= 0) + c1 = m.createCons(x <= 1) + c2 = m.createCons(x <= 0) + c3 = m.createCons(y <= 0) m.addConsElemDisjunction(disj_cons, c1) disj_cons = m.addConsElemDisjunction(disj_cons, c2) disj_cons = m.addConsElemDisjunction(disj_cons, c3) From cd4ca3841e7bbb024f2ff07e8e6f4603963c6b6c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 3 May 2024 14:46:30 +0200 Subject: [PATCH 012/135] Move debug print statements --- src/pyscipopt/scip.pxi | 7 +++---- tests/test_cons.py | 5 ----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5e142b5ef..94574b35b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2541,14 +2541,14 @@ cdef class Model: return PyCons def addConsElemDisjunction(self, Constraint disj_cons, Constraint cons): - PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, (disj_cons).scip_cons, (cons).scip_cons)) - PY_SCIP_CALL(SCIPreleaseCons(self._scip, &((cons).scip_cons))) """Appends a constraint to a disjunction. - + :param Constraint disj_cons: the disjunction constraint to append to. :param Constraint cons: the Constraint to append :return The disjunction constraint with added @ref scip#Constraint object. """ + PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, (disj_cons).scip_cons, (cons).scip_cons)) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(cons).scip_cons)) return disj_cons @@ -3513,7 +3513,6 @@ cdef class Model: self.optimize() else: PY_SCIP_CALL(SCIPsolveConcurrent(self._scip)) - print("solveconcurrent") self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) def presolve(self): diff --git a/tests/test_cons.py b/tests/test_cons.py index 86046f554..e33827f86 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -11,7 +11,6 @@ def test_getConsNVars(): x[i] = m.addVar("%i" % i) c = m.addCons(quicksum(x[i] for i in x) <= 10) - print(c.__hash__()) assert m.getConsNVars(c) == n_vars m.optimize() @@ -200,7 +199,3 @@ def test_getValsLinear(): @pytest.mark.skip(reason="TODO: test getRowLinear()") def test_getRowLinear(): assert True - - -if __name__ == "__main__": - test_getConsNVars() From 75a0c0ab37219f75f1658497e910540569192838 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 3 May 2024 15:05:23 +0200 Subject: [PATCH 013/135] Revert "Rename createExprCons" This reverts commit ddfe540418c47c1d5df825bdff97c79fdee9e2be. --- src/pyscipopt/scip.pxi | 6 +++--- tests/test_cons.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 470852aeb..94574b35b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2334,7 +2334,7 @@ cdef class Model: return PyCons - def createCons(self, cons, name='', initial=True, separate=True, + def createExprCons(self, cons, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -2415,7 +2415,7 @@ cdef class Model: # object to create a new python constraint & handle constraint release # correctly. Otherwise, segfaults when trying to query information # about the created constraint later. - pycons_initial = self.createCons(cons, **kwargs) + pycons_initial = self.createExprCons(cons, **kwargs) scip_cons = (pycons_initial).scip_cons PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) @@ -2529,7 +2529,7 @@ cdef class Model: # TODO add constraints to disjunction for i, cons in enumerate(conss): - pycons = self.createCons(cons, name=name, initial = initial, + pycons = self.createExprCons(cons, name=name, initial = initial, enforce=enforce, check=check, local=local, modifiable=modifiable, dynamic=dynamic ) diff --git a/tests/test_cons.py b/tests/test_cons.py index 72021b92f..e33827f86 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -163,9 +163,9 @@ def test_addConsElemDisjunction(): m.addCons(o <= (x + y)) disj_cons = m.addConsDisjunction([]) - c1 = m.createCons(x <= 1) - c2 = m.createCons(x <= 0) - c3 = m.createCons(y <= 0) + c1 = m.createExprCons(x <= 1) + c2 = m.createExprCons(x <= 0) + c3 = m.createExprCons(y <= 0) m.addConsElemDisjunction(disj_cons, c1) disj_cons = m.addConsElemDisjunction(disj_cons, c2) disj_cons = m.addConsElemDisjunction(disj_cons, c3) From 07e1e9d8b9302301d43fe6384aa1a7adb6f20b95 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 3 May 2024 15:11:49 +0200 Subject: [PATCH 014/135] Change naming convention --- src/pyscipopt/scip.pxi | 6 +++--- tests/test_cons.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 94574b35b..a1fecdfd7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2334,7 +2334,7 @@ cdef class Model: return PyCons - def createExprCons(self, cons, name='', initial=True, separate=True, + def createConsFromExpr(self, cons, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -2415,7 +2415,7 @@ cdef class Model: # object to create a new python constraint & handle constraint release # correctly. Otherwise, segfaults when trying to query information # about the created constraint later. - pycons_initial = self.createExprCons(cons, **kwargs) + pycons_initial = self.createConsFromExpr(cons, **kwargs) scip_cons = (pycons_initial).scip_cons PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) @@ -2529,7 +2529,7 @@ cdef class Model: # TODO add constraints to disjunction for i, cons in enumerate(conss): - pycons = self.createExprCons(cons, name=name, initial = initial, + pycons = self.createConsFromExpr(cons, name=name, initial = initial, enforce=enforce, check=check, local=local, modifiable=modifiable, dynamic=dynamic ) diff --git a/tests/test_cons.py b/tests/test_cons.py index e33827f86..a11b60987 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -163,9 +163,9 @@ def test_addConsElemDisjunction(): m.addCons(o <= (x + y)) disj_cons = m.addConsDisjunction([]) - c1 = m.createExprCons(x <= 1) - c2 = m.createExprCons(x <= 0) - c3 = m.createExprCons(y <= 0) + c1 = m.createConsFromExpr(x <= 1) + c2 = m.createConsFromExpr(x <= 0) + c3 = m.createConsFromExpr(y <= 0) m.addConsElemDisjunction(disj_cons, c1) disj_cons = m.addConsElemDisjunction(disj_cons, c2) disj_cons = m.addConsElemDisjunction(disj_cons, c3) From e750b298828658b00a954d7fa6d37a2c04492f88 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Sat, 4 May 2024 11:43:26 +0200 Subject: [PATCH 015/135] Add getnorigconss (#845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add getnorigconss * Add test for getorigconss * Add an explicit multiplier * Add transformed option to getconss. Fix test * Update CHANGELOG * Update docstring --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 2 ++ src/pyscipopt/scip.pxd | 4 ++++ src/pyscipopt/scip.pxi | 38 +++++++++++++++++++++++++------------- tests/test_cons.py | 17 ++++++++++++++++- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb671ae57..ca9b38e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Added methods for creating expression constraints without adding to problem - Added methods for creating/adding/appending disjunction constraints - Added check for pt_PT locale in test_model.py +- Added SCIPgetOrigConss and SCIPgetNOrigConss Cython bindings. +- Added transformed=False option to getConss, getNConss, and getNVars ### Fixed ### Changed ### Removed diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 4ccae1a70..73c2227ff 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -637,6 +637,10 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPdelVar(SCIP* scip, SCIP_VAR* var, SCIP_Bool* deleted) SCIP_RETCODE SCIPaddCons(SCIP* scip, SCIP_CONS* cons) SCIP_RETCODE SCIPdelCons(SCIP* scip, SCIP_CONS* cons) + SCIP_CONS** SCIPgetOrigConss(SCIP* scip) + int SCIPgetNOrigConss(SCIP* scip) + SCIP_CONS* SCIPfindOrigCons(SCIP* scip, const char*) + SCIP_CONS* SCIPfindCons(SCIP* scip, const char*) SCIP_RETCODE SCIPsetObjsense(SCIP* scip, SCIP_OBJSENSE objsense) SCIP_OBJSENSE SCIPgetObjsense(SCIP* scip) SCIP_RETCODE SCIPsetObjlimit(SCIP* scip, SCIP_Real objlimit) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a1fecdfd7..f078428cc 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1774,13 +1774,15 @@ cdef class Model: return vars - def getNVars(self): - """Retrieve number of variables in the problems""" - return SCIPgetNVars(self._scip) - - def getNConss(self): - """Retrieve the number of constraints.""" - return SCIPgetNConss(self._scip) + def getNVars(self, transformed=True): + """Retrieve number of variables in the problems. + + :param transformed: get transformed variables instead of original (Default value = True) + """ + if transformed: + return SCIPgetNVars(self._scip) + else: + return SCIPgetNOrigVars(self._scip) def getNIntVars(self): """gets number of integer active problem variables""" @@ -3366,19 +3368,29 @@ cdef class Model: """sets the value of the given variable in the global relaxation solution""" PY_SCIP_CALL(SCIPsetRelaxSolVal(self._scip, NULL, var.scip_var, val)) - def getConss(self): - """Retrieve all constraints.""" + def getConss(self, transformed=True): + """Retrieve all constraints. + + :param transformed: get transformed variables instead of original (Default value = True) + """ cdef SCIP_CONS** _conss cdef int _nconss conss = [] - _conss = SCIPgetConss(self._scip) - _nconss = SCIPgetNConss(self._scip) + if transformed: + _conss = SCIPgetConss(self._scip) + _nconss = SCIPgetNConss(self._scip) + else: + _conss = SCIPgetOrigConss(self._scip) + _nconss = SCIPgetNOrigConss(self._scip) return [Constraint.create(_conss[i]) for i in range(_nconss)] - def getNConss(self): + def getNConss(self, transformed=True): """Retrieve number of all constraints""" - return SCIPgetNConss(self._scip) + if transformed: + return SCIPgetNConss(self._scip) + else: + return SCIPgetNOrigConss(self._scip) def delCons(self, Constraint cons): """Delete constraint from the model diff --git a/tests/test_cons.py b/tests/test_cons.py index a11b60987..443f8a111 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -133,7 +133,6 @@ def test_cons_indicator_fail(): m.setSolVal(sol, binvar, 0) assert m.checkSol(sol) # solution should be feasible - def test_addConsCardinality(): m = Model() x = {} @@ -145,6 +144,22 @@ def test_addConsCardinality(): assert m.isEQ(m.getVal(quicksum(x[i] for i in range(5))), 3) +def test_getOrigConss(): + m = Model() + x = m.addVar("x", lb=0, ub=2, obj=-1) + y = m.addVar("y", lb=0, ub=4, obj=0) + z = m.addVar("z", lb=0, ub=5, obj=2) + m.addCons(x <= y + z) + m.addCons(x <= z + 100) + m.addCons(y >= -100) + m.addCons(x + y <= 1000) + m.addCons(2* x + 2 * y <= 1000) + m.addCons(x + y + z <= 7) + m.optimize() + assert len(m.getConss(transformed=False)) == m.getNConss(transformed=False) + assert m.getNConss(transformed=False) == 6 + assert m.getNConss(transformed=True) < m.getNConss(transformed=False) + def test_printCons(): m = Model() From 001f42395686ffcccef5ffab30892d41151e893a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 5 May 2024 09:33:07 +0100 Subject: [PATCH 016/135] Fix locale fix (#843) * Fix overambitious locale * Update CHANGELOG * Add locale fix to write methods * Write locale test * Update CHANGELOG * Fix path --------- Co-authored-by: Mohammed Ghannam --- CHANGELOG.md | 3 + src/pyscipopt/scip.pxi | 101 +++--- tests/data/test_locale.cip | 623 +++++++++++++++++++++++++++++++++++++ tests/test_model.py | 2 + 4 files changed, 690 insertions(+), 39 deletions(-) create mode 100644 tests/data/test_locale.cip diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9b38e1e..1da6546e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,14 @@ ## Unreleased ### Added +- Expanded locale test - Added methods for creating expression constraints without adding to problem - Added methods for creating/adding/appending disjunction constraints - Added check for pt_PT locale in test_model.py - Added SCIPgetOrigConss and SCIPgetNOrigConss Cython bindings. - Added transformed=False option to getConss, getNConss, and getNVars ### Fixed +- Fixed locale errors in reading ### Changed ### Removed @@ -18,6 +20,7 @@ - Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation - New test for model setLogFile ### Fixed +- Fixed locale fix - Fixed model.setLogFile(None) error - Add recipes sub-package - Fixed "weakly-referenced object no longer exists" when calling dropEvent in test_customizedbenders diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f078428cc..8db6570d7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1129,12 +1129,12 @@ cdef class Model: def printVersion(self): """Print version, copyright information and compile mode""" - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") SCIPprintVersion(self._scip, NULL) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def getProbName(self): """Retrieve problem name""" @@ -1463,8 +1463,8 @@ cdef class Model: :param genericnames: indicates whether the problem should be written with generic variable and constraint names (Default value = False) """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") str_absfile = abspath(filename) absfile = str_conversion(str_absfile) @@ -1479,7 +1479,7 @@ cdef class Model: PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, fn, ext, genericnames)) print('wrote problem to file ' + str_absfile) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) # Variable Functions @@ -4495,13 +4495,13 @@ cdef class Model: """writes current LP to a file :param filename: file name (Default value = "LP.lp") """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") absfile = str_conversion(abspath(filename)) PY_SCIP_CALL( SCIPwriteLP(self._scip, absfile) ) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def createSol(self, Heur heur = None): """Create a new primal solution. @@ -4540,12 +4540,12 @@ cdef class Model: def printBestSol(self, write_zeros=False): """Prints the best feasible primal solution.""" - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") PY_SCIP_CALL(SCIPprintBestSol(self._scip, NULL, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def printSol(self, Solution solution=None, write_zeros=False): """Print the given primal solution. @@ -4555,15 +4555,15 @@ cdef class Model: write_zeros -- include variables that are set to zero """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") if solution is None: PY_SCIP_CALL(SCIPprintSol(self._scip, NULL, NULL, write_zeros)) else: PY_SCIP_CALL(SCIPprintSol(self._scip, solution.sol, NULL, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def writeBestSol(self, filename="origprob.sol", write_zeros=False): """Write the best feasible primal solution to a file. @@ -4573,8 +4573,8 @@ cdef class Model: write_zeros -- include variables that are set to zero """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") # use this doubled opening pattern to ensure that IOErrors are # triggered early and in Python not in C,Cython or SCIP. @@ -4582,7 +4582,7 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintBestSol(self._scip, cfile, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def writeBestTransSol(self, filename="transprob.sol", write_zeros=False): """Write the best feasible primal solution for the transformed problem to a file. @@ -4591,8 +4591,8 @@ cdef class Model: filename -- name of the output file write_zeros -- include variables that are set to zero """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") # use this double opening pattern to ensure that IOErrors are # triggered early and in python not in C, Cython or SCIP. @@ -4600,7 +4600,7 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintBestTransSol(self._scip, cfile, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def writeSol(self, Solution solution, filename="origprob.sol", write_zeros=False): """Write the given primal solution to a file. @@ -4610,8 +4610,8 @@ cdef class Model: filename -- name of the output file write_zeros -- include variables that are set to zero """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") # use this doubled opening pattern to ensure that IOErrors are # triggered early and in Python not in C,Cython or SCIP. @@ -4619,7 +4619,7 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintSol(self._scip, solution.sol, cfile, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def writeTransSol(self, Solution solution, filename="transprob.sol", write_zeros=False): """Write the given transformed primal solution to a file. @@ -4629,8 +4629,8 @@ cdef class Model: filename -- name of the output file write_zeros -- include variables that are set to zero """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") # use this doubled opening pattern to ensure that IOErrors are # triggered early and in Python not in C,Cython or SCIP. @@ -4638,7 +4638,7 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintTransSol(self._scip, solution.sol, cfile, write_zeros)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) # perhaps this should not be included as it implements duplicated functionality # (as does it's namesake in SCIP) @@ -4648,9 +4648,14 @@ cdef class Model: Keyword arguments: filename -- name of the input file """ + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + absfile = str_conversion(abspath(filename)) PY_SCIP_CALL(SCIPreadSol(self._scip, absfile)) + locale.setlocale(locale.LC_NUMERIC, user_locale) + def readSolFile(self, filename): """Reads a given solution file. @@ -4668,7 +4673,14 @@ cdef class Model: str_absfile = abspath(filename) absfile = str_conversion(str_absfile) solution = self.createSol() + + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + PY_SCIP_CALL(SCIPreadSolFile(self._scip, absfile, solution.sol, False, &partial, &error)) + + locale.setlocale(locale.LC_NUMERIC, user_locale) + if error: raise Exception("SCIP: reading solution from file " + str_absfile + " failed!") @@ -4896,12 +4908,12 @@ cdef class Model: :param Variable var: variable """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") PY_SCIP_CALL(SCIPwriteVarName(self._scip, NULL, var.scip_var, False)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def getStage(self): """Retrieve current SCIP stage""" @@ -5032,12 +5044,12 @@ cdef class Model: def printStatistics(self): """Print statistics.""" - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") PY_SCIP_CALL(SCIPprintStatistics(self._scip, NULL)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def writeStatistics(self, filename="origprob.stats"): """Write statistics to a file. @@ -5045,8 +5057,8 @@ cdef class Model: Keyword arguments: filename -- name of the output file """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") # use this doubled opening pattern to ensure that IOErrors are # triggered early and in Python not in C,Cython or SCIP. @@ -5054,7 +5066,7 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def getNLPs(self): """gets total number of LPs solved so far""" @@ -5241,7 +5253,13 @@ cdef class Model: """ absfile = str_conversion(abspath(file)) + + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + PY_SCIP_CALL(SCIPreadParams(self._scip, absfile)) + + locale.setlocale(locale.LC_NUMERIC, user_locale) def writeParams(self, filename='param.set', comments = True, onlychanged = True): """Write parameter settings to an external file. @@ -5251,15 +5269,15 @@ cdef class Model: :param onlychanged: write only modified parameters (Default value = True) """ - user_locale = locale.getlocale() - locale.setlocale(locale.LC_ALL, "C") + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") str_absfile = abspath(filename) absfile = str_conversion(str_absfile) PY_SCIP_CALL(SCIPwriteParams(self._scip, absfile, comments, onlychanged)) print('wrote parameter settings to file ' + str_absfile) - locale.setlocale(locale.LC_ALL, user_locale) + locale.setlocale(locale.LC_NUMERIC,user_locale) def resetParam(self, name): """Reset parameter setting to its default value @@ -5290,6 +5308,9 @@ cdef class Model: :param extension: specify file extension/type (Default value = None) """ + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + absfile = str_conversion(abspath(filename)) if extension is None: PY_SCIP_CALL(SCIPreadProb(self._scip, absfile, NULL)) @@ -5297,6 +5318,8 @@ cdef class Model: extension = str_conversion(extension) PY_SCIP_CALL(SCIPreadProb(self._scip, absfile, extension)) + locale.setlocale(locale.LC_NUMERIC, user_locale) + # Counting functions def count(self): diff --git a/tests/data/test_locale.cip b/tests/data/test_locale.cip new file mode 100644 index 000000000..81a6f6e0d --- /dev/null +++ b/tests/data/test_locale.cip @@ -0,0 +1,623 @@ +STATISTICS + Problem name : model + Variables : 207 (60 binary, 0 integer, 0 implicit integer, 147 continuous) + Constraints : 0 initial, 407 maximal +OBJECTIVE + Sense : minimize +VARIABLES + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [binary] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=-1, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,1.33] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,59.75] + [continuous] : obj=0, original bounds=[0,77.42] + [continuous] : obj=0, original bounds=[0,35.4] + [continuous] : obj=0, original bounds=[0,+inf] +CONSTRAINTS + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -0.0282485875706215[C] == 0; + [linear] : [C] -0.0167364016736402[C] == 0; + [linear] : [C] -0.0129165590286748[C] == 0; + [linear] : [C] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -13.68[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -4.73[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] -3.46[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] == 0; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : [B] -[B] <= 1e-05; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -6[B] +[B] +[B] +[B] +[B] +[B] +[B] +7[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : -5[B] +[B] +[B] +[B] +[B] +[B] +6[B] >= 0; + [linear] : 0 <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : 0 <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : 0 <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [B] -[B] <= 0; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] +1.69[B] <= 1.69001; + [linear] : [C] +1.33[B] <= 1.33001; + [linear] : [C] +2.74[B] <= 2.74001; + [linear] : [C] == 35.4; + [linear] : [C] == 59.75; + [linear] : [C] == 77.42; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -0.9978[C] +2.233[C] -41.40677[B] <= -2.233; + [linear] : [C] -0.9682[C] +2.337[C] -65.19521[B] <= -2.337; + [linear] : [C] -0.9101[C] +0.25[C] -78.355[B] <= -0.25; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] -7.59[C] <= 20.86001; + [linear] : [C] -6.37[C] <= 20.45001; + [linear] : [C] -6.25[C] <= 21.63001; + [linear] : [C] <= 1.48901; + [linear] : [C] <= 1.36501; + [linear] : [C] <= 0.59901; + [linear] : [C] <= 1.38701; + [linear] : [C] <= 1.72601; + [linear] : [C] <= 1.65601; + [linear] : [C] <= 0.85201; + [linear] : [C] <= 1.81001; + [linear] : [C] <= 1.02101; + [linear] : [C] <= 1.94701; + [linear] : [C] <= 0.82801; + [linear] : [C] <= 0.89801; + [linear] : [C] <= 1.43601; + [linear] : [C] <= 0.16201; + [linear] : [C] <= 1.74101; + [linear] : [C] <= 1.10501; + [linear] : [C] <= 0.20301; + [linear] : [C] <= 0.64801; + [linear] : [C] <= 0.86101; + [linear] : [C] <= 0.24601; +END diff --git a/tests/test_model.py b/tests/test_model.py index e705dadfb..9dca52cde 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -360,5 +360,7 @@ def test_locale(): with open("model.cip") as file: assert "1,1" not in file.read() + + m.readProblem(os.path.join("tests", "data", "test_locale.cip")) locale.setlocale(locale.LC_NUMERIC,"") From 10671fca18156dd0e6d0899f82553baf4c39aa3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 5 May 2024 09:36:44 +0100 Subject: [PATCH 017/135] Add verbosity argument to writeParams and writeProblems (#844) * Fix overambitious locale * Update CHANGELOG * Remove forgotten print * Add verbose option for writeParams/Problem * Update CHANGELOG * Update src/pyscipopt/scip.pxi Co-authored-by: Mohammed Ghannam * Grammar --------- Co-authored-by: Mohammed Ghannam Co-authored-by: Mohammed Ghannam --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxi | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da6546e1..3b699ee56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added verbose option for writeProblem and writeParams - Expanded locale test - Added methods for creating expression constraints without adding to problem - Added methods for creating/adding/appending disjunction constraints diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 8db6570d7..dfa5bd93b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1329,7 +1329,6 @@ cdef class Model: # turn the constant value into an Expr instance for further processing if not isinstance(expr, Expr): - print(expr) assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__ expr = Expr() + expr @@ -1455,13 +1454,13 @@ cdef class Model: if not onlyroot: self.setIntParam("propagating/maxrounds", 0) - def writeProblem(self, filename='model.cip', trans=False, genericnames=False): + def writeProblem(self, filename='model.cip', trans=False, genericnames=False, verbose=True): """Write current model/problem to a file. :param filename: the name of the file to be used (Default value = 'model.cip'). Should have an extension corresponding to one of the readable file formats, described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. :param trans: indicates whether the transformed problem is written to file (Default value = False) :param genericnames: indicates whether the problem should be written with generic variable and constraint names (Default value = False) - + :param verbose: indicates whether a success message should be printed """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -1469,15 +1468,19 @@ cdef class Model: str_absfile = abspath(filename) absfile = str_conversion(str_absfile) fn, ext = splitext(absfile) + if len(ext) == 0: ext = str_conversion('.cip') fn = fn + ext ext = ext[1:] + if trans: PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, fn, ext, genericnames)) else: PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, fn, ext, genericnames)) - print('wrote problem to file ' + str_absfile) + + if verbose: + print('wrote problem to file ' + str_absfile) locale.setlocale(locale.LC_NUMERIC,user_locale) @@ -5261,13 +5264,13 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC, user_locale) - def writeParams(self, filename='param.set', comments = True, onlychanged = True): + def writeParams(self, filename='param.set', comments=True, onlychanged=True, verbose=True): """Write parameter settings to an external file. :param filename: file to be written (Default value = 'param.set') :param comments: write parameter descriptions as comments? (Default value = True) :param onlychanged: write only modified parameters (Default value = True) - + :param verbose: indicates whether a success message should be printed """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -5275,7 +5278,9 @@ cdef class Model: str_absfile = abspath(filename) absfile = str_conversion(str_absfile) PY_SCIP_CALL(SCIPwriteParams(self._scip, absfile, comments, onlychanged)) - print('wrote parameter settings to file ' + str_absfile) + + if verbose: + print('wrote parameter settings to file ' + str_absfile) locale.setlocale(locale.LC_NUMERIC,user_locale) From 19b3ec9b450e190482a3f09bb18f7dd34835bfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 5 May 2024 12:30:20 +0100 Subject: [PATCH 018/135] Wrapped createOrigSol (#848) * Fix overambitious locale * Update CHANGELOG * Add locale fix to write methods * Write locale test * Update CHANGELOG * Fix path * Add createOrigSol and tests * Update CHANGELOG * Update scip.pxi * Update test_solution.py --------- Co-authored-by: Mohammed Ghannam --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 21 ++++++++++++++++++++- tests/test_solution.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b699ee56..8405580d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams - Expanded locale test - Added methods for creating expression constraints without adding to problem diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 73c2227ff..dd1bb5bf4 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -841,6 +841,7 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetSolTransObj(SCIP* scip, SCIP_SOL* sol) SCIP_RETCODE SCIPcreateSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) SCIP_RETCODE SCIPcreatePartialSol(SCIP* scip, SCIP_SOL** sol,SCIP_HEUR* heur) + SCIP_RETCODE SCIPcreateOrigSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) SCIP_RETCODE SCIPsetSolVal(SCIP* scip, SCIP_SOL* sol, SCIP_VAR* var, SCIP_Real val) SCIP_RETCODE SCIPtrySolFree(SCIP* scip, SCIP_SOL** sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* stored) SCIP_RETCODE SCIPtrySol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* stored) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index dfa5bd93b..95545b2da 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -4507,7 +4507,7 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def createSol(self, Heur heur = None): - """Create a new primal solution. + """Create a new primal solution in the transformed space. :param Heur heur: heuristic that found the solution (Default value = None) @@ -4541,6 +4541,25 @@ cdef class Model: partialsolution = Solution.create(self._scip, _sol) return partialsolution + def createOrigSol(self, Heur heur = None): + """Create a new primal solution in the original space. + + :param Heur heur: heuristic that found the solution (Default value = None) + + """ + cdef SCIP_HEUR* _heur + cdef SCIP_SOL* _sol + + if isinstance(heur, Heur): + n = str_conversion(heur.name) + _heur = SCIPfindHeur(self._scip, n) + else: + _heur = NULL + + PY_SCIP_CALL(SCIPcreateOrigSol(self._scip, &_sol, _heur)) + solution = Solution.create(self._scip, _sol) + return solution + def printBestSol(self, write_zeros=False): """Prints the best feasible primal solution.""" user_locale = locale.getlocale(category=locale.LC_NUMERIC) diff --git a/tests/test_solution.py b/tests/test_solution.py index d1b7c9d33..61846b167 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -47,6 +47,34 @@ def test_solution_create(): assert m.getSolObjVal(s1) == -1 m.freeSol(s1) +def test_createOrigSol(): + m = Model() + + x = m.addVar("x", lb=0, ub=2, obj=-1) + y = m.addVar("y", lb=1, ub=4, obj=1) + z = m.addVar("z", lb=1, ub=5, obj=10) + m.addCons(x * x <= y*z) + m.presolve() + + s = m.createOrigSol() + s[x] = 2.0 + s[y] = 5.0 + s[z] = 10.0 + assert not m.checkSol(s) + assert m.addSol(s, free=True) + + s1 = m.createOrigSol() + m.setSolVal(s1, x, 1.0) + m.setSolVal(s1, y, 1.0) + m.setSolVal(s1, z, 1.0) + assert m.checkSol(s1) + assert m.addSol(s1, free=False) + + m.optimize() + + assert m.getSolObjVal(s1) == 10.0 + m.freeSol(s1) + def test_solution_evaluation(): m = Model() From 1200c4feae9247f1c316e7bf2ea540c9328414bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Fri, 17 May 2024 05:28:52 +0100 Subject: [PATCH 019/135] Minor changes to test_pricer (#852) --- tests/test_pricer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_pricer.py b/tests/test_pricer.py index abc5599a2..73cb438d8 100644 --- a/tests/test_pricer.py +++ b/tests/test_pricer.py @@ -38,13 +38,15 @@ def pricerredcost(self): objval = 1 + subMIP.getObjVal() + # testing methods assert type(self.model.getNSolsFound()) == int assert type(self.model.getNBestSolsFound()) == int assert self.model.getNBestSolsFound() <= self.model.getNSolsFound() + self.model.data["nSols"] = self.model.getNSolsFound() - # Adding the column to the master problem - if objval < -1e-08: + # Adding the column to the master problem (model.LT because of numerics) + if self.model.isLT(objval, 0): currentNumVar = len(self.data['var']) # Creating new var; must set pricedVar to True @@ -59,7 +61,7 @@ def pricerredcost(self): newPattern.append(coeff) # Testing getVarRedcost - assert round(self.model.getVarRedcost(newVar),6) == round(objval,6) + assert self.model.isEQ(self.model.getVarRedcost(newVar), objval) # Storing the new variable in the pricer data. self.data['patterns'].append(newPattern) @@ -99,7 +101,6 @@ def test_cuttingstock(): varBaseName = "Pattern" patterns = [] - initialCoeffs = [] for i in range(len(widths)): varNames.append(varBaseName + "_" + str(i)) cutPatternVars.append(s.addVar(varNames[i], obj = 1.0)) @@ -142,8 +143,8 @@ def test_cuttingstock(): print('\t\tSol Value', '\tWidths\t', printWidths) for i in range(len(pricer.data['var'])): rollUsage = 0 - solValue = round(s.getVal(pricer.data['var'][i])) - if solValue > 0: + solValue = s.getVal(pricer.data['var'][i]) + if s.isGT(solValue, 0): outline = 'Pattern_' + str(i) + ':\t' + str(solValue) + '\t\tCuts:\t ' for j in range(len(widths)): rollUsage += pricer.data['patterns'][i][j]*widths[j] From 41c3b63027e9482615bbc7bc1b2064aaedbcd50b Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:28:23 +0100 Subject: [PATCH 020/135] Add recipe for infeasibilities --- src/pyscipopt/recipes/infeasibilities.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/pyscipopt/recipes/infeasibilities.py diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py new file mode 100644 index 000000000..b33677f77 --- /dev/null +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -0,0 +1,42 @@ +from pyscipopt import Model, quicksum + + +def get_infeasible_constraints(orig_model: Model, verbose=False): + """ + Given a model, adds slack variables to all the constraints and minimizes their sum. + Non-zero slack variables correspond to infeasible constraints. + """ + + model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model + slack = {} + aux = {} + for c in model.getConss(): + + slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) + + model.addConsCoeff(c, slack[c.name], 1) + + # getting the absolute value because of <= and >= constraints + aux[c.name] = model.addVar(obj=1) + model.addCons(aux[c.name] >= slack[c.name]) + model.addCons(aux[c.name] >= -slack[c.name]) + + + model.hideOutput() + model.setPresolve(0) # just to be safe, maybe we can use presolving + model.optimize() + + n_infeasibilities_detected = 0 + for v in aux: + if model.isGT(model.getVal(aux[v]), 0): + n_infeasibilities_detected += 1 + print("Constraint %s is causing an infeasibility." % v) + + if verbose: + if n_infeasibilities_detected > 0: + print("If the constraint names are unhelpful, consider giving them\ + a suitable name when creating the model with model.addCons(..., name=\"the_name_you_want\")") + else: + print("Model is feasible.") + + return n_infeasibilities_detected, aux \ No newline at end of file From 53f61d624572c482eb70c430b460ac83bd2e9f97 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:28:30 +0100 Subject: [PATCH 021/135] Add tests --- CHANGELOG.md | 1 + tests/test_recipe_infeasibilities.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/test_recipe_infeasibilities.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8405580d4..a1508233f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams - Expanded locale test diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py new file mode 100644 index 000000000..db6e405c4 --- /dev/null +++ b/tests/test_recipe_infeasibilities.py @@ -0,0 +1,27 @@ +from pyscipopt import Model +from pyscipopt.recipes.infeasibilities import get_infeasible_constraints + + +def test_get_infeasible_constraints(): + m = Model() + + x = m.addVar(lb=0) + m.addCons(x <= 4) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 0 + + m.addCons(x <= -1) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 1 + + m.addCons(x == 2) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 1 + + m.addCons(x == -4) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 3 # with x == -4, x == 2 also becomes infeasible \ No newline at end of file From 2bc90145e82a5ed635430ca37e26b6b0f6613349 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:40:22 +0100 Subject: [PATCH 022/135] Set feasibility emphasis --- src/pyscipopt/recipes/infeasibilities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index b33677f77..d1ddf966a 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -1,4 +1,4 @@ -from pyscipopt import Model, quicksum +from pyscipopt import Model, SCIP_PARAMEMPHASIS def get_infeasible_constraints(orig_model: Model, verbose=False): @@ -24,6 +24,8 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving + model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility + #model.setParam("limits/solutions", 1) # SCIP sometimes returns the incorrect stage when models are prematurely stopped model.optimize() n_infeasibilities_detected = 0 From 14d0b520c5b6b76f5bc4f14f756531175ea1518f Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 15:03:31 +0100 Subject: [PATCH 023/135] Changed comment --- src/pyscipopt/recipes/infeasibilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index d1ddf966a..63debc5f7 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -25,7 +25,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility - #model.setParam("limits/solutions", 1) # SCIP sometimes returns the incorrect stage when models are prematurely stopped + #model.setParam("limits/solutions", 1) # PySCIPOpt sometimes incorrectly raises an error when a model is prematurely stopped. See PR # 815. model.optimize() n_infeasibilities_detected = 0 From 195016dad1debe3c124d4284f8d4c49d4fbfd236 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 5 Jun 2024 18:09:39 +0100 Subject: [PATCH 024/135] Add binary variable --- src/pyscipopt/recipes/infeasibilities.py | 31 ++++++++++++++---------- src/pyscipopt/scip.pxi | 4 +-- tests/test_recipe_infeasibilities.py | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 63debc5f7..34153aed0 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -3,36 +3,41 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): """ - Given a model, adds slack variables to all the constraints and minimizes their sum. - Non-zero slack variables correspond to infeasible constraints. + Given a model, adds slack variables to all the constraints and minimizes a binary variable that indicates if they're positive. + Positive slack variables correspond to infeasible constraints. """ - + model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model - slack = {} - aux = {} + slack = {} + aux = {} + binary = {} + aux_binary = {} + for c in model.getConss(): slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) - model.addConsCoeff(c, slack[c.name], 1) + binary[c.name] = model.addVar(obj=1, vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857.) # getting the absolute value because of <= and >= constraints - aux[c.name] = model.addVar(obj=1) + aux[c.name] = model.addVar() model.addCons(aux[c.name] >= slack[c.name]) model.addCons(aux[c.name] >= -slack[c.name]) - + + # modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation + aux_binary[c.name] = model.addVar(ub=1) + model.addCons(binary[c.name]+aux_binary[c.name] == 1) + model.addConsSOS1([aux[c.name], aux_binary[c.name]]) model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving - model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility - #model.setParam("limits/solutions", 1) # PySCIPOpt sometimes incorrectly raises an error when a model is prematurely stopped. See PR # 815. model.optimize() n_infeasibilities_detected = 0 - for v in aux: - if model.isGT(model.getVal(aux[v]), 0): + for c in binary: + if model.isGT(model.getVal(binary[c]), 0): n_infeasibilities_detected += 1 - print("Constraint %s is causing an infeasibility." % v) + print("Constraint %s is causing an infeasibility." % c) if verbose: if n_infeasibilities_detected > 0: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 95545b2da..5b9c1b398 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1607,7 +1607,6 @@ cdef class Model: PY_SCIP_CALL(SCIPtightenVarLb(self._scip, var.scip_var, lb, force, &infeasible, &tightened)) return infeasible, tightened - def tightenVarUb(self, Variable var, ub, force=False): """Tighten the upper bound in preprocessing or current node, if the bound is tighter. @@ -1624,7 +1623,6 @@ cdef class Model: PY_SCIP_CALL(SCIPtightenVarUb(self._scip, var.scip_var, ub, force, &infeasible, &tightened)) return infeasible, tightened - def tightenVarUbGlobal(self, Variable var, ub, force=False): """Tighten the global upper bound, if the bound is tighter. @@ -2556,7 +2554,6 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(cons).scip_cons)) return disj_cons - def getConsNVars(self, Constraint constraint): """ Gets number of variables in a constraint. @@ -2699,6 +2696,7 @@ cdef class Model: PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) return Constraint.create(scip_cons) + def addConsSOS2(self, vars, weights=None, name="SOS2cons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py index db6e405c4..59d3262ff 100644 --- a/tests/test_recipe_infeasibilities.py +++ b/tests/test_recipe_infeasibilities.py @@ -24,4 +24,4 @@ def test_get_infeasible_constraints(): m.addCons(x == -4) n_infeasibilities_detected = get_infeasible_constraints(m)[0] - assert n_infeasibilities_detected == 3 # with x == -4, x == 2 also becomes infeasible \ No newline at end of file + assert n_infeasibilities_detected == 2 \ No newline at end of file From fecd0c9698d28119581b5009b3be9151d6e0bd6e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 6 Jun 2024 15:36:25 +0100 Subject: [PATCH 025/135] Overwrite old objective --- src/pyscipopt/recipes/infeasibilities.py | 6 ++++-- tests/test_recipe_infeasibilities.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 34153aed0..2b1aa6439 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -1,4 +1,4 @@ -from pyscipopt import Model, SCIP_PARAMEMPHASIS +from pyscipopt import Model, quicksum def get_infeasible_constraints(orig_model: Model, verbose=False): @@ -8,6 +8,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): """ model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model + slack = {} aux = {} binary = {} @@ -17,7 +18,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) model.addConsCoeff(c, slack[c.name], 1) - binary[c.name] = model.addVar(obj=1, vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857.) + binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857. # getting the absolute value because of <= and >= constraints aux[c.name] = model.addVar() @@ -29,6 +30,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.addCons(binary[c.name]+aux_binary[c.name] == 1) model.addConsSOS1([aux[c.name], aux_binary[c.name]]) + model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss())) model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving model.optimize() diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py index 59d3262ff..e86db13f9 100644 --- a/tests/test_recipe_infeasibilities.py +++ b/tests/test_recipe_infeasibilities.py @@ -6,6 +6,8 @@ def test_get_infeasible_constraints(): m = Model() x = m.addVar(lb=0) + m.setObjective(2*x) + m.addCons(x <= 4) n_infeasibilities_detected = get_infeasible_constraints(m)[0] From f255c70a70df3237945f70b02199a5bda7cdbaec Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:25:30 +0100 Subject: [PATCH 026/135] Add readStatistics --- src/pyscipopt/scip.pxi | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5b9c1b398..6d58a7956 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5087,6 +5087,85 @@ cdef class Model: PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) locale.setlocale(locale.LC_NUMERIC,user_locale) + + def readStatistics(self, filename): + """ + Given a .stats file, reads it and returns a dictionary with some statistics. + + Keyword arguments: + filename -- name of the input file + """ + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Root LP Estimate", "Solutions found", "First Solution", + "Primal Bound", "Dual Bound", "Gap", "primal-dual"] + + result = {} + file = open(filename) + data = file.readlines() + seen_cons = 0 + for i, line in enumerate(data): + split_line = line.split(":") + split_line[1] = split_line[1][:-1] # removing \n + stat_name = split_line[0].strip() + + if seen_cons == 2 and stat_name == "Constraints": + continue + + if stat_name in available_stats: + cur_stat = split_line[0].strip() + relevant_value = split_line[1].strip() + + if stat_name == "Variables": + relevant_value = relevant_value[:-1] # removing ")" + var_stats = {} + split_var = relevant_value.split("(") + var_stats["total"] = int(split_var[0]) + split_var = split_var[1].split(",") + + for var_type in split_var: + split_result = var_type.strip().split(" ") + var_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-2]: + result["Variables"] = var_stats + else: + result["Presolved Variables"] = var_stats + + continue + + if stat_name == "Constraints": + seen_cons += 1 + con_stats = {} + split_con = relevant_value.split(",") + for con_type in split_con: + split_result = con_type.strip().split(" ") + con_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-3]: + result["Constraints"] = con_stats + else: + result["Presolved Constraints"] = con_stats + continue + + relevant_value = relevant_value.split(" ")[0] + if stat_name == "Problem name": + if "Original" in data[i-1]: + result["Problem name"] = relevant_value + else: + result["Presolved Problem name"] = relevant_value + continue + + if stat_name == "Gap": + result["Gap (%)"] = float(relevant_value[:-1]) + continue + + if _is_number(relevant_value): + result[cur_stat] = float(relevant_value) + else: # it's a string + result[cur_stat] = relevant_value + + return result def getNLPs(self): """gets total number of LPs solved so far""" From 0252cb190393ce9c4729fee8ad17aa55fc9c514c Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:25:37 +0100 Subject: [PATCH 027/135] Add test for readStatistics --- tests/test_reader.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 2f4712271..dabae6c82 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -78,4 +78,32 @@ def test_sudoku_reader(): input = f.readline() assert input == "sudoku" - deleteFile("model.sod") \ No newline at end of file + deleteFile("model.sod") + +def test_readStatistics(): + m = Model(problemName="readStats") + x = m.addVar(vtype="I") + y = m.addVar() + + m.addCons(x+y <= 3) + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + m2 = Model() + result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert result["Variables"]["total"] == 2 + assert result["Variables"]["integer"] == 1 + + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert type(result["Total Time"]) == float + assert result["Problem name"] == "readStats" + assert result["Presolved Problem name"] == "t_readStats" + assert type(result["primal-dual"]) == float + assert result["Solutions found"] == 1 + assert type(result["Gap (%)"]) == float + assert result["Presolved Constraints"] == {"initial": 1, "maximal": 1} + assert result["Variables"] == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} + assert result["Presolved Variables"] == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file From ef5d30c4e00059c6798b35277f4c4ab4df5153e5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:26:07 +0100 Subject: [PATCH 028/135] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1508233f..54e5f40c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added parser to read .stats file - Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams From 6b886e81ba0bf2e1a4973da3c3f810131fa0d980 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 14 Jun 2024 11:08:11 +0100 Subject: [PATCH 029/135] Mark comments --- src/pyscipopt/recipes/infeasibilities.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 2b1aa6439..fde70868c 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -16,7 +16,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): for c in model.getConss(): - slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) + slack[c.name] = model.addVar(lb=-float("inf"), name="s_"+c.name) model.addConsCoeff(c, slack[c.name], 1) binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857. @@ -26,13 +26,12 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.addCons(aux[c.name] >= -slack[c.name]) # modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation - aux_binary[c.name] = model.addVar(ub=1) + aux_binary[c.name] = model.addVar(vtype="B") model.addCons(binary[c.name]+aux_binary[c.name] == 1) model.addConsSOS1([aux[c.name], aux_binary[c.name]]) model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss())) model.hideOutput() - model.setPresolve(0) # just to be safe, maybe we can use presolving model.optimize() n_infeasibilities_detected = 0 From d47d9a0e227446adb2374f4538b529d6fcaea3c8 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:54:31 +0200 Subject: [PATCH 030/135] Add SCIPprintExternalCodes (#860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add SCIPprintExternalCodes * CHANGELOG in reverse chronological order --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 9 +++++++++ tests/test_model.py | 6 ++++++ 4 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1508233f..8f7a4bb69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) - Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index dd1bb5bf4..72beecfa2 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -546,6 +546,7 @@ cdef extern from "scip/scip.h": void SCIPsetMessagehdlrLogfile(SCIP* scip, const char* filename) SCIP_Real SCIPversion() void SCIPprintVersion(SCIP* scip, FILE* outfile) + void SCIPprintExternalCodes(SCIP* scip, FILE* outfile) SCIP_Real SCIPgetTotalTime(SCIP* scip) SCIP_Real SCIPgetSolvingTime(SCIP* scip) SCIP_Real SCIPgetReadingTime(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5b9c1b398..288100716 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1136,6 +1136,15 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) + def printExternalCodeVersions(self): + """Print external code versions, e.g. symmetry, non-linear solver, lp solver""" + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + + SCIPprintExternalCodes(self._scip, NULL) + + locale.setlocale(locale.LC_NUMERIC,user_locale) + def getProbName(self): """Retrieve problem name""" return bytes(SCIPgetProbName(self._scip)).decode('UTF-8') diff --git a/tests/test_model.py b/tests/test_model.py index 9dca52cde..0e6357d9d 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -364,3 +364,9 @@ def test_locale(): m.readProblem(os.path.join("tests", "data", "test_locale.cip")) locale.setlocale(locale.LC_NUMERIC,"") + + +def test_version_external_codes(): + scip = Model() + scip.printVersion() + scip.printExternalCodeVersions() From 19430ce7f5600898b316425ba8dc0b184bac45c3 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:08:04 +0100 Subject: [PATCH 031/135] Organize statistics in class --- src/pyscipopt/scip.pxi | 84 +++++++++++++++++++++++++++++++++++++++--- tests/test_reader.py | 30 +++++++-------- 2 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6d58a7956..1ef271e22 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -17,6 +17,7 @@ from posix.stdio cimport fileno from collections.abc import Iterable from itertools import repeat +from dataclasses import dataclass include "expr.pxi" include "lp.pxi" @@ -5095,14 +5096,16 @@ cdef class Model: Keyword arguments: filename -- name of the input file """ - available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Root LP Estimate", "Solutions found", "First Solution", - "Primal Bound", "Dual Bound", "Gap", "primal-dual"] - result = {} file = open(filename) data = file.readlines() + + assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved" + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found", "First Solution", "Primal Bound", + "Dual Bound", "Gap", "primal-dual"] + seen_cons = 0 for i, line in enumerate(data): split_line = line.split(":") @@ -5165,7 +5168,16 @@ cdef class Model: else: # it's a string result[cur_stat] = relevant_value - return result + treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", + "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", + "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} + treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + stats = Statistics(**treated_result) + stats._populate_remaining() + + return stats def getNLPs(self): """gets total number of LPs solved so far""" @@ -5509,6 +5521,66 @@ cdef class Model: """Get an estimation of the final tree size """ return SCIPgetTreesizeEstimation(self._scip) +@dataclass +class Statistics: + # Total time since model was created + total_time: float + # Time spent solving the problem + solving_time: float + # Time spent on presolving + presolving_time: float + # Time spent on reading + reading_time: float + # Time spent on copying + copying_time: float + # Name of problem + problem_name: str + # Name of presolved problem + presolved_problem_name: str + # Dictionary with number of variables by type + _variables: dict + # Dictionary with number of presolved variables by type + _presolved_variables: dict + # Dictionary with number of constraints by type + _constraints: dict + # Dictionary with number of presolved constraints by type + _presolved_constraints: dict + # The number of restarts it took to solve the problem (TODO: check this) + n_runs: int + # The number of nodes explored in the branch-and-bound tree + n_nodes: int + # number of found solutions + solutions_found: int + # objective value of first found solution + first_solution: float + # The best primal bound found + primal_bound: float + # The best dual bound found + dual_bound: float + # The gap between the primal and dual bounds + gap: float + # The primal-dual integral + primal_dual_integral: float + + def _populate_remaining(self): + self.n_vars: int = self._variables["total"] + self.n_binary_vars: int = self._variables["binary"] + self.n_implicit_integer_vars: int = self._variables["implicit"] + self.n_continuous_vars: int = self._variables["continuous"] + + self.n_presolved_vars: int = self._presolved_variables["total"] + self.n_presolved_binary_vars: int = self._presolved_variables["binary"] + self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] + self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] + + self.n_conss: int = self._constraints["initial"] + self.n_maximal_cons: int = self._constraints["maximal"] + + self.n_presolved_conss: int = self._presolved_constraints["initial"] + self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] + + + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index dabae6c82..71a04066b 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -87,23 +87,19 @@ def test_readStatistics(): m.addCons(x+y <= 3) m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - m2 = Model() - result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - assert result["Variables"]["total"] == 2 - assert result["Variables"]["integer"] == 1 + m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - assert type(result["Total Time"]) == float - assert result["Problem name"] == "readStats" - assert result["Presolved Problem name"] == "t_readStats" - assert type(result["primal-dual"]) == float - assert result["Solutions found"] == 1 - assert type(result["Gap (%)"]) == float - assert result["Presolved Constraints"] == {"initial": 1, "maximal": 1} - assert result["Variables"] == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} - assert result["Presolved Variables"] == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 31 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert type(result.total_time) == float + assert result.problem_name == "readStats" + assert result.presolved_problem_name == "t_readStats" + assert type(result.primal_dual_integral) == float + assert result.solutions_found == 1 + assert type(result.gap) == float + assert result._presolved_constraints == {"initial": 1, "maximal": 1} + assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} + assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file From 2e8dd6707038de39a17d47b308cee42d13221dc8 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:14:51 +0100 Subject: [PATCH 032/135] Add some comments --- src/pyscipopt/scip.pxi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4ef04f20b..65cca80ba 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5572,20 +5572,32 @@ class Statistics: primal_dual_integral: float def _populate_remaining(self): + # number of variables in the model self.n_vars: int = self._variables["total"] + # number of binary variables in the model self.n_binary_vars: int = self._variables["binary"] + # number of implicit integer variables in the model self.n_implicit_integer_vars: int = self._variables["implicit"] + # number of continuous variables in the model self.n_continuous_vars: int = self._variables["continuous"] + # number of variables in the presolved model self.n_presolved_vars: int = self._presolved_variables["total"] + # number of binary variables in the presolved model self.n_presolved_binary_vars: int = self._presolved_variables["binary"] + # number of implicit integer variables in the presolved model self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] + # number of continuous variables in the presolved model self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] + # number of initial constraints in the model self.n_conss: int = self._constraints["initial"] + # number of maximal constraints in the model self.n_maximal_cons: int = self._constraints["maximal"] + # number of initial constraints in the presolved model self.n_presolved_conss: int = self._presolved_constraints["initial"] + # number of maximal constraints in the presolved model self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] From 14a39a927dea089a13a4b20d375eb793636c03d6 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:30:15 +0100 Subject: [PATCH 033/135] Some more comments --- src/pyscipopt/scip.pxi | 11 ++++++----- tests/test_reader.py | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 65cca80ba..bf8af3ec9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5100,7 +5100,8 @@ cdef class Model: def readStatistics(self, filename): """ - Given a .stats file, reads it and returns a dictionary with some statistics. + Given a .stats file of a solved model, reads it and returns an instance of the Statistics class + holding some statistics. Keyword arguments: filename -- name of the input file @@ -5121,7 +5122,7 @@ cdef class Model: split_line[1] = split_line[1][:-1] # removing \n stat_name = split_line[0].strip() - if seen_cons == 2 and stat_name == "Constraints": + if seen_cons == 2 and stat_name == "Constraints": continue if stat_name in available_stats: @@ -5177,14 +5178,16 @@ cdef class Model: else: # it's a string result[cur_stat] = relevant_value + # changing keys to pythonic variable names treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + stats = Statistics(**treated_result) - stats._populate_remaining() + stats._populate_remaining() # retrieve different variable/constraint types from the variable/constraint dictionary return stats @@ -5600,8 +5603,6 @@ class Statistics: # number of maximal constraints in the presolved model self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] - - # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index 71a04066b..ff1af5c7a 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -86,8 +86,6 @@ def test_readStatistics(): y = m.addVar() m.addCons(x+y <= 3) - m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) From 044d961d3f653f34d7bdc4f8a3d6c9403e0b1e03 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 21 Jun 2024 14:09:33 +0100 Subject: [PATCH 034/135] Update Statistics class and documentation --- src/pyscipopt/scip.pxi | 181 +++++++++++++++++++++++++++++------------ tests/test_reader.py | 10 ++- 2 files changed, 136 insertions(+), 55 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index bf8af3ec9..56a1c01eb 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5182,13 +5182,11 @@ cdef class Model: treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution", "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) stats = Statistics(**treated_result) - stats._populate_remaining() # retrieve different variable/constraint types from the variable/constraint dictionary - return stats def getNLPs(self): @@ -5535,74 +5533,153 @@ cdef class Model: @dataclass class Statistics: - # Total time since model was created + """ + Attributes + ---------- + total_time : float + Total time since model was created + solving_time: float + Time spent solving the problem + presolving_time: float + Time spent on presolving + reading_time: float + Time spent on reading + copying_time: float + Time spent on copying + problem_name: str + Name of problem + presolved_problem_name: str + Name of presolved problem + _variables: dict + Dictionary with number of variables by type + _presolved_variables: dict + Dictionary with number of presolved variables by type + _constraints: dict + Dictionary with number of constraints by type + _presolved_constraints: dict + Dictionary with number of presolved constraints by type + n_nodes: int + The number of nodes explored in the branch-and-bound tree + n_solutions_found: int + number of found solutions + first_solution: float + objective value of first found solution + primal_bound: float + The best primal bound found + dual_bound: float + The best dual bound found + gap: float + The gap between the primal and dual bounds + primal_dual_integral: float + The primal-dual integral + n_vars: int + number of variables in the model + n_binary_vars: int + number of binary variables in the model + n_integer_vars: int + number of integer variables in the model + n_implicit_integer_vars: int + number of implicit integer variables in the model + n_continuous_vars: int + number of continuous variables in the model + n_presolved_vars: int + number of variables in the presolved model + n_presolved_continuous_vars: int + number of continuous variables in the presolved model + n_presolved_binary_vars: int + number of binary variables in the presolved model + n_presolved_integer_vars: int + number of integer variables in the presolved model + n_presolved_implicit_integer_vars: int + number of implicit integer variables in the presolved model + n_maximal_cons: int + number of maximal constraints in the model + n_initial_cons: int + number of initial constraints in the presolved model + n_presolved_maximal_cons + number of maximal constraints in the presolved model + n_presolved_conss + number of initial constraints in the model + """ + total_time: float - # Time spent solving the problem solving_time: float - # Time spent on presolving presolving_time: float - # Time spent on reading reading_time: float - # Time spent on copying copying_time: float - # Name of problem problem_name: str - # Name of presolved problem presolved_problem_name: str - # Dictionary with number of variables by type _variables: dict - # Dictionary with number of presolved variables by type _presolved_variables: dict - # Dictionary with number of constraints by type _constraints: dict - # Dictionary with number of presolved constraints by type _presolved_constraints: dict - # The number of restarts it took to solve the problem (TODO: check this) n_runs: int - # The number of nodes explored in the branch-and-bound tree n_nodes: int - # number of found solutions - solutions_found: int - # objective value of first found solution + n_solutions_found: int first_solution: float - # The best primal bound found primal_bound: float - # The best dual bound found dual_bound: float - # The gap between the primal and dual bounds gap: float - # The primal-dual integral primal_dual_integral: float - def _populate_remaining(self): - # number of variables in the model - self.n_vars: int = self._variables["total"] - # number of binary variables in the model - self.n_binary_vars: int = self._variables["binary"] - # number of implicit integer variables in the model - self.n_implicit_integer_vars: int = self._variables["implicit"] - # number of continuous variables in the model - self.n_continuous_vars: int = self._variables["continuous"] - - # number of variables in the presolved model - self.n_presolved_vars: int = self._presolved_variables["total"] - # number of binary variables in the presolved model - self.n_presolved_binary_vars: int = self._presolved_variables["binary"] - # number of implicit integer variables in the presolved model - self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] - # number of continuous variables in the presolved model - self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] - - # number of initial constraints in the model - self.n_conss: int = self._constraints["initial"] - # number of maximal constraints in the model - self.n_maximal_cons: int = self._constraints["maximal"] - - # number of initial constraints in the presolved model - self.n_presolved_conss: int = self._presolved_constraints["initial"] - # number of maximal constraints in the presolved model - self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] - + # unpacking the _variables, _presolved_variables, _constraints + # _presolved_constraints dictionaries + @property + def n_vars(self): + return self._variables["total"] + + @property + def n_binary_vars(self): + return self._variables["binary"] + + @property + def n_integer_vars(self): + return self._variables["integer"] + + @property + def n_implicit_integer_vars(self): + return self._variables["implicit"] + + @property + def n_continuous_vars(self): + return self._variables["continuous"] + + @property + def n_presolved_vars(self): + return self._presolved_variables["total"] + + @property + def n_presolved_binary_vars(self): + return self._presolved_variables["binary"] + + @property + def n_presolved_integer_vars(self): + return self._presolved_variables["integer"] + + @property + def n_presolved_implicit_integer_vars(self): + return self._presolved_variables["implicit"] + + @property + def n_presolved_continuous_vars(self): + return self._presolved_variables["continuous"] + + @property + def n_conss(self): + return self._constraints["initial"] + + @property + def n_maximal_cons(self): + return self._constraints["maximal"] + + @property + def n_presolved_conss(self): + return self._presolved_constraints["initial"] + + @property + def n_presolved_maximal_cons(self): + return self._presolved_constraints["maximal"] + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index ff1af5c7a..4bc976c76 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -91,13 +91,17 @@ def test_readStatistics(): m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 31 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 19 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 assert type(result.total_time) == float assert result.problem_name == "readStats" assert result.presolved_problem_name == "t_readStats" assert type(result.primal_dual_integral) == float - assert result.solutions_found == 1 + assert result.n_solutions_found == 1 assert type(result.gap) == float assert result._presolved_constraints == {"initial": 1, "maximal": 1} assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} - assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file + assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} + assert result.n_vars == 2 + assert result.n_presolved_vars == 0 + assert result.n_binary_vars == 0 + assert result.n_integer_vars == 1 \ No newline at end of file From ab94980037fe09ce6630e0ce92ee2c52de767992 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 21 Jun 2024 14:09:39 +0100 Subject: [PATCH 035/135] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3105f56..6aafd04e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Created Statistics class - Added parser to read .stats file - Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) - Added recipe with reformulation for detecting infeasible constraints From feb38fd43e7ddbfd0350ea3538a5564a5aaea8d1 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:27:12 +0200 Subject: [PATCH 036/135] Add SCIP_STATUS_(PRIMAL;DUAL)LIMIT (#864) * Add SCIP_STATUS_(PRIMAL;DUAL)LIMIT * Update README to SCIP 9.1 * Change version to 5.1.0 --- .github/workflows/coverage.yml | 2 +- CHANGELOG.md | 1 + README.md | 3 ++- setup.py | 2 +- src/pyscipopt/_version.py | 2 +- src/pyscipopt/scip.pxd | 4 +++- src/pyscipopt/scip.pxi | 6 ++++++ tests/test_model.py | 17 +++++++++++++++++ 8 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index da8979a41..b344ab396 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,6 +1,6 @@ name: Run tests with coverage env: - version: 9.0.0 + version: 9.1.0 on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7a4bb69..371e451d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added SCIP_STATUS_DUALLIMIT and SCIP_STATUS_PRIMALLIMIT - Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) - Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests diff --git a/README.md b/README.md index d30ebdbb5..345d4b00c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ The following table summarizes which version of PySCIPOpt is required for a give |SCIP| PySCIPOpt | |----|----| -9.0 | 5.x +9.1 | 5.1+ +9.0 | 5.0.x 8.0 | 4.x 7.0 | 3.x 6.0 | 2.x diff --git a/setup.py b/setup.py index dfdd33fbc..5c3b74a43 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ setup( name="PySCIPOpt", - version="5.0.1", + version="5.1.0", description="Python interface and modeling environment for SCIP", long_description=long_description, long_description_content_type="text/markdown", diff --git a/src/pyscipopt/_version.py b/src/pyscipopt/_version.py index 3d96b2761..a5e451313 100644 --- a/src/pyscipopt/_version.py +++ b/src/pyscipopt/_version.py @@ -1 +1 @@ -__version__ = '5.0.1' +__version__ = '5.1.0' diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 72beecfa2..f6fe8e8d8 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -78,7 +78,9 @@ cdef extern from "scip/scip.h": SCIP_STATUS SCIP_STATUS_GAPLIMIT SCIP_STATUS SCIP_STATUS_SOLLIMIT SCIP_STATUS SCIP_STATUS_BESTSOLLIMIT - SCIP_STATUS SCIP_STATUS_RESTARTLIMIT + SCIP_STATUS SCIP_STATUS_RESTARTLIMIT + SCIP_STATUS SCIP_STATUS_PRIMALLIMIT + SCIP_STATUS SCIP_STATUS_DUALLIMIT SCIP_STATUS SCIP_STATUS_OPTIMAL SCIP_STATUS SCIP_STATUS_INFEASIBLE SCIP_STATUS SCIP_STATUS_UNBOUNDED diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 288100716..d12cd5c86 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -106,6 +106,8 @@ cdef class PY_SCIP_STATUS: SOLLIMIT = SCIP_STATUS_SOLLIMIT BESTSOLLIMIT = SCIP_STATUS_BESTSOLLIMIT RESTARTLIMIT = SCIP_STATUS_RESTARTLIMIT + PRIMALLIMIT = SCIP_STATUS_PRIMALLIMIT + DUALLIMIT = SCIP_STATUS_DUALLIMIT OPTIMAL = SCIP_STATUS_OPTIMAL INFEASIBLE = SCIP_STATUS_INFEASIBLE UNBOUNDED = SCIP_STATUS_UNBOUNDED @@ -4992,6 +4994,10 @@ cdef class Model: return "bestsollimit" elif stat == SCIP_STATUS_RESTARTLIMIT: return "restartlimit" + elif stat == SCIP_STATUS_PRIMALLIMIT: + return "primallimit" + elif stat == SCIP_STATUS_DUALLIMIT: + return "duallimit" else: return "unknown" diff --git a/tests/test_model.py b/tests/test_model.py index 0e6357d9d..80be96998 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -370,3 +370,20 @@ def test_version_external_codes(): scip = Model() scip.printVersion() scip.printExternalCodeVersions() + +def test_primal_dual_limit(): + from pyscipopt import SCIP_PARAMSETTING + scip = Model() + scip.readProblem("tests/data/10teams.mps") + scip.setParam("limits/primal", 1000000) + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.setSeparating(SCIP_PARAMSETTING.OFF) + scip.setPresolve(SCIP_PARAMSETTING.OFF) + scip.optimize() + assert(scip.getStatus() == "primallimit"), scip.getStatus() + + scip = Model() + scip.readProblem("tests/data/10teams.mps") + scip.setParam("limits/dual", -10) + scip.optimize() + assert (scip.getStatus() == "duallimit"), scip.getStatus() From 4730401475d700635315d2610f2c3be4f373eefd Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:47:52 +0200 Subject: [PATCH 037/135] Mt/status limits (#865) * Add SCIP_STATUS_(PRIMAL;DUAL)LIMIT * Update README to SCIP 9.1 * Change version to 5.1.0 * Add non data based tests --- tests/test_model.py | 60 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 80be96998..585401745 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,7 +1,8 @@ import pytest import os +import itertools -from pyscipopt import Model, SCIP_STAGE +from pyscipopt import Model, SCIP_STAGE, SCIP_PARAMSETTING, quicksum def test_model(): # create solver instance @@ -372,18 +373,63 @@ def test_version_external_codes(): scip.printExternalCodeVersions() def test_primal_dual_limit(): - from pyscipopt import SCIP_PARAMSETTING - scip = Model() - scip.readProblem("tests/data/10teams.mps") - scip.setParam("limits/primal", 1000000) + + def build_scip_model(): + scip = Model() + # Make a basic minimum spanning hypertree problem + # Let's construct a problem with 15 vertices and 40 hyperedges. The hyperedges are our variables. + v = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + e = {} + for i in range(40): + e[i] = scip.addVar(vtype='B', name='hyperedge_{}'.format(i)) + + # Construct a dummy incident matrix + A = [[1, 2, 3], [2, 3, 4, 5], [4, 9], [7, 8, 9], [0, 8, 9], + [1, 6, 8], [0, 1, 2, 9], [0, 3, 5, 7, 8], [2, 3], [6, 9], + [5, 8], [1, 9], [2, 7, 8, 9], [3, 8], [2, 4], + [0, 1], [0, 1, 4], [2, 5], [1, 6, 7, 8], [1, 3, 4, 7, 9], + [11, 14], [0, 2, 14], [2, 7, 8, 10], [0, 7, 10, 14], [1, 6, 11], + [5, 8, 12], [3, 4, 14], [0, 12], [4, 8, 12], [4, 7, 9, 11, 14], + [3, 12, 13], [2, 3, 4, 7, 11, 14], [0, 5, 10], [2, 7, 13], [4, 9, 14], + [7, 8, 10], [10, 13], [3, 6, 11], [2, 8, 9, 11], [3, 13]] + + # Create a cost vector for each hyperedge + c = [2.5, 2.9, 3.2, 7, 1.2, 0.5, + 8.6, 9, 6.7, 0.3, 4, + 0.9, 1.8, 6.7, 3, 2.1, + 1.8, 1.9, 0.5, 4.3, 5.6, + 3.8, 4.6, 4.1, 1.8, 2.5, + 3.2, 3.1, 0.5, 1.8, 9.2, + 2.5, 6.4, 2.1, 1.9, 2.7, + 1.6, 0.7, 8.2, 7.9, 3] + + # Add constraint that your hypertree touches all vertices + scip.addCons(quicksum((len(A[i]) - 1) * e[i] for i in range(len(A))) == len(v) - 1) + + # Now add the sub-tour elimination constraints. + for i in range(2, len(v) + 1): + for combination in itertools.combinations(v, i): + scip.addCons( + quicksum(max(len(set(combination) & set(A[j])) - 1, 0) * e[j] for j in range(len(A))) <= i - 1, + name='cons_{}'.format(combination)) + + # Add objective to minimise the cost + scip.setObjective(quicksum(c[i] * e[i] for i in range(len(A))), sense='minimize') + return scip + + scip = build_scip_model() + scip.setParam("limits/primal", 100) scip.setHeuristics(SCIP_PARAMSETTING.OFF) scip.setSeparating(SCIP_PARAMSETTING.OFF) scip.setPresolve(SCIP_PARAMSETTING.OFF) + scip.setParam("branching/random/priority", 1000000) scip.optimize() assert(scip.getStatus() == "primallimit"), scip.getStatus() - scip = Model() - scip.readProblem("tests/data/10teams.mps") + scip = build_scip_model() + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.setSeparating(SCIP_PARAMSETTING.OFF) + scip.setPresolve(SCIP_PARAMSETTING.OFF) scip.setParam("limits/dual", -10) scip.optimize() assert (scip.getStatus() == "duallimit"), scip.getStatus() From 50139ec82b2526841b734fa9da3db60c6f31f898 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 21 Jun 2024 15:56:29 +0200 Subject: [PATCH 038/135] Release workflow based on manual input (#866) * Release workflow based on manual input * Avoid line tracing when release workflow is run --------- Co-authored-by: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> --- .github/workflows/build_wheels.yml | 11 ++++++++--- pyproject.toml | 8 ++++---- setup.py | 7 +++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index ee82d6a4c..59ab53863 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -1,9 +1,13 @@ name: Build on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + scip_version: + type: string + description: SCIPOptSuite deployment version + required: true + default: "0.4.0" jobs: build_wheels: @@ -32,6 +36,7 @@ jobs: CIBW_ARCHS: ${{ matrix.arch }} CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: "pytest {project}/tests" + SCIPOPTSUITE_VERSION: ${{ github.event.inputs.scip_version }} - uses: actions/upload-artifact@v3 with: diff --git a/pyproject.toml b/pyproject.toml index dac575de9..a7fb53c63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ skip="pp*" # currently doesn't work with PyPy skip="pp* cp36* cp37* *musllinux*" before-all = [ "(apt-get update && apt-get install --yes wget) || yum install -y wget zlib libgfortran || brew install wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.1.0/libscip-linux.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-linux.zip -O scip.zip", "unzip scip.zip", "mv scip_install scip" ] @@ -57,9 +57,9 @@ before-all = ''' #!/bin/bash brew install wget zlib gcc if [[ $CIBW_ARCHS == *"arm"* ]]; then - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.1.0/libscip-macos-arm.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-macos-arm.zip -O scip.zip else - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.1.0/libscip-macos.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-macos.zip -O scip.zip fi unzip scip.zip mv scip_install src/scip @@ -75,7 +75,7 @@ repair-wheel-command = [ skip="pp* cp36* cp37*" before-all = [ "choco install 7zip wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.1.0/libscip-windows.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-windows.zip -O scip.zip", "\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"", "mv .\\scip-test\\scip_install .\\test", "mv .\\test .\\scip" diff --git a/setup.py b/setup.py index 5c3b74a43..7918c17dd 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,10 @@ ext = ".pyx" if use_cython else ".c" + on_github_actions = os.getenv('GITHUB_ACTIONS') == 'true' +release_mode = os.getenv('SCIPOPTSUITE_VERSION') is not None +compile_with_line_tracing = on_github_actions and not release_mode extensions = [ Extension( @@ -94,12 +97,12 @@ libraries=[libname], extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, - define_macros= [("CYTHON_TRACE_NOGIL", 1), ("CYTHON_TRACE", 1)] if on_github_actions else [] + define_macros= [("CYTHON_TRACE_NOGIL", 1), ("CYTHON_TRACE", 1)] if compile_with_line_tracing else [] ) ] if use_cython: - extensions = cythonize(extensions, compiler_directives={"language_level": 3, "linetrace": on_github_actions}) + extensions = cythonize(extensions, compiler_directives={"language_level": 3, "linetrace": compile_with_line_tracing}) with open("README.md") as f: long_description = f.read() From c50ce12256974056443b063645a9c9e6f2c4233c Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 21 Jun 2024 17:19:30 +0200 Subject: [PATCH 039/135] Fix release workflow & control uploading to pypi with a parameter (#867) * Test workflow * Fix usage of env variable * Add missing v * Skip locale test in release * Reenable commented out wheels, add parameter to control uploading to pypi --- .github/workflows/build_wheels.yml | 10 ++++++++-- pyproject.toml | 8 ++++---- tests/test_model.py | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 59ab53863..52bcbcda8 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -1,4 +1,4 @@ -name: Build +name: Build wheels on: workflow_dispatch: @@ -7,7 +7,12 @@ on: type: string description: SCIPOptSuite deployment version required: true - default: "0.4.0" + default: "v0.4.0" + upload_to_pypi: + type: boolean + description: Whether the artifacts should be uploaded to PyPI + required: false + default: false jobs: build_wheels: @@ -59,6 +64,7 @@ jobs: upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest + if: github.event.inputs.upload_to_pypi == 'true' steps: - uses: actions/download-artifact@v3 with: diff --git a/pyproject.toml b/pyproject.toml index a7fb53c63..1d8bce122 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ skip="pp*" # currently doesn't work with PyPy skip="pp* cp36* cp37* *musllinux*" before-all = [ "(apt-get update && apt-get install --yes wget) || yum install -y wget zlib libgfortran || brew install wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-linux.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-linux.zip -O scip.zip", "unzip scip.zip", "mv scip_install scip" ] @@ -57,9 +57,9 @@ before-all = ''' #!/bin/bash brew install wget zlib gcc if [[ $CIBW_ARCHS == *"arm"* ]]; then - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-macos-arm.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-macos-arm.zip -O scip.zip else - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-macos.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-macos.zip -O scip.zip fi unzip scip.zip mv scip_install src/scip @@ -75,7 +75,7 @@ repair-wheel-command = [ skip="pp* cp36* cp37*" before-all = [ "choco install 7zip wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/$SCIPOPTSUITE_VERSION/libscip-windows.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-windows.zip -O scip.zip", "\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"", "mv .\\scip-test\\scip_install .\\test", "mv .\\test .\\scip" diff --git a/tests/test_model.py b/tests/test_model.py index 585401745..c5eaeea12 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -345,6 +345,10 @@ def test_setLogFile_none(): os.remove(log_file_name) def test_locale(): + on_release = os.getenv('SCIPOPTSUITE_VERSION') is not None + if on_release: + pytest.skip("Skip this test on release builds") + import locale m = Model() From 147f067160065cf4664751e773f213a747eb5731 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Sat, 22 Jun 2024 10:11:47 +0200 Subject: [PATCH 040/135] More fixes to the release workflow (#868) * Test workflow * Fix usage of env variable * Add missing v * Skip locale test in release * Reenable commented out wheels, add parameter to control uploading to pypi * Try out global env var * Try out without curly braces on linux * Check if windows works * Add env var definition back to build_wheels workflow * Fix sending env variables in release build * Reenable other OSs * Append existing env vars * Hardcode the scipoptsuite-deploy release version for now * Change runner to macos-13 * Make locale test less annoying --- .github/workflows/build_wheels.yml | 13 ++++++------- pyproject.toml | 8 ++++---- tests/test_model.py | 19 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 52bcbcda8..8dc7dc1ba 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -3,11 +3,11 @@ name: Build wheels on: workflow_dispatch: inputs: - scip_version: - type: string - description: SCIPOptSuite deployment version - required: true - default: "v0.4.0" + # scip_version: + # type: string + # description: SCIPOptSuite deployment version + # required: true + # default: "v0.4.0" upload_to_pypi: type: boolean description: Whether the artifacts should be uploaded to PyPI @@ -25,7 +25,7 @@ jobs: arch: x86_64 - os: macos-14 arch: arm64 - - os: macos-latest + - os: macos-13 arch: x86_64 - os: windows-latest arch: AMD64 @@ -41,7 +41,6 @@ jobs: CIBW_ARCHS: ${{ matrix.arch }} CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: "pytest {project}/tests" - SCIPOPTSUITE_VERSION: ${{ github.event.inputs.scip_version }} - uses: actions/upload-artifact@v3 with: diff --git a/pyproject.toml b/pyproject.toml index 1d8bce122..738c6f79c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ skip="pp*" # currently doesn't work with PyPy skip="pp* cp36* cp37* *musllinux*" before-all = [ "(apt-get update && apt-get install --yes wget) || yum install -y wget zlib libgfortran || brew install wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-linux.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-linux.zip -O scip.zip", "unzip scip.zip", "mv scip_install scip" ] @@ -57,9 +57,9 @@ before-all = ''' #!/bin/bash brew install wget zlib gcc if [[ $CIBW_ARCHS == *"arm"* ]]; then - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-macos-arm.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-macos-arm.zip -O scip.zip else - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-macos.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-macos.zip -O scip.zip fi unzip scip.zip mv scip_install src/scip @@ -75,7 +75,7 @@ repair-wheel-command = [ skip="pp* cp36* cp37*" before-all = [ "choco install 7zip wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/${SCIPOPTSUITE_VERSION}/libscip-windows.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-windows.zip -O scip.zip", "\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"", "mv .\\scip-test\\scip_install .\\test", "mv .\\test .\\scip" diff --git a/tests/test_model.py b/tests/test_model.py index c5eaeea12..d50437417 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -356,19 +356,18 @@ def test_locale(): try: locale.setlocale(locale.LC_NUMERIC, "pt_PT") - except Exception: - pytest.skip("pt_PT locale was not found. It might need to be installed.") - - assert locale.str(1.1) == "1,1" + assert locale.str(1.1) == "1,1" - m.writeProblem("model.cip") + m.writeProblem("model.cip") - with open("model.cip") as file: - assert "1,1" not in file.read() - - m.readProblem(os.path.join("tests", "data", "test_locale.cip")) + with open("model.cip") as file: + assert "1,1" not in file.read() + + m.readProblem(os.path.join("tests", "data", "test_locale.cip")) - locale.setlocale(locale.LC_NUMERIC,"") + locale.setlocale(locale.LC_NUMERIC,"") + except Exception: + pytest.skip("pt_PT locale was not found. It might need to be installed.") def test_version_external_codes(): From 75ef4205026fd670a90c21004951a0f5b64d1b16 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Sat, 22 Jun 2024 12:17:10 +0200 Subject: [PATCH 041/135] Better way to mark release mode in pyproject & Ability to upload to test-pypi from workflow (#869) * Better way to mark release mode in pyproject * Accept test_pypi input * Update workflow inputs * Update default values --- .github/workflows/build_wheels.yml | 19 +++++++++++++++++-- pyproject.toml | 6 +++--- setup.py | 2 +- tests/test_model.py | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 8dc7dc1ba..a397fbc91 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -10,9 +10,14 @@ on: # default: "v0.4.0" upload_to_pypi: type: boolean - description: Whether the artifacts should be uploaded to PyPI + description: Should upload required: false - default: false + default: true + test_pypi: + type: boolean + description: Use Test PyPI + required: false + default: true jobs: build_wheels: @@ -71,7 +76,17 @@ jobs: path: dist - uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event.inputs.test_pypi == 'false' with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} verbose: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event.inputs.test_pypi == 'true' + with: + repository-url: https://test.pypi.org/legacy/ + user: __token__ + password: ${{ secrets.TESTPYPI_API_TOKEN }} + verbose: true + diff --git a/pyproject.toml b/pyproject.toml index 738c6f79c..43e97359d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ before-all = [ "unzip scip.zip", "mv scip_install scip" ] -environment = { SCIPOPTDIR="$(pwd)/scip", LD_LIBRARY_PATH="$(pwd)/scip/lib:$LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/scip/lib/pkgconfig:$PKG_CONFIG_PATH"} +environment = { SCIPOPTDIR="$(pwd)/scip", LD_LIBRARY_PATH="$(pwd)/scip/lib:$LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} [tool.cibuildwheel.macos] @@ -64,7 +64,7 @@ fi unzip scip.zip mv scip_install src/scip ''' -environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH"} +environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} repair-wheel-command = [ "delocate-listdeps {wheel}", "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}", @@ -81,5 +81,5 @@ before-all = [ "mv .\\test .\\scip" ] before-build = "pip install delvewheel" -environment = { SCIPOPTDIR='D:\\a\\PySCIPOpt\\PySCIPOpt\\scip' } +environment = { SCIPOPTDIR='D:\\a\\PySCIPOpt\\PySCIPOpt\\scip', RELEASE="true" } repair-wheel-command = "delvewheel repair --add-path c:/bin;c:/lib;c:/bin/src;c:/lib/src;D:/a/PySCIPOpt/PySCIPOpt/scip/;D:/a/PySCIPOpt/PySCIPOpt/scip/lib/;D:/a/PySCIPOpt/PySCIPOpt/scip/bin/ -w {dest_dir} {wheel}" diff --git a/setup.py b/setup.py index 7918c17dd..9424b5749 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ on_github_actions = os.getenv('GITHUB_ACTIONS') == 'true' -release_mode = os.getenv('SCIPOPTSUITE_VERSION') is not None +release_mode = os.getenv('RELEASE') == 'true' compile_with_line_tracing = on_github_actions and not release_mode extensions = [ diff --git a/tests/test_model.py b/tests/test_model.py index d50437417..96e7e51a9 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -345,7 +345,7 @@ def test_setLogFile_none(): os.remove(log_file_name) def test_locale(): - on_release = os.getenv('SCIPOPTSUITE_VERSION') is not None + on_release = os.getenv('RELEASE') is not None if on_release: pytest.skip("Skip this test on release builds") From c40518787d6fa8da7ff642b5870ed31b4c0df891 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Sat, 22 Jun 2024 13:13:26 +0200 Subject: [PATCH 042/135] Update changelog & version (#870) --- CHANGELOG.md | 6 ++++++ src/pyscipopt/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 371e451d1..3be8e99bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased ### Added +### Fixed +### Changed +### Removed + +## 5.1.1 - 2024-06-22 +### Added - Added SCIP_STATUS_DUALLIMIT and SCIP_STATUS_PRIMALLIMIT - Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) - Added recipe with reformulation for detecting infeasible constraints diff --git a/src/pyscipopt/_version.py b/src/pyscipopt/_version.py index a5e451313..40f5033b9 100644 --- a/src/pyscipopt/_version.py +++ b/src/pyscipopt/_version.py @@ -1 +1 @@ -__version__ = '5.1.0' +__version__ = '5.1.1' From 9996848eb0f2e22da592f956eaf2b00169233bd0 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 22 Jun 2024 13:42:04 +0100 Subject: [PATCH 043/135] Update documentation --- src/pyscipopt/scip.pxi | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 56a1c01eb..b65c4ee09 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5549,15 +5549,7 @@ class Statistics: problem_name: str Name of problem presolved_problem_name: str - Name of presolved problem - _variables: dict - Dictionary with number of variables by type - _presolved_variables: dict - Dictionary with number of presolved variables by type - _constraints: dict - Dictionary with number of constraints by type - _presolved_constraints: dict - Dictionary with number of presolved constraints by type + Name of presolved problem n_nodes: int The number of nodes explored in the branch-and-bound tree n_solutions_found: int @@ -5596,9 +5588,9 @@ class Statistics: number of maximal constraints in the model n_initial_cons: int number of initial constraints in the presolved model - n_presolved_maximal_cons + n_presolved_maximal_cons: int number of maximal constraints in the presolved model - n_presolved_conss + n_presolved_conss: int number of initial constraints in the model """ @@ -5609,10 +5601,10 @@ class Statistics: copying_time: float problem_name: str presolved_problem_name: str - _variables: dict - _presolved_variables: dict - _constraints: dict - _presolved_constraints: dict + _variables: dict # Dictionary with number of variables by type + _presolved_variables: dict # Dictionary with number of presolved variables by type + _constraints: dict # Dictionary with number of constraints by type + _presolved_constraints: dict # Dictionary with number of presolved constraints by type n_runs: int n_nodes: int n_solutions_found: int From b12f1847edff57ea56956a446f66a32b50a56a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:55:43 +0100 Subject: [PATCH 044/135] Add parser to read SCIP statistics (#859) * Add readStatistics * Add test for readStatistics * Update CHANGELOG * Organize statistics in class * Add some comments * Some more comments * Update Statistics class and documentation * Update CHANGELOG --- CHANGELOG.md | 2 + src/pyscipopt/scip.pxi | 233 +++++++++++++++++++++++++++++++++++++++++ tests/test_model.py | 1 - tests/test_reader.py | 28 ++++- 4 files changed, 262 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be8e99bf..43cb87181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased ### Added +- Created Statistics class +- Added parser to read .stats file ### Fixed ### Changed ### Removed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d12cd5c86..943e27f93 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -17,6 +17,7 @@ from posix.stdio cimport fileno from collections.abc import Iterable from itertools import repeat +from dataclasses import dataclass include "expr.pxi" include "lp.pxi" @@ -5102,6 +5103,97 @@ cdef class Model: PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) locale.setlocale(locale.LC_NUMERIC,user_locale) + + def readStatistics(self, filename): + """ + Given a .stats file of a solved model, reads it and returns an instance of the Statistics class + holding some statistics. + + Keyword arguments: + filename -- name of the input file + """ + result = {} + file = open(filename) + data = file.readlines() + + assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved" + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found", "First Solution", "Primal Bound", + "Dual Bound", "Gap", "primal-dual"] + + seen_cons = 0 + for i, line in enumerate(data): + split_line = line.split(":") + split_line[1] = split_line[1][:-1] # removing \n + stat_name = split_line[0].strip() + + if seen_cons == 2 and stat_name == "Constraints": + continue + + if stat_name in available_stats: + cur_stat = split_line[0].strip() + relevant_value = split_line[1].strip() + + if stat_name == "Variables": + relevant_value = relevant_value[:-1] # removing ")" + var_stats = {} + split_var = relevant_value.split("(") + var_stats["total"] = int(split_var[0]) + split_var = split_var[1].split(",") + + for var_type in split_var: + split_result = var_type.strip().split(" ") + var_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-2]: + result["Variables"] = var_stats + else: + result["Presolved Variables"] = var_stats + + continue + + if stat_name == "Constraints": + seen_cons += 1 + con_stats = {} + split_con = relevant_value.split(",") + for con_type in split_con: + split_result = con_type.strip().split(" ") + con_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-3]: + result["Constraints"] = con_stats + else: + result["Presolved Constraints"] = con_stats + continue + + relevant_value = relevant_value.split(" ")[0] + if stat_name == "Problem name": + if "Original" in data[i-1]: + result["Problem name"] = relevant_value + else: + result["Presolved Problem name"] = relevant_value + continue + + if stat_name == "Gap": + result["Gap (%)"] = float(relevant_value[:-1]) + continue + + if _is_number(relevant_value): + result[cur_stat] = float(relevant_value) + else: # it's a string + result[cur_stat] = relevant_value + + # changing keys to pythonic variable names + treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", + "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution", + "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} + treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + + stats = Statistics(**treated_result) + return stats def getNLPs(self): """gets total number of LPs solved so far""" @@ -5445,6 +5537,147 @@ cdef class Model: """Get an estimation of the final tree size """ return SCIPgetTreesizeEstimation(self._scip) +@dataclass +class Statistics: + """ + Attributes + ---------- + total_time : float + Total time since model was created + solving_time: float + Time spent solving the problem + presolving_time: float + Time spent on presolving + reading_time: float + Time spent on reading + copying_time: float + Time spent on copying + problem_name: str + Name of problem + presolved_problem_name: str + Name of presolved problem + n_nodes: int + The number of nodes explored in the branch-and-bound tree + n_solutions_found: int + number of found solutions + first_solution: float + objective value of first found solution + primal_bound: float + The best primal bound found + dual_bound: float + The best dual bound found + gap: float + The gap between the primal and dual bounds + primal_dual_integral: float + The primal-dual integral + n_vars: int + number of variables in the model + n_binary_vars: int + number of binary variables in the model + n_integer_vars: int + number of integer variables in the model + n_implicit_integer_vars: int + number of implicit integer variables in the model + n_continuous_vars: int + number of continuous variables in the model + n_presolved_vars: int + number of variables in the presolved model + n_presolved_continuous_vars: int + number of continuous variables in the presolved model + n_presolved_binary_vars: int + number of binary variables in the presolved model + n_presolved_integer_vars: int + number of integer variables in the presolved model + n_presolved_implicit_integer_vars: int + number of implicit integer variables in the presolved model + n_maximal_cons: int + number of maximal constraints in the model + n_initial_cons: int + number of initial constraints in the presolved model + n_presolved_maximal_cons: int + number of maximal constraints in the presolved model + n_presolved_conss: int + number of initial constraints in the model + """ + + total_time: float + solving_time: float + presolving_time: float + reading_time: float + copying_time: float + problem_name: str + presolved_problem_name: str + _variables: dict # Dictionary with number of variables by type + _presolved_variables: dict # Dictionary with number of presolved variables by type + _constraints: dict # Dictionary with number of constraints by type + _presolved_constraints: dict # Dictionary with number of presolved constraints by type + n_runs: int + n_nodes: int + n_solutions_found: int + first_solution: float + primal_bound: float + dual_bound: float + gap: float + primal_dual_integral: float + + # unpacking the _variables, _presolved_variables, _constraints + # _presolved_constraints dictionaries + @property + def n_vars(self): + return self._variables["total"] + + @property + def n_binary_vars(self): + return self._variables["binary"] + + @property + def n_integer_vars(self): + return self._variables["integer"] + + @property + def n_implicit_integer_vars(self): + return self._variables["implicit"] + + @property + def n_continuous_vars(self): + return self._variables["continuous"] + + @property + def n_presolved_vars(self): + return self._presolved_variables["total"] + + @property + def n_presolved_binary_vars(self): + return self._presolved_variables["binary"] + + @property + def n_presolved_integer_vars(self): + return self._presolved_variables["integer"] + + @property + def n_presolved_implicit_integer_vars(self): + return self._presolved_variables["implicit"] + + @property + def n_presolved_continuous_vars(self): + return self._presolved_variables["continuous"] + + @property + def n_conss(self): + return self._constraints["initial"] + + @property + def n_maximal_cons(self): + return self._constraints["maximal"] + + @property + def n_presolved_conss(self): + return self._presolved_constraints["initial"] + + @property + def n_presolved_maximal_cons(self): + return self._presolved_constraints["maximal"] + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_model.py b/tests/test_model.py index 96e7e51a9..fe0ed607f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -369,7 +369,6 @@ def test_locale(): except Exception: pytest.skip("pt_PT locale was not found. It might need to be installed.") - def test_version_external_codes(): scip = Model() scip.printVersion() diff --git a/tests/test_reader.py b/tests/test_reader.py index 2f4712271..4bc976c76 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -78,4 +78,30 @@ def test_sudoku_reader(): input = f.readline() assert input == "sudoku" - deleteFile("model.sod") \ No newline at end of file + deleteFile("model.sod") + +def test_readStatistics(): + m = Model(problemName="readStats") + x = m.addVar(vtype="I") + y = m.addVar() + + m.addCons(x+y <= 3) + m.hideOutput() + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 19 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert type(result.total_time) == float + assert result.problem_name == "readStats" + assert result.presolved_problem_name == "t_readStats" + assert type(result.primal_dual_integral) == float + assert result.n_solutions_found == 1 + assert type(result.gap) == float + assert result._presolved_constraints == {"initial": 1, "maximal": 1} + assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} + assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} + assert result.n_vars == 2 + assert result.n_presolved_vars == 0 + assert result.n_binary_vars == 0 + assert result.n_integer_vars == 1 \ No newline at end of file From 24fcb2a33785bbf35bf0ae92d1e5bf06f65e69a3 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:10:28 +0100 Subject: [PATCH 045/135] Expand readStats to unbounded, infeasible, user-interrupted prorblems --- src/pyscipopt/scip.pxi | 56 ++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 943e27f93..146e2beca 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5116,11 +5116,23 @@ cdef class Model: file = open(filename) data = file.readlines() - assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved" + if "optimal solution found" in data[0]: + result["status"] = "optimal" + elif "infeasible" in data[0]: + result["status"] = "infeasible" + elif "unbounded" in data[0]: + result["status"] = "unbounded" + elif "limit reached" in data[0]: + result["status"] = "user_interrupt" + else: + raise "readStatistics can only be called if the problem was solved" + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Solutions found", "First Solution", "Primal Bound", - "Dual Bound", "Gap", "primal-dual"] + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found"] + + if result["status"] in ["optimal", "user_interrupt"]: + available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) seen_cons = 0 for i, line in enumerate(data): @@ -5181,15 +5193,24 @@ cdef class Model: if _is_number(relevant_value): result[cur_stat] = float(relevant_value) + if cur_stat == "Solutions found" and result[cur_stat] == 0: + break else: # it's a string result[cur_stat] = relevant_value # changing keys to pythonic variable names - treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", - "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", + "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution", - "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} + + if result["status"] in ["optimal", "user_interrupt"]: + if result["Solutions found"] > 0: + treated_keys["First Solution"] = "first_solution" + treated_keys["Primal Bound"] = "primal_bound" + treated_keys["Dual Bound"] = "dual_bound" + treated_keys["Gap (%)"] = "gap" + treated_keys["primal-dual"] = "primal_dual_integral" treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) stats = Statistics(**treated_result) @@ -5542,6 +5563,8 @@ class Statistics: """ Attributes ---------- + status: str + Status of the problem (optimal solution found, infeasible, etc.) total_time : float Total time since model was created solving_time: float @@ -5600,6 +5623,7 @@ class Statistics: number of initial constraints in the model """ + status: str total_time: float solving_time: float presolving_time: float @@ -5611,14 +5635,14 @@ class Statistics: _presolved_variables: dict # Dictionary with number of presolved variables by type _constraints: dict # Dictionary with number of constraints by type _presolved_constraints: dict # Dictionary with number of presolved constraints by type - n_runs: int - n_nodes: int - n_solutions_found: int - first_solution: float - primal_bound: float - dual_bound: float - gap: float - primal_dual_integral: float + n_runs: int = None + n_nodes: int = None + n_solutions_found: int = -1 + first_solution: float = None + primal_bound: float = None + dual_bound: float = None + gap: float = None + primal_dual_integral: float = None # unpacking the _variables, _presolved_variables, _constraints # _presolved_constraints dictionaries From 2c1f0c9d522d00fa431002b4aa85473e4722aaa4 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:10:34 +0100 Subject: [PATCH 046/135] Expand testing --- tests/test_reader.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 4bc976c76..69001dae1 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -91,7 +91,8 @@ def test_readStatistics(): m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 19 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert result.status == "optimal" + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 20 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 assert type(result.total_time) == float assert result.problem_name == "readStats" assert result.presolved_problem_name == "t_readStats" @@ -104,4 +105,36 @@ def test_readStatistics(): assert result.n_vars == 2 assert result.n_presolved_vars == 0 assert result.n_binary_vars == 0 - assert result.n_integer_vars == 1 \ No newline at end of file + assert result.n_integer_vars == 1 + + m = Model() + x = m.addVar() + m.setObjective(-x) + m.hideOutput() + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "unbounded" + + m = Model() + x = m.addVar() + m.addCons(x <= -1) + m.hideOutput() + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "infeasible" + assert result.gap == None + assert result.n_solutions_found == 0 + + m = Model() + x = m.addVar() + m.hideOutput() + m.setParam("limits/solutions", 0) + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "user_interrupt" + assert result.gap == None + +test_readStatistics() \ No newline at end of file From 6140996e347eb7c63296320ecaf61607adcd50dd Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:11:01 +0100 Subject: [PATCH 047/135] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cb87181..6dc7e053e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Expanded Statistics class to more problems - Created Statistics class - Added parser to read .stats file ### Fixed From 1ce2152a126463960bb7591b28c2004522d497b8 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 25 Jun 2024 16:05:26 +0100 Subject: [PATCH 048/135] Make readStatistics standalone --- src/pyscipopt/scip.pxi | 223 ++++++++++++++++++++--------------------- tests/test_reader.py | 14 ++- 2 files changed, 117 insertions(+), 120 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 146e2beca..b385bf2cf 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5103,118 +5103,6 @@ cdef class Model: PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) locale.setlocale(locale.LC_NUMERIC,user_locale) - - def readStatistics(self, filename): - """ - Given a .stats file of a solved model, reads it and returns an instance of the Statistics class - holding some statistics. - - Keyword arguments: - filename -- name of the input file - """ - result = {} - file = open(filename) - data = file.readlines() - - if "optimal solution found" in data[0]: - result["status"] = "optimal" - elif "infeasible" in data[0]: - result["status"] = "infeasible" - elif "unbounded" in data[0]: - result["status"] = "unbounded" - elif "limit reached" in data[0]: - result["status"] = "user_interrupt" - else: - raise "readStatistics can only be called if the problem was solved" - - available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Solutions found"] - - if result["status"] in ["optimal", "user_interrupt"]: - available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) - - seen_cons = 0 - for i, line in enumerate(data): - split_line = line.split(":") - split_line[1] = split_line[1][:-1] # removing \n - stat_name = split_line[0].strip() - - if seen_cons == 2 and stat_name == "Constraints": - continue - - if stat_name in available_stats: - cur_stat = split_line[0].strip() - relevant_value = split_line[1].strip() - - if stat_name == "Variables": - relevant_value = relevant_value[:-1] # removing ")" - var_stats = {} - split_var = relevant_value.split("(") - var_stats["total"] = int(split_var[0]) - split_var = split_var[1].split(",") - - for var_type in split_var: - split_result = var_type.strip().split(" ") - var_stats[split_result[1]] = int(split_result[0]) - - if "Original" in data[i-2]: - result["Variables"] = var_stats - else: - result["Presolved Variables"] = var_stats - - continue - - if stat_name == "Constraints": - seen_cons += 1 - con_stats = {} - split_con = relevant_value.split(",") - for con_type in split_con: - split_result = con_type.strip().split(" ") - con_stats[split_result[1]] = int(split_result[0]) - - if "Original" in data[i-3]: - result["Constraints"] = con_stats - else: - result["Presolved Constraints"] = con_stats - continue - - relevant_value = relevant_value.split(" ")[0] - if stat_name == "Problem name": - if "Original" in data[i-1]: - result["Problem name"] = relevant_value - else: - result["Presolved Problem name"] = relevant_value - continue - - if stat_name == "Gap": - result["Gap (%)"] = float(relevant_value[:-1]) - continue - - if _is_number(relevant_value): - result[cur_stat] = float(relevant_value) - if cur_stat == "Solutions found" and result[cur_stat] == 0: - break - else: # it's a string - result[cur_stat] = relevant_value - - # changing keys to pythonic variable names - treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", - "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", - "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} - - if result["status"] in ["optimal", "user_interrupt"]: - if result["Solutions found"] > 0: - treated_keys["First Solution"] = "first_solution" - treated_keys["Primal Bound"] = "primal_bound" - treated_keys["Dual Bound"] = "dual_bound" - treated_keys["Gap (%)"] = "gap" - treated_keys["primal-dual"] = "primal_dual_integral" - treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) - - stats = Statistics(**treated_result) - return stats def getNLPs(self): """gets total number of LPs solved so far""" @@ -5701,6 +5589,117 @@ class Statistics: @property def n_presolved_maximal_cons(self): return self._presolved_constraints["maximal"] + +def readStatistics(filename): + """ + Given a .stats file of a solved model, reads it and returns an instance of the Statistics class + holding some statistics. + + Keyword arguments: + filename -- name of the input file + """ + result = {} + file = open(filename) + data = file.readlines() + + if "optimal solution found" in data[0]: + result["status"] = "optimal" + elif "infeasible" in data[0]: + result["status"] = "infeasible" + elif "unbounded" in data[0]: + result["status"] = "unbounded" + elif "limit reached" in data[0]: + result["status"] = "user_interrupt" + else: + raise "readStatistics can only be called if the problem was solved" + + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found"] + + if result["status"] in ["optimal", "user_interrupt"]: + available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) + + seen_cons = 0 + for i, line in enumerate(data): + split_line = line.split(":") + split_line[1] = split_line[1][:-1] # removing \n + stat_name = split_line[0].strip() + + if seen_cons == 2 and stat_name == "Constraints": + continue + + if stat_name in available_stats: + relevant_value = split_line[1].strip() + + if stat_name == "Variables": + relevant_value = relevant_value[:-1] # removing ")" + var_stats = {} + split_var = relevant_value.split("(") + var_stats["total"] = int(split_var[0]) + split_var = split_var[1].split(",") + + for var_type in split_var: + split_result = var_type.strip().split(" ") + var_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-2]: + result["Variables"] = var_stats + else: + result["Presolved Variables"] = var_stats + + continue + + if stat_name == "Constraints": + seen_cons += 1 + con_stats = {} + split_con = relevant_value.split(",") + for con_type in split_con: + split_result = con_type.strip().split(" ") + con_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-3]: + result["Constraints"] = con_stats + else: + result["Presolved Constraints"] = con_stats + continue + + relevant_value = relevant_value.split(" ")[0] + if stat_name == "Problem name": + if "Original" in data[i-1]: + result["Problem name"] = relevant_value + else: + result["Presolved Problem name"] = relevant_value + continue + + if stat_name == "Gap": + relevant_value = relevant_value[:-1] # removing % + + if _is_number(relevant_value): + result[stat_name] = float(relevant_value) + if stat_name == "Solutions found" and result[stat_name] == 0: + break + + else: # it's a string + result[stat_name] = relevant_value + + # changing keys to pythonic variable names + treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", + "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} + + if result["status"] in ["optimal", "user_interrupt"]: + if result["Solutions found"] > 0: + treated_keys["First Solution"] = "first_solution" + treated_keys["Primal Bound"] = "primal_bound" + treated_keys["Dual Bound"] = "dual_bound" + treated_keys["Gap"] = "gap" + treated_keys["primal-dual"] = "primal_dual_integral" + treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + + stats = Statistics(**treated_result) + return stats # debugging memory management def is_memory_freed(): diff --git a/tests/test_reader.py b/tests/test_reader.py index 69001dae1..ef2534ded 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,7 +1,7 @@ import pytest import os -from pyscipopt import Model, quicksum, Reader, SCIP_RESULT +from pyscipopt import Model, quicksum, Reader, SCIP_RESULT, readStatistics class SudokuReader(Reader): @@ -89,7 +89,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "optimal" assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 20 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 @@ -113,7 +113,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "unbounded" m = Model() @@ -122,7 +122,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "infeasible" assert result.gap == None assert result.n_solutions_found == 0 @@ -133,8 +133,6 @@ def test_readStatistics(): m.setParam("limits/solutions", 0) m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "user_interrupt" - assert result.gap == None - -test_readStatistics() \ No newline at end of file + assert result.gap == None \ No newline at end of file From 3547dbb4914a2475358721623d3b6b879f2425b5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 25 Jun 2024 16:06:40 +0100 Subject: [PATCH 049/135] Update CHANGELOG --- CHANGELOG.md | 3 ++- src/pyscipopt/__init__.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc7e053e..64cc913e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ ## Unreleased ### Added -- Expanded Statistics class to more problems +- Expanded Statistics class to more problems. - Created Statistics class - Added parser to read .stats file ### Fixed ### Changed +- Made readStatistics a standalone function ### Removed ## 5.1.1 - 2024-06-22 diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index a370c60b7..cd6528f74 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -24,6 +24,7 @@ from pyscipopt.scip import Reader from pyscipopt.scip import Sepa from pyscipopt.scip import LP +from pyscipopt.scip import readStatistics from pyscipopt.scip import Expr from pyscipopt.scip import quicksum from pyscipopt.scip import quickprod From 14a8581c7d6187a6023ca5edf936a888090c53d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:56:44 +0100 Subject: [PATCH 050/135] Relax stage for checking getObjVal, getVal (#872) * Set correct stages. Add SCIPsolIsOriginal * Add test for getObjVal * Remove noexcept * Update changelog * Add noexcept again * Add everything back * Segfault commit * Update CHANGELOG * Add test * Relax stage checks * Update test_customizedbenders.py * Update variable name --- CHANGELOG.md | 19 +++++++++++++++ src/pyscipopt/scip.pxd | 1 + src/pyscipopt/scip.pxi | 36 +++++++++++++++++++++------- tests/test_cons.py | 1 - tests/test_customizedbenders.py | 2 +- tests/test_model.py | 42 +++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cb87181..b7eb2fc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ - Created Statistics class - Added parser to read .stats file ### Fixed +- Fixed too strict getObjVal, getVal check +### Changed +### Removed + +## 5.1.1 - 2024-06-22 +### Added +- Added SCIP_STATUS_DUALLIMIT and SCIP_STATUS_PRIMALLIMIT +- Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) +- Added recipe with reformulation for detecting infeasible constraints +- Wrapped SCIPcreateOrigSol and added tests +- Added verbose option for writeProblem and writeParams +- Expanded locale test +- Added methods for creating expression constraints without adding to problem +- Added methods for creating/adding/appending disjunction constraints +- Added check for pt_PT locale in test_model.py +- Added SCIPgetOrigConss and SCIPgetNOrigConss Cython bindings. +- Added transformed=False option to getConss, getNConss, and getNVars +### Fixed +- Fixed locale errors in reading ### Changed ### Removed diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index f6fe8e8d8..73650c87a 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -839,6 +839,7 @@ cdef extern from "scip/scip.h": int SCIPgetNBestSolsFound(SCIP* scip) SCIP_SOL* SCIPgetBestSol(SCIP* scip) SCIP_Real SCIPgetSolVal(SCIP* scip, SCIP_SOL* sol, SCIP_VAR* var) + SCIP_Bool SCIPsolIsOriginal(SCIP_SOL* sol) SCIP_RETCODE SCIPwriteVarName(SCIP* scip, FILE* outfile, SCIP_VAR* var, SCIP_Bool vartype) SCIP_Real SCIPgetSolOrigObj(SCIP* scip, SCIP_SOL* sol) SCIP_Real SCIPgetSolTransObj(SCIP* scip, SCIP_SOL* sol) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 943e27f93..4c40dd86a 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -627,7 +627,9 @@ cdef class Solution: def _checkStage(self, method): if method in ["SCIPgetSolVal", "getSolObjVal"]: - if self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING: + stage_check = SCIPgetStage(self.scip) not in [SCIP_STAGE_INIT, SCIP_STAGE_FREE] + + if not stage_check or self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING: raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})") @@ -3543,6 +3545,7 @@ cdef class Model: def presolve(self): """Presolve the problem.""" PY_SCIP_CALL(SCIPpresolve(self._scip)) + self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) # Benders' decomposition methods def initBendersDefault(self, subproblems): @@ -3606,7 +3609,7 @@ cdef class Model: PY_SCIP_CALL(SCIPsetupBendersSubproblem(self._scip, _benders[i], self._bestSol.sol, j, SCIP_BENDERSENFOTYPE_CHECK)) PY_SCIP_CALL(SCIPsolveBendersSubproblem(self._scip, - _benders[i], self._bestSol.sol, j, &_infeasible, solvecip, NULL)) + _benders[i], self._bestSol.sol, j, &_infeasible, solvecip, NULL)) def freeBendersSubproblems(self): """Calls the free subproblem function for the Benders' decomposition. @@ -4838,11 +4841,14 @@ cdef class Model: """ if sol == None: sol = Solution.create(self._scip, NULL) + sol._checkStage("getSolObjVal") + if original: objval = SCIPgetSolOrigObj(self._scip, sol.sol) else: objval = SCIPgetSolTransObj(self._scip, sol.sol) + return objval def getSolTime(self, Solution sol): @@ -4855,13 +4861,24 @@ cdef class Model: def getObjVal(self, original=True): """Retrieve the objective value of value of best solution. - Can only be called after solving is completed. :param original: objective value in original space (Default value = True) - """ - if not self.getStage() >= SCIP_STAGE_SOLVING: - raise Warning("method cannot be called before problem is solved") + + if SCIPgetNSols(self._scip) == 0: + if self.getStage() != SCIP_STAGE_SOLVING: + raise Warning("Without a solution, method can only be called in stage SOLVING.") + else: + assert self._bestSol.sol != NULL + + if SCIPsolIsOriginal(self._bestSol.sol): + min_stage_requirement = SCIP_STAGE_PROBLEM + else: + min_stage_requirement = SCIP_STAGE_TRANSFORMING + + if not self.getStage() >= min_stage_requirement: + raise Warning("method cannot be called in stage %i." % self.getStage) + return self.getSolObjVal(self._bestSol, original) def getSolVal(self, Solution sol, Expr expr): @@ -4889,8 +4906,11 @@ cdef class Model: Note: a variable is also an expression """ - if not self.getStage() >= SCIP_STAGE_SOLVING: - raise Warning("method cannot be called before problem is solved") + stage_check = SCIPgetStage(self._scip) not in [SCIP_STAGE_INIT, SCIP_STAGE_FREE] + + if not stage_check or self._bestSol.sol == NULL and SCIPgetStage(self._scip) != SCIP_STAGE_SOLVING: + raise Warning("Method cannot be called in stage ", self.getStage()) + return self.getSolVal(self._bestSol, expr) def hasPrimalRay(self): diff --git a/tests/test_cons.py b/tests/test_cons.py index 443f8a111..8b11250ae 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -72,7 +72,6 @@ def test_cons_logical(): assert m.isEQ(m.getVal(result1), 1) assert m.isEQ(m.getVal(result2), 0) - def test_SOScons(): m = Model() x = {} diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py index 146d80279..aaa461296 100644 --- a/tests/test_customizedbenders.py +++ b/tests/test_customizedbenders.py @@ -54,7 +54,7 @@ def bendersgetvar(self, variable, probnumber): def benderssolvesubconvex(self, solution, probnumber, onlyconvex): self.model.setupBendersSubproblem(probnumber, self, solution) self.subprob.solveProbingLP() - subprob = self.model.getBendersSubproblem(probnumber, self) + subprob = self.model.getBendersSubproblem(probnumber, self) assert self.subprob.getObjVal() == subprob.getObjVal() result_dict = {} diff --git a/tests/test_model.py b/tests/test_model.py index fe0ed607f..365f3e919 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -435,3 +435,45 @@ def build_scip_model(): scip.setParam("limits/dual", -10) scip.optimize() assert (scip.getStatus() == "duallimit"), scip.getStatus() + +def test_getObjVal(): + m = Model() + + x = m.addVar(obj=0) + y = m.addVar(obj = 1) + z = m.addVar(obj = 2) + + m.addCons(x+y+z >= 0) + m.addCons(y+z >= 3) + m.addCons(z >= 8) + + m.setParam("limits/solutions", 0) + m.optimize() + + try: + m.getObjVal() + except Warning: + pass + + try: + m.getVal(x) + except Warning: + pass + + m.freeTransform() + m.setParam("limits/solutions", 1) + m.presolve() + + assert m.getObjVal() + assert m.getVal(x) + + m.freeTransform() + m.setParam("limits/solutions", -1) + + m.optimize() + + assert m.getObjVal() == 16 + assert m.getVal(x) == 0 + + assert m.getObjVal() == 16 + assert m.getVal(x) == 0 From 2ac19e18ae5a997e5d38ec80bc522f67128cc02b Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 2 Aug 2024 10:50:10 +0200 Subject: [PATCH 051/135] Move doc generation to separate workflow --- .github/workflows/generate_docs.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/generate_docs.yml diff --git a/.github/workflows/generate_docs.yml b/.github/workflows/generate_docs.yml new file mode 100644 index 000000000..01a42df24 --- /dev/null +++ b/.github/workflows/generate_docs.yml @@ -0,0 +1,16 @@ +name: Generate documentation + +on: + workflow_dispatch: + inputs: {} + +jobs: + generate-documentation: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: Generate documentation + run: | + sudo apt-get install doxygen graphviz + bash -ex generate-docs.sh "${{ secrets.GITHUB_TOKEN }}" "gh-pages" \ No newline at end of file From 1932131cd58e5dcc5196532697624115a098736f Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:15:20 +0200 Subject: [PATCH 052/135] Strong Branching (#873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add strong branching wrappers * Update CHANGELOG * Add new wrappers and unfinished tests * Update strong branching test * Fix typo * Update CHANGELOG * Add test for other features in branching rule * Fix spelling errors * Remove commented out line * Add changes from joao * Add assert to ensure safe branching * Change names to copy those of SCIp interface --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 3 + src/pyscipopt/scip.pxd | 12 +- src/pyscipopt/scip.pxi | 142 +++++++++++++++++++++- tests/test_strong_branching.py | 210 +++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 tests/test_strong_branching.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b7eb2fc61..abd412d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added - Created Statistics class - Added parser to read .stats file +- Added Python definitions and wrappers for SCIPstartStrongbranch, SCIPendStrongbranch SCIPgetBranchScoreMultiple, + SCIPgetVarStrongbranchInt, SCIPupdateVarPseudocost, SCIPgetVarStrongbranchFrac, SCIPcolGetAge, + SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge ### Fixed - Fixed too strict getObjVal, getVal check ### Changed diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 73650c87a..4dbe9247f 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -788,6 +788,7 @@ cdef extern from "scip/scip.h": int SCIPgetNLPCols(SCIP* scip) SCIP_COL** SCIPgetLPCols(SCIP *scip) SCIP_ROW** SCIPgetLPRows(SCIP *scip) + SCIP_Bool SCIPallColsInLP(SCIP* scip) # Cutting Plane Methods SCIP_RETCODE SCIPaddPoolCut(SCIP* scip, SCIP_ROW* row) @@ -1258,7 +1259,15 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPgetLPBranchCands(SCIP* scip, SCIP_VAR*** lpcands, SCIP_Real** lpcandssol, SCIP_Real** lpcandsfrac, int* nlpcands, int* npriolpcands, int* nfracimplvars) SCIP_RETCODE SCIPgetPseudoBranchCands(SCIP* scip, SCIP_VAR*** pseudocands, int* npseudocands, int* npriopseudocands) - + SCIP_RETCODE SCIPstartStrongbranch(SCIP* scip, SCIP_Bool enablepropogation) + SCIP_RETCODE SCIPendStrongbranch(SCIP* scip) + SCIP_RETCODE SCIPgetVarStrongbranchLast(SCIP* scip, SCIP_VAR* var, SCIP_Real* down, SCIP_Real* up, SCIP_Bool* downvalid, SCIP_Bool* upvalid, SCIP_Real* solval, SCIP_Real* lpobjval) + SCIP_Longint SCIPgetVarStrongbranchNode(SCIP* scip, SCIP_VAR* var) + SCIP_Real SCIPgetBranchScoreMultiple(SCIP* scip, SCIP_VAR* var, int nchildren, SCIP_Real* gains) + SCIP_RETCODE SCIPgetVarStrongbranchWithPropagation(SCIP* scip, SCIP_VAR* var, SCIP_Real solval, SCIP_Real lpobjval, int itlim, int maxproprounds, SCIP_Real* down, SCIP_Real* up, SCIP_Bool* downvalid, SCIP_Bool* upvalid, SCIP_Longint* ndomredsdown, SCIP_Longint* ndomredsup, SCIP_Bool* downinf, SCIP_Bool* upinf, SCIP_Bool* downconflict, SCIP_Bool* upconflict, SCIP_Bool* lperror, SCIP_Real* newlbs, SCIP_Real* newubs) + SCIP_RETCODE SCIPgetVarStrongbranchInt(SCIP* scip, SCIP_VAR* var, int itlim, SCIP_Bool idempotent, SCIP_Real* down, SCIP_Real* up, SCIP_Bool* downvalid, SCIP_Bool* upvalid, SCIP_Bool* downinf, SCIP_Bool* upinf, SCIP_Bool* downconflict, SCIP_Bool* upconflict, SCIP_Bool* lperror) + SCIP_RETCODE SCIPupdateVarPseudocost(SCIP* scip, SCIP_VAR* var, SCIP_Real solvaldelta, SCIP_Real objdelta, SCIP_Real weight) + SCIP_RETCODE SCIPgetVarStrongbranchFrac(SCIP* scip, SCIP_VAR* var, int itlim, SCIP_Bool idempotent, SCIP_Real* down, SCIP_Real* up, SCIP_Bool* downvalid, SCIP_Bool* upvalid, SCIP_Bool* downinf, SCIP_Bool* upinf, SCIP_Bool* downconflict, SCIP_Bool* upconflict, SCIP_Bool* lperror) # Numerical Methods SCIP_Real SCIPinfinity(SCIP* scip) @@ -1841,6 +1850,7 @@ cdef extern from "scip/pub_lp.h": int SCIPcolGetNNonz(SCIP_COL* col) SCIP_ROW** SCIPcolGetRows(SCIP_COL* col) SCIP_Real* SCIPcolGetVals(SCIP_COL* col) + int SCIPcolGetAge(SCIP_COL* col) int SCIPcolGetIndex(SCIP_COL* col) SCIP_Real SCIPcolGetObj(SCIP_COL *col) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4c40dd86a..38f74cc75 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -417,6 +417,11 @@ cdef class Column: """gets objective value coefficient of a column""" return SCIPcolGetObj(self.scip_col) + def getAge(self): + """Gets the age of the column, i.e., the total number of successive times a column was in the LP + and was 0.0 in the solution""" + return SCIPcolGetAge(self.scip_col) + def __hash__(self): return hash(self.scip_col) @@ -518,6 +523,10 @@ cdef class Row: cdef SCIP_Real* vals = SCIProwGetVals(self.scip_row) return [vals[i] for i in range(self.getNNonz())] + def getAge(self): + """Gets the age of the row. (The consecutive times the row has been non-active in the LP)""" + return SCIProwGetAge(self.scip_row) + def getNorm(self): """gets Euclidean norm of row vector """ return SCIProwGetNorm(self.scip_row) @@ -891,6 +900,10 @@ cdef class Variable(Expr): """Retrieve the current LP solution value of variable""" return SCIPvarGetLPSol(self.scip_var) + def getAvgSol(self): + """Get the weighted average solution of variable in all feasible primal solutions found""" + return SCIPvarGetAvgSol(self.scip_var) + cdef class Constraint: """Base class holding a pointer to corresponding SCIP_CONS""" @@ -1960,6 +1973,20 @@ cdef class Model: """returns whether the current LP solution is basic, i.e. is defined by a valid simplex basis""" return SCIPisLPSolBasic(self._scip) + def allColsInLP(self): + """checks if all columns, i.e. every variable with non-empty column is present in the LP. + This is not True when performing pricing for instance.""" + + return SCIPallColsInLP(self._scip) + + # LP Col Methods + def getColRedCost(self, Column col): + """gets the reduced cost of the column in the current LP + + :param Column col: the column of the LP for which the reduced cost will be retrieved + """ + return SCIPgetColRedcost(self._scip, col.scip_col) + #TODO: documentation!! # LP Row Methods def createEmptyRowSepa(self, Sepa sepa, name="row", lhs = 0.0, rhs = None, local = True, modifiable = False, removable = True): @@ -5553,6 +5580,119 @@ cdef class Model: assert isinstance(var, Variable), "The given variable is not a pyvar, but %s" % var.__class__.__name__ PY_SCIP_CALL(SCIPchgVarBranchPriority(self._scip, var.scip_var, priority)) + def startStrongbranch(self): + """Start strong branching. Needs to be called before any strong branching. Must also later end strong branching. + TODO: Propagation option has currently been disabled via Python. + If propagation is enabled then strong branching is not done on the LP, but on additionally created nodes (has some overhead)""" + + PY_SCIP_CALL(SCIPstartStrongbranch(self._scip, False)) + + def endStrongbranch(self): + """End strong branching. Needs to be called if startStrongBranching was called previously. + Between these calls the user can access all strong branching functionality. """ + + PY_SCIP_CALL(SCIPendStrongbranch(self._scip)) + + def getVarStrongbranchLast(self, Variable var): + """Get the results of the last strong branching call on this variable (potentially was called + at another node). + + down - The dual bound of the LP after branching down on the variable + up - The dual bound of the LP after branchign up on the variable + downvalid - Whether down stores a valid dual bound or is NULL + upvalid - Whether up stores a valid dual bound or is NULL + solval - The solution value of the variable at the last strong branching call + lpobjval - The LP objective value at the time of the last strong branching call + + :param Variable var: variable to get the previous strong branching information from + """ + + cdef SCIP_Real down + cdef SCIP_Real up + cdef SCIP_Real solval + cdef SCIP_Real lpobjval + cdef SCIP_Bool downvalid + cdef SCIP_Bool upvalid + + PY_SCIP_CALL(SCIPgetVarStrongbranchLast(self._scip, var.scip_var, &down, &up, &downvalid, &upvalid, &solval, &lpobjval)) + + return down, up, downvalid, upvalid, solval, lpobjval + + def getVarStrongbranchNode(self, Variable var): + """Get the node number from the last time strong branching was called on the variable + + :param Variable var: variable to get the previous strong branching node from + """ + + cdef SCIP_Longint node_num + node_num = SCIPgetVarStrongbranchNode(self._scip, var.scip_var) + + return node_num + + def getVarStrongbranch(self, Variable var, itlim, idempotent=False, integral=False): + """ Strong branches and gets information on column variable. + + :param Variable var: Variable to get strong branching information on + :param itlim: LP iteration limit for total strong branching calls + :param idempotent: Should SCIP's state remain the same after the call? + :param integral: Boolean on whether the variable is currently integer. + """ + + cdef SCIP_Real down + cdef SCIP_Real up + cdef SCIP_Bool downvalid + cdef SCIP_Bool upvalid + cdef SCIP_Bool downinf + cdef SCIP_Bool upinf + cdef SCIP_Bool downconflict + cdef SCIP_Bool upconflict + cdef SCIP_Bool lperror + + if integral: + PY_SCIP_CALL(SCIPgetVarStrongbranchInt(self._scip, var.scip_var, itlim, idempotent, &down, &up, &downvalid, + &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror)) + else: + PY_SCIP_CALL(SCIPgetVarStrongbranchFrac(self._scip, var.scip_var, itlim, idempotent, &down, &up, &downvalid, + &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror)) + + return down, up, downvalid, upvalid, downinf, upinf, downconflict, upconflict, lperror + + def updateVarPseudocost(self, Variable var, valdelta, objdelta, weight): + """Updates the pseudo costs of the given variable and the global pseudo costs after a change of valdelta + in the variable's solution value and resulting change of objdelta in the LP's objective value. + Update is ignored if objdelts is infinite. Weight is in range (0, 1], and affects how it updates + the global weighted sum. + + :param Variable var: Variable whos pseudo cost will be updated + :param valdelta: The change in variable value (e.g. the fractional amount removed or added by branching) + :param objdelta: The change in objective value of the LP after valdelta change of the variable + :param weight: the weight in range (0,1] of how the update affects the stored weighted sum. + """ + + PY_SCIP_CALL(SCIPupdateVarPseudocost(self._scip, var.scip_var, valdelta, objdelta, weight)) + + def getBranchScoreMultiple(self, Variable var, gains): + """Calculates the branching score out of the gain predictions for a branching with + arbitrary many children. + + :param Variable var: variable to calculate the score for + :param gains: list of gains for each child. + """ + + assert isinstance(gains, list) + nchildren = len(gains) + + cdef int _nchildren = nchildren + _gains = malloc(_nchildren * sizeof(SCIP_Real)) + for i in range(_nchildren): + _gains[i] = gains[i] + + score = SCIPgetBranchScoreMultiple(self._scip, var.scip_var, _nchildren, _gains) + + free(_gains) + + return score + def getTreesizeEstimation(self): """Get an estimation of the final tree size """ return SCIPgetTreesizeEstimation(self._scip) @@ -5697,7 +5837,7 @@ class Statistics: @property def n_presolved_maximal_cons(self): return self._presolved_constraints["maximal"] - + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_strong_branching.py b/tests/test_strong_branching.py new file mode 100644 index 000000000..8cdc6c2d7 --- /dev/null +++ b/tests/test_strong_branching.py @@ -0,0 +1,210 @@ +from pyscipopt import Model, Branchrule, SCIP_RESULT, quicksum, SCIP_PARAMSETTING + +""" +This is a test for strong branching. It also gives a basic outline of what a function that imitates strong +branching should do. An example of when to use this would be to access strong branching scores etc. + +If this was done fully, then one would need to handle the following cases: +- What happens if a new primal solution is found and the bound is larger than the cutoff bound? Return CUTOFF status +- What happens if the bound for one of the children is above a cutoff bound? Change variable bound +- If probing is ever enabled then one would need to handle new found bounds appropriately. +""" + + +class StrongBranchingRule(Branchrule): + + def __init__(self, scip, idempotent=False): + self.scip = scip + self.idempotent = idempotent + + def branchexeclp(self, allowaddcons): + + # Get the branching candidates. Only consider the number of priority candidates (they are sorted to be first) + # The implicit integer candidates in general shouldn't be branched on. Unless specified by the user + # npriocands and ncands are the same (npriocands are variables that have been designated as priorities) + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands() + + # Initialise scores for each variable + scores = [-self.scip.infinity() for _ in range(npriocands)] + down_bounds = [None for _ in range(npriocands)] + up_bounds = [None for _ in range(npriocands)] + + # Initialise placeholder values + num_nodes = self.scip.getNNodes() + lpobjval = self.scip.getLPObjVal() + lperror = False + best_cand_idx = 0 + + # Start strong branching and iterate over the branching candidates + self.scip.startStrongbranch() + for i in range(npriocands): + + # Check the case that the variable has already been strong branched on at this node. + # This case occurs when events happen in the node that should be handled immediately. + # When processing the node again (because the event did not remove it), there's no need to duplicate work. + if self.scip.getVarStrongbranchNode(branch_cands[i]) == num_nodes: + down, up, downvalid, upvalid, _, lastlpobjval = self.scip.getVarStrongbranchLast(branch_cands[i]) + if downvalid: + down_bounds[i] = down + if upvalid: + up_bounds[i] = up + downgain = max([down - lastlpobjval, 0]) + upgain = max([up - lastlpobjval, 0]) + scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain]) + continue + + # Strong branch! + down, up, downvalid, upvalid, downinf, upinf, downconflict, upconflict, lperror = self.scip.getVarStrongbranch( + branch_cands[i], 200, idempotent=self.idempotent) + + # In the case of an LP error handle appropriately (for this example we just break the loop) + if lperror: + break + + # In the case of both infeasible sub-problems cutoff the node + if not self.idempotent and downinf and upinf: + return {"result": SCIP_RESULT.CUTOFF} + + # Calculate the gains for each up and down node that strong branching explored + if not downinf and downvalid: + down_bounds[i] = down + downgain = max([down - lpobjval, 0]) + else: + downgain = 0 + if not upinf and upvalid: + up_bounds[i] = up + upgain = max([up - lpobjval, 0]) + else: + upgain = 0 + + # Update the pseudo-costs + if not self.idempotent: + lpsol = branch_cands[i].getLPSol() + if not downinf and downvalid: + self.scip.updateVarPseudocost(branch_cands[i], -self.scip.frac(lpsol), downgain, 1) + if not upinf and upvalid: + self.scip.updateVarPseudocost(branch_cands[i], 1 - self.scip.frac(lpsol), upgain, 1) + + scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain]) + if scores[i] > scores[best_cand_idx]: + best_cand_idx = i + + # End strong branching + self.scip.endStrongbranch() + + # In the case of an LP error + if lperror: + return {"result": SCIP_RESULT.DIDNOTRUN} + + # Branch on the variable with the largest score + down_child, eq_child, up_child = self.model.branchVarVal( + branch_cands[best_cand_idx], branch_cands[best_cand_idx].getLPSol()) + + # Update the bounds of the down node and up node + if self.scip.allColsInLP() and not self.idempotent: + if down_child is not None and down_bounds[best_cand_idx] is not None: + self.scip.updateNodeLowerbound(down_child, down_bounds[best_cand_idx]) + if up_child is not None and up_bounds[best_cand_idx] is not None: + self.scip.updateNodeLowerbound(up_child, up_bounds[best_cand_idx]) + + return {"result": SCIP_RESULT.BRANCHED} + + +class FeatureSelectorBranchingRule(Branchrule): + + def __init__(self, scip): + self.scip = scip + + def branchexeclp(self, allowaddcons): + + if self.scip.getNNodes() == 1 or self.scip.getNNodes() == 250: + + rows = self.scip.getLPRowsData() + cols = self.scip.getLPColsData() + + # This is just a dummy rule to check functionality. + # A user should be able to see how they can access information without affecting the solve process. + + age_row = rows[0].getAge() + age_col = cols[0].getAge() + red_cost_col = self.scip.getColRedCost(cols[0]) + + avg_sol = cols[0].getVar().getAvgSol() + + return {"result": SCIP_RESULT.DIDNOTRUN} + + +def create_model(): + scip = Model() + # Disable separating and heuristics as we want to branch on the problem many times before reaching optimality. + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.setSeparating(SCIP_PARAMSETTING.OFF) + scip.setLongintParam("limits/nodes", 2000) + + x0 = scip.addVar(lb=-2, ub=4) + r1 = scip.addVar() + r2 = scip.addVar() + y0 = scip.addVar(lb=3) + t = scip.addVar(lb=None) + l = scip.addVar(vtype="I", lb=-9, ub=18) + u = scip.addVar(vtype="I", lb=-3, ub=99) + + more_vars = [] + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-12, ub=40)) + scip.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-52, ub=10)) + scip.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) + + scip.addCons(r1 >= x0) + scip.addCons(r2 >= -x0) + scip.addCons(y0 == r1 + r2) + scip.addCons(t + l + 7 * u <= 300) + scip.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) + scip.addCons(more_vars[3] >= l + 2) + scip.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) + scip.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) + + scip.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) + + return scip + + +def test_strong_branching(): + scip = create_model() + + strong_branch_rule = StrongBranchingRule(scip, idempotent=False) + scip.includeBranchrule(strong_branch_rule, "strong branch rule", "custom strong branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() + if scip.getStatus() == "optimal": + assert scip.isEQ(-112196, scip.getObjVal()) + else: + assert -112196 <= scip.getObjVal() + + +def test_strong_branching_idempotent(): + scip = create_model() + + strong_branch_rule = StrongBranchingRule(scip, idempotent=True) + scip.includeBranchrule(strong_branch_rule, "strong branch rule", "custom strong branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() + if scip.getStatus() == "optimal": + assert scip.isEQ(-112196, scip.getObjVal()) + else: + assert -112196 <= scip.getObjVal() + + +def test_dummy_feature_selector(): + scip = create_model() + + feature_dummy_branch_rule = FeatureSelectorBranchingRule(scip) + scip.includeBranchrule(feature_dummy_branch_rule, "dummy branch rule", "custom feature creation branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() \ No newline at end of file From 112f8b24c835894ccfc8abd41dbb02a8c39fa649 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:42:56 +0200 Subject: [PATCH 053/135] Adds bipartite graph function (#874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add strong branching wrappers * Update CHANGELOG * Add new wrappers and unfinished tests * Update strong branching test * Fix typo * Update CHANGELOG * Add test for other features in branching rule * Fix spelling errors * Remove commented out line * Add bipartite graph generation function * Update CHANGELOG * Add changes from joao * Add assert to ensure safe branching * Add changes from Joao * Change names to copy those of SCIp interface --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxi | 254 +++++++++++++++++++++++++++++++++++++++- tests/test_bipartite.py | 118 +++++++++++++++++++ 3 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 tests/test_bipartite.py diff --git a/CHANGELOG.md b/CHANGELOG.md index abd412d07..0c2585e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added Python definitions and wrappers for SCIPstartStrongbranch, SCIPendStrongbranch SCIPgetBranchScoreMultiple, SCIPgetVarStrongbranchInt, SCIPupdateVarPseudocost, SCIPgetVarStrongbranchFrac, SCIPcolGetAge, SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge +- Added getBipartiteGraphRepresentation ### Fixed - Fixed too strict getObjVal, getVal check ### Changed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 38f74cc75..0ffbc996c 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2066,8 +2066,9 @@ cdef class Model: return SCIPgetRowObjParallelism(self._scip, row.scip_row) def getRowParallelism(self, Row row1, Row row2, orthofunc=101): - """Returns the degree of parallelism between hyplerplanes. 1 if perfectly parallel, 0 if orthognal. - 101 in this case is an 'e' (euclidean) in ASCII. The other accpetable input is 100 (d for discrete).""" + """Returns the degree of parallelism between hyplerplanes. 1 if perfectly parallel, 0 if orthogonal. + For two row vectors v, w the parallelism is calculated as: |v*w|/(|v|*|w|). + 101 in this case is an 'e' (euclidean) in ASCII. The other acceptable input is 100 (d for discrete).""" return SCIProwGetParallelism(row1.scip_row, row2.scip_row, orthofunc) def getRowDualSol(self, Row row): @@ -5697,6 +5698,255 @@ cdef class Model: """Get an estimation of the final tree size """ return SCIPgetTreesizeEstimation(self._scip) + + def getBipartiteGraphRepresentation(self, prev_col_features=None, prev_edge_features=None, prev_row_features=None, + static_only=False, suppress_warnings=False): + """This function generates the bipartite graph representation of an LP, which was first used in + the following paper: + @inproceedings{conf/nips/GasseCFCL19, + title={Exact Combinatorial Optimization with Graph Convolutional Neural Networks}, + author={Gasse, Maxime and Chételat, Didier and Ferroni, Nicola and Charlin, Laurent and Lodi, Andrea}, + booktitle={Advances in Neural Information Processing Systems 32}, + year={2019} + } + The exact features have been modified compared to the original implementation. + This function is used mainly in the machine learning community for MIP. + A user can only call it during the solving process, when there is an LP object. This means calling it + from some user defined plugin on the Python side. + An example plugin is a branching rule or an event handler, which is exclusively created to call this function. + The user must then make certain to return the appropriate SCIP_RESULT (e.g. DIDNOTRUN) + + :param prev_col_features: The list of column features previously returned by this function + :param prev_edge_features: The list of edge features previously returned by this function + :param prev_row_features: The list of row features previously returned by this function + :param static_only: Whether exclusively static features should be generated + :param suppress_warnings: Whether warnings should be suppressed + """ + + cdef SCIP* scip = self._scip + cdef int i, j, k, col_i + cdef SCIP_VARTYPE vtype + cdef SCIP_Real sim, prod + + # Check if SCIP is in the correct stage + if SCIPgetStage(scip) != SCIP_STAGE_SOLVING: + raise Warning("This functionality can only been called in SCIP_STAGE SOLVING. The row and column" + "information is then accessible") + + # Generate column features + cdef SCIP_COL** cols = SCIPgetLPCols(scip) + cdef int ncols = SCIPgetNLPCols(scip) + + if static_only: + n_col_features = 5 + col_feature_map = {"continuous": 0, "binary": 1, "integer": 2, "implicit_integer": 3, "obj_coef": 4} + else: + n_col_features = 19 + col_feature_map = {"continuous": 0, "binary": 1, "integer": 2, "implicit_integer": 3, "obj_coef": 4, + "has_lb": 5, "has_ub": 6, "sol_at_lb": 7, "sol_at_ub": 8, "sol_val": 9, "sol_frac": 10, + "red_cost": 11, "basis_lower": 12, "basis_basic": 13, "basis_upper": 14, + "basis_zero": 15, "best_incumbent_val": 16, "avg_incumbent_val": 17, "age": 18} + + if prev_col_features is None: + col_features = [[0 for _ in range(n_col_features)] for _ in range(ncols)] + else: + assert len(prev_col_features) > 0, "Previous column features is empty" + col_features = prev_col_features + if len(prev_col_features) != ncols: + if not suppress_warnings: + raise Warning(f"The number of columns has changed. Previous column data being ignored") + else: + col_features = [[0 for _ in range(n_col_features)] for _ in range(ncols)] + prev_col_features = None + if len(prev_col_features[0]) != n_col_features: + raise Warning(f"Dimension mismatch in provided previous features and new features:" + f"{len(prev_col_features[0])} != {n_col_features}") + + cdef SCIP_SOL* sol = SCIPgetBestSol(scip) + cdef SCIP_VAR* var + cdef SCIP_Real lb, ub, solval + cdef SCIP_BASESTAT basis_status + for i in range(ncols): + col_i = SCIPcolGetLPPos(cols[i]) + var = SCIPcolGetVar(cols[i]) + + lb = SCIPcolGetLb(cols[i]) + ub = SCIPcolGetUb(cols[i]) + solval = SCIPcolGetPrimsol(cols[i]) + + # Collect the static features first (don't need to changed if previous features are passed) + if prev_col_features is None: + # Variable types + vtype = SCIPvarGetType(var) + if vtype == SCIP_VARTYPE_BINARY: + col_features[col_i][col_feature_map["binary"]] = 1 + elif vtype == SCIP_VARTYPE_INTEGER: + col_features[col_i][col_feature_map["integer"]] = 1 + elif vtype == SCIP_VARTYPE_CONTINUOUS: + col_features[col_i][col_feature_map["continuous"]] = 1 + elif vtype == SCIP_VARTYPE_IMPLINT: + col_features[col_i][col_feature_map["implicit_integer"]] = 1 + # Objective coefficient + col_features[col_i][col_feature_map["obj_coef"]] = SCIPcolGetObj(cols[i]) + + # Collect the dynamic features + if not static_only: + # Lower bound + if not SCIPisInfinity(scip, abs(lb)): + col_features[col_i][col_feature_map["has_lb"]] = 1 + + # Upper bound + if not SCIPisInfinity(scip, abs(ub)): + col_features[col_i][col_feature_map["has_ub"]] = 1 + + # Basis status + basis_status = SCIPcolGetBasisStatus(cols[i]) + if basis_status == SCIP_BASESTAT_LOWER: + col_features[col_i][col_feature_map["basis_lower"]] = 1 + elif basis_status == SCIP_BASESTAT_BASIC: + col_features[col_i][col_feature_map["basis_basic"]] = 1 + elif basis_status == SCIP_BASESTAT_UPPER: + col_features[col_i][col_feature_map["basis_upper"]] = 1 + elif basis_status == SCIP_BASESTAT_ZERO: + col_features[col_i][col_feature_map["basis_zero"]] = 1 + + # Reduced cost + col_features[col_i][col_feature_map["red_cost"]] = SCIPgetColRedcost(scip, cols[i]) + + # Age + col_features[col_i][col_feature_map["age"]] = SCIPcolGetAge(cols[i]) + + # LP solution value + col_features[col_i][col_feature_map["sol_val"]] = solval + col_features[col_i][col_feature_map["sol_frac"]] = SCIPfeasFrac(scip, solval) + col_features[col_i][col_feature_map["sol_at_lb"]] = int(SCIPisEQ(scip, solval, lb)) + col_features[col_i][col_feature_map["sol_at_ub"]] = int(SCIPisEQ(scip, solval, ub)) + + # Incumbent solution value + if sol is NULL: + col_features[col_i][col_feature_map["best_incumbent_val"]] = None + col_features[col_i][col_feature_map["avg_incumbent_val"]] = None + else: + col_features[col_i][col_feature_map["best_incumbent_val"]] = SCIPgetSolVal(scip, sol, var) + col_features[col_i][col_feature_map["avg_incumbent_val"]] = SCIPvarGetAvgSol(var) + + # Generate row features + cdef int nrows = SCIPgetNLPRows(scip) + cdef SCIP_ROW** rows = SCIPgetLPRows(scip) + + if static_only: + n_row_features = 6 + row_feature_map = {"has_lhs": 0, "has_rhs": 1, "n_non_zeros": 2, "obj_cosine": 3, "bias": 4, "norm": 5} + else: + n_row_features = 14 + row_feature_map = {"has_lhs": 0, "has_rhs": 1, "n_non_zeros": 2, "obj_cosine": 3, "bias": 4, "norm": 5, + "sol_at_lhs": 6, "sol_at_rhs": 7, "dual_sol": 8, "age": 9, + "basis_lower": 10, "basis_basic": 11, "basis_upper": 12, "basis_zero": 13} + + if prev_row_features is None: + row_features = [[0 for _ in range(n_row_features)] for _ in range(nrows)] + else: + assert len(prev_row_features) > 0, "Previous row features is empty" + row_features = prev_row_features + if len(prev_row_features) != nrows: + if not suppress_warnings: + raise Warning(f"The number of rows has changed. Previous row data being ignored") + else: + row_features = [[0 for _ in range(n_row_features)] for _ in range(nrows)] + prev_row_features = None + if len(prev_row_features[0]) != n_row_features: + raise Warning(f"Dimension mismatch in provided previous features and new features:" + f"{len(prev_row_features[0])} != {n_row_features}") + + cdef int nnzrs = 0 + cdef SCIP_Real lhs, rhs, cst + for i in range(nrows): + + # lhs <= activity + cst <= rhs + lhs = SCIProwGetLhs(rows[i]) + rhs = SCIProwGetRhs(rows[i]) + cst = SCIProwGetConstant(rows[i]) + activity = SCIPgetRowLPActivity(scip, rows[i]) + + if prev_row_features is None: + # number of coefficients + row_features[i][row_feature_map["n_non_zeros"]] = SCIProwGetNLPNonz(rows[i]) + nnzrs += row_features[i][row_feature_map["n_non_zeros"]] + + # left-hand-side + if not SCIPisInfinity(scip, abs(lhs)): + row_features[i][row_feature_map["has_lhs"]] = 1 + + # right-hand-side + if not SCIPisInfinity(scip, abs(rhs)): + row_features[i][row_feature_map["has_rhs"]] = 1 + + # bias + row_features[i][row_feature_map["bias"]] = cst + + # Objective cosine similarity + row_features[i][row_feature_map["obj_cosine"]] = SCIPgetRowObjParallelism(scip, rows[i]) + + # L2 norm + row_features[i][row_feature_map["norm"]] = SCIProwGetNorm(rows[i]) + + if not static_only: + + # Dual solution + row_features[i][row_feature_map["dual_sol"]] = SCIProwGetDualsol(rows[i]) + + # Basis status + basis_status = SCIProwGetBasisStatus(rows[i]) + if basis_status == SCIP_BASESTAT_LOWER: + row_features[i][row_feature_map["basis_lower"]] = 1 + elif basis_status == SCIP_BASESTAT_BASIC: + row_features[i][row_feature_map["basis_basic"]] = 1 + elif basis_status == SCIP_BASESTAT_UPPER: + row_features[i][row_feature_map["basis_upper"]] = 1 + elif basis_status == SCIP_BASESTAT_ZERO: + row_features[i][row_feature_map["basis_zero"]] = 1 + + # Age + row_features[i][row_feature_map["age"]] = SCIProwGetAge(rows[i]) + + # Is tight + row_features[i][row_feature_map["sol_at_lhs"]] = int(SCIPisEQ(scip, activity, lhs)) + row_features[i][row_feature_map["sol_at_rhs"]] = int(SCIPisEQ(scip, activity, rhs)) + + # Generate edge (coefficient) features + cdef SCIP_COL** row_cols + cdef SCIP_Real * row_vals + n_edge_features = 3 + edge_feature_map = {"col_idx": 0, "row_idx": 1, "coef": 2} + if prev_edge_features is None: + edge_features = [[0 for _ in range(n_edge_features)] for _ in range(nnzrs)] + j = 0 + for i in range(nrows): + # coefficient indexes and values + row_cols = SCIProwGetCols(rows[i]) + row_vals = SCIProwGetVals(rows[i]) + for k in range(row_features[i][row_feature_map["n_non_zeros"]]): + edge_features[j][edge_feature_map["col_idx"]] = SCIPcolGetLPPos(row_cols[k]) + edge_features[j][edge_feature_map["row_idx"]] = i + edge_features[j][edge_feature_map["coef"]] = row_vals[k] + j += 1 + else: + assert len(prev_edge_features) > 0, "Previous edge features is empty" + edge_features = prev_edge_features + if len(prev_edge_features) != nnzrs: + if not suppress_warnings: + raise Warning(f"The number of coefficients in the LP has changed. Previous edge data being ignored") + else: + edge_features = [[0 for _ in range(3)] for _ in range(nnzrs)] + prev_edge_features = None + if len(prev_edge_features[0]) != 3: + raise Warning(f"Dimension mismatch in provided previous features and new features:" + f"{len(prev_edge_features[0])} != 3") + + + return (col_features, edge_features, row_features, + {"col": col_feature_map, "edge": edge_feature_map, "row": row_feature_map}) + @dataclass class Statistics: """ diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py new file mode 100644 index 000000000..a25253524 --- /dev/null +++ b/tests/test_bipartite.py @@ -0,0 +1,118 @@ +from pyscipopt import Model, Branchrule, SCIP_RESULT, quicksum, SCIP_PARAMSETTING + +""" +This is a test for the bipartite graph generation functionality. +To make the test more practical, we embed the function in a dummy branching rule. Such functionality would allow +users to then extract the feature set before any branching decision. This can be used to gather data for training etc +and to to deploy actual branching rules trained on data from the graph representation. +""" + + +class DummyFeatureExtractingBranchRule(Branchrule): + + def __init__(self, scip, static=False, use_prev_states=True): + self.scip = scip + self.static = static + self.use_prev_states = use_prev_states + self.prev_col_features = None + self.prev_row_features = None + self.prev_edge_features = None + + def branchexeclp(self, allowaddcons): + + # Get the bipartite graph data + if self.use_prev_states: + prev_col_features = self.prev_col_features + prev_edge_features = self.prev_edge_features + prev_row_features = self.prev_row_features + else: + prev_col_features = None + prev_edge_features = None + prev_row_features = None + col_features, edge_features, row_features, feature_maps = self.scip.getBipartiteGraphRepresentation( + prev_col_features=prev_col_features, prev_edge_features=prev_edge_features, + prev_row_features=prev_row_features, static_only=self.static + ) + + # Here is now where a decision could be based off the features. If no decision is made just return DIDNOTRUN + + return {"result": SCIP_RESULT.DIDNOTRUN} + + + + + +def create_model(): + scip = Model() + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.setSeparating(SCIP_PARAMSETTING.OFF) + scip.setLongintParam("limits/nodes", 250) + scip.setParam("presolving/maxrestarts", 0) + + x0 = scip.addVar(lb=-2, ub=4) + r1 = scip.addVar() + r2 = scip.addVar() + y0 = scip.addVar(lb=3) + t = scip.addVar(lb=None) + l = scip.addVar(vtype="I", lb=-9, ub=18) + u = scip.addVar(vtype="I", lb=-3, ub=99) + + more_vars = [] + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-12, ub=40)) + scip.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-52, ub=10)) + scip.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) + + scip.addCons(r1 >= x0) + scip.addCons(r2 >= -x0) + scip.addCons(y0 == r1 + r2) + scip.addCons(t + l + 7 * u <= 300) + scip.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) + scip.addCons(more_vars[3] >= l + 2) + scip.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) + scip.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) + + scip.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) + + return scip + + +def test_bipartite_graph(): + scip = create_model() + + dummy_branch_rule = DummyFeatureExtractingBranchRule(scip) + scip.includeBranchrule(dummy_branch_rule, "dummy branch rule", "custom feature extraction branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() + + +def test_bipartite_graph_static(): + scip = create_model() + + dummy_branch_rule = DummyFeatureExtractingBranchRule(scip, static=True) + scip.includeBranchrule(dummy_branch_rule, "dummy branch rule", "custom feature extraction branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() + +def test_bipartite_graph_use_prev(): + scip = create_model() + + dummy_branch_rule = DummyFeatureExtractingBranchRule(scip, use_prev_states=True) + scip.includeBranchrule(dummy_branch_rule, "dummy branch rule", "custom feature extraction branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() + +def test_bipartite_graph_static_use_prev(): + scip = create_model() + + dummy_branch_rule = DummyFeatureExtractingBranchRule(scip, static=True, use_prev_states=True) + scip.includeBranchrule(dummy_branch_rule, "dummy branch rule", "custom feature extraction branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + + scip.optimize() \ No newline at end of file From 94815ee38d62fb1be82660e1e67bd6764e21149c Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 2 Aug 2024 16:45:01 +0200 Subject: [PATCH 054/135] Fix path where version is loaded --- generate-docs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate-docs.sh b/generate-docs.sh index 044c1ecd5..8e222cb34 100755 --- a/generate-docs.sh +++ b/generate-docs.sh @@ -31,7 +31,7 @@ echo "Downloading SCIP tagfile to create links to SCIP docu" wget -q -O docs/scip.tag https://scip.zib.de/doc/scip.tag #get version number for doxygen -export VERSION_NUMBER=$(grep "__version__" src/pyscipopt/__init__.py | cut -d ' ' -f 3 | tr --delete \') +export VERSION_NUMBER=$(grep "__version__" src/pyscipopt/_version.py | cut -d ' ' -f 3 | tr --delete \') # generate html documentation in docs/html echo "Generating documentation" From be1c5489197f9c19374ef94851182e4a2bcd0e35 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 2 Aug 2024 16:58:33 +0200 Subject: [PATCH 055/135] Add release guide (#878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add basic release guide * Add update documentation step * Make the release guide a checklist instead of a list --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + CONTRIBUTING.md | 9 +-------- RELEASE.md | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 RELEASE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2585e6b..a17c223c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Created Statistics class - Added parser to read .stats file +- Release checklist in `RELEASE.md` - Added Python definitions and wrappers for SCIPstartStrongbranch, SCIPendStrongbranch SCIPgetBranchScoreMultiple, SCIPgetVarStrongbranchInt, SCIPupdateVarPseudocost, SCIPgetVarStrongbranchFrac, SCIPcolGetAge, SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b002ea69..f996ce613 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,14 +43,7 @@ If you find this contributing guide unclear, please open an issue! :) How to craft a release ---------------------- -1. update `CHANGELOG` -2. increase version number in `src/pyscipopt/__init__.py` according to semantic versioning -3. commit changes to the master branch -3. tag new version `git tag vX.Y.Z` -4. `git push` && `git push --tags` -5. [create GitHub release](https://github.com/scipopt/PySCIPOpt/releases) based on that tag - -A new PyPI package is automatically created by the GitHub actions when pushing a new tag onto the master and the version has been increased. Also the documentation is autmatically created in the process. +Moved to [RELEASE.md](RELEASE.md). Design principles of PySCIPOpt ============================== diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..9f600ec82 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,19 @@ +# Release Checklist +The following are the steps to follow to make a new PySCIPOpt release. They should mostly be done in order. +- [ ] Check if [scipoptsuite-deploy](https://github.com/scipopt/scipoptsuite-deploy) needs a new release, if a new SCIP version is released for example, or new dependencies (change symmetry dependency, add support for papilo/ parallelization.. etc). And Update release links in `pyproject.toml` +- [ ] Check if the table in [readme](https://github.com/scipopt/PySCIPOpt#installation) needs to be updated. +- [ ] Update version number according to semantic versioning [rules](https://semver.org/) in `_version.py`.  +- [ ] Update `CHANGELOG.md`; Change the `Unlreased` to the new version number and add an empty unreleased section. +- [ ] Create a release candidate on test-pypi by running the workflow “Build wheels” in Actions->build wheels, with these parameters `upload:true, test-pypi:true`  +- [ ] If the pipeline passes, test the released pip package on test-pypi by running and checking that it works +```bash +pip install -i https://test.pypi.org/simple/ PySCIPOpt +``` +- [ ] If it works, release on pypi.org with running the same workflow but with `test-pypi:false`. +- [ ] Then create a tag wit the new version (from the master branch) +```bash +git tag vX.X.X +git push origin vX.X.X +``` +- [ ] Then make a github [release](https://github.com/scipopt/PySCIPOpt/releases/new) from this new tag. +- [ ] Update documentation by running the `Generate Docs` workflow in Actions->Generate Docs. \ No newline at end of file From d9b2fabe289ed8de4c9e99bc53ba891b2d02139a Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 5 Aug 2024 11:00:03 +0100 Subject: [PATCH 056/135] Add prototype for utils functions --- tests/helpers/utils.py | 109 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/helpers/utils.py diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py new file mode 100644 index 000000000..92ce7108d --- /dev/null +++ b/tests/helpers/utils.py @@ -0,0 +1,109 @@ +from pyscipopt import Model, quicksum, SCIP_PARAMSETTING, exp, log, sqrt, sin +from typing import List + +def random_MIP_1(): + scip = Model() + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.setSeparating(SCIP_PARAMSETTING.OFF) + scip.setLongintParam("limits/nodes", 250) + scip.setParam("presolving/maxrestarts", 0) + + x0 = scip.addVar(lb=-2, ub=4) + r1 = scip.addVar() + r2 = scip.addVar() + y0 = scip.addVar(lb=3) + t = scip.addVar(lb=None) + l = scip.addVar(vtype="I", lb=-9, ub=18) + u = scip.addVar(vtype="I", lb=-3, ub=99) + + more_vars = [] + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-12, ub=40)) + scip.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + + for i in range(100): + more_vars.append(scip.addVar(vtype="I", lb=-52, ub=10)) + scip.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) + + scip.addCons(r1 >= x0) + scip.addCons(r2 >= -x0) + scip.addCons(y0 == r1 + r2) + scip.addCons(t + l + 7 * u <= 300) + scip.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) + scip.addCons(more_vars[3] >= l + 2) + scip.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) + scip.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) + + scip.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) + + return scip + +def random_lp_1(): + return random_MIP_1().relax() + + +def random_nlp_1(): + model = Model() + + v = model.addVar() + w = model.addVar() + x = model.addVar() + y = model.addVar() + z = model.addVar() + + model.addCons(exp(v)+log(w)+sqrt(x)+sin(y)+z**3 * y <= 5) + model.setObjective(v + w + x + y + z, sense='maximize') + + return + +def knapsack_model(weights = [4, 2, 6, 3, 7, 5], costs = [7, 2, 5, 4, 3, 4]): + # create solver instance + s = Model("Knapsack") + s.hideOutput() + + # setting the objective sense to maximise + s.setMaximize() + + assert len(weights) == len(costs) + + # knapsack size + knapsackSize = 15 + + # adding the knapsack variables + knapsackVars = [] + varNames = [] + varBaseName = "Item" + for i in range(len(weights)): + varNames.append(varBaseName + "_" + str(i)) + knapsackVars.append(s.addVar(varNames[i], vtype='I', obj=costs[i], ub=1.0)) + + + # adding a linear constraint for the knapsack constraint + s.addCons(quicksum(w*v for (w, v) in zip(weights, knapsackVars)) <= knapsackSize) + + return s + +def bin_packing_model(sizes: List[int], capacity: int) -> Model: + model = Model("Binpacking") + n = len(sizes) + x = {} + for i in range(n): + for j in range(n): + x[i, j] = model.addVar(vtype="B", name=f"x{i}_{j}") + y = [model.addVar(vtype="B", name=f"y{i}") for i in range(n)] + + for i in range(n): + model.addCons( + quicksum(x[i, j] for j in range(n)) == 1 + ) + + for j in range(n): + model.addCons( + quicksum(sizes[i] * x[i, j] for i in range(n)) <= capacity * y[j] + ) + + model.setObjective( + quicksum(y[j] for j in range(n)), "minimize" + ) + + return model \ No newline at end of file From 2e78244fc46094c68005b8df5aa50d0aa1c88d3e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 5 Aug 2024 14:48:17 +0100 Subject: [PATCH 057/135] Fix typo --- tests/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 92ce7108d..99c9807bc 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -54,7 +54,7 @@ def random_nlp_1(): model.addCons(exp(v)+log(w)+sqrt(x)+sin(y)+z**3 * y <= 5) model.setObjective(v + w + x + y + z, sense='maximize') - return + return model def knapsack_model(weights = [4, 2, 6, 3, 7, 5], costs = [7, 2, 5, 4, 3, 4]): # create solver instance From 964d9dad8b65754160d335aed1cfb729bc2f0350 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 15 Aug 2024 10:23:14 +0100 Subject: [PATCH 058/135] Add test for relax --- tests/test_relax.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_relax.py b/tests/test_relax.py index f797ae548..055ebb6e5 100644 --- a/tests/test_relax.py +++ b/tests/test_relax.py @@ -1,6 +1,7 @@ from pyscipopt import Model, SCIP_RESULT from pyscipopt.scip import Relax import pytest +from helpers.utils import random_MIP_1 calls = [] @@ -14,7 +15,7 @@ def relaxexec(self): } -def test_relax(): +def test_relaxator(): m = Model() m.hideOutput() @@ -62,3 +63,15 @@ def test_empty_relaxator(): with pytest.raises(Exception): m.optimize() + +def test_relax(): + model = random_MIP_1() + + x = model.addVariable(vtype="B") + + model.relax() + + assert x.lb() == 0 and x.ub() == 1 + + for var in model.getVars(): + assert var.getType() == "C" \ No newline at end of file From 698df851e3851ebc6e2e282c53325634a8853337 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 15 Aug 2024 10:25:38 +0100 Subject: [PATCH 059/135] Fix relax test errors --- tests/test_relax.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_relax.py b/tests/test_relax.py index 055ebb6e5..588067a2e 100644 --- a/tests/test_relax.py +++ b/tests/test_relax.py @@ -67,11 +67,11 @@ def test_empty_relaxator(): def test_relax(): model = random_MIP_1() - x = model.addVariable(vtype="B") + x = model.addVar(vtype="B") model.relax() - assert x.lb() == 0 and x.ub() == 1 + assert x.getLbGlobal() == 0 and x.getUbGlobal() == 1 for var in model.getVars(): - assert var.getType() == "C" \ No newline at end of file + assert var.vtype() == "CONTINUOUS" \ No newline at end of file From 2191d0ae7592799ca7f767d5e31474d80be98cb4 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 15 Aug 2024 10:37:33 +0100 Subject: [PATCH 060/135] Added gas transportation model --- tests/helpers/utils.py | 179 ++++++++++++++++++++++++++++++++++------- 1 file changed, 150 insertions(+), 29 deletions(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 99c9807bc..f6b5946fa 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -2,45 +2,40 @@ from typing import List def random_MIP_1(): - scip = Model() - scip.setHeuristics(SCIP_PARAMSETTING.OFF) - scip.setSeparating(SCIP_PARAMSETTING.OFF) - scip.setLongintParam("limits/nodes", 250) - scip.setParam("presolving/maxrestarts", 0) - - x0 = scip.addVar(lb=-2, ub=4) - r1 = scip.addVar() - r2 = scip.addVar() - y0 = scip.addVar(lb=3) - t = scip.addVar(lb=None) - l = scip.addVar(vtype="I", lb=-9, ub=18) - u = scip.addVar(vtype="I", lb=-3, ub=99) + model = Model() + + x0 = model.addVar(lb=-2, ub=4) + r1 = model.addVar() + r2 = model.addVar() + y0 = model.addVar(lb=3) + t = model.addVar(lb=None) + l = model.addVar(vtype="I", lb=-9, ub=18) + u = model.addVar(vtype="I", lb=-3, ub=99) more_vars = [] for i in range(100): - more_vars.append(scip.addVar(vtype="I", lb=-12, ub=40)) - scip.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + more_vars.append(model.addVar(vtype="I", lb=-12, ub=40)) + model.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) for i in range(100): - more_vars.append(scip.addVar(vtype="I", lb=-52, ub=10)) - scip.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) + more_vars.append(model.addVar(vtype="I", lb=-52, ub=10)) + model.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) - scip.addCons(r1 >= x0) - scip.addCons(r2 >= -x0) - scip.addCons(y0 == r1 + r2) - scip.addCons(t + l + 7 * u <= 300) - scip.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) - scip.addCons(more_vars[3] >= l + 2) - scip.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) - scip.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) + model.addCons(r1 >= x0) + model.addCons(r2 >= -x0) + model.addCons(y0 == r1 + r2) + model.addCons(t + l + 7 * u <= 300) + model.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) + model.addCons(more_vars[3] >= l + 2) + model.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) + model.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) - scip.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) + model.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) - return scip + return model def random_lp_1(): return random_MIP_1().relax() - def random_nlp_1(): model = Model() @@ -106,4 +101,130 @@ def bin_packing_model(sizes: List[int], capacity: int) -> Model: quicksum(y[j] for j in range(n)), "minimize" ) - return model \ No newline at end of file + return model + +# test gastrans: see example in /examples/CallableLibrary/src/gastrans.c +# of course there is a more pythonic/elegant way of implementing this, probably +# starting by using a proper graph structure +def gastrans_model(): + GASTEMP = 281.15 + RUGOSITY = 0.05 + DENSITY = 0.616 + COMPRESSIBILITY = 0.8 + nodes = [ + # name supplylo supplyup pressurelo pressureup cost + ("Anderlues", 0.0, 1.2, 0.0, 66.2, 0.0 ), # 0 + ("Antwerpen", None, -4.034, 30.0, 80.0, 0.0 ), # 1 + ("Arlon", None, -0.222, 0.0, 66.2, 0.0 ), # 2 + ("Berneau", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 3 + ("Blaregnies", None, -15.616, 50.0, 66.2, 0.0 ), # 4 + ("Brugge", None, -3.918, 30.0, 80.0, 0.0 ), # 5 + ("Dudzele", 0.0, 8.4, 0.0, 77.0, 2.28 ), # 6 + ("Gent", None, -5.256, 30.0, 80.0, 0.0 ), # 7 + ("Liege", None, -6.385, 30.0, 66.2, 0.0 ), # 8 + ("Loenhout", 0.0, 4.8, 0.0, 77.0, 2.28 ), # 9 + ("Mons", None, -6.848, 0.0, 66.2, 0.0 ), # 10 + ("Namur", None, -2.120, 0.0, 66.2, 0.0 ), # 11 + ("Petange", None, -1.919, 25.0, 66.2, 0.0 ), # 12 + ("Peronnes", 0.0, 0.96, 0.0, 66.2, 1.68 ), # 13 + ("Sinsin", 0.0, 0.0, 0.0, 63.0, 0.0 ), # 14 + ("Voeren", 20.344, 22.012, 50.0, 66.2, 1.68 ), # 15 + ("Wanze", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 16 + ("Warnand", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 17 + ("Zeebrugge", 8.87, 11.594, 0.0, 77.0, 2.28 ), # 18 + ("Zomergem", 0.0, 0.0, 0.0, 80.0, 0.0 ) # 19 + ] + arcs = [ + # node1 node2 diameter length active */ + ( 18, 6, 890.0, 4.0, False ), + ( 18, 6, 890.0, 4.0, False ), + ( 6, 5, 890.0, 6.0, False ), + ( 6, 5, 890.0, 6.0, False ), + ( 5, 19, 890.0, 26.0, False ), + ( 9, 1, 590.1, 43.0, False ), + ( 1, 7, 590.1, 29.0, False ), + ( 7, 19, 590.1, 19.0, False ), + ( 19, 13, 890.0, 55.0, False ), + ( 15, 3, 890.0, 5.0, True ), + ( 15, 3, 395.0, 5.0, True ), + ( 3, 8, 890.0, 20.0, False ), + ( 3, 8, 395.0, 20.0, False ), + ( 8, 17, 890.0, 25.0, False ), + ( 8, 17, 395.0, 25.0, False ), + ( 17, 11, 890.0, 42.0, False ), + ( 11, 0, 890.0, 40.0, False ), + ( 0, 13, 890.0, 5.0, False ), + ( 13, 10, 890.0, 10.0, False ), + ( 10, 4, 890.0, 25.0, False ), + ( 17, 16, 395.5, 10.5, False ), + ( 16, 14, 315.5, 26.0, True ), + ( 14, 2, 315.5, 98.0, False ), + ( 2, 12, 315.5, 6.0, False ) + ] + + model = Model() + + # create flow variables + flow = {} + for arc in arcs: + flow[arc] = model.addVar("flow_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0]), # names of nodes in arc + lb = 0.0 if arc[4] else None) # no lower bound if not active + + # pressure difference variables + pressurediff = {} + for arc in arcs: + pressurediff[arc] = model.addVar("pressurediff_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0]), # names of nodes in arc + lb = None) + + # supply variables + supply = {} + for node in nodes: + supply[node] = model.addVar("supply_%s"%(node[0]), lb = node[1], ub = node[2], obj = node[5]) + + # square pressure variables + pressure = {} + for node in nodes: + pressure[node] = model.addVar("pressure_%s"%(node[0]), lb = node[3]**2, ub = node[4]**2) + + + # node balance constrains, for each node i: outflows - inflows = supply + for nid, node in enumerate(nodes): + # find arcs that go or end at this node + flowbalance = 0 + for arc in arcs: + if arc[0] == nid: # arc is outgoing + flowbalance += flow[arc] + elif arc[1] == nid: # arc is incoming + flowbalance -= flow[arc] + else: + continue + + model.addCons(flowbalance == supply[node], name="flowbalance%s"%node[0]) + + # pressure difference constraints: pressurediff[node1 to node2] = pressure[node1] - pressure[node2] + for arc in arcs: + model.addCons(pressurediff[arc] == pressure[nodes[arc[0]]] - pressure[nodes[arc[1]]], "pressurediffcons_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + + # pressure loss constraints: + # active arc: flow[arc]^2 + coef * pressurediff[arc] <= 0.0 + # regular pipes: flow[arc] * abs(flow[arc]) - coef * pressurediff[arc] == 0.0 + # coef = 96.074830e-15*diameter(i)^5/(lambda*compressibility*temperatur*length(i)*density) + # lambda = (2*log10(3.7*diameter(i)/rugosity))^(-2) + from math import log10 + for arc in arcs: + coef = 96.074830e-15 * arc[2]**5 * (2.0*log10(3.7*arc[2]/RUGOSITY))**2 / COMPRESSIBILITY / GASTEMP / arc[3] / DENSITY + if arc[4]: # active + model.addCons(flow[arc]**2 + coef * pressurediff[arc] <= 0.0, "pressureloss_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + else: + model.addCons(flow[arc]*abs(flow[arc]) - coef * pressurediff[arc] == 0.0, "pressureloss_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + + return model + +def knapsack_lp(weights, costs): + return knapsack_model(weights, costs).relax() + +def bin_packing_lp(sizes, capacity): + return bin_packing_model(sizes, capacity).relax() + +def gastrans_lp(): + return gastrans_model().relax() \ No newline at end of file From 13465566fc4c2ffa07831dbd4b08d626464022f3 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 15 Aug 2024 10:38:14 +0100 Subject: [PATCH 061/135] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17c223c0..7ceb6b42c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ SCIPgetVarStrongbranchInt, SCIPupdateVarPseudocost, SCIPgetVarStrongbranchFrac, SCIPcolGetAge, SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge - Added getBipartiteGraphRepresentation +- Added helper functions that facilitate testing ### Fixed - Fixed too strict getObjVal, getVal check ### Changed From ec448583533cd7498c1c6c14693cf6078b4b700b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:42:15 +0000 Subject: [PATCH 062/135] Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index a397fbc91..739a5ef5b 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest if: github.event.inputs.upload_to_pypi == 'true' steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: artifact path: dist From 016b03ab613e8841146b12750b3c420651066f7f Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:55:24 +0200 Subject: [PATCH 063/135] Update build_wheels.yml --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 739a5ef5b..92b474828 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest if: github.event.inputs.upload_to_pypi == 'true' steps: - - uses: actions/download-artifact@v4.1.7 + - uses: actions/download-artifact@v4 with: name: artifact path: dist From 85086fa44f67e9f31270dbe3beaaf38da246175c Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:31:10 +0200 Subject: [PATCH 064/135] Migrate to Readthedocs and Sphinx (#890) * Add initial commit for readthedocs * Finish tutorials/model.rst * Add skeleton for readwrite * Add read and write docu * Add outline for expressions * Add expressions * Add outline for vartypes * Add more info to variable.rst * Add cons, var, and cutsel * Add outline for autodoc * Add basic branching rule * Finish rbanching and add sepa tutorial * Add heur to docu * Add lazycons * Fix small errors * Add node selector to docs * Go through writing of all tutorials * Add half complete draft of whyscip * Finish why scip * Finish why scip * Add similar software and logfile reading * Update Changelog and README" git push * rename tutorials * Add \ to asterisk and abs signs * Update nodesel and heur tests * Remove util. Update lazycons * Add autosummary to gitignore * Remove older sphinx themes * Add branch mostinfeas test * Swap out np.inf for python inf --- .gitignore | 1 + CHANGELOG.md | 6 + README.md | 11 + docs/DoxygenLayout.xml | 194 -- docs/Makefile | 20 + .../scip_structure_landscape-compressed.png | Bin 0 -> 300857 bytes docs/_static/skippy_logo_blue.png | Bin 0 -> 168883 bytes docs/api.rst | 79 + docs/build.rst | 186 ++ docs/conf.py | 90 + docs/customdoxygen.css | 1475 ---------- docs/doxy | 2617 ----------------- docs/faq.rst | 65 + docs/footer.html | 21 - docs/header.html | 55 - docs/index.rst | 24 + docs/install.rst | 47 + docs/maindoc.py | 53 - docs/requirements.txt | 4 + docs/similarsoftware.rst | 98 + docs/tutorials/branchrule.rst | 210 ++ docs/tutorials/constypes.rst | 180 ++ docs/tutorials/cutselector.rst | 104 + docs/tutorials/expressions.rst | 187 ++ docs/tutorials/heuristic.rst | 106 + docs/tutorials/index.rst | 24 + docs/tutorials/lazycons.rst | 182 ++ docs/tutorials/logfile.rst | 283 ++ docs/tutorials/model.rst | 145 + docs/tutorials/nodeselector.rst | 92 + docs/tutorials/readwrite.rst | 91 + docs/tutorials/separator.rst | 311 ++ docs/tutorials/vartypes.rst | 232 ++ docs/whyscip.rst | 138 + src/pyscipopt/scip.pxd | 12 +- src/pyscipopt/scip.pxi | 53 +- tests/helpers/utils.py | 186 +- tests/test_branch_incomplete.py | 19 - tests/test_branch_mostinfeas.py | 37 + tests/test_heur.py | 70 +- tests/test_memory.py | 2 +- tests/test_nodesel.py | 96 +- tests/test_relax.py | 4 +- tests/test_strong_branching.py | 59 +- tests/util.py | 7 - 45 files changed, 3285 insertions(+), 4591 deletions(-) delete mode 100644 docs/DoxygenLayout.xml create mode 100644 docs/Makefile create mode 100644 docs/_static/scip_structure_landscape-compressed.png create mode 100644 docs/_static/skippy_logo_blue.png create mode 100644 docs/api.rst create mode 100644 docs/build.rst create mode 100644 docs/conf.py delete mode 100644 docs/customdoxygen.css delete mode 100644 docs/doxy create mode 100644 docs/faq.rst delete mode 100644 docs/footer.html delete mode 100644 docs/header.html create mode 100644 docs/index.rst create mode 100644 docs/install.rst delete mode 100644 docs/maindoc.py create mode 100644 docs/requirements.txt create mode 100644 docs/similarsoftware.rst create mode 100644 docs/tutorials/branchrule.rst create mode 100644 docs/tutorials/constypes.rst create mode 100644 docs/tutorials/cutselector.rst create mode 100644 docs/tutorials/expressions.rst create mode 100644 docs/tutorials/heuristic.rst create mode 100644 docs/tutorials/index.rst create mode 100644 docs/tutorials/lazycons.rst create mode 100644 docs/tutorials/logfile.rst create mode 100644 docs/tutorials/model.rst create mode 100644 docs/tutorials/nodeselector.rst create mode 100644 docs/tutorials/readwrite.rst create mode 100644 docs/tutorials/separator.rst create mode 100644 docs/tutorials/vartypes.rst create mode 100644 docs/whyscip.rst delete mode 100644 tests/test_branch_incomplete.py create mode 100644 tests/test_branch_mostinfeas.py delete mode 100644 tests/util.py diff --git a/.gitignore b/.gitignore index c27befd09..5b27f8bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/_autosummary/ # PyBuilder target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 822724f3f..1afd5e9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,15 @@ SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge - Added getBipartiteGraphRepresentation - Added helper functions that facilitate testing +- Added Python definitions and wrappers for SCIPgetNImplVars, SCIPgetNContVars, SCIPvarMayRoundUp, + SCIPvarMayRoundDown, SCIPcreateLPSol, SCIPfeasFloor, SCIPfeasCeil, SCIPfeasRound, SCIPgetPrioChild, + SCIPgetPrioSibling +- Added additional tests to test_nodesel, test_heur, and test_strong_branching +- Migrated documentation to Readthedocs ### Fixed - Fixed too strict getObjVal, getVal check ### Changed +- Changed createSol to now have an option of initialising at the current LP solution ### Removed ## 5.1.1 - 2024-06-22 diff --git a/README.md b/README.md index 345d4b00c..55985f631 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,17 @@ Please consult the [online documentation](https://scipopt.github.io/PySCIPOpt/do See [CHANGELOG.md](https://github.com/scipopt/PySCIPOpt/blob/master/CHANGELOG.md) for added, removed or fixed functionality. +You can also build the documentation locally with the command +```shell +pip install -r docs/requirements.txt +sphinx-build docs docs/_build +``` +Às the documentation requires additional python packages, one should run the following command +before building the documentation for the first time: +```shell +(venv) pip install -r docs/requirements.txt +``` + Installation ------------ diff --git a/docs/DoxygenLayout.xml b/docs/DoxygenLayout.xml deleted file mode 100644 index 1370407d2..000000000 --- a/docs/DoxygenLayout.xml +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..d4bb2cbb9 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/scip_structure_landscape-compressed.png b/docs/_static/scip_structure_landscape-compressed.png new file mode 100644 index 0000000000000000000000000000000000000000..9747a461dd91ba6a556550c196ec7687a95fa074 GIT binary patch literal 300857 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

CXE62k5fQzdYw7`j1d9yBgr&WTG4D$iFz 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

0gIAj; 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 literal 0 HcmV?d00001 diff --git a/docs/_static/skippy_logo_blue.png b/docs/_static/skippy_logo_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..6eaf91309cd2bea141349b4cac0c1172bb1aabeb GIT binary patch literal 168883 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

G_XOro)Tok7o?pZ7wK(AW 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%0DhRisVx@Nnh zlh3O!&dnn^AaU`#!sv6OH4b@6YBs=t(?I6izn>XZFmBf8rh}8T?4n2T-l4ZEN8I%O zE6__%XewvSv=~pY15vTv{<^5F@rO{?8gti?ShFY`d7_k(G_zlk2nOfhRRu}&Q{+c? zt?vk(As-+#OxbX}_dP+Ee;BBk%?pp`s!lahkESRj8&S=lxuye1>xlr+VG@3v^4f?- zbEo!U+VAU}Rbi)yGNw)3{wabMeVasZ<{Gn|AWFOQ<2ZRNT}Rjx#_yp~d}x(QZJ|zwZ7ZIm3zQfDj(alTN!Z^?V~tJzGm^D|NPmRIm9;sb|rr|wsCLa4hI2I8b&jwk=(pu2K1HS{byWfkh73iL=f z5lBFr{d)U7?d?*h^la}IT-#UTc=^am|7QH_uq-J)NBIp@eP_7q-YoxAtKT1d53FT= z2Qj3QXt6lawqtTUVn3ly*jSUY6+Gd8eIxhd(#H4&XX+?W$a4NzfcHXdFo|L2G5@B& zhyAiJK;ZO4|tOMI}H>vLO-QDo6?P@I4qhy;QMLOiZ{ z8~jEkXY4VNG4u9n$Wpe%%?s>IwJ@pBo;Ug99=&;2l3{{F^gkXOc`$wI*sEYGbTL%s zsso-PL=-35FpmE)ZqAx?!k?2L-M?6vxOspi2~27z#K=c0^0}g@&ox6M1K0e10J;{< z^i+D`8g{S{&M6@Rx%k-mb4idKU_qofCFre@m|*(VKhD9eWI} zDGchV?CgI#WEpMrg8CQy+k@EHQzd7b8%O@IM&E;6+@62BG*ff3b|r`6mpALpxE+t4 zfX#>o!=bR!6(>@Mc?*d-scDq)j(4^8slor6!!J60JT_e=J#03N3sz-gECe@D!XWa?6c~E9zWYm3njM_ zRR0)li4nE3GpBxAT_MnV$6RMJggVHySK>a}9!@w0bbioC+bZY_C(8%luOdjmZW#;2FMt1fcf6_zrMJT z9OLDl-ivVCAf6VB-MSxp=HH9IpRo&@2+eQiIlvab z4(Ek;R9(>sj+pBB^<*L(rj4naaMgX|z`NQg=Q(#53mOX+R%Vagp{c3=N{^ppP_-&Q z!#xfHU3y*dC%y`XhV}L;O`VCk#sNC@8wnZ4d3i0rXPOpD zjt=FZ&u{swwqQI)cT`=6%{oatXp%a?5|4Ai|{RD$9wKVH&i4Sy0S+JSbN5BY--FLx` zYHuUGFZ$4*`V^B+9s0h@MbHwFQrRV>kH>WnTqPdExEe%h8;&VW``sOgzpH%gr#z)z zNqw+`qZcpz;_%@!>;4t!MQ}{wF*0vR_lgWayfbViwoWL>V){QXfXmkIV3LDmTO%d zSFp%DH~->#iTx0RoWm%cDf;K~<$v;J((8|YT_mB>FEo{}thpa3S!xMuTt-@TaaH?9 zf}b7ZyEfS1Oeg7eNkWFC)21c~yp#)jY3&*G2YyD+se|V(g6)E3p-otp5hAnEa(R%ytQRYo9k!Ev)H>pWj2WJZI*j=6?e- zXcsJLZkWMm`Y|8&YmXZ*iL&st-{heyb=0~?iT)8rvyP3EV}Nw<12B)!$kkEYK__cJ5+@Nx1xu zAZDsS!P`x%-qkb?w8`kzGGhHUDg1LICX2Mfew`lV!V4B}A6D3_vBcImtt5=_ z#v=xH`iDq;%p8p0vVm_LNm+5TsCY7G8fBVKa6!E8_x&7$zEN+P6wQVE#W|EHm8$Sm z>(ptk{Qmb*^Eku&6U5%`M$|cQ2j&NF-j>j2eayvTh}_oCDRH6$dUeMUA+^z`ft3;a zPdM+DCNB7gF&BlX9W?d3b=6ib-R`jbE!~M*huCUbnU(Ebvv7~m!qVTPe(!HY#U?Oa2ibT%f zHt@EX)nYr?zs9O7thg(;4uoOS?_-oKAvijK+3IWRZ`W~!5U@=|4&5U;5!e`{o*J}YC884=N zoq;R-6KixV^h%J#R{488v^&Hg!2^kb0G^P(;-osNRi|`74;!5S4!ZzB$==7ra&Z$Cqso@^_X!nD=s^!NDjI4( zggzfd3F(0!ZHFiT#*e(MZW%OQl|}X;?%xeHf_jaUR{?y;g;Xe|a{Z@IwC3_{UMTlT znS?W%prZ2+PuTZFoQ?_5du=g6#efnws(_^5IkTr=K|=!@mT%1v9oJ`LGNII=)8X*w zf6MdNQyYrF%5`nH_&vB{UP9k+E8lBq?0ZZI>5D>`cKU}qjzICca*t50vxjLD5XUJR zO4fVn*R{8{)bmHw0(Elt#8>WpLRG=6v0#DVFu`+unVTnsDuo- zc=ZH9tjD?MfYtIdhg?19wBMY^84=1l^oJ{&L`lFPJiBK@zrSOZxS=HJAe%og(g8Mh85s)w0E%JCpZhJKu9 z^<%3BMIGD*v9_OhdZ2kMR5Bx5c=_LcGq|Q&a>Gf!L)woyYS`Lvum9#Jcx*MO#e&se zeWpMNt21Yas)@z*8Z2#~dddobTEjO)Aw2AB&{V?H?IN?A7jb(MM|@met=NB^v){dq z?#nGiHRiZ#s}}ImIZiK+l$1(1sKawR)j`waPvHHnVO;Qntg#!%2p>h}xnRf72xdVZp(PjinU0BMU@Dn%S3AeY7~qez3V+5!}l2Gf;tu08wLKTJK> znN-4~*jtMf0KSxI*i#~v`!2%wD+PL|tqoQ{p@5oaGDr4)Kcu(UetP?+VheWvRP0ZkKy^`tvCAogHtT>%7fx7AwChz1ypr+2sV z#s$-PY?^pZbtzN#3c_vOuLD`M#JjZWHnSIl?{XPXH#mvg;{z>S5+NwEAoxi`+AMgb zt%{Sd!X|K_T02A}n0)-`Zp=MtMcFa55F>+1`X!^U2N;2^x7oE5E1m8T8NSX0KD~ zj_4IXRpxWQDXWNp!@Tg3u3&91Wqs8you3t61Bp~dxNsrTapf%yn%OKX=4GL776~kS zx+^k(DEeYxi?+1OGmwt4xaUf0ypRi5VBhil z?xp%p$lk-FBLd|SlsWZ#n`g1tP8)O);7qup24IrKaFboE4Z0lub5BZM&|7T3V`tPg zr`uyzcD@W_g57of>R7Dt)6=du-72qi*v@L^t2IR@Tc7EyD}K9G3@*M(jnv2I`xFin zNm*SnoQb;mQ|KoqysP!Y!QH4Cj55x57vSW<@QWtUSkhuTJ9?zw)~ISEpXK3YzqI!+ z`6X>T9;7-Yh{HVp^nQH%Go5IP?kXenesWkM&Re}Kxk#=uD1>BI)m#~aDFMJ8IGco= zD5Kv7H7Uy&yQyz#KBBP;fWw|DrzOHXNOn*4Z~;xc1JI=bViKB5Br&!Q(yx)3<$S=T zGkZ_Lk(T=dn1n0@*B3k%xzg%Io9KZnEp;&<5qwoOa%e#Ydhzu~#J%+v?qF^8akp3d zBxWc#b(u9^YdU_3X2| zl(zJUePlC5B(7H;+iIg=^2Ay1@J=Wo$k&y*PmX!0QmDDTk}xM2i>rxAaT=oFk`5Mh z&{ClGXGq0xC&ITOEagwdzh+K_nu$EVXN9y2);KY5qU$L_XF}wIvo}rpL9Zz${s__Q zQXwpa9{zG0SpWa=Vyf&)S+r9`d3|yL)&?!}*^Iv_qECa-pZe77ifEmzSWK37HW zYL8FoJyhc<7}uG0t(&n#=oq~;GlVwn=F z2r?*&2uW3j1*w3N4>cbLJ9X!2H_8y~%n3GqQlM|6l<@R-nK$>03u^}R>h-#vjA{sx z_~x>7)QP34py%1ayhA8XFHQ#<@h|&)rpYBq;qD@mO0c1#-`Vj~a-&*;U1TrH^p+yN z(pR(=8K&#@)NiVWhe{F`nGCk=1_J#(dK?3~!H%SkH6b_YHismPHxO85uFxFav)-5*MuzomeNML)VVvKpQ5HQL=KAGerkwe z+q{*Ck>g#JH-}86Y@O4q|JX$?LAD!LK8rDbKZo(?{g z*)rzY7Uk)GYx53Qbln=xzvnA*H=2yCJ9L4u{`DWj1ubMje0srs)I>N8)7LOqzTps~ z`K4jAKpEE)2p(qE@iE1Zp|M{nzdc6@GG353<|qIw;T2CN{(J z&B#?cZvfwc0nYINg}=6G9VE4oe5S9I#q?VWI$px$M>;Wjl`SlZ6sA$FF?%9D!W4TmP}x|7;GFKmvL!dzh5wW-#GCt9&u=B z+AqVMvfZwt)pXzq62_{tlx17~-G!w4Xo=Jye~;teLIdnZ-@pN~A55E)6v{LrAx$qS zk(qN}5*|j0e)FuV)HgTpynaon^ED;<-LNAD*Hb&JmI~rL_1dd?(E1v&qW4V4zqT** z_$PN~OpGb)yh*!H2zD`~3o_U1`y;iY^0J0b5X?{@B~(5;===6tf7Cn-|<+ml!%D2#^?8Yf3GyJJ<3|`(<`%IQGpQ@JEx_ zx^c~AiFo|%R=N2fb4#*1At+$_y0wY}SP@1~&V(yJ0MVs^)C23Oo0~1FnikwwV5X4* z=v7C#ktC=NJm@r)Yipe1sj^Y%H3J^VQg5v2MSx_JyLV?>#^9JFJfi;lLwmJko<|IPdFhG< zWhEw3GT-@|6ISyA=F&`X&z{3qrk(*Gz|9(??NElW=v&`74n7iY0h=fXbyXSXh^D;r zTlJ{`E&8X_6T>#^PgGAq7xmF)-?v)0lK}3)`szNYSZs_?^?*88L(>TWF&HjH186k$ z9_dUqjGmu0O#ew_jL>U9)0Ot-Fg%dAq`jXFZQt*Rb6xr+lkD;k>5|_rhvh3a^5lqp zIGKJ1I`;UyKAm) zY^}Tcc{W;6&vJ<&pbZ+5&O?i|vyX7jy&jzHUOorLDB^LI5mu_7RaRkOjm!nN>O34{ z+T9v`?Y;ecpWi+irmVt`_0Pqk)a|i*EyS`LSS&j@T!hIpP76H9^_wu!w!e(grPF4& z1J9Pj?FgTuMchU4tZ(@TRqTmhRHCe<$S<(<;>h1ZSS7yVSX3n0!x2ie&>6msx-zy8 zdR0tU8eGh&|7}?c&Y~)K21^nZS7Is+NfN=`ls4t$?f~weAh}F`Tm<5%SBzaj*9g{h zhig~Aa3PexxNa)yUZtXvCTlQw_;!HV;&SF+LYg~pRZ~ShZ$68^YP`v^ILEIDlQZB- znt^Jt!&W|2FUe7s=$b#nP4->&cd*Go1UhZFXuv4j2y&&lp7Ox9yHK@i-NomJqPY=@ z-|uuso9i!PqBjU3%!@(%K=4Iy9vnu)cUOQ|qcfM8h4Pl*B+NfODjKGe4v@G6Nu66( zb<_`o_A9O!X1%J6MW_`@f`DB%pn0L0@^9c4Fkrc63QgT@x}dW7=!TT z4*sjVnErRnU7_FiU9d$}w#SyrtzCjBKcKqV>YR2J?ykS%6}%W_6ORedK2s;Zv#sRa zaFS%9Fas<9msrrlksRkOija>F;!yE?^B(M={{uBagJw?vUsE~dYp@UR28Npu*d=1| zwTGxV_EvwEQ0-X`yuTMtC)r98pAd@4AaB_~PEANokOLXO-aQYJIj zT$boC6%AL>iOt7rQfAU4+>6GbsFvBapn!bD225CEcdVbY3Vs$ z^~qEah->WRGSU{@N~|6Mc;!8XPiz^g)JQ7LV0chm!h!vkK5u z9o^>pU0yb?=KS}YFtKl#l{%+KDw(|%=E?>TGqBqkUrq$aw<@U>!Y>p+H>23H}kmfdv8sjpY1(?O5DXI~JEU1Y>(u)7n6 zf!M2);~$1%v8kzc{b^Q%Xf$HK7u|u%t$tes@guH{bH@NVGcdH^gm>)X@|*~I_Nzl} zTZfi8wiR%GwuSC@9sjZ043e^j@cgiFtDH2tq^i5CUfue z_49Q^#>bTO4G_S!=ceeoyrV4lL2yX@Bl4VT#q}Btg)0U5-#C2|V?4{=^Yp_7eS2zP z69F#HYfUo|6-2Ff_|1uSlE*ASo0Sez^@ zq9vQBPC!BXls%Lc>=uPXhz>G`bypR@qn~YIHNv%rfBvu@j$?e~>G0mTDJ9^;>URE% z$&XudG?L;BkcYEA2dCf^C;(^B3-VI= zUn%mgUcYt5odcf>A82EcIhR@rFp4I)#5muRKJbYz3bIk zwZWn=S9nW>SULAhw)qY!N2=WZB_BZ0|APvAO;q`ge3Jy|+dvIm{Y#kRr7e6ELi#K& z&5A0POvOzksg6yUH+xOhe9-4KWrku!<5zp!kFrRS+vN|5yasDV@3U&Y zTpxo55dWY6y+2|Rh$`^;MRMQ)RFm7@Fs%gApU#krggr_u(A;3Oalo*`}gAWzpnCU}Iztf8r6M!PmF3F`%SNx!PARo-qkB{o_M{J>w#lL3}$5 z6Qjl-*WS05gghqzvaw2^FL;N zLJNNwcow1wp~HxpbM^9mcK3#>tADqDcb9SF_Ug**RK7pyIEQP>xL;@hvYXg*jPZG* z(r`mkpr+y^%W&!MSo>RpN3aD<-KxsrnOG?n@3mK-bxf#&_tBYaDhnBx;K>sZ%*Zrn zE?z@?gphW}p`n9&iC)_;ZCN6VkN=ORuMDWFd%nIjNP~p5G}7Id?v(CE8l;i#2I=mW zl#&v-bhmU$Bi#+};rYG)uYBO1bN1|+HEXTeyRq{(4=Z|fZdZF@TC%tR{k-*3SGa-H zwjq*oQIj#~n=+%`VBZq;P9kQyohnqvz?SlGm-j4QL~G?giX(xn$WNW-XJcr+AF-~m zR0Ji^hvV-^mxr3-19A)RP9Anl2%+X_Xu_ylT6!p&hg)(fO3F5wng{llAHn`fzp0IL z(o&`)${%oOc;2UQ)?1iGrvVqL_fDa3XnTJ`O<+1);&JJt-`Tm|k%jeuE^If6iojEsNNp|~BGkbBUtZ$yf`X*JM46*aCADyjb!mev3> zqNr$<%)omoSM}SNbG`)9{T0OUA;i^zP7db$g(2=d1puuwK!U>Rm zVx#79m3jb0P%$dk_~98jSt`ou;L;8y;bw@AYEtOh5f`j~m?Bsm^WgJ;{OsMCxWuNi zOI;I0OHD4auHfWb7Lw+7=t|^ea`prGXO)4>0*S@KKAT(-R51ZT&RIM9bf&RN{Ye8$ zO{cB65FthZXL}Z`&f($hu2A*Ikti(Z^soH3M@Q^`?Ro3SZC!tF2#TrZz+1eRswmp6 zCW6d@=7y}F;{QosYI+g=E^IvZ)UrZN%$A;v`qNdq4-;g)DXTrZ&Lx5j#RpTYe`n{O z8F8;CrmD!rVO}`jh4_tpj}I;ljO9KFHvmV7^^x4k(|K4adVTOZB1_47HxU2`nJlF|>{UGqZtwZf{&TvT!v9{jXRqVJ5QxyVDsh+BG98~z z(JD>Bs(bnXi3cK+4j5s@kbn3d-fe1J)o&pV`e}90%ca_l0?M`UN!iZ zTL4(~!743%UTM#~Ne7jXC;ZxTo3YVfNT8P^R|frIsMcP}%j~=i4{eR>AuqJDa(&)l z|79W_CCYsX$yf$b9JT8<`lX){UFgs}!-r`^y&$2b6Ps6SHeBDiLnBxVmP-T0G4luS z=Z>X1;8r2>k=q>jCUIC?0(CGH)8RST6V?0NGFLjN&4GT_@f&=s<*4vnsxeRn~^^En63LghWmISh}-l>5chOQ&}X04w%M$M2wl!m*KAhbr#1o6y9 za%J_lpQ0Mqm(b-M7~8r%kg=~v>yA^FA0yPnkPTa{UV4?<(Img31!T!ie%pd@8Z`F2 ze<(<2aE&{q!ziyqH9r&B*5Z#0pzzt)$;ncLi;E1+1(iw#oe6+2_LY8U?K5)EQx3fj z=tUj3L{PR4jp1Pz%N~*32y0x!f&TrDHC2-L*7-!>+gEiDa~**ke*by9*#^xB)&E0i z$(3i4U|v5dw$o>%Cvi(71@Vu}IYVDiuE zZJWYBMsKA_*3s>CVETQ?R*)e1y%{8|4U^{Mqf`s(Rs91Q5DAbztWnzhc{3&IM$_@~ zvL7{=<65DvoDc+!CV$$^BE?jiL#&SBx!%U@T@Krb`(S44;=QQAKk zF;%B$9|pu%u~7kb16;x0ze32_S?U1tGc6AAEz>@jxK^S?noW}L>*OJJ~w$PGjHzSh#+=`q61O+5M|Bs9PSW{F(mirD~FZY9?J5ad? zUL?W_vBuuQCA<_ZMk@(Kxy-V4N0hrxNPN!yglbE=l~K z+hfg@dN~+BAdflQ__i3lGU99KkW`xIa!_z~QkM=dbxA)FQF~IST2z^Ix5I>~txfA1 z%Cz|I7;lW~u{{3?56N+2C+X7flHRnX?m&P97!0V8EkjjO7!jw3{_p!lJUWf0#}B1AI81=?(1OracZ}u=J3hj)E_4I75_I!aR?0`M)KqY z+@HI8SU&zk;R1vh%l&^j9eQv3{o?eR#-C}3HOURyUeS8khZk(928|k_I%qau-M9O9 z-Q12J-Ou6NtVVYamoYxi0#j`Zk9D85W^2D+@&|*D+Q!EENm0L3*XO}V7U_(jaSFdubK;_^AbcuUU^M5I&45(_LJQ8uOGn^{<|*84Gu>WAkXVP!cbW`V` zTnNOmPfMUQuGe1jRaww((Wt)XRC>o1enQUbuSwp4C9dP z^}V~4mayYtE<+D3n3gqS9Y-=9n)2^nP(i-2dZa`;BGTF;kBXQ3x#&*y!a`Z$zda?7YQ-?&o7QObsR# zFs;Jn@+5p;HcuFS4(-2MvE+-!Rf=Eh=!md|De#6L6efc}lHZ8sFsgkb0qZOJ{mf;`@-P6!_Y2o$&n_Sg#zV=p-JRq$@?1#B%G+{o@UU!1e3UpX<$!~ zsv@Ej`e?~@L4boGgKhSZED|XAjQg$h&s=x>UB&)@Jk<^9d3=BmGGC-Fm1lhu-XD%jjK@n$0Ri%xGC$5p`bl7MnBhi6&KVhJW zStwl;k|mrUA-WTM-r=h0!C0AfRce~Yo7ceHVjfC!3c+?pdX~FxFb^7Qcom>1fBH62 zGgJ=#0ATFK2pF;A~6Ws`iTj86}tP<2+(jMDF(J$wfDkc zA0$Xrz59blR6kSM&?9NbtRqf3Og;L+{D?+R3*dX@Hl{EB7DaiULfjF{nBd0@4|=V3s~^avV%H|XeBA^8 znc@;JsOyrxG$JQK+3YeiVUgp2rA9Lm#bGUP_1xWkb*do+90+s@wbN_Tw8j*(z473) zHpS6Ch!34Y%UVQB@Qdly)$ER46~6%jZTCi)ikRyB(KnNd!=eFaP%O+Dkl}rkF9T;R z?gMk-?JWvO$(q)mY85U9+7-$cl|Q~DO)6*@8B zIYnZ#{7uhR=@_2`X?C$5kg2s51mHGSm|ud0YZ}xqT1Y#}j~0B)Xp2%C(RPqg6~4VE z8+YFk_IXk>@g!>L7^M<_CgyRlOT3WjsBIo{>S`CIsH`SZ(Vo%m`tr9^JkVY&FaO?y zZH)T;Um zfH0$O@K9JbWZM-~OgXD{NsZs9HAO7{wD)hxyr(_3C-xtmXUyVXhj9wtb@U*{m9j2YPIzgW#O)$P;M6^DDvE6BrbaWmIWfw)V7}OPtZA+^b=N? zWq@uC=s@W02qb6!J*qbtL-{8<=^>+l)$Nm`E)a6#!qUx33WksJtMJE0Q(?5WT%`kz z0+1k6+0c;0G&%she)Qb$x=?V;f@U1WR?PIm?uj}|>Uz#7nOts+& zBIcE61x0(AalS~aAB!Rwdh7(zF}e!C8Ar41&W1@%?E+P(>EE$37_H~LL8J*1z5 z$SNM(L6IKBQt0+eJbU{ZK$FNxE;1p)rgMo8qhK&F71Xvfcu{b4uLodTH#H58p!+PM z(z~n(rUs`XVvw1-P$Z=@t3QR4-l<&LS1)0O@Ly~`8uL@`jB3oa9PwQfP#ACuur+o@ z8@TMp`~YsKv3|RpD6Wv7n>wCnbB#2uWI+V)U5Tg8rKbF(+|Q~o}eU=Fs4*_}10 z?{35jWAN6)?pU;QKHv!GwH?y)s6bBbhF7t4AG0Dua~jxqFl2}jHWZpCzXU?i)hfV0>LHsQPOh)EHLL(Il^CBAPKFaYVSww#LyDu!1w!Si<{TDz!+NF=-wkqq4h|7l#U4PLSHW)LkHgrA%s+_eBNPZ znEC*!%kf@;2{_Z$R(Kje?rr+A)>#t_~N(Iq)aksh?x#L|%vZioi7GhdvE^VP;EXTn1bm8^$R zKnQG?JNg5gz2k)&!S0C7Yx0k1TqeU*L;cDH^HT}+QY*&0Y78>8g0aMo6VeHHi!@pZ z)+p`PZfo)5`Gaihw9NLt<(^A?2sIWi%sA>_EQGh>y}A|>pW2ohMc><={9aaCclXNH zWAXUG%0cyxeousQ|2|ESLM-7MUF2nKc`nt#LHSa0%Mtk=V;*&NUUUnJaP{rq^<=i) zJ2UxOJwzaP`E_p~KrPsX{Yu2QplX~HV^qNAl3~amg!nNCa3?8b&{RAiW*#C@jZY?3 z{m{CrkzrIrCIbP77pw(J&%t@Qv7PZ?Dsge_loge%+zJgWc=fd8NdGhDBahF!MIS%T z%Vh9fLrSTd7aig0NrWo06&m%9UF^7a)t5Qt^m8y&VnpdoJvYXJp|?{~OoQ$x44dO7 z&2sj~ZEy0p;G!VNFur{ez&>Fy$)DFlK?SKI+>-_%bEXho)T8R`8yX;d{wE%I;{UPg zj?>={NOMj27H~k&g=ooYTo*1q**Svt_IUcx{F&vfhWj!J2W*=zgx|o(_S}ZE|Fh_O zSZ~CQJ2i%@xg0~Ah*<`>nZ=(sLHFTk7wv9mbPvL9tcX4%4<(MTsnH>%931BVfX`(J zq6xb91#Oa~{7gr>&A_6@&tO{$|KvAVa~)mpKBLF}SR?6-&Dh(pQ*~P+MUM6l^PAuKDz{^F?~EZ>b~>=hsOC}OsGrML{awD*iU8D z)#hhE>}wJWd&94Ot$PrQ8QifzpgD z|4LPYfus*kkFB}{f!R~949iQUUFhHp)j5c!E4V31Py*&BP1TAPE` zlz47^Ol3F2+a~ESA`JWUq@Ym93sTExJ?tE|baxf+DxEC*WI#E?#w`HQED)X5ww~%% z@SP!ro=PA=V4X{K-1P>|&e1mqatA*AiIa1TKm+?sv4VoD8~qLPDh5)k*qqeBb5MYx z7f`+6Gr$Mwe+QTx8&P4LADp&RJOtF!t5I%K7*4I;ni`fhvOKP^>y*bC9;U{;FJiv} zLUBFc`_D$+8NRyUlQ4-pV&q@pPt_g*Q0JqV>Nq>cj?wrTCh)4kozNbHH_MZVeLrAE zYp^a`_~{*uRFL(PqC<9h{YWSNk~^w=o>Rdj+S|B|p^I%s&bD)j*f)OSF2%PDG)PwW zadMcb$`Nc`ofO5xiW`YNWJ#f{Qf73RXX4on8vcmm3j1KnZMY@_=Ce_-{hhod!ONd# zJWDp5HqqV1hc#YcSYndj)E-+<1H5sNxB24}G=%@HE*B0XB)54UQQ~C&Ovm4YNm;&b zVBMD$Q)SXx<#=$eI2?@MDfIrp0B;<9Oct?gpE^u#llgumSsYd29tWF2y^ylMuP&o` z#!XMOGf@ZMm-$Bs30uMN4!~h7YDf5ldpK!!wUk0tO4ti(hjc#@ZFItTYx7#5v6=i; zO{_hu*JxAP9-P9fR*^gca?_C(m2HBz2|{~r;^8$RXa(J9A||38H2TAOa^MyW10IPs5Pypx5c zpaIiVdwXovx@jd%!8{wp=B2JnfzcVA-tx1s_N2`By+KV?-HuTk%AeDbkTKg0VJ%4b- zDldXjW0NCF2Y)f$I`bdqclobKZ*8OkUzV^G+gD2bu%I(_5P~PME@=wXlvCOh0b3up+A{t>=1Vss>m|MUdzv56%=zuR68Qk+ zv_8PFtb*!&4llA|jqpH0sc1IE*jPw?jTA9&QbW&?))3_6n8gjcd1x{T;b@#j3TrP2 zn5jm4Z@lG&!su!-m@Htr9u+jEk(<8D8Wj*5tYPGsen8tp*xld*wVXS7b^k*C~eidTQ81v&CG_xz>pCk3=9V(wH# z4h~YKk`FK&2$SRi77doh%k9iN;l9|2odJpKHNm|euSi6Gf+XJYNKvoNGES|ZUq z9|~}=Jks;&fF7ls|LqgkFUB_>e-hdEO-!2I|ONfP%ci?-JSEceOx4`sCa+@ zRo?)(;wXu&#AK&9$|x6D?f1&pnMlpchdiF$!5@BJyWVFLK%#^IRk}S0guYYzYDeBsN%&9CL^7t z2D;TcR3fnJ(9a9`dwLM{4#p;*&|Pn%&cfi|#R7~nB|YI$B>-eh>`RIvuWq@%2X2L& zT;)hC-YD3GLjiXkDtrEnVJG@^!Nu+_-?Gw&_8-G}P=nIJ5+>0EgKuW6N6=gPVe0IV zw=%$*6Fdhy2A{b{Q0sWEw~G^$2Gp zfnT|6-8cRXzcwL5q+q>4lTxEu)~TyUMJ*wRqX%1cl6{+E8RKsc$dYCN$G=3MlW^eo|Y$F?apjtgZF@}9w(0R zQ2rm)df9}^K18%@L6BgaBvSCPmzF}gN#f&&CthqnruZvf|0*(DM!8bm)e{L_lt9e( zSR~}EteLAvXbmt6aFCljd7At6G%CN5en57pYRF(`bFt5&(d$ zeex;HSrw7!N`G*@%I9L%Fblyc^+QXz$fn{Y+=S6WeMT3%!fsay~O8)!MK$q2b5Aemut{6Vq0GsnH!bb2w2-WAUbhwh2 z(L8-f2CbSQm0~fBx0jHGy^jh?6-!FyQ4YL`K3*I99fKVA5PSKx(Vtjci!zluSYk zEHCjvK3>>cG~T^K)Ur6`oDslC9|&F{3;C?5k-+$^_$r^jcVHUk9X+b zBRJK?QPfIL!hs-wUjTcz;hj;;n3U8#n`LFr+!GFcP@>MABilWP%N#*n5a_{5c=ohU zo@sccR8~RW=+j#FG_3c+$zgN%A|Mi1d{1> zCExT9%GC1W=#AR{cM^5VnDaZP=pqrM^~2+d^R@aW^=&(_h4H`wYZ@3H$}3=A@&|bj zX^tm7k9~0OkDlm(9KZvdkkLVt*@WB(dXL$17xt7g)m}L>wvY+n+0H@T4cbS;>d`PO z3TOhVN%u*;JprIT(gftfC<830;0DV6w8ABg?>|?v=Md>hhI9_BieY!M}IZMHaz$ z;D^6)^^kS^-JWZNz)y{F=~=pq_51DT0g+Z0*m=88SbNWwV_}r(s-OiMT^W4U96E=} za(%0V`aDcRJQ`93=(q-A$}I+f{1d3KX{q|Z$SY~w&4G3=H2FXNr`XKO|oLg z!9%N7<|RLX4me@84M!GxjAB$;=h+*82u1^#KTWBa=Y6jn*2Ge@_MRf+r2w{1>vKI3urs%oH;dA(Qh(w zL7U(s2U^6s1*`@7Wd+i4?R7O8>!kN-(vyO5wkVx2-ESNnvdN|tv})!+bBpxIN{<`n zS3dlTN`%nUy+mOe(_v9+Jw(U~AmGIH4wRaFY-2`w3t0u=iL0{~MJ#esWE>cm=)D3t z-$-iv%ce2YGo;r`xB}#Di81uO#3CA??6F~ygN}YVWC;u&d0cO(UIl2t*Vq$0-^XcW zbwjpp3VZWQlsxGHRxP!s{#*QECXVE}ffpQbX@{{-p-IzO?gl76&#M<`g5jIfzK5X} zHQY~^>8u{02fp?IRLBRRlCDi3s>HLzcTviJr%SdIEHx$px{}cnFb`=U_PR64;L0GU~KYw-bDXfzmPQwI| z0?b^s35`LW&M0HNGM|dhekR$9m5?k7)n>nu&n6+SKxX0_t}?nj{pr_NZ-oD(_X+o< z0*DYza*;qJUnSeO*B?WsW;AsxZ53_Blb7+mi95Vy*$7ETyMLFVgj}(a zzuJPra825YiUHDWUgXLGc+$y!Ua2N%9#Y|@LoHA7w7RLz)I*;SG5ZUyWCw>Ja1)Dn zu=Z|ls)Jl}k;dhgyr=Ge>gP85ci=JeU5FnWT&`w5;R4KNgem_%h^&@!DIq28pVgS5 zRTwionVGo&Yc+bgr$H`^>ZOfFyuKP8$^Ur)#8E-N7QC+x`E{1Rgow88oGvHfzED8e zjh_1`u(u-37&s=r*z#HtM|y$YKS{Tv5|;!!qbe-EWwmr5ioaM6lbdK&OP-)UDCU8E z^Oq~|rwFoa?^dg-oB1CiA6-C_23B|TG56$wDdS@m!shfdGrq`kMq!|A3c$E zhn1go=Mz9nz;9%TRwjq#zA+bG#NvLbm+e@9VbS6qx~fi6xN1KCssPnC$EdfzZ`D8= z@UegdkdW7NVLf*yuPiLMIA?KA>4F*1EVVc-;}$=U1fTbo!Ix!qEl2FX;x9{GoFD?x_i1@AUi-Qb&7mJ_Z1W0gDRj!dh8j zdMNILHrQt5O@tHxrF?2U-L*1x+7}(A9AOSA2S^<*pJ}}-m#nONUkcogxi3V#NQ&C5 z@l;k&{!k%W%-lsE5^YMB?kyOa#g6juf&iz2dC4m@eB^zZE0we-@50aSYkqbBH=v%m zS49If(&^)kFIg@*wZR8obs<^+f)kp#JrP1|dhb~MouR;q{!x%8L6a8>o_Y<9YtF9l z7t@Mo5jc_kOt$e`VT7P2e!)`Zh9eTz6P$QT@L+158pjo-aB9zuzFiJo#Y5G7r5&)& zNZxGz#9X9;N(M>I6XP&%QJ*q>5CLkXgO+E;AE2A}yPwF`bln{Olv0HE`Ogq9c=+y) zJU$B#AwCZ;q7gav-ani<@VOf|#04XBn!eCR^K|D%G!*wmF#M#BEd?nRVRkpSjW{t8 z`ubs~17(iwP6y#_ZndL`%JjIgeJT}RMPpY=5k54MYnk~bIFoLE=BfdQJ?*^>;r z6AYrvD2!LMtcEZUd!a}Eo$wsZiVa-GmEO_P9^Zgrn?UPLTwNPZg$1(`p8?GMnAMj% zM)v1LxDaW{ic&^G26MI=s0HB2jte8Uzul;p$$nnM;gd3e1FRRIuxXT;7|Guqq;ajs zmR=sjSS4dtwai)@%E7;~DCge&T4v?_nAd+w-robV?c66bG6cD25bLJG0)0yMcSVd~ z6G?gdqdZ-@ak?ugs{~)r22ANLdmoyoj9h;=I@VX1BkF_Tb=W^122wq$wO`-&&iz9> z4(;l!)Yxit%h3j`Kyq*5BAEbs_rELuii?^3UNhce?Hsx)Tb;>zqYMZVMJ*orm6vBFhFnh{5ngkIX>d)d^e|c0)AUqT zN|hgBfJe*(LOS~D8?5)fD{Z^U%6nyO&E|G_@_ZND)Zs6npDLeK$osLoLVMpgZ4FBR z+9x*DJTK3V5xFN5P)?n`Dgr>`LyL0XYQvv3m3&jhPFmH$IIq>B78w5vA;TX&Z5zR7OqV0`cW zePwoJNG`%xR{!Xjfof!~%1^^ys{ncthkjl=L=Oj24%QVecva}H9NJS_avY}y9g>&SPu4vbD$D1Ko+99PFo`d6jrWy*wz*Ie+O{RD{x5>xvRf>xJ{tF_v?^PQKgcLMf zS7A9}u2ey;C`8fYDF)^sG&3V>3wq7SVe3u(7K6^#QhOFjwU!wC&c+Ynx%f(O6b}dY zPpg!L`JI^cJ6!hi92B|NS-5ZJ=7U57IORW#jvt=%R8(ynE3w}J7+4|(KUfab;6W^d zJA1tC^`qR)IAj_5L&k;D7d{+e-MOylm~9*9c3BHI+j?T8J}5nkfWz>86p&Ka(rP8l zbKBDCPf`FxQ1zat=2PE`2d*i_nei$51Ykl`O^@5Av01D%b zFNI+)sp;6MJv1qf6xdn3VyeIDl=zS}uv8orQl5fx8N?qO0SN?`bzF?uJsB~LG5PfJ zI;90WOK{v`ri|{D2NpsZ)x$3DQk5Ul54@mo6t2Cjq!=?nhEqWA^!ZP>->-E>J&|J0 z-?f0S-n&CMMF`Kk+;T!cv-0w~C<1sHIE^#0q9LjF zsRtM_m6Z^?b-!&)(Hc{Ut3iY8GR!2^%cL$we8vc#o@`Y8b6oq!`wp^+<|#DA=KjRB zK;JvdkHx;E#XnqV(q-qPQ1e}nfa{9J{lV19?-&%ctXT=x1# z>;N!9!Xckxd=u;`!W5msKs?Tj`dcl$;Y0bzAD@?sD26QfZr47s$wV~zFvz+)0=%yM zrY82@4cYtkqP%>0tp;1k6A>gARFr-nnS{$!&cD!GQapT&wZ%%mg*N8>BH*u7k3uNu z{V%~-V;37X-=%?nh5QhCho2pQc7|XlTWY-4cZjQr6cM^mT1Q3lACvxIh zLkaM(Sw?n?g2Fu%lQYud@=?7?EO;pW7=Ceuvm$wy3R^R`dsFo5X-1dvT=u$ovR(1OM#Lj1nRfu^3- z&g@>2U!>`o2J`DD*RVWt^wujEVr+_*0C~Z-FKph!HMbRDBFwvczTR*IUE&`;f5=+> zqh?XO@Kc!*_?RXMF{WVhh;iX_ycl|m#8;AV%2+p_VxZRbEsw;{=@3fJeMiEo`xSfH zBLKPZ1|Ts!+o~nuVn@f1YQ$E3#E_nGs(q%0XY7~xHxt}&um$Lj0bG{|1+yqb^Cj)8 zGY~pvI~Kw~om4}s5I}&~;BMV@gN>Yh)t@6xj|8?6TC3O__)QpUKSWUgtScv$2N&P+ z0!}^gQL3Wha*@^&16E`hu(Ol_jBQjv4_Ji)eZudqU-ZpIxh|)y|%JUmr~!t1N;ofcih~3<{NDkf;E#G<^^Z>evALovt=Q zomG>+{#v=Bp0>Gc8cZJTQrmT4#C_ek=#1zOOeD_;U{5Orp7Gag{oKI#VrB2wj{W?% zVi*l{YDOr9BF89G`TO$;IoJhRlvIowXfb&pUBoVV1zHcvw-Nwh^FcD2kO9T+iz2DH zWxfCjhJ% zLR_NzWI%S%a4%D<+%TK@9u$6-`NHFI$IXM9t!UG$LFWI)QR375Wb!nJm)sNq2i?x~ z2je#aWZB2O6xw5Cw@?h!3tjHZ{^cUo2}65!B2mi|TlhwSIKt1)`S`b*k}em|@m>oE zjd{DZh0LgfmGWEWydz=@Hc*Ugtjpz6ogcEQ+ax7o0+8%pvY9oMboV^a*F_0=)h9fA zz0sW+qUq?_N0<-JE3!KWa{`ke6@wKB2?k0Tq-YRDBaHS=^Xh?pMo8BqhzBzwqFqvX z=Z-Cxlsx0wdv^{Tl2z~OE&`+%THcmjLPZ?)Wk807Li)|MGR4>(d^5YX&%D)nAUi zcN*>vNl(a^lkdl1rB{Qh=D$^jtKf+{e^63Fm&`{N_}sisOY}0r%Ezhdbo}RA1jCeIkNt z4AEgn&rR%IU--|g1%r73L<^V<2f1A#8X3g%5HwHh61qYFm=-qe<)*UJ8R88+;UOXb z%h)u`tj+gJ$V)O*-gl+@T^A%_H_`KhL?G zKmj+B9qtU6ZC)nE{H=8T9=u5dJRtc8RRi+LDuV%IR6~Mj%99(ucWuxP2dPKr@YmbK zFTc3as~o^QhdsUrpS3&!fTJQ3*9;6A7>Jsd`D5M9+7bJ4ae6j@csjPGl~|68aF1y9pHd+SL}*nnEUhyeZ~*<=C1^utkX`B7 zKt^sfFu-prD)_7U8%^wMC=h0GMYyemc~ZY)d)`rdbLaKHDU%w+Z(M9!hu!m zZa%C^?X?j_QoR9l=})RqzmL1GQ2OnF)F6Vldy+ylIw?12j)~}bJ0Td-npA2ULgaDH7Cao6cD}C`r79TSAW8iShLu6b-U=!>l2Z?1=rwA zu{)`eEX;IAV8>w3oE%KS-s+11>E0>7c^tX%xuHM-IhIpN2Duf$#9MT)ix2%7Dl|Hj z`^((ngAfShXz{a3>|C3rV1j&8sL}Fo^5P~gY?ol$4M{^yl3T|vy4>C9xlD-4PfWA0)m`&7Ln}O+g#Ub-zWBlb$+qG?jKZ)G&$H2(wk!h1{0)tkC{3P{9@z;O{ zllN!Ba!`FoZjV1k^LiUi021*J`iZ1YG^5|y9-g(>5OYcSZ%g_2bqRpq0CFdjKR1Ft z5ZXB1SitsmUB&a(Kcw>i`;9x^LuhMEYay^-^Yd{|5FD7G%h%)~3k*HeWO^3j$wIdr z!Wvry!tu6;nCIJO9KRQ2ZR^VYkRhnRltPc7x&`Wg?(|dmudT_vw-8V#;>EhP_(1i@ z-)|xq7ixkMHLO&WSvjdlnneY}hHpMXdnd{lDGZ^iH~ zo?RLsOlQ9l-wK`UOOC?b|As#%b5>Y>oa7_BnH%>4iiUz=vpL>!!{qBIp{sO$LZRs9PWM(CG z2h7=Nb_TOxo_CrwRu+xari1^!M^ujx#iS3JbM^Og2keCbN+NLvR0|^k2sVf)z`Lf2 zt#k3?{kV^i&ZY6M{Qo5N7yt2>JvSGOv#>~)oe2%@m=}R2;bUaU%{`W>M38L$@+3_{ z1QIYXPy}S@acuX=M_Rx(TLlZo7H*0#i9g4lnt7W5s)0FBEE&}_b=>Dtyi6CQ+5^Q! z(;3Kc zfSWN3`-TpRVsBJt+tUSc3cp<)fzaijb_~XNcg15YLK19q(wsx7fsn{ky|P zKHe(7@?{vGJa{e2p&kT5^w0B6AP-@agEYX?r!u&$r?F+hHib%x+d37H zAS)%1xuKpI0ftP`@M^AeZjMjk-AN}-HpkpMhwc_*CAF{ak`&N3Ev;%-S z^KHNYzL1-4kb0PzYzs5r+y6Q+DaxJPcq|YM!3(;V0ga*HgLLo((8b+ZjYHGd+~V@- z<{Nsg*IZv6{&qrNpQ}Q7!c(baM&WC|8Iyj;ihWg*@uqm!!rpL3;pDUVmBnx3P>X>U zVFnqPaBDEJ#*u`OA@_h&o*q|B%{z`&?wx~1V;|#PH8aUUyISea?r((nvLYCD!}JJj zBSyz?>rF(_PPcd7N;?KFBLFAR=%hQi_k7n^ zSyb%c1+B=BcYdXn+4Tlomvj)Fz{#4H#vrETRDi4?{nr}VIQ8Q`jWRy3jS`qsMV>@{ z^J+a0hS31xTFQ24Ze_oE%Kj+`-h()bN_>Labol#QDFWOPk3s2XU#6Ftpf58G!c0C> zVH==|V-0*GmQEehmSUM(NRNxCB3iGT*L9b9(9x2qyx9 zn!@LzCO-@$PoY}C=5Eny0yzM^^Q@ske`~N@i|(~*V4!FZnP7SvoKSpk5h`7jdGY`o z?TCXbOjM==9IZ|6wO|_HFiwxHA=2SBvZNpG+U3FU4I5u8)MjOA*Ft_m5;YX@C*5*mKV+!tk2;s`tb;xm6LYjs=vTGR9MlNM>NSs=ix ze1daW|J!g%sG(&2t{4CS&(fO6Nmj6Cb}^qRZ@7lC2-C1g_slVNq?zF+4+-T=GE6KK zp%s2|j266&d}CZr9QA^XU}QMP!|76sfQbFibp~{TlD2v);h)y8gQS8iBa$QJCu>*| z`zEkOz9I#!E{MGT3E>TyK3786E01d;qUPr@l!;UKA#Lg4RS9og`#$F_o$%qKQF@Q6 z9|ST2%z<{h<3CYAw?jk5N7~qK?qv&T9f9T)B3}dZbA@g|HCRD~PteY1W`PkH7L`0 zrvu@{3X@%5JfjmYaoUV1ebM~=0>)C!)k!~xb0z}Q#6leH$2{<_h%HT1&(Xp)EK`l! zg*_+=A9ALXW@5^mGBMR(;-{# z^p%CyGO^#ftnE%Z6ncR1p5>y@Xv1kQpk7y2>FVr~{Hidq+zA2US`d!pec&R!;Iy^Tj!lrNnN0yJc}-BQJO0B-uk(uq}4?hdHrMYj0p7V zlq+|8sj4skXv3xXV zTYjevGm!EYM=Ub}Ns3M#QTXdD9DP%GR-#rvy@NIOy|$${y)lFWrtiTEiqWbGtmek~ zcqip*InXQ(xAB!tk9;Ne8A9iBq~2m5+igdb1^OT^^fp-g-(?tG^c_s0dTyWp>hZ4J$PfHOTE zh}PPt!|(+J0WK7i?ks?dQ{E2&R6N+FzY@^L%?+S1b(p;ffHjO|3{>k3? zPag{4uZk%N%GR2Z0kEzWZB^h`qYW=|-qUrU98P@ryfbgqv3|&7TSnPG;^gym7-HZt z9~=7!`fXvnFHLTpr0&zGS2KZDc1{K8*@O_Y3LDI`n(dAs{W%>Z?+(n#4(JwWow~&} zi%s&q!O`YzHL~eYIFEa*on(lsaZA5iFE^uliLZPo7G+PuNHDceB&%S7sgUQ-Df_OV zE&~Lt@=tCBN;hRGMRfyEX^Z`sc$&2fZ)n+GJgqAGkhY~Z;;(2M=9(q>kv%nNad>a$D?>4s^68^!uNA2hLCzSIUW@#xZ4 z_w1cUwe8Qneqin32MC-46NVgVuGsr<|JQ-FmH}>UL%%2IoT6ez^7pI1#V(7;Bj?(S}+ zm6DJKk!~ap0@B^xAl=;!(jh4g(%n+`<-7O(^g+B-1BeKRW=oJ%jvD4`9iQ;pQt(9V+S{v#P22i}F|UfD#htiM#F^Wt4^Z3%nM3;*M7@bhUve`f_vH4ou=o1ddXLUZFuy zZW`(Ziv7B+EH<5At*Qz|&*Z{vbv}8>^8_=NlaPrN<{$lj3Ks-1uN!iyTKJ`swCZI* zoq#zV-)faPogFOeeflWg%%yrN#)f@chvg%FCNf@X(%TpFx_Sb(vh-bc(fQRcK-eF% zWKUFf+B;MdA>GuBDq_O*h^-!fiYhAL6xN{4d%=JMLl_p`t}zvw81^_Zfs~C2RV}Lh z!Yw;p>S6g_add|O(=f3A{lSX3ct_ugn?+XNudp=Jq`3mqDtCdd;5o#9V7-Ux1uis3 zIew9O0i}e~!zIZ&BWIc-_yF1ZDS!Gfn8X~W&du4PKHmOcqfv5pGNzZ2q}} z&=1`9DE~hTfXYWK217wfl#I6u?Q$FTE%=o#R$^DSlMJik@4EQCYjX>0;y=$1Bp;X2 zU}BsyAdtk8PJNO{+jXeT4BO9UFP$t=yDGD-IJ1^ZwV|511R*gS?bQ2@%0CIOGs#RO zPybF45~MR34oLr8dIlpfEL8ZSa;(RGJ^4!cvnt!(-J`;*UdQ`eWYf);RBKPS_A4I- zR6^ww=iBu-*O*T^UKOLM=X{_-7q$yL$hMg0(xT43IL<%-utqCW4~Mx1RJ27P`DWre zglX~Sbz^Yl*`!pT=r%sAD7?7V?}N{ckb@nxY^Kc=iHhPA3ffji)a->04BE^RmMaAmm=o_uQ8E4CfhH_->@NSdAuXqZ|wSZpAG zs`4$@y*N_}c~^JPS4~$D23P4bfA@f@zt`6?7-!cszO5HBuwHIC`R2!Ty>?g7@;W0d zvIs79rwuPt*>#4L*^lIoF~m!n6rI#X*uB0BOrgptDIT;t+AI~*CN<@jsV-GFK43)= zbNKrfvokl;p+utyh0T7r8N34vcEfkDNHPZ!8Oek$n9=x=Iq`WAc6jH5s9#^iwWina zq94Df`ehB$h%qF=PqbQ*&L}s65K#3xgcCwuO49uaSE_J^rvF8nGUbNU8xp;v%tS@wHjmoQHfM*2s3H@uLmnXQR;+v)F*t_))F16LdAlV7+w z&i_tz(Qfg*AZ?&3IHMMbsO?X)tfFL~7*YNsi>`xlks6@&{+ZrmNn)`fv5LZ*JTxjs zy^$Qlgj61CeZk~RR8M_#b9FKSXX{6|F(a9g4VR7losYB1PBRxq9fuBpG3tU`Uy!+ zqpo%-vwa9M0ZAo9!*(z&ot2yHQPx^Nwhia1k;H2y;9jd+h%YD~T@Z2c(>SyBkGWv( zEL60T7@!fx{{F)iz!SXi4^_Ml>Nu{~lfRn|d*2(PJnUzPjO9IFTI3je{9QRa?cW1# zW}`ut>G{&Lk6V(r43YB57tS>{7mBd0!i2xvIB>m71U5!JRnYV`9lv+VA~Z9a%gvQ0 z?1X2G>n$(r(s=}Rup50OwpRKV=^+m7CNaN(qdBvs)LQ%-b;e9@NEFsVUi5FOY$}up znrDXp#YzV*4KXQB+R@?1rkr^cZ&k?W7?EIDa^keTta#08qybQ`G1%_i)NcW|HmYMT zwBZ_6nb0lviU-)az2Q~u&BJqtF;SkZcYdGLg8zf2XdI z1F#6ZAD>$Apo@u}FQhwIv|^mh;l<5Ls!x*_9Us_j&CBlK+Hd(Mc9QE~p4(xb!M6Hi zxWH5D2^+7~hUtpWX_6#-9=)9goIz}yTD&zGt3V^#I~jmP3%C)19*7{Ntb`mDkP6sX`?XC^My;d5udtk(UX&1aLNDCliIP-EwX zC%w8;r`+~T5420wHD#b>9L-*<`^d}0V29Lg7(u%sEgXwUYI=vMY8Q_+q`cs zd6JYLVZPUjFjcD?0}Nj-9q{}>*hjIYFYEPln}}qIq3dBDyC@iDL&HD5x}Bh>X2dgO zqdgmODO3iHV)Se%BFAP#mo~ZKtEYFt#$T~e-WrCir&}PHSXXmlFG?#5{`uB*m3A@X5HP@~511jRetWJzL@I}4U`#|*r z00m2rWE3ic(lKZ-FdOJkdR*G-dPVQ*DB@I`b^fepFY}YK#PiAVxsWS%my@(Ui^Gr; zTDgfNNo9YS=W`o3LWlJCFSgOBD zh&TYXL^&KjsxEsjYlLatnWT6{#4?)-$RZcAS041GA+>Cps79BMjZ` z_eJwo_dds!-J3ajRLJJ$!a2c(n^38YS?;}W)~>nA0cyi?pwA$7KR>>>mQW*dw;G-> z;L)WP9}YlmVjz|gVDId;KSHhAJW^7uZqdW;_nL*xG+5-2q1bjSZf#g+uxTw1WAIBsHS!~Q?8DzwPKe!p>0o+8yx!2Tes%92 z8a73TYoPzp8x-~3a|ZJNv3U}$92jE1w<_Q(y=NfkGLAZ+>~fqvbZhho$US z8P${f$H5K-1VCFr+}$*?%yrL3Dylt0EG4nbf>l}C?XCQ+UHJG~r%TXi>-&dCPHp;s z)Z{FuZHuu;kD+lnh+x0` z5O%@bCA9c^^=|xB4w*D0k6Yb(9_LeT`g{=RR1Wclnd5wDn0f&>MM7o;F~^Hp3_)S# z60w2$z72y*r#%@7L`@| z80fNYak`Kx$ju9K~^Bt2|brBtKO@q%Xa|Dvms8TS{BZ{d@`O$oi#*WyYN>OAWO=zHB^ z8!m`u&e{&~KJ{NTXhEKwy7JbqH?>QqVNTCqfv!|u!UW1`mZ6m_u&i&q5+3UPBG1qC zd2EnM^7VtUdHH3I(p@#W*=?4Erl!qKvC~WHf+0pF0w3`mjv$JR)my8j;^0w{jhVlG zXZ$w=ZkI%C)91CB4T*I4mVbOhjKJHClXzx$wymr)XKrD#$$dE(7D~Qk7kl53(r|Uh zj89la^_-h_;`h`1dsJ`Nw#jFcs3!)T=kKSeFW6%roe$!Jv}V_P&K z0NpYG#;y$GCVpCWT&>b^vi{L`er zR3ww$YDB^N3O1;LG0tAWzl}ys@N_~0nTNcR3}+Hh!F&u{eTnKL7A~v0(;nv$rQOiB zx^I)@V2GQQemniji1XM?+o5V0!pM)5I{8q@|z0xOcE^OK9?af&vn>lNy68n z@_N2YM8uv@*2Wk&)?KhYT(0*hAQWc5+IR`Bt8o=G8I#|R6Iyw8J)_~$F<;pw0kvpD z(Wc3o8E$<+h!}6N528ZfF&)6(Bi!GdFs`A*@C0u zVy39d5l0W;X-4%`6yYNq{eM7K0Yx*|68cO0wK+U{9mdgq7-}({M?|$1G88`&sUrA# zETgQ^O$Kf0Knw?`1t?$>?*;?t$Kl1*p$)JuTVC|{#X&hmzP>>gSEx>Q;bQRA8f@yrtYnL+wc6esDm+yK^ZOZl4m zl1-}7*A80g+HTckNl=nb=e-O5r z_?nRJXE`&zaRiLxvF}tpM1=TaeRUJ*x(Sc^(XBJHuPna~bJ1d){aE!aq>9=h#a|J@ z1>ciQCxPgD>SXl&DmuO(#z40u&3;(>#es~kqO_xH9Xj?+tLyUgM=*4US@=Z@lWs))3Y6=tAX+z5?QQwA`#a^z zg3@D^Xjek)oh(^w-JW}rZbx90VZalF-7C7CiBhRa{||1pehnPcrlpe?v}vIY&gVdq zIbD3?N9Gro*;wb;ptfRH4r3mOQ0an^1>?o!8t)~=j*z-2JUdC;9UH7CH`y%f(HDWO z(Ta%-#0|-js~5eAi|K)%0rA>JleGeE8SaW}>p1TmX3yX%~3w1U%5>ZWrIetPIA@U2 zxx1-b`^h9d{X$xFFNoXZgg8v*~6RcI?V{S9!;a+3HFDtPncPeM75$Xiyu- z!(#m%zE;t)*s!O`{?N*^%xb4%*uk{C6 zTJD^{5Ap~GUb|@uW(0|ePewU}s@>aEFarn9n#cE9*GBE(24uE(?;hQI+s3`c^zCag z>+ zXQmOBgrF4xZ@+mC9U%Sz7JE-9#)LNdiHV6-kQ?pO;?k6$#C&H$iGYPdWXSTme1l|T$N_l2 zK4v|$43!IM0PQENL98~F?;RHra1m_YpTsn*USsC^fgn7bGJ?;p?>u#{%>+_HD^1S> zU|Un6K5_6vZ@Hca zn^@U6n*%Q=e!8xW&($U(JBt7GdBi7Ej%IV+7lmqFL(Ez7a!a(>1KT`LP%JKAdMDcYo1}rO+D#|l5>oLP#bl> zcX(etmMHfruCtfsvSG&As#lsOF2LMm=G+iqUSrzlMwDKiQs&QWH<&2(awtgVt%#jc zZM+MA^kMTS^VpI>NaxNJ1C^=o`!!5Gkz`nbJ)38U@ED;T>c|Xn4T_?XVkEc@IjuQm zox?u8O}Z%CreM3wDB#-9t6opkxE==}nJVY=GL3^VBPDsTY^WiTlR2(<(cpYYTmQ#u>BZ3GCE9Qna2C(_E}!D)1RJ9_L>m0 z^2?T_p$1^2e{EtED&mMrbU|uF$#Z$?!0lV{brZltVrG=;4B?HcGZclfZlm=mO_$%4 zI?$+w=pbVazf6rf-fG4ptU(M`=*MepB~TXVyuw#d4d(u=0X3hv$M+RvtYaM^uItYZ1We+r|HQf=E-q#a|Z>MIfcl78sB>AFGI z29HP*b#Ny6Epby3XTANw3k$W(AAXsTR*fib@egWv%hOa^HO!XfF`kY_TA>K>gzgB#La{%gE}!Lw0$o=qWZ>6U`AjT4UZJ|59PqayB$9)|SDA?qe#B9=O z23@ER`lc!SHlQQ(Ned%y%cYp`M^F2uV~*7V_3X17IH()6D%bhjs^I6!p_5);gaE_% z8yaNnb?uL0Vol4~&mprYPax(O)SMhWhgvhZp#}v~o1miu#CD>5)codAiRY4;X^;j^#o?;#ak$V-?9IJrE4 z#(s9~m>0x;%H}&MB>W(wPyUUl7&YtfmwjPsGT37w^!ICCK_=EI(v{fPH`%qRxZXpkol@HXGOVjZuW8?B!sJm}rqwUr?U zx1GE`epI5{I?x6XZZZn_jX4{s_jb>gKF{85cHmQ!sdvK-&E~I{c+ST{vEN+F zkVr#J_M`zg{60O+d5ewN^gG~-ajxkIH1riu6zdREknSul3ehyMJ=|qn3j+{~Zzl$6 zuw=rabWjE-TWwe00modMyu-#OK|t>HLsuZ=9d%zAR5M1-9LqiL9|ZCD9?>ahq4z_@ z2G6Fb!M`4gJz6_)!@MPP^y&6XJu8PHuMHlfl7X`yJ>fd{=Npyu<~eE2omNh(yrQ`U z$u2|BD|sdOs69XS_iKVW6RsPAeH7ilFS%~TOx{=u-LtcIu^zDT1X{aJZmA1gC0ok= z)@yTL{xJr(9|r758f4I%rBhX|+D=%#m;-K@e%+hu)fTc}hoZ#b!u$z~GO`GNs2uB~ zt&|!*kxO!uI7Bv3?2zI4 z5)x}v-i34)!o<&_R9b{;8AAyc_3fplXZZzD`2pt;1j*i++9d``9z*a84or&r&HSml zkgt6AtN*|!&(zUd(avG48K=?X*O0OQopX zA<5dtK)-i(0b9@a=!h}rCffRxp0t;<4P1&p-}N+O5;+TN0YxK5 zdpkLvn8K>ptVVws1TcD}iDKno+n+xxpID*l0H{jh*ExqSrlVkTOOgwDqqWtOC*j=0 zbpRtz?tQ88R|0xBdP_(AH`+t4)z4_3z%i%OMi`eZrR3uEZ$tpFMoKt=_N%+nC#Dkk zz}Y)LzrPT6;}J|^U*!v&7ZXge|4#GwT&>RvS*l5ctUyJ4FH-Pps+j*l5{3hOABK&B zw&$(P@R_Hst_&$X_IJP3{qf=IsccXqwU1T7G>n4WvPBHSW2De_jv%Cf%z`>-lOA`>kqb~f`crNgtLXCts;V{8^x^!4{IZ%vw&ZqP+ z=n_%!OS^pdxpd>rC{)bH9-I21%J9f|99&@|{m(C3sg>}FZ$~3g4Ea&t>wN=oX~1)! z9T*x!r~rILr@~(pXG4>s9C`{dCmu1g#&xw!t4lz_?ySzF*g~VD!K)r+4`cfkL^2gT z0>y@u_alvXWb5C7p)DUs^^SMYz?C=95Ndd$@A9E!>N)WOjHaE%v+ z84oIECXRBWHfHkPl(FsKzlU3NTPfe3Ktm|{_=`+PbVKdpm(;GimmNqb@(&knG|v$6 zVqC!_8{^)N*wD&s6L6^HP@WnH5QT6kTNhgKBJ|Y%iDjQTWvGUZxhLCbHvS*mLWAgz zD1HXa0N`21OpEHE9=TCDDbd^uHlehLfm-c-i*q?pF-|EAh+Pctwrw&gS}u23Ol8(H z+KZc1%l{kh4eh$o>2c$dkZ6WSKiy%`hgek8!{n3Nkh-V;JlKG3NpvBNA#dk>NZp-Ja1rhQO_;K1Ol zVhdyU`@yMw-q)!$BMq1czt^W(>a6lACziNxD0mob#uM{+)2JAwf)~D0%hJ}Zjdd%+ zJ~1A1ttnV63axj7x{VYT)`0y#N>NlkEfOwQV<8lgatq}+iOM-^+5bmkx5lVOQw0YJ zAboebXTVGKz1J1hlkyCBhui_$T>QUnzO{Qv-Nh&g3IDH5A}ItBOV2nRV3%7j)@`C$Uhxs5dWVmd;A$?z;n3mtGgB%J9rt0Z+%y)4ko~Wu;72y1ZTye?(8I z#P68N;A%TiN1k_#^zngi;ypo}T_X#4F;m6^x;x%1pU@}t6|g4EXoX>nnj^TNlY+e~ zIi8o+KCc-4M%*F~*f*o@V!#2dQ4J#VZVh2KSTW)t$uR!c#>?=buDnQ;&YBtw26~x! zBcN`7bmcrCJ9>`+qnt=DfXom;G&vVQr2`|Ew4H1o={f>~t-~?IxQw zM!l)tw6^}~2ICR0z;Q?7>;8%Z05q;)QkcIzAywGy=f0WAR95IYO_yD<_At{j@=qc+ zg_=7oa#;-r|78AA;itGIV;P;DB7Nb0%CCwkF{nJ`RWq&^0=A|`V&I>nq>l7)oir7@*jxH5EAK` zO(`J=`~NI}O>ZMC45a}48V%Neup;|X)elI81EhuyLu9eTcT_c8-Js=e&{|;U z??3Sz0KXQ{&1^{5x|jN|qiwcEIm-Qsmw|rCo6M=ZA5JPy6L<3a=6InYAW3gV$k$CL zD0U$Q-I56tKHsDB7vq#ItQ7IX&@OrLPt;Yz2hq8upw_!CV-T6klP~P}h)UV8U z?7hj%!x=019d-Mdp!Fp7b+w6$*t5qWpV;AUKm+KNUQi(GQo!b?p2c%nt^e`ekq+Muzcly-GchPWSX%k;U7SyfQviMnZrnF9B6rk0(cemT(0>=E*YGMc z%CVOI3^hzr5MW5LKoYSke}M;nO~wguVZJRL?52vsRga#$qM%eqNthi} z%!lLp||s&CZdYS((HwO zkN`T&xZHg6%fIK+3sw~G3sLll*axd~v{tQg6E~1x)IpAfdL}P2KM$%W&1~9A9m$|t z^--Q^ISfa|dl}`>5bP?u&A(lv110|5h7~kv>d?nykjNZY5|F6WQfHHCDTW_=dHx*P+dJfxa zHg;`}$k*?EU0Z`GF3HhCYLu0BvahRZvgSus7;5a4HeZN?AFlR~Y0i`4577l-ad8=L zjv5XY!0z6M6H@n%S6V}f;$J$O3@FhMpzm93EMX3H7#Qpufhdlh4N4Fr;Bt9_4z{M$ z2c1eoPQS`JNQh#NxjZI=z)uNt|C5j z(|Rrf_UbaaB?C^F@8A-F%faWPrdBJT?(b_*9$<)6t%HDu^ulQlS1-q-DX2gcpC_b1 z)8)&3u^DqiRFLhE!t(71yuFexyaKK#$jk0<+1r~l01(M=BlhB#Y+$BW;g$5+rI$Mq z-qIuKvo>D-{$5ZTID%OuQ_2zKKhh(Q!nQYqB`M!hNi+(JdoN|+Ea(xszXRx*X6rN0 zNp`yNNTi6M`bnnJfBLzr3`bv^PDUfh&}p zZyVX|lAhjC3w3L=Zpq03zR>uZw#nD_3b}w!rO#q=8kiUFQbd_nN!x&9x^Gkp=q3l( zI_~>LxE4Kl1YIC@#$d~1wVM+_#8j*<*CEN&`q*WrljqJ9todNpmLq@O;ABZFD{XrR zt;1wGx-r+v5Xb~7PZrFmv9B*R`H^6Y`N0}|*f!Fb(6i3s!rTJ!XVaN&2NX{ETvK&= z&i}S*;p(0p)Y2e~jyeFl{Rp5dS1$H^K=$vL>bZ7dBJeYbB9#YjCw)3YMpog@K(C`)F;iNY)HGcf0RiLQDY3x_Btg zz)5?oLy zg|&zetqW@wVHg^ z|6~n>d2pd>%lv@uuk45B=gcpf2DZ<6J~E${W;?c288UKB0b}olg(cp!L@Cqa+yZPE zos~2X*MDqz{&apG#XBad`94@YSIsO6K*tKgs1YO4RB<|Sl~CXb0mTUX?kD{LJIxof zG7i^ST_kbZC2zW8F0AE)B3A@RVNA=q*%SE2ka(I(kOCn{ma> zbMr@VVTHNNk@5cv2-eW!EahRj3_yCVz*QFKXY7zcK7^DN<>hEGo65(ZQ#A$Qv-NgB zhlQ5@P*c6`#eM!QPF+f5(%{TWxwAiQuJ@Od_fM zA%QRJ&8l#b)%$-6!%2*nB>8VxH0GQBswPq=+@0HDdkSR|f;a9UXu1E{nwOxsfc=sG zm@6*q64Kct&Ub*}Y`*z1mtNs5E0HO(Mn#(iALFK}hm;rE zLL`StG#WObnPhvavi98XQg_|cp0D;r(EEI_z!jyP!ML@pT-@R@qji!n!3Ku`?eC+^ zn43`z&EBKRC;mUiKnXkN2WvTzQAI{;jI@|g{I?gW0YVtM+`39J-0(XQ&GO6b{fY=n zz`!UyR}RdKCn){6m(}k|YYbx|p2Ktl8Txei809d5BYY)~hvEA$t;9MJYPU4iKi~#G zX_qS3Z|XLHHGc^KfarHh&jpZs{lEt^d02Ggh=SwPj+s!x4FeL?9rT6Xh!hU;@URWo z9Sh2jqBZ3pP&BvsBsFt_Y3#RJh9YnK0y-BNN@(n(w#B}P3Pc@PC{JE9AWPM_V-I+g ze_DdJ+V1ncqXD4Ztk#&?eh$z&0=9O~F!t*K{Yw!cg_v05R7JZ^HkTe3;En{<wIo7g6$k}F`)i8{Cns951HXK~M08-@T+CCP$l^Ht~p z{#ZvDku?yspm>7Hi3c4cg8J`mbyt-GEooQ?yG@`CV8p*B1X>L}|0R_2Q2Q0dF$h>d zN?Rd_-r>h3IQU(O^~xKdmb2kB325GxAW-L8%mobrelQ*3cCFROBs|ZJ;*SH1b7NHD zVE=+`6&i5BexsV}eX~i%g*N?v);Uc2Qt_aa?=~aI5_(Z%-@yq1bKe`Yq3g&7L5anM zeMKtAnxL6bywnWvpIPaCY8@|{@RMSd`G7*2@se#xPqs;tK6wA7-m+IGUU=7){mSa# zU6WsPBJ@odpyT)<`{W>Bv<-S{vXzGs7ZKs+3;p#~O^2-)G z%X4JT&QJLnH2b=rjvYb7W;SpiFB$()Z-2u`QXuoOiEPq*-_3&Dzrkkv;YhNJmb1x@ zLEI1a%5 zj@w@WRInVN81k%>?#`G^Y(@bGj0Drax`+)MOYi zimfDHn)ZS!4ZQyv8E)nsRdm8&NsqP0jip|>yb`ajEZJV_rMp^!;;Oo#4hVV<9svu$ zZ=HIz&LiP#yfzNJ638u{lSW@(sKqrR!m8>;`bdjy0Cef^KGI5NENtP z{ok3JZoG}Bbmr;bZw0oypeY-u1VPY6XF}%2Tks)V@(@Y`!UdhS+1=U#!5X)apaVTD z3v$caI40qe)6%y>&@c8fiy}EnoG%jY4!F!5SWj4x9U|&NaK2Pb3c+Ptp)Ve8*ata) zZe;$r{U)2PRYZtV51_8TRD|=fUTd;|$UI_Dl)3ft-kcBFZA&G!as<@|?828wrX z;=mcRk%vIv5&?e-cyQ6;FGBgPRPpNZsdz$xqueo3Pe^`K4gPvF)~3LE+(CD z8!w0z@`GI z3i^86De8>ywxxa3Vp}y7t83GmoxGcfaB^%}VaBllvVCddxSJsecwT55w5RdCSAR9O@G0) zT0NtQINz1%Fqs_P9)C&A$jKM~TP_Zwc$Y{z^v&h1-8(TP|9w6$%E8j8`Y2O74i%(%F#Uf*{PuFlW3rMAj@hib2mp@p$60`3}`TASs*_&hLGiaTl%eK0{U zIsh>~v(nNWpydz!sl_e?qzD0Be^lxDH?NLLZmvj2*?mIwB#V$~7Vs*>KVk&21omh5 zMavzV{(fU!+czp00t48NqfWELiF_h!xu(Y>GmRxE(8P~<#BwX-m`k$cDwM}R z?j!$LWJ$7Qykac6R84LyLVMk#Ln^vj3(cLF8!99@9NwyQ@xVdNgmJGD2*#0TZm-|P z3ecuor~I#j`mlw$TOCJ#{kB)3_c4GFmLDL0Ck1_IO28sR<0Jm?{>(=mU?3?L(SASX za63*9S&wJ5AB*~k`O{NgVu42h^>4O43R_O^8ixN=iw}$IcfMDn7 zLTC6Om<*J*sU<2fae?Nzb+@_wWvmSP_xINpebhun*GVFKtU6f6N-(HsZx1vhCW%(9 zpJ5~5smG&{9wXoo*H0eCypg*p<6d4y9kb>ZV1`cqn;_s@(gl%Z^m5zb-^La)s~w($ zC!w!asfE6)>)IIQWdnv=G~l5J0e4puiVdhJ0`au1bJ%2z3mZ^A5~*s`10J?mVfy?h z=6Xe{t3xeZ!JLZFdjorX!5{jY`iokVZ=OM zS+V1)IvV6r0LayFiKs0p1Wh1oe`3F%Sp^Gh9@=Sv$wLY#-fqE1)U@yg#=eGCW2AZ-_^reR{Q+d{I)A9 z70yk~&WP;FpsG3J`MLe6w+Rs`<`U3Z+wi(0lqQsZj5k`xV_0`V$9xS4E?R29hB_z^ zY~k`cM#OAbM>9%J7#za~$;aiyLkOF(D3*ABw#7$20jyl?)0Wr_3fbcL6ZHqWJ)@1p z56TmAK>LF9M|ErnKq27^C@b&x%_C||b2vgQr2uFj06q8@ZA_L&rkK9p+yet(uth{f zMVnqj=q}kRis+xRq;z0Xk2?8X`hyJc=XW>G@2`!Tmu})aeRRq|5%LEC2jzgJUyJ(N z7_3Tj{!#wfrtA(7x9X; zanNdI05x?+$%df8h(fz@tFjVP_m!vuSrG6mg3!L?YyjY9U?gBH+Pl3(z4t~hQ7{ur zQ`k*|5BWg+#jw@_6D5Fy2}W z33|SnMwPtg>{fnYJYms&c+jZ|QKps~T=>4(^=Vvkw@P;*wFA(y8|Lk~JCRRc zkr33(Uz1p3wfX2Bi4Q{bgd9Et@rBZ;}dR+d*X!1K`NaSKp+ejNA)aSc@C3p-|j*;WlOc*ju0ic zumM!LBGM;ir-F1Jypu+SWkO-0EoVatXFE<=FT686niUvO_n2NvHEE76203Ad^^aQ&$$8O!v}Mo?JiTm-P0qlb4> z`rlF+u3Y?bcFf@_vQa=gLopSA;>(^$3Ii`lluUwa)q6B@Oy%od*Jo`Y6(Qt=Yl`(k z44+AGnm?LPFs*!qVsp@mog)Gm6=YVmLk@q#@U&H0g>l0CzXS4M*vXgdk9Pt^lY)a6 zg}yKxl|$X$Upw&qv*(qMGMe$a%-&46To^`We%Ig4!AnaSoWL+5{t)F|cSxfxT zV2*x!L1A)E-7{C*(b{@ZVgsMmHYWF zo9W+CaD9zMy{}{wkI`MWHi%D8*Q=_&s0_ZV*Nz`EC4>-Qf_!NAnVbBGZ%^+XK%^_} zI(X=my45R@U;boGqX>W?J#FSo?tbUNjgRbdudq$w(UyDu|*^_^n^-zkXSOp@#n(JmGw%UQw!RNM!gSGrDQ9e^#*6&yPO(SkhdqUpN>@ z44e)hrfq_M;!NQk(Bc+8MgrK%qrY3;u$wd>JAqs5TfJq?9E#tfy*^alN-Y8lB?>dt z#;NtVDMt+u{`_UE%ZG@1W@1FQm(5}Pb#X5%H_kaI&mN#JYSffF?j3kECl1sYt3(tU z(8BrcrB5Qod>$sb0SvgMCab0tcZC14#$MLU*f*I4qMrbVim(1^ zP>KYR7$$l8wNRg2Mess)HKq_>;j6DB!~q5MC#?fLM4KxTT|A>l zH9it8>^{PfC0)c~X#IgI*UjmOVAb>?lmnUbh)pq>9%2|s_gi_SG+!onQ9+GsA1Mfh zOI{kH2jn@gKFwx<*lVXKcVlIhH*AZoR!!IIANanKTT$thejSs9trd`2s7^99 z?3mjV0y0?5ikt`72Z;SQ-DC`0y=@1NYyX_$-cOxSuOWk1)j^|Pua7_80v9TJiDSZPH#3}r}K_SXldy~ zQ`-hFq&Tv5$Hy_G7x&@Wjw%A7X8I3R;EF$A*1ee_8KK`f>&37aW7tUzwo$!{^9f($ zZdOGn5uo@x2T`KvLYNS^QA==-pp0-svZ6j7s}59_0jYg8h$X@l4Z;207ABNne&3#M z{Rx#~kYsF#|4VL;+D^iMHSRE(q%v`U9C|xJawuNz7eOdI)BbF6h(P z;BEQbC?IZ3u-hnn%dX!I;G?Z@_NZ7d)S$A=+Bny>lu2vCl^sj??Mzobt!yn?@HcTl zVc7WlXWTLmr+A-PQjnXnd0CAc6h(H9pK|bI148%Ro93u#w~@ajYiCjlcfW#C>pwtC zdq~mi#r(y%-|$Z5!cUdsz+wd*s?!xxRtEzjc;fbF-9K-c)71{5LVZKA0tLBIOhf3D zQy>z~bGw#&YkWYgaUmT3td^^ zX}NU+RlMb$lk4F?i#8XEpX|hh3RX-WjYc=Z0=OwPqdsRLZ&FfLC(m8szyH%XIb1$K z3gTw*<%akbcp1Dk_tGx5KX5(XFKw{r_12c;WKci0_;j z`D5;4o?sTf*s3smo=Yi3ZR3c~ykjwyt5D)*`D=J^01diONj~*LtkI#@^;YW8qF-TJKu^Yym^sN zV1O9L0}$l;aN0vUTy0cxm_ZS%PMn`l$&LqG(y195qD`e;M3Ou-8|BLan*1y zJSa0AxJic>5)e&EjB=kRO~+{P=4NhrS%vN)t~I2{=PJie2mckd z`&YsOyJ4;u-e{;&5yckyD}z7Y@FHh@Mrlja;~z!wFNrlYgAWHG1E~Mg(_2SH)qd~8 zND3%Q=g=i7ARx_9BHf@8!jRJ4E!~pRf;0@0L&GE8LwAD=InoWkgP-sFvevA{A7{?J z&%SqD*WUZwgZ}vLgf>v*eNTu)dy7Vnt&v1D)IlTbvvjE~t~-Xz&}EGz2fAQHAF8j1 zu`__NFcShFs%otgE_^m}AK%()laPsL*}xnvFO(of;Nv*?pUMbpm(R@05eS{WC3r*{ ziK1Rf_?^G^1vQPUFU$4A+5HG3-6Dh-AHSb5@0(5tITsG}3A3Kgjv416b=i_({khkC zcKv<*f}QTZ+rrYp;dK4Bjji|m)cgc6xLrid8KOj67a(sgX#*kth7I$oK;I;gH*Z6( z&|s4=e!pV;zL_z2bLH{ZL;iB}#uuy8ba`d;=ETq9Bk6cEXzJwU@~b|W&k-fV(I#$! z$MJ*Amv1@I9EwZ&Rpo2k^hcX8X$0Ql$XB#W%N|!ZCm7Kpfy*nUAn5uVljkdgz zg)bjC?3gf~p51r33Apb5fg%PvAZzb=Gkh~OqTC@^RZ|^eLEH`CrWj)VfZKmRSC`vFZ z$+)3d_%7vl+3rxbn(lKvMQBGzN|p7L_`&FSG!RQttg!}AK_svT%2>Y%vkPlV?G)fg z&oBeSR6^!gss-^4Xk0jYqKQ&+DK#ZWr#U_N4Jbbq~gh@oRF{1VE;m~s4x`xK{2OEulw${Jg-sYU30bs&0E{`ldF6E)!$pqJ#;Gh z&ll2J1V~L=z7;Q!yKt9_A#=U&UO8eV7^XCO%krO=Mb~eGPN57$VIVHv&)7}HetpJs z*Dmz+?XNGKXTiD;`-Vhi-B^wy5Uy+cWcKr<(^n0A`^z4~NaCeGZmL%`_sd8nZrg_I zxAE?qAe2&Mxqj6~kkqn&F>AYH?^J?EDT#;H4^?f=PPnm_oxMue{q5@Yo<6%xG!GsJ zSkN4$c5=k~WdqJEkqbw#rO+=v0{ctw)i{X3Q{{F~bn8i&nX&d&)!_FD_7t(J!syg1 zp~mf3Ibe$M2v0GUl!{V2Dpk00#Mkznbn8WnjW^y*b`jgoA*{BTlE;UWsED-p8-}|_ z1b2&IT`$2GOPZ;UpM>ON(OVM-Gybkak|yt`)*=4#t9DK7bM5a;AWSr#I3EPO|)-k z?g2f)u@-Pv%I=d&g&El;>Jc)CS2uwR1XErZe<8<^Dkk3tlZ?}%T8b7)IO-2=VZMrq z+-jC{iCEd}Jbt}e4G}@Z7W-hBcw3e|5+Ijin=`}pqc^a4@2-2px$*wi6tQWN+jr&_ zQurHfXMR@j+8Z=huIVWPpIz|=Zhj1R58Ut~qkD&lFpbXLWpvo9uC zz!F();vXkkjPC<;gL%wD@|{y)s-_tN$afyxSIO*5A%i`5qcM$tXb{!9pY5!s2N2b$ZKU zm@-dDao_hSnB89YPJM9m6br}c)uLQ^36Xi%{E6XQNW-1a{?V<_TX)A%Az--1k=>cQ z-EmucQ#>c@A=)V$HPM!wAiOSof!a$oUO zQqQhB#2t}ypQyIt?&%x&vbr^tYa8Lt54Y}>awX%k1Rf-_XH(S^N}4a8x_tP_d_~N? z&;RP$F*jHFB9u2I%WyibfbgVkd1CS2Yow-=4cK?@xt65` zP${kx3nJ((6M_z#_zn1iFYF=umiW1!!2TUlYFU>mHr?Z^H#C+2iQB+$-^@v%%5^Af zxuns{>%N**J{{EdAaY3jz#?{G;7M{i(Vd=^tz&W=8nc{}Msu87}{EK&|uk7qYOYLzZ#1(%ve;m<~~P<{RI?OdWStzcg=u zr(^CjgMOFS_h-`eu4#s{rFLz1qxc>X{UQt1uRPmbS{T2XH!JPp%wmtEc z+~O~Zi?PZlzhqM*aZw81w>mO4c3x`X-V`2zM8D!tgvXGZ0;}|oSC5koqz1(1ZjhUj zmi75on2Bl{PB`?+fB&)EvbLKVm=U0hd2NXE0RI*)o@8$jVdR8nS9w%fJ2`x(EQ~KQ zUQO2|U_g4K1xTULl2?*)Z@j2PP~=SKkF+xk*P%7DtaPIHiD9+!k*vJ> z@3@OyFUZeI)U_bbb5x0aSM;jm{WhNSa%Fz_I5iDazs>Rlofszf1J#)gFZn^6n_l$Y zV4>&elXlln2A90<-8Wo+sRk?}`Mn*5@x9##LmS2X>|dBoO=bKh<}bOZud5ehFMUY7 z-MEuWcY}K?3@U8%+lLnh;q|1A73OA%#_-rRWy`D>_-({Kd@FxI`)mj2d0wrcer5cV z|5!Fc(C6ygP!$d7}xgmeJc~btH=p-#CcNC6}GT+249nC z_Isk%XtE$v$>1U$U_>N5Uu$<5AQ4aIoyqPqx&!WX4!Nc#7>ml>#bUf5cL9#Is$_uk zuvLS-4m-Hy$DT{Zy)aWy?aL{)jC0*k}Y!t-B7N$S%{p#Z{bg{4gEb?v5 zZFC=)pzCcH%3z5yOjxZAUHbvai2B1$<(&Y&k_vLJ`#V()xO;;Z9RN*Hu! zrH{>*Y;*W(ravdpuPHv6i_>+iD3Be;wUt&Tzdvu9w-rKjjG-Qb4pNoWC_TPUX_}n? zW0?$Dnu#EK(2LFP77h4#-;oygks>&-scgA4Hv{k1u-nV@Qf>#O;vUFthAIEt31k|# z{T{{jv68*g5XQf2*rWU+y99X11cy6H`ZTAHt-Fb#*W>5)H|GGGzKWJCRQ_-@Nk?`^ z-Tv0#!#C#2Jsx>`&QMzS{I|)ATL*r9-spV`#XLq4&B(^&22wUKa{mJz(oxULpN*K^ zl3&A-d6Rcn?ZsCXA4ZMD>-RsIJtckZCDCyjCq6!>8UT}zlNvUJk{Z|{>4IGpf!W?T^4ZRaap&N-A-bQK`E9|j zCT(gmC;Sm7K%n_=a0S2`{SA5Ao7{Z5qeZ#HYbc_T`fZ^jH5;Y=fxN9RFSJiP0esHs zdw1(Xe@>}yi^pT7G?JdsP0TcaULG(T1D7Y>|eA6M0bi@@IF!NHDsMne!;1nj#78gO{x=^6ny zw$G92D_wyZS^v2V)n{fOfebQl=j1PxMD6vX5T0tKE*2SW#LTgnnF5o@oCTlN}t_S}d)DV!uIIfq_HlZ(5o3xt1iu7;Ceu@z09s+M4*Q!zM(&MX6!g) zI28SAE9z`vc`M*Fau=mQ{4Y$5xSH557VtY^k z-b`cAZp>R5i)FX~h}%WpVya4)S9+bu&x3_uxfl(wJ<^HFrH{gH`eU*va4l=b&KYXdwJl?s1p)_B=T>r(pQCKu6 zbpWElpHfBD42os>%$b z^mBma_-6v3ayG8zAJHkIgccB1-GwNOa2wp43dL@vf9uEFK{rrfHi-R{U$GP-EGSea ze8!wRYizY#WoG9LH+$AmvF!H9}QPV zzn{nT_8#1Lz7heD4Gu&=C3RJGMQ<^opfi=V1|k1Ib}m)xHcLIaqi*n5F@C> zWHPr^gD8Qjuf&GgI8F`*Gdf5!_p98hYC2(+$c2QPwl#c>4C3_YpvHM-or}FJFem=s zMFAV&Ucz4l;6s_3QMvX0I=)mEQlJs5Mn^wLl{)?0k`{w@_iXldUrAw2uKbE^B9p|+ z(A^h(gnkpb4z*xJ6mKfPTk)i3a?Tplhm-Y7Qv1s>gx%*s1Vs4#pF)7PIiBxOZE}AV z-k#U0@2s)9ZVA>2=1rlJ6;uSZRN=)8H81^LTiZ87iiSIMu@*t%Sf7< zz-gr~cCP0}3(Lz9ACy##dRm`>(aVX6h}rR!dBlUTnus9s;>baR+&)bcU-eE+YW(T7 z-(sn;T(tn)yXEgsOMt6UxxDTn6LiA_p8BWtG*XbBU&KGqyzTi&|E&Dh6G3lr!FTJ= zU5nlAudf^1Ki6G?u}Gc~%X2#FH(KT9?{I5p9gIh{#e){brb>2QG$oLD!YWLMN!j;t ziZzvDRT83WPsR4B%Mi~y_AH=cE$LJii|*Ob_;4E)<&L$$7R!5o!60hX{sq#{`(s0% zl9i*Xx;>PCE)k+(1!ypDyr&H*4HlAac}?14^`?L8Hx<3=HV_Zr^00H9D2AC^YJ5IO zD=RLweC+j5@z0CQxmA_5z&>{9O}o(-K#yQKuuvWFPb3O9PU*%=!GIQ2W^7=vE0M!pej~r@0Y! zUljtSEW>cRm7ZS;|^2`NgDqcFjX2u=9x0h;4KlqI@;cyhg zoj($7^;kB8#U*#NC#HUKvK#tq4q%^7{fVZK$^ybM)%-a@_Ul1_Nk}oX`%Dj- zW&oA9dHI$fy>>GUvH(#;8rMq2zrZekg6G~0r@Y=Cy^Ce2aVR^U*X&bS4g$(V2YxAc zl)8;wMoxe1Z`D|5+Kss<$EwIWPWt&DeEc=(&nL;--50?uK#D-N?>cd=w94GNKB}5B ztmZg-ARqga6gz3ezU9G9|Q({lU`Cgn!uAHl)q+uITEDAMaOPG9YBju zCV`(W5)A{0e`-!ufyKQ!l6!j}!^_5xxpWHnt?$g-GAPCsB&(rVlb*qH8RT-d=myvR zIb^ova2)h^}}rp^z!VSH*=Y$zvOD{rvfdlnN#pu1-OOqlBbDgjR?9g4Z&y^RdG90^=#zM4 z6BEMEn9xNgli)P3Jm=w=@cK7-BKL#Y`=gq^v0;P8AA7Ss<=H8h>lMD8VVQ1lUvB@1 z=HgF5%TdhMC`sJ5Y22?_R)mS~4DF!{q(wfXRl(jX?(ipioNZx#JiU>4fwo!i(l$vX z67Qti4+W#OxRiBdzGysbx>E_|_Mvqw=0Ye;<|UC<5@X^bS>bpt5}1%7+M1&Ho@hyf zZTfb-#y6hr+}Ex$I+&n|mwXtPd(WJ7JzI>WpB`GXS$o!6kDP?N{*Bn1C&S#BLiw9^ zFj-y^z%bp^kc6M(Rd3g(n72FLZBSV0pSMX{n{^++wukQ}?Chh=x=}fBqoP|ags z93LN)1tz_fRxvnCFa-V^y@{8;IGB!wfFurvT~{p4&kA{r$#vJlTB@^qR6LDGMkk7( ziSq3<0Rl`WaWd;AEP)s>9Mt{ayE^T*kNn1;e-Ut27=u!WaBH+mi9f(+9^nkyuB)Gr}cCz3tb)sbBdCR8@|B*sAH+x{3(m zaJ1QEie(ySv7Y$7Sc(%mSyNVtqeTGFD~~{7iHrP|U1`RNNH4Ms-)wrkh-<_HM6(!o zOI%8d1sZ*MUXGJvQxbCqv|ysi@gMU5k>&DRdxJOWN>p4Mo#Km23&#`c>jcDYT0ies zWY=SJms4f$rjs# z#++&Q0(Eeb+@kO2q^B;NMk5rJ)9>qu!VK*HE9esQmS=kCDU9?6&Tat(MGwM8MDkl& zO|GD?z76Y`w%Qc164NH563@?#-Jq`QU8k}KxcQ!0JhW#CJQkkZ4Q6GtoY;Lo{V z?tLdGMvC6*>s;ZA&J7!-PVSzfAL!Id{Cd%eU*2u#P7ktJX?Q6xbl8txp>6rNUgBz2 z`YW%?<;#l-p{=zx37w^)GBjbyKgpEZq%8I;C=l@K+MSCb7kk{Xq`6NL+pisTyN?f6 zx?bD`=fO6rTLpx*agNO-sO~iwv*%eaU(_cLsNM+Q%r^9gw$9nP;(mlfyQ!6&m5%k} zB;VdeiyeAz??-wfylAdUH+)rY&i*@_$#NDjq}p(J<@iF~MSiExo70g(6O)+#d6brC zROh%OE0S#!H~CTvt{ilTyDcIHXNBWk%j-6psJ<6)-n7kY!SY~NnDtxTJhLRb+dQ1t z6Gliz^8}AlIFRuv@Y|>P=L+Hb#$Dd8kEr~|jP>FTv=0q3|Ca2-^j`_~l%hN^7QaSg ztlk8wV7TU}Z5PFF#n?R{lCdb~xQvXr&QLkSZZZo+=kLO%Ug4!OJ0=<(SazG$d<4k5 zQI^{%*4Ijnv87%N?iKu8!ea45Z@(`nzbSYIQU!(pZnttS65GN$Al)%M+oPruDZa)q z13-l2X2KH=AwZwiqzVwZ#y>G9Fw~h8Zq+xU$=( zkb<~(bPDP}Hi=DD{rXsw^FBJK( zN${Xo;ktBywza_KRY$As!o0U6#xby4!Z1pFT#pI>ZUQz-(u?dw6N%<0*g1!?*`tk1 z3(et`bW}|%SBCcl7bCb%Pt(5Jf2g+{_uMQN=zDsT`2~s9^Zw^eB@9Tm|NVz&-Vg!w zKP${QuN zm$yT^2P>yi$FOjp_(Kcc3}c9PSlL^Rt+;j>Cxl*jH_ zTJDCY@#NZULnry^NJz`>IB%<7wGg38f6KI#N6=`;S*H-{8{9W#@Skl0VPDO8=L!q2 zG1_7Najmr{9&v_^Fv;|f{{GE$l0+&75Bh71)O1CLYq&U(#o_W6T!L{dVi0R}O%ij9@&Qw{tM@bJVQ-;a$vQahwUN z`qDcqzdgm+&Y2rnFrNH&L!A%y^`P?1sAqLiJLrgi^NoPgvAZLd+LZMAuaX??;qM&> z6-G|Hz+%)5U0ZEpmKfuxRw1>tR^6&$bgd%z)>Vux4{WJ6mB-T6k;5ems=(Yh*M93C zTu}~j|A|_j`A6oK_G1{J;!1`9Sd#erLvf?TyAvUXgZ#ame>TlD?rFM8xOHSou{Jhh zD%RBfEqqnG;tPkJwXO4x=)D4WEN8=Q^sp!Ak!kSt^^8y@ZTZ0tl}Lc_eR-;A)vaSz zCf`7X2%VJYgLs{Dn21r3pBw>nv{*i$<(rOC#(2ameyW}lGE-#$qj4&X9cTx7Bz_Iu ztl2qpcE@gi;8C4DLO#A^C8Ew&*9ct`##(aTUx${4c@^gz1xk0&?d+>$ zxx0Ug%pzi1CiPi_Y zOOWgG0y{%aHOdIVF7P2H2^MJL^~?v%%6k_2iiFhS_jgLw_oFrURmy1am&2Q5dPc6# zJnHv!xl33}S}pu!ns(&4;n_$9RLa^SbOZjQ+j&oxV;{d+D`*QOv7x-LDgKiCau;;` z#=B1wsn+wNbw1T@QhAT0ZF+J+mDg9`ZoZ^f%A=GZao4Fsx$IsQNIx%UQFGU zr)1-VeLC@#kOBI^McINULvaxrjC~)>PQJ%O55DwS$i)xr+D`BjHN9VBmPx?}rv4M& zojHPa1ZTPhFhG$!=BAG(r|FR3;lzXZ5)WhmUMVrOatvx^?pV9wDcT`2z5&1#fCKo2 zN-5rbz}63IU7>vXX)@9O%iPQ1IkewE6MN9`R}2qYA@8zUmh2%(F`h86j_lW7bWaYS zJkF!Vl{ISY0(ilH0$1xRH88jP%E#ItAMXdin#djatm{`+ zOMJ%MCb9G}XG7KYt_%-O_Y*L!+suagG!0r``Q%#OqH2G6S&2DyrDycx2{ZQDCi0WO zkgq60KAMLi0|4D=V7?mj8-BXyz8}XH9QGV+ueaa!5r#9%Fpjm*rQ-!q|reh zS&*T}@0);86k>2MWR|6GL;Ut5+;*q>l&`#zpVCFG=a~rLW63n7#m9duwe_O=o_8>T z!^|uRru_TOAR31D>Wlxzr1bARRENa^!MbPplmZf3draxB zuPkt*4n9Ot2#)Q|+`ym>`9*FPl_Qj1o5jkPuBB^_LzCo~YRy{vRIl8Uh+UT^avsAk z`|MJ8uMe=Rf&H*B9&1o%5iaa1L2Vd{x|w^hNXs;{ai2F=}Lk9x(H0DtE| zbVTn|mhSc(u|%opAFw;iY*Hi%bz}st=R)|zys%Hjr}kkL>%C-rRF46WT&EQ1VC1}A zJa-Em%^P|lOubR=Tm(`s*)FIRTL6@D9|guTQ&Kh8L1ylU)Y!{~M=I1>TioJw;0gb9RC)CM?sNBpgcg`aFU;<{!n@$ zEUu}Bqk(nrYgTbA&qloW6mZq(SLog+U!utq=m)biLrAY*4tNKi0AfFDAPraGBfY&@#8JD2PzXRum zxC1QVfw6A?f5(M9{HdYLQPC@XMh|?rRYyTbLoV<#qdfNUQoY9A*oifS1?Wx##em4i z7@!U@A3nF1qPPyogBdsP{;}x|2<~7<->7^z&aS=Lhr_iSiPYKN(Td>WQsb>q$Nr^!zwze18U4=EQ#v=)nQ6HKb|G`=bCj~ zbr9vgS^Ycw_Z!A^nS(#8=uSeRYiibGh>a#!`X@)-V*;v?l||xDU7)67fa1uB=L0u7 zutPLE@WH$t_;9ztjW!3=DG6p~dKOtB>y#RtgK^I+=VD&NT*UJ|MRkyK55J9-ObxHEqK-~fz(_|ZI8<_U=&{ije7C?rl^+=*aZ z*>`-+-2luKAF8~nF58}=(zRRu;ww+jqc`wu7eAU}HDa?_ZMh6_HXCgUSR4_}Wismv zDqP~bNiShC$k5!QjigbTbNcSCE3uw%C24o${kx^{E;*e4!>Iq~l~4>$Fcu`5M>-kU zXtE8&xX13m%ln+*#n3EW;JZHOPabA28vK!=vo-&&>kNFnZUQS=v0nFz4vDwQ9asW{ zUEl`bZOb>ldhu8j5eO0yT|mUvN4C~;+FKeXwH3xN7_cDYDFWoIcp!Mk*X7;nkI}#~ zXQ~tUVdlYjePoc(fAOPGO-GC{Ga26hBc2itQ)k+cgbf!Jqyj z=RaL-we0-ww)y`w%qOGt={uE3p>m~WLUP_Bsy>yA60pU4#9+UjDCqMLzP$`!K!Fnx z5dD7}Ev>Wl?Lgaawwc_F$pLhpZX3OmV-Zl-k-yVDR#8qO@$bsa;pKfmp4VzsJ~Ko{ zB6gDeTzxpJKyR71=b0xr?kx@@-S=3dH~*5cB;eMMy)QuHKRz6V%9C*8QRsTGzotW^ zMe_t&qs;|bO34raV-$zKFzo@o{e#OuCEoR{@NeLHgsyvxV>5Xgng_VoESj9iz-%x5 zFJG?!@{!^EljSt)pZb1lL1!Jo_0p-~K$EoZxg_zqm;I3ZDq2j??EtB7OL4Gr^B%j3{tq1#FyTtBcbI$@S0i%J>q)z5`8#SO4Vv@){DOpk!Ai}2do%#^c40)0@#eXr;j9=jCV1@$&L*-H0#Av^90k z0yk!}4T5}cTxs`7w#a2Ev`@dsBl!igW=^TB?S?TynSh`Pga5uBSPWez@)H;JlMCC{ zj@Y$PFVS!jcumRrTv(is&4rcfr3a=Z(J8rTCC|eB$BpG{CV+eaqoYHJe^DWgjtP($ zs6VoGmZsiN6X$8sYvpR8@&%~DdD4xUaQEL?N?B?A5c)ZvbhO#{{oq~3F3@WW!h-`i zL+FtR1CK5TOqU3MEKD_}rT4&>WkAJ0*k|=;a+~(8-JQoytz9pYWvqxlml?NZx)u>( z00NSE!(+mS42APNw(}!}h3UnWPJFm_l~&Etovzy(i53EBVm}7H`5Tls8YTZx%Sg>b z{MpWu8RyNuO~d1pcl=v8Uv8?jXFYbBaAHB08{af^+c_apkob&S(iF!5-vK|^!~E!87rQ^VMpym)MD!zFxfJQ;0g}6Xsn?x4#%Yj)!Og8lQ(K#ocf`HoouS0Y2Jb zi1{|M?^P0EB=touXe%6teD&$TD^Zo=UB?%dd!wgm;nXA&lU@TG4sOx@vm+jqz%GRx aJ)rrs|0co`R=q|6{@%-~$Uvn&2mL>x2ohZY literal 0 HcmV?d00001 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 000000000..1aeae47b4 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,79 @@ +############# +API reference +############# + +This page provides an auto-generated summary of PySCIPOpt's API. + +SCIP Model +========== + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.Model + +SCIP Constraint +=============== + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.Constraint + +SCIP Variable +============= + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.Variable + +SCIP Row +======== + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.scip.Row + +SCIP Column +=========== + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.scip.Column + +SCIP Node +========= + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.scip.Node + +SCIP Solution +============= + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.scip.Solution + +SCIP Event +=========== + +.. autosummary:: + :toctree: _autosummary + :recursive: + + pyscipopt.scip.Event + + diff --git a/docs/build.rst b/docs/build.rst new file mode 100644 index 000000000..9a18b51e1 --- /dev/null +++ b/docs/build.rst @@ -0,0 +1,186 @@ +##################### +Building From Source +##################### + +When building PySCIPOpt from source, one must have their own installation of `SCIP `_. +To download SCIP please either use the pre-built SCIP Optimization Suite available +`here `__ (recommended) or download SCIP and build from source itself from +`here `__. A more minimal and experimental pre-built SCIP is available +`here `__. + +.. contents:: Contents + +.. note:: The latest PySCIPOpt version is usually only compatible with the latest major release of the + SCIP Optimization Suite. The following table summarizes which version of PySCIPOpt is required for a + given SCIP version: + + .. list-table:: Supported SCIP versions for each PySCIPOpt version + :widths: 25 25 + :align: center + :header-rows: 1 + + * - SCIP + - PySCIPOpt + * - 9.1 + - 5.1+ + * - 9.0 + - 5.0.x + * - 8.0 + - 4.x + * - 7.0 + - 3.x + * - 6.0 + - 2.x + * - 5.0 + - 1.4, 1.3 + * - 4.0 + - 1.2, 1.1 + * - 3.2 + - 1.0 + +.. note:: If you install SCIP yourself and are not using the pre-built packages, + you need to install the SCIP Optimization Suite using CMake. + The Makefile system is not compatible with PySCIPOpt! + +Download Source Code +====================== + +To download the source code for PySCIPOpt we recommend cloning the repository using Git. The two methods +for doing so are via SSH (recommended) and HTTPS. + +.. code-block:: bash + + git clone git@github.com:scipopt/PySCIPOpt.git + +.. code-block:: bash + + git clone https://github.com/scipopt/PySCIPOpt.git + +One can also download the repository itself from GitHub `here `__. + +Requirements +============== + +When building from source you must have the packages ``setuptools`` and ``Cython`` installed in your Python +environment. These can be installed via PyPI: + +.. code-block:: bash + + pip install setuptools + pip install Cython + +.. note:: Since the introduction of Cython 3 we recommend building using ``Cython>=3``. + +Furthermore, you must have the Python development files installed on your system. +Not having these files will produce an error similar to: ``(error message "Python.h not found")``. +To install these development files on Linux use the following command (change according to your distributions +package manager): + +.. code-block:: bash + + sudo apt-get install python3-dev # Linux + +.. note:: For other operating systems this may not be necessary as it comes with many Python installations. + + +Environment Variables +======================== + +When installing PySCIPOpt from source, Python must be able to find your installation of SCIP. +If SCIP is installed globally then this is not an issue, although we still encourage users to explicitly use +such an environment variable. If SCIP is not installed globally, then the user must set the appropriate +environment variable that points to the installation location of SCIP. The environment variable that must +be set is ``SCIPOPTDIR``. + +For Linux and MacOS systems set the variable with the following command: + +.. code-block:: bash + + export SCIPOPTDIR= + +For Windows use the following command: + +.. code-block:: bash + + set SCIPOPTDIR= # This is done for command line interfaces (cmd, Cmder, WSL) + $Env:SCIPOPTDIR = "" # This is done for command line interfaces (powershell) + +``SCIPOPTDIR`` should be a directory. It needs to have a subdirectory lib that contains the +library, e.g. libscip.so (for Linux) and a subdirectory include that contains the corresponding header files: + +.. code-block:: RST + + SCIPOPTDIR + > lib + > libscip.so ... + > include + > scip + > lpi + > ... + +.. note:: It is always recommended to use virtual environments for Python, see `here `_. + + A virtual environment allows one to have multiple environments with different packages installed in each. + To install a virtual environment simply run the command: + + .. code-block:: + + python -m venv + + +Build Instructions +=================== + +After setting up the environment variables ``SCIPOPTDIR`` (see above) and installing all requirements +(see above), you can now install PySCIPOpt from source. To do so run the following command from the +main directory of PySCIPOpt (one with ``setup.py``, ``pyproject.toml`` and ``README.md``): + +.. code-block:: bash + + # Set environment variable SCIPOPTDIR if not yet done + python -m pip install . + +For recompiling the source in the current directory use the command: + +.. code-block:: bash + + python -m pip install --compile . + +.. note:: Building PySCIPOpt from source can be slow. This is normal. + + If you want to build it quickly and unoptimised, which will affect performance + (highly discouraged if running any meaningful time dependent experiments), + you can set the environment variable ``export CFLAGS="-O0 -ggdb"`` (Linux example command) + +Build with Debug +================== +To use debug information in PySCIPOpt you need to build it with the following command: + +.. code-block:: + + python -m pip install --install-option="--debug" . + +.. note:: Be aware that you will need the debug library of the SCIP Optimization Suite for this to work + (cmake .. -DCMAKE_BUILD_TYPE=Debug). + +Testing the Installation +========================== + +To test your brand-new installation of PySCIPOpt you need `pytest `_ +on your system. To get pytest simply run the command: + +.. code-block:: bash + + pip install pytest + +Tests can be run in the PySCIPOpt directory with the commands: + +.. code-block:: bash + + pytest # Will run all the available tests + pytest tests/test_name.py # Will run a specific tests/test_name.py (Unix) + +Ideally, the status of your tests must be passed or skipped. +Running tests with pytest creates the __pycache__ directory in tests and, occasionally, +a model file in the working directory. They can be removed harmlessly. + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..0e3d665ab --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,90 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +import pyscipopt + +# sys.path.insert(0, os.path.abspath('.')) + + +sys.path.insert(0, os.path.abspath("../src/")) +# -- Project information ----------------------------------------------------- + +project = "PySCIPOpt" +copyright = "2024, Zuse Institute Berlin" +author = "Zuse Institute Berlin " +html_logo = "_static/skippy_logo_blue.png" +html_favicon = '_static/skippy_logo_blue.png' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "sphinx.ext.autosectionlabel", + "sphinxcontrib.bibtex", + "sphinxcontrib.jquery", + "sphinx.ext.mathjax", + "sphinx.ext.intersphinx", + "sphinx.ext.extlinks", +] + +# You can define documentation here that will be repeated often +# e.g. XXX = """WARNING: This method will only work if your model has status=='optimal'""" +# rst_epilog = f""" .. |XXX| replace:: {XXX}""" +# Then in the documentation of a method you can put |XXX| + +autosectionlabel_prefix_document = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The intersphinx mapping dictionary is used to automatically reference +# python object types to their respective documentation. +# As PySCIPOpt has few dependencies this is not done. + +intersphinx_mapping = {} + +extlinks = { + "pypi": ("https://pypi.org/project/%s/", "%s"), +} + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_nefertiti" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +autosummary_generate = True +autoclass_content = "class" + +pygments_style = "sphinx" + +bibtex_bibfiles = ["ref.bib"] diff --git a/docs/customdoxygen.css b/docs/customdoxygen.css deleted file mode 100644 index 1425ec530..000000000 --- a/docs/customdoxygen.css +++ /dev/null @@ -1,1475 +0,0 @@ -/* The standard CSS for doxygen 1.8.11 */ - -body, table, div, p, dl { - font: 400 14px/22px Roboto,sans-serif; -} - -/* @group Heading Levels */ - -h1.groupheader { - font-size: 150%; -} - -.title { - font: 400 14px/28px Roboto,sans-serif; - font-size: 150%; - font-weight: bold; - margin: 10px 2px; -} - -h2.groupheader { - border-bottom: 1px solid #879ECB; - color: #354C7B; - font-size: 150%; - font-weight: normal; - margin-top: 1.75em; - padding-top: 8px; - padding-bottom: 4px; - width: 100%; -} - -h3.groupheader { - font-size: 100%; -} - -h1, h2, h3, h4, h5, h6 { - -webkit-transition: text-shadow 0.5s linear; - -moz-transition: text-shadow 0.5s linear; - -ms-transition: text-shadow 0.5s linear; - -o-transition: text-shadow 0.5s linear; - transition: text-shadow 0.5s linear; - margin-right: 15px; -} - -h1.glow, h2.glow, h3.glow, h4.glow, h5.glow, h6.glow { - text-shadow: 0 0 15px cyan; -} - -dt { - font-weight: bold; -} - -div.multicol { - -moz-column-gap: 1em; - -webkit-column-gap: 1em; - -moz-column-count: 3; - -webkit-column-count: 3; -} - -p.startli, p.startdd { - margin-top: 2px; -} - -p.starttd { - margin-top: 0px; -} - -p.endli { - margin-bottom: 0px; -} - -p.enddd { - margin-bottom: 4px; -} - -p.endtd { - margin-bottom: 2px; -} - -/* @end */ - -caption { - font-weight: bold; -} - -span.legend { - font-size: 70%; - text-align: center; -} - -h3.version { - font-size: 90%; - text-align: center; -} - -div.qindex, div.navtab{ - background-color: #EBEFF6; - border: 1px solid #A3B4D7; - text-align: center; -} - -div.qindex, div.navpath { - width: 100%; - line-height: 140%; -} - -div.navtab { - margin-right: 15px; -} - -/* @group Link Styling */ - -a { - color: #3D578C; - font-weight: normal; - text-decoration: none; -} - -.contents a:visited { - color: #4665A2; -} - -a:hover { - text-decoration: underline; -} - -a.qindex { - font-weight: bold; -} - -a.qindexHL { - font-weight: bold; - background-color: #9CAFD4; - color: #ffffff; - border: 1px double #869DCA; -} - -.contents a.qindexHL:visited { - color: #ffffff; -} - -a.el { - font-weight: bold; -} - -a.elRef { -} - -a.code, a.code:visited, a.line, a.line:visited { - color: #4665A2; -} - -a.codeRef, a.codeRef:visited, a.lineRef, a.lineRef:visited { - color: #4665A2; -} - -/* @end */ - -dl.el { - margin-left: -1cm; -} - -pre.fragment { - border: 1px solid #C4CFE5; - background-color: #FBFCFD; - padding: 4px 6px; - margin: 4px 8px 4px 2px; - overflow: auto; - word-wrap: break-word; - font-size: 9pt; - line-height: 125%; - font-family: monospace, fixed; - font-size: 105%; -} - -div.fragment { - padding: 4px 6px; - margin: 4px 8px 4px 2px; - background-color: #FBFCFD; - border: 1px solid #C4CFE5; -} - -div.line { - font-family: monospace, fixed; - font-size: 13px; - min-height: 13px; - line-height: 1.0; - text-wrap: unrestricted; - white-space: -moz-pre-wrap; /* Moz */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - white-space: pre-wrap; /* CSS3 */ - word-wrap: break-word; /* IE 5.5+ */ - text-indent: -53px; - padding-left: 53px; - padding-bottom: 0px; - margin: 0px; - -webkit-transition-property: background-color, box-shadow; - -webkit-transition-duration: 0.5s; - -moz-transition-property: background-color, box-shadow; - -moz-transition-duration: 0.5s; - -ms-transition-property: background-color, box-shadow; - -ms-transition-duration: 0.5s; - -o-transition-property: background-color, box-shadow; - -o-transition-duration: 0.5s; - transition-property: background-color, box-shadow; - transition-duration: 0.5s; -} - -div.line:after { - content:"\000A"; - white-space: pre; -} - -div.line.glow { - background-color: cyan; - box-shadow: 0 0 10px cyan; -} - - -span.lineno { - padding-right: 4px; - text-align: right; - border-right: 2px solid #0F0; - background-color: #E8E8E8; - white-space: pre; -} -span.lineno a { - background-color: #D8D8D8; -} - -span.lineno a:hover { - background-color: #C8C8C8; -} - -div.ah, span.ah { - background-color: black; - font-weight: bold; - color: #ffffff; - margin-bottom: 3px; - margin-top: 3px; - padding: 0.2em; - border: solid thin #333; - border-radius: 0.5em; - -webkit-border-radius: .5em; - -moz-border-radius: .5em; - box-shadow: 2px 2px 3px #999; - -webkit-box-shadow: 2px 2px 3px #999; - -moz-box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 2px; - background-image: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#000),color-stop(0.3, #444)); - background-image: -moz-linear-gradient(center top, #eee 0%, #444 40%, #000 110%); -} - -div.classindex ul { - list-style: none; - padding-left: 0; -} - -div.classindex span.ai { - display: inline-block; -} - -div.groupHeader { - margin-left: 16px; - margin-top: 12px; - font-weight: bold; -} - -div.groupText { - margin-left: 16px; - font-style: italic; -} - -body { - background-color: white; - color: black; - margin: 0; -} - -div.contents { - margin-top: 10px; - margin-left: 12px; - margin-right: 8px; -} - -td.indexkey { - background-color: #EBEFF6; - font-weight: bold; - border: 1px solid #C4CFE5; - margin: 2px 0px 2px 0; - padding: 2px 10px; - white-space: nowrap; - vertical-align: top; -} - -td.indexvalue { - background-color: #EBEFF6; - border: 1px solid #C4CFE5; - padding: 2px 10px; - margin: 2px 0px; -} - -tr.memlist { - background-color: #EEF1F7; -} - -p.formulaDsp { - text-align: center; -} - -img.formulaDsp { - -} - -img.formulaInl { - vertical-align: middle; -} - -div.center { - text-align: center; - margin-top: 0px; - margin-bottom: 0px; - padding: 0px; -} - -div.center img { - border: 0px; -} - -address.footer { - text-align: right; - padding-right: 12px; -} - -img.footer { - border: 0px; - vertical-align: middle; -} - -/* @group Code Colorization */ - -span.keyword { - color: #008000 -} - -span.keywordtype { - color: #604020 -} - -span.keywordflow { - color: #e08000 -} - -span.comment { - color: #800000 -} - -span.preprocessor { - color: #806020 -} - -span.stringliteral { - color: #002080 -} - -span.charliteral { - color: #008080 -} - -span.vhdldigit { - color: #ff00ff -} - -span.vhdlchar { - color: #000000 -} - -span.vhdlkeyword { - color: #700070 -} - -span.vhdllogic { - color: #ff0000 -} - -blockquote { - background-color: #F7F8FB; - border-left: 2px solid #9CAFD4; - margin: 0 24px 0 4px; - padding: 0 12px 0 16px; -} - -/* @end */ - -/* -.search { - color: #003399; - font-weight: bold; -} - -form.search { - margin-bottom: 0px; - margin-top: 0px; -} - -input.search { - font-size: 75%; - color: #000080; - font-weight: normal; - background-color: #e8eef2; -} -*/ - -td.tiny { - font-size: 75%; -} - -.dirtab { - padding: 4px; - border-collapse: collapse; - border: 1px solid #A3B4D7; -} - -th.dirtab { - background: #EBEFF6; - font-weight: bold; -} - -hr { - height: 0px; - border: none; - border-top: 1px solid #4A6AAA; -} - -hr.footer { - height: 1px; -} - -/* @group Member Descriptions */ - -table.memberdecls { - border-spacing: 0px; - padding: 0px; -} - -.memberdecls td, .fieldtable tr { - -webkit-transition-property: background-color, box-shadow; - -webkit-transition-duration: 0.5s; - -moz-transition-property: background-color, box-shadow; - -moz-transition-duration: 0.5s; - -ms-transition-property: background-color, box-shadow; - -ms-transition-duration: 0.5s; - -o-transition-property: background-color, box-shadow; - -o-transition-duration: 0.5s; - transition-property: background-color, box-shadow; - transition-duration: 0.5s; -} - -.memberdecls td.glow, .fieldtable tr.glow { - background-color: cyan; - box-shadow: 0 0 15px cyan; -} - -.mdescLeft, .mdescRight, -.memItemLeft, .memItemRight, -.memTemplItemLeft, .memTemplItemRight, .memTemplParams { - background-color: #F9FAFC; - border: none; - margin: 4px; - padding: 1px 0 0 8px; -} - -.mdescLeft, .mdescRight { - padding: 0px 8px 4px 8px; - color: #555; -} - -.memSeparator { - border-bottom: 1px solid #DEE4F0; - line-height: 1px; - margin: 0px; - padding: 0px; -} - -.memItemLeft, .memTemplItemLeft { - white-space: nowrap; -} - -.memItemRight { - width: 100%; -} - -.memTemplParams { - color: #4665A2; - white-space: nowrap; - font-size: 80%; -} - -/* @end */ - -/* @group Member Details */ - -/* Styles for detailed member documentation */ - -.memtemplate { - font-size: 80%; - color: #4665A2; - font-weight: normal; - margin-left: 9px; -} - -.memnav { - background-color: #EBEFF6; - border: 1px solid #A3B4D7; - text-align: center; - margin: 2px; - margin-right: 15px; - padding: 2px; -} - -.mempage { - width: 100%; -} - -.memitem { - padding: 0; - margin-bottom: 10px; - margin-right: 5px; - -webkit-transition: box-shadow 0.5s linear; - -moz-transition: box-shadow 0.5s linear; - -ms-transition: box-shadow 0.5s linear; - -o-transition: box-shadow 0.5s linear; - transition: box-shadow 0.5s linear; - display: table !important; - width: 100%; -} - -.memitem.glow { - box-shadow: 0 0 15px cyan; -} - -.memname { - font-weight: bold; - margin-left: 6px; -} - -.memname td { - vertical-align: bottom; -} - -.memproto, dl.reflist dt { - border-top: 1px solid #A8B8D9; - border-left: 1px solid #A8B8D9; - border-right: 1px solid #A8B8D9; - padding: 6px 0px 6px 0px; - color: #253555; - font-weight: bold; - text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); - background-image:url('nav_f.png'); - background-repeat:repeat-x; - background-color: #E2E8F2; - /* opera specific markup */ - box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); - border-top-right-radius: 4px; - border-top-left-radius: 4px; - /* firefox specific markup */ - -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; - -moz-border-radius-topright: 4px; - -moz-border-radius-topleft: 4px; - /* webkit specific markup */ - -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); - -webkit-border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; - -} - -.memdoc, dl.reflist dd { - border-bottom: 1px solid #A8B8D9; - border-left: 1px solid #A8B8D9; - border-right: 1px solid #A8B8D9; - padding: 6px 10px 2px 10px; - background-color: #FBFCFD; - border-top-width: 0; - background-image:url('nav_g.png'); - background-repeat:repeat-x; - background-color: #FFFFFF; - /* opera specific markup */ - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); - /* firefox specific markup */ - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-bottomright: 4px; - -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; - /* webkit specific markup */ - -webkit-border-bottom-left-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); -} - -dl.reflist dt { - padding: 5px; -} - -dl.reflist dd { - margin: 0px 0px 10px 0px; - padding: 5px; -} - -.paramkey { - text-align: right; -} - -.paramtype { - white-space: nowrap; -} - -.paramname { - color: #602020; - white-space: nowrap; -} -.paramname em { - font-style: normal; -} -.paramname code { - line-height: 14px; -} - -.params, .retval, .exception, .tparams { - margin-left: 0px; - padding-left: 0px; -} - -.params .paramname, .retval .paramname { - font-weight: bold; - vertical-align: top; -} - -.params .paramtype { - font-style: italic; - vertical-align: top; -} - -.params .paramdir { - font-family: "courier new",courier,monospace; - vertical-align: top; -} - -table.mlabels { - border-spacing: 0px; -} - -td.mlabels-left { - width: 100%; - padding: 0px; -} - -td.mlabels-right { - vertical-align: bottom; - padding: 0px; - white-space: nowrap; -} - -span.mlabels { - margin-left: 8px; -} - -span.mlabel { - background-color: #728DC1; - border-top:1px solid #5373B4; - border-left:1px solid #5373B4; - border-right:1px solid #C4CFE5; - border-bottom:1px solid #C4CFE5; - text-shadow: none; - color: white; - margin-right: 4px; - padding: 2px 3px; - border-radius: 3px; - font-size: 7pt; - white-space: nowrap; - vertical-align: middle; -} - - - -/* @end */ - -/* these are for tree view inside a (index) page */ - -div.directory { - margin: 10px 0px; - border-top: 1px solid #9CAFD4; - border-bottom: 1px solid #9CAFD4; - width: 100%; -} - -.directory table { - border-collapse:collapse; -} - -.directory td { - margin: 0px; - padding: 0px; - vertical-align: top; -} - -.directory td.entry { - white-space: nowrap; - padding-right: 6px; - padding-top: 3px; -} - -.directory td.entry a { - outline:none; -} - -.directory td.entry a img { - border: none; -} - -.directory td.desc { - width: 100%; - padding-left: 6px; - padding-right: 6px; - padding-top: 3px; - border-left: 1px solid rgba(0,0,0,0.05); -} - -.directory tr.even { - padding-left: 6px; - background-color: #F7F8FB; -} - -.directory img { - vertical-align: -30%; -} - -.directory .levels { - white-space: nowrap; - width: 100%; - text-align: right; - font-size: 9pt; -} - -.directory .levels span { - cursor: pointer; - padding-left: 2px; - padding-right: 2px; - color: #3D578C; -} - -.arrow { - color: #9CAFD4; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: pointer; - font-size: 80%; - display: inline-block; - width: 16px; - height: 22px; -} - -.icon { - font-family: Arial, Helvetica; - font-weight: bold; - font-size: 12px; - height: 14px; - width: 16px; - display: inline-block; - background-color: #728DC1; - color: white; - text-align: center; - border-radius: 4px; - margin-left: 2px; - margin-right: 2px; -} - -.icona { - width: 24px; - height: 22px; - display: inline-block; -} - -.iconfopen { - width: 24px; - height: 18px; - margin-bottom: 4px; - background-image:url('folderopen.png'); - background-position: 0px -4px; - background-repeat: repeat-y; - vertical-align:top; - display: inline-block; -} - -.iconfclosed { - width: 24px; - height: 18px; - margin-bottom: 4px; - background-image:url('folderclosed.png'); - background-position: 0px -4px; - background-repeat: repeat-y; - vertical-align:top; - display: inline-block; -} - -.icondoc { - width: 24px; - height: 18px; - margin-bottom: 4px; - background-image:url('doc.png'); - background-position: 0px -4px; - background-repeat: repeat-y; - vertical-align:top; - display: inline-block; -} - -table.directory { - font: 400 14px Roboto,sans-serif; -} - -/* @end */ - -div.dynheader { - margin-top: 8px; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -address { - font-style: normal; - color: #2A3D61; -} - -table.doxtable caption { - caption-side: top; -} - -table.doxtable { - border-collapse:collapse; - margin-top: 4px; - margin-bottom: 4px; -} - -table.doxtable td, table.doxtable th { - border: 1px solid #2D4068; - padding: 3px 7px 2px; -} - -table.doxtable th { - background-color: #374F7F; - color: #FFFFFF; - font-size: 110%; - padding-bottom: 4px; - padding-top: 5px; -} - -table.fieldtable { - /*width: 100%;*/ - margin-bottom: 10px; - border: 1px solid #A8B8D9; - border-spacing: 0px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - -moz-box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 2px; - -webkit-box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); - box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); -} - -.fieldtable td, .fieldtable th { - padding: 3px 7px 2px; -} - -.fieldtable td.fieldtype, .fieldtable td.fieldname { - white-space: nowrap; - border-right: 1px solid #A8B8D9; - border-bottom: 1px solid #A8B8D9; - vertical-align: top; -} - -.fieldtable td.fieldname { - padding-top: 3px; -} - -.fieldtable td.fielddoc { - border-bottom: 1px solid #A8B8D9; - /*width: 100%;*/ -} - -.fieldtable td.fielddoc p:first-child { - margin-top: 0px; -} - -.fieldtable td.fielddoc p:last-child { - margin-bottom: 2px; -} - -.fieldtable tr:last-child td { - border-bottom: none; -} - -.fieldtable th { - background-image:url('nav_f.png'); - background-repeat:repeat-x; - background-color: #E2E8F2; - font-size: 90%; - color: #253555; - padding-bottom: 4px; - padding-top: 5px; - text-align:left; - -moz-border-radius-topleft: 4px; - -moz-border-radius-topright: 4px; - -webkit-border-top-left-radius: 4px; - -webkit-border-top-right-radius: 4px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom: 1px solid #A8B8D9; -} - - -.tabsearch { - top: 0px; - left: 10px; - height: 36px; - background-image: url('tab_b.png'); - z-index: 101; - overflow: hidden; - font-size: 13px; -} - -.navpath ul -{ - font-size: 11px; - background-image:url('tab_b.png'); - background-repeat:repeat-x; - background-position: 0 -5px; - height:30px; - line-height:30px; - color:#8AA0CC; - border:solid 1px #C2CDE4; - overflow:hidden; - margin:0px; - padding:0px; -} - -.navpath li -{ - list-style-type:none; - float:left; - padding-left:10px; - padding-right:15px; - background-image:url('bc_s.png'); - background-repeat:no-repeat; - background-position:right; - color:#364D7C; -} - -.navpath li.navelem a -{ - height:32px; - display:block; - text-decoration: none; - outline: none; - color: #283A5D; - font-family: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; - text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); - text-decoration: none; -} - -.navpath li.navelem a:hover -{ - color:#6884BD; -} - -.navpath li.footer -{ - list-style-type:none; - float:right; - padding-left:10px; - padding-right:15px; - background-image:none; - background-repeat:no-repeat; - background-position:right; - color:#364D7C; - font-size: 8pt; -} - - -div.summary -{ - float: right; - font-size: 8pt; - padding-right: 5px; - width: 50%; - text-align: right; -} - -div.summary a -{ - white-space: nowrap; -} - -table.classindex -{ - margin: 10px; - white-space: nowrap; - margin-left: 3%; - margin-right: 3%; - width: 94%; - border: 0; - border-spacing: 0; - padding: 0; -} - -div.ingroups -{ - font-size: 8pt; - width: 50%; - text-align: left; -} - -div.ingroups a -{ - white-space: nowrap; -} - -div.header -{ - background-image:url('nav_h.png'); - background-repeat:repeat-x; - background-color: #F9FAFC; - margin: 0px; - border-bottom: 1px solid #C4CFE5; -} - -div.headertitle -{ - padding: 5px 5px 5px 10px; -} - -dl -{ - padding: 0 0 0 10px; -} - -/* dl.note, dl.warning, dl.attention, dl.pre, dl.post, dl.invariant, dl.deprecated, dl.todo, dl.test, dl.bug */ -dl.section -{ - margin-left: 0px; - padding-left: 0px; -} - -dl.note -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #D0C000; -} - -dl.warning, dl.attention -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #FF0000; -} - -dl.pre, dl.post, dl.invariant -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #00D000; -} - -dl.deprecated -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #505050; -} - -dl.todo -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #00C0E0; -} - -dl.test -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #3030E0; -} - -dl.bug -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #C08050; -} - -dl.section dd { - margin-bottom: 6px; -} - - -#projectlogo -{ - text-align: center; - vertical-align: bottom; - border-collapse: separate; -} - -#projectlogo img -{ - border: 0px none; -} - -#projectalign -{ - vertical-align: middle; -} - -#projectname -{ - font: 300% Tahoma, Arial,sans-serif; - margin: 0px; - padding: 2px 0px; -} - -#projectbrief -{ - font: 120% Tahoma, Arial,sans-serif; - margin: 0px; - padding: 0px; -} - -#projectnumber -{ - font: 50% Tahoma, Arial,sans-serif; - margin: 0px; - padding: 0px; -} - -#titlearea -{ - padding: 0px; - margin: 0px; - width: 100%; - border-bottom: 1px solid #5373B4; -} - -.image -{ - text-align: center; -} - -.dotgraph -{ - text-align: center; -} - -.mscgraph -{ - text-align: center; -} - -.diagraph -{ - text-align: center; -} - -.caption -{ - font-weight: bold; -} - -div.zoom -{ - border: 1px solid #90A5CE; -} - -dl.citelist { - margin-bottom:50px; -} - -dl.citelist dt { - color:#334975; - float:left; - font-weight:bold; - margin-right:10px; - padding:5px; -} - -dl.citelist dd { - margin:2px 0; - padding:5px 0; -} - -div.toc { - padding: 14px 25px; - background-color: #F4F6FA; - border: 1px solid #D8DFEE; - border-radius: 7px 7px 7px 7px; - float: right; - height: auto; - margin: 0 8px 10px 10px; - width: 200px; -} - -div.toc li { - background: url("bdwn.png") no-repeat scroll 0 5px transparent; - font: 10px/1.2 Verdana,DejaVu Sans,Geneva,sans-serif; - margin-top: 5px; - padding-left: 10px; - padding-top: 2px; -} - -div.toc h3 { - font: bold 12px/1.2 Arial,FreeSans,sans-serif; - color: #4665A2; - border-bottom: 0 none; - margin: 0; -} - -div.toc ul { - list-style: none outside none; - border: medium none; - padding: 0px; -} - -div.toc li.level1 { - margin-left: 0px; -} - -div.toc li.level2 { - margin-left: 15px; -} - -div.toc li.level3 { - margin-left: 30px; -} - -div.toc li.level4 { - margin-left: 45px; -} - -.inherit_header { - font-weight: bold; - color: gray; - cursor: pointer; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.inherit_header td { - padding: 6px 0px 2px 5px; -} - -.inherit { - display: none; -} - -tr.heading h2 { - margin-top: 12px; - margin-bottom: 4px; -} - -/* tooltip related style info */ - -.ttc { - position: absolute; - display: none; -} - -#powerTip { - cursor: default; - white-space: nowrap; - background-color: white; - border: 1px solid gray; - border-radius: 4px 4px 4px 4px; - box-shadow: 1px 1px 7px gray; - display: none; - font-size: smaller; - max-width: 80%; - opacity: 0.9; - padding: 1ex 1em 1em; - position: absolute; - z-index: 2147483647; -} - -#powerTip div.ttdoc { - color: grey; - font-style: italic; -} - -#powerTip div.ttname a { - font-weight: bold; -} - -#powerTip div.ttname { - font-weight: bold; -} - -#powerTip div.ttdeci { - color: #006318; -} - -#powerTip div { - margin: 0px; - padding: 0px; - font: 12px/16px Roboto,sans-serif; -} - -#powerTip:before, #powerTip:after { - content: ""; - position: absolute; - margin: 0px; -} - -#powerTip.n:after, #powerTip.n:before, -#powerTip.s:after, #powerTip.s:before, -#powerTip.w:after, #powerTip.w:before, -#powerTip.e:after, #powerTip.e:before, -#powerTip.ne:after, #powerTip.ne:before, -#powerTip.se:after, #powerTip.se:before, -#powerTip.nw:after, #powerTip.nw:before, -#powerTip.sw:after, #powerTip.sw:before { - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; -} - -#powerTip.n:after, #powerTip.s:after, -#powerTip.w:after, #powerTip.e:after, -#powerTip.nw:after, #powerTip.ne:after, -#powerTip.sw:after, #powerTip.se:after { - border-color: rgba(255, 255, 255, 0); -} - -#powerTip.n:before, #powerTip.s:before, -#powerTip.w:before, #powerTip.e:before, -#powerTip.nw:before, #powerTip.ne:before, -#powerTip.sw:before, #powerTip.se:before { - border-color: rgba(128, 128, 128, 0); -} - -#powerTip.n:after, #powerTip.n:before, -#powerTip.ne:after, #powerTip.ne:before, -#powerTip.nw:after, #powerTip.nw:before { - top: 100%; -} - -#powerTip.n:after, #powerTip.ne:after, #powerTip.nw:after { - border-top-color: #ffffff; - border-width: 10px; - margin: 0px -10px; -} -#powerTip.n:before { - border-top-color: #808080; - border-width: 11px; - margin: 0px -11px; -} -#powerTip.n:after, #powerTip.n:before { - left: 50%; -} - -#powerTip.nw:after, #powerTip.nw:before { - right: 14px; -} - -#powerTip.ne:after, #powerTip.ne:before { - left: 14px; -} - -#powerTip.s:after, #powerTip.s:before, -#powerTip.se:after, #powerTip.se:before, -#powerTip.sw:after, #powerTip.sw:before { - bottom: 100%; -} - -#powerTip.s:after, #powerTip.se:after, #powerTip.sw:after { - border-bottom-color: #ffffff; - border-width: 10px; - margin: 0px -10px; -} - -#powerTip.s:before, #powerTip.se:before, #powerTip.sw:before { - border-bottom-color: #808080; - border-width: 11px; - margin: 0px -11px; -} - -#powerTip.s:after, #powerTip.s:before { - left: 50%; -} - -#powerTip.sw:after, #powerTip.sw:before { - right: 14px; -} - -#powerTip.se:after, #powerTip.se:before { - left: 14px; -} - -#powerTip.e:after, #powerTip.e:before { - left: 100%; -} -#powerTip.e:after { - border-left-color: #ffffff; - border-width: 10px; - top: 50%; - margin-top: -10px; -} -#powerTip.e:before { - border-left-color: #808080; - border-width: 11px; - top: 50%; - margin-top: -11px; -} - -#powerTip.w:after, #powerTip.w:before { - right: 100%; -} -#powerTip.w:after { - border-right-color: #ffffff; - border-width: 10px; - top: 50%; - margin-top: -10px; -} -#powerTip.w:before { - border-right-color: #808080; - border-width: 11px; - top: 50%; - margin-top: -11px; -} - -@media print -{ - #top { display: none; } - #side-nav { display: none; } - #nav-path { display: none; } - body { overflow:visible; } - h1, h2, h3, h4, h5, h6 { page-break-after: avoid; } - .summary { display: none; } - .memitem { page-break-inside: avoid; } - #doc-content - { - margin-left:0 !important; - height:auto !important; - width:auto !important; - overflow:inherit; - display:inline; - } -} - diff --git a/docs/doxy b/docs/doxy deleted file mode 100644 index a4e8f717b..000000000 --- a/docs/doxy +++ /dev/null @@ -1,2617 +0,0 @@ -# Doxyfile 1.9.1 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. -# -# All text after a double hash (##) is considered a comment and is placed in -# front of the TAG it is preceding. -# -# All text after a single hash (#) is considered a comment and will be ignored. -# The format is: -# TAG = value [value, ...] -# For lists, items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (\" \"). - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the configuration -# file that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# https://www.gnu.org/software/libiconv/ for the list of possible encodings. -# The default value is: UTF-8. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by -# double-quotes, unless you are using Doxywizard) that should identify the -# project for which the documentation is generated. This name is used in the -# title of most generated pages and in a few other places. -# The default value is: My Project. - -PROJECT_NAME = PySCIPOpt - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. This -# could be handy for archiving the generated documentation or if some version -# control system is used. - -PROJECT_NUMBER = $(VERSION_NUMBER) - -# Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a -# quick idea about the purpose of the project. Keep the description short. - -PROJECT_BRIEF = "Python Interface for the SCIP Optimization Suite" - -# With the PROJECT_LOGO tag one can specify a logo or an icon that is included -# in the documentation. The maximum height of the logo should not exceed 55 -# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy -# the logo to the output directory. - -PROJECT_LOGO = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path -# into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If -# left blank the current directory will be used. - -OUTPUT_DIRECTORY = docs - -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. -# The default value is: NO. - -CREATE_SUBDIRS = NO - -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII -# characters to appear in the names of generated files. If set to NO, non-ASCII -# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode -# U+3044. -# The default value is: NO. - -ALLOW_UNICODE_NAMES = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. -# The default value is: English. - -OUTPUT_LANGUAGE = English - -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member -# descriptions after the members that are listed in the file and class -# documentation (similar to Javadoc). Set to NO to disable this. -# The default value is: YES. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief -# description of a member or function before the detailed description -# -# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. -# The default value is: YES. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator that is -# used to form the text in various listings. Each string in this list, if found -# as the leading text of the brief description, will be stripped from the text -# and the result, after processing the whole list, is used as the annotated -# text. Otherwise, the brief description is used as-is. If left blank, the -# following values are used ($name is automatically replaced with the name of -# the entity):The $name class, The $name widget, The $name file, is, provides, -# specifies, contains, represents, a, an and the. - -ABBREVIATE_BRIEF = - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief -# description. -# The default value is: NO. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. -# The default value is: NO. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path -# before files name in the file list and in the header files. If set to NO the -# shortest path that makes the file name unique will be used -# The default value is: YES. - -FULL_PATH_NAMES = YES - -# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. -# Stripping is only done if one of the specified strings matches the left-hand -# part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to -# strip. -# -# Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. -# This tag requires that the tag FULL_PATH_NAMES is set to YES. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the -# path mentioned in the documentation of a class, which tells the reader which -# header file to include in order to use a class. If left blank only the name of -# the header file containing the class definition is used. Otherwise one should -# specify the list of include paths that are normally passed to the compiler -# using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't -# support long names like on DOS, Mac, or CD-ROM. -# The default value is: NO. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) -# The default value is: NO. - -JAVADOC_AUTOBRIEF = NO - -# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line -# such as -# /*************** -# as being the beginning of a Javadoc-style comment "banner". If set to NO, the -# Javadoc-style will behave just like regular comments and it will not be -# interpreted by doxygen. -# The default value is: NO. - -JAVADOC_BANNER = NO - -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) -# The default value is: NO. - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a -# multi-line C++ special comment block (i.e. a block of //! or /// comments) as -# a brief description. This used to be the default behavior. The new default is -# to treat a multi-line C++ comment block as a detailed description. Set this -# tag to YES if you prefer the old behavior instead. -# -# Note that setting this tag to YES also means that rational rose comments are -# not recognized any more. -# The default value is: NO. - -MULTILINE_CPP_IS_BRIEF = NO - -# By default Python docstrings are displayed as preformatted text and doxygen's -# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the -# doxygen's special commands can be used and the contents of the docstring -# documentation blocks is shown as doxygen documentation. -# The default value is: YES. - -PYTHON_DOCSTRING = YES - -# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the -# documentation from any documented member that it re-implements. -# The default value is: YES. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new -# page for each member. If set to NO, the documentation of a member will be part -# of the file/class/namespace that contains it. -# The default value is: NO. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen -# uses this value to replace tabs by spaces in code fragments. -# Minimum value: 1, maximum value: 16, default value: 4. - -TAB_SIZE = 4 - -# This tag can be used to specify a number of aliases that act as commands in -# the documentation. An alias has the form: -# name=value -# For example adding -# "sideeffect=@par Side Effects:\n" -# will allow you to put the command \sideeffect (or @sideeffect) in the -# documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) - -ALIASES = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources -# only. Doxygen will then generate output that is more tailored for C. For -# instance, some of the names that are used will be different. The list of all -# members will be omitted, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or -# Python sources only. Doxygen will then generate output that is more tailored -# for that language. For instance, namespaces will be presented as packages, -# qualified scopes will look different, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_JAVA = YES - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources. Doxygen will then generate output that is tailored for Fortran. -# The default value is: NO. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for VHDL. -# The default value is: NO. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice -# sources only. Doxygen will then generate output that is more tailored for that -# language. For instance, namespaces will be presented as modules, types will be -# separated into more groups, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_SLICE = NO - -# Doxygen selects the parser to use depending on the extension of the files it -# parses. With this tag you can assign which parser to use for a given -# extension. Doxygen has a built-in mapping, but you can override or extend it -# using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: -# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser -# tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files). For instance to make doxygen treat .inc files -# as Fortran files (default is PHP), and .f files as C (default is Fortran), -# use: inc=Fortran f=C. -# -# Note: For files without extension you can use no_extension as a placeholder. -# -# Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. When specifying no_extension you should add -# * to the FILE_PATTERNS. -# -# Note see also the list of default file extension mappings. - -EXTENSION_MAPPING = pyx=Python \ - pxd=Python \ - pxi=Python - -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments -# according to the Markdown format, which allows for more readable -# documentation. See https://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in -# case of backward compatibilities issues. -# The default value is: YES. - -MARKDOWN_SUPPORT = YES - -# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up -# to that level are automatically included in the table of contents, even if -# they do not have an id attribute. -# Note: This feature currently applies only to Markdown headings. -# Minimum value: 0, maximum value: 99, default value: 5. -# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. - -TOC_INCLUDE_HEADINGS = 5 - -# When enabled doxygen tries to link words that correspond to documented -# classes, or namespaces to their corresponding documentation. Such a link can -# be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. -# The default value is: YES. - -AUTOLINK_SUPPORT = YES - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. -# The default value is: NO. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. -# The default value is: NO. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. -# The default value is: NO. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate -# getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. -# This will only work if the methods are indeed getting or setting a simple -# type. If this is not the case, or you want to show the methods anyway, you -# should set this option to NO. -# The default value is: YES. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. -# The default value is: NO. - -DISTRIBUTE_GROUP_DOC = NO - -# If one adds a struct or class to a group and this option is enabled, then also -# any nested class or struct is added to the same group. By default this option -# is disabled and one has to add nested compounds explicitly via \ingroup. -# The default value is: NO. - -GROUP_NESTED_COMPOUNDS = NO - -# Set the SUBGROUPING tag to YES to allow class member groups of the same type -# (for instance a group of public functions) to be put as a subgroup of that -# type (e.g. under the Public Functions section). Set it to NO to prevent -# subgrouping. Alternatively, this can be done per class using the -# \nosubgrouping command. -# The default value is: YES. - -SUBGROUPING = YES - -# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions -# are shown inside the group in which they are included (e.g. using \ingroup) -# instead of on a separate page (for HTML and Man pages) or section (for LaTeX -# and RTF). -# -# Note that this feature does not work in combination with -# SEPARATE_MEMBER_PAGES. -# The default value is: NO. - -INLINE_GROUPED_CLASSES = NO - -# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions -# with only public data fields or simple typedef fields will be shown inline in -# the documentation of the scope in which they are defined (i.e. file, -# namespace, or group documentation), provided this scope is documented. If set -# to NO, structs, classes, and unions are shown on a separate page (for HTML and -# Man pages) or section (for LaTeX and RTF). -# The default value is: NO. - -INLINE_SIMPLE_STRUCTS = NO - -# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or -# enum is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically be -# useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. -# The default value is: NO. - -TYPEDEF_HIDES_STRUCT = NO - -# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This -# cache is used to resolve symbols given their name and scope. Since this can be -# an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The -# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range -# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest -# the optimal cache size from a speed point of view. -# Minimum value: 0, maximum value: 9, default value: 0. - -LOOKUP_CACHE_SIZE = 0 - -# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use -# during processing. When set to 0 doxygen will based this on the number of -# cores available in the system. You can set it explicitly to a value larger -# than 0 to get more control over the balance between CPU load and processing -# speed. At this moment only the input processing can be done using multiple -# threads. Since this is still an experimental feature the default is set to 1, -# which efficively disables parallel processing. Please report any issues you -# encounter. Generating dot graphs in parallel is controlled by the -# DOT_NUM_THREADS setting. -# Minimum value: 0, maximum value: 32, default value: 1. - -NUM_PROC_THREADS = 1 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in -# documentation are documented, even if no documentation was available. Private -# class members and static file members will be hidden unless the -# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. -# Note: This will also disable the warnings about undocumented members that are -# normally produced when WARNINGS is set to YES. -# The default value is: NO. - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will -# be included in the documentation. -# The default value is: NO. - -EXTRACT_PRIVATE = NO - -# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual -# methods of a class will be included in the documentation. -# The default value is: NO. - -EXTRACT_PRIV_VIRTUAL = NO - -# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal -# scope will be included in the documentation. -# The default value is: NO. - -EXTRACT_PACKAGE = NO - -# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be -# included in the documentation. -# The default value is: NO. - -EXTRACT_STATIC = NO - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined -# locally in source files will be included in the documentation. If set to NO, -# only classes defined in header files are included. Does not have any effect -# for Java sources. -# The default value is: YES. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. If set to YES, local methods, -# which are defined in the implementation section but not in the interface are -# included in the documentation. If set to NO, only methods in the interface are -# included. -# The default value is: NO. - -EXTRACT_LOCAL_METHODS = NO - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base name of -# the file that contains the anonymous namespace. By default anonymous namespace -# are hidden. -# The default value is: NO. - -EXTRACT_ANON_NSPACES = NO - -# If this flag is set to YES, the name of an unnamed parameter in a declaration -# will be determined by the corresponding definition. By default unnamed -# parameters remain unnamed in the output. -# The default value is: YES. - -RESOLVE_UNNAMED_PARAMS = YES - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all -# undocumented members inside documented classes or files. If set to NO these -# members will be included in the various overviews, but no documentation -# section is generated. This option has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. If set -# to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# declarations. If set to NO, these declarations will be included in the -# documentation. -# The default value is: NO. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -# documentation blocks found inside the body of a function. If set to NO, these -# blocks will be appended to the function's detailed documentation block. -# The default value is: NO. - -HIDE_IN_BODY_DOCS = YES - -# The INTERNAL_DOCS tag determines if documentation that is typed after a -# \internal command is included. If the tag is set to NO then the documentation -# will be excluded. Set it to YES to include the internal documentation. -# The default value is: NO. - -INTERNAL_DOCS = NO - -# With the correct setting of option CASE_SENSE_NAMES doxygen will better be -# able to match the capabilities of the underlying filesystem. In case the -# filesystem is case sensitive (i.e. it supports files in the same directory -# whose names only differ in casing), the option must be set to YES to properly -# deal with such files in case they appear in the input. For filesystems that -# are not case sensitive the option should be be set to NO to properly deal with -# output files written for symbols that only differ in casing, such as for two -# classes, one named CLASS and the other named Class, and to also support -# references to files without having to specify the exact matching casing. On -# Windows (including Cygwin) and MacOS, users should typically set this option -# to NO, whereas on Linux or other Unix flavors it should typically be set to -# YES. -# The default value is: system dependent. - -CASE_SENSE_NAMES = YES - -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -# their full class and namespace scopes in the documentation. If set to YES, the -# scope will be hidden. -# The default value is: NO. - -HIDE_SCOPE_NAMES = YES - -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will -# append additional text to a page's title, such as Class Reference. If set to -# YES the compound reference will be hidden. -# The default value is: NO. - -HIDE_COMPOUND_REFERENCE= NO - -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of -# the files that are included by a file in the documentation of that file. -# The default value is: YES. - -SHOW_INCLUDE_FILES = YES - -# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each -# grouped member an include statement to the documentation, telling the reader -# which file to include in order to use the member. -# The default value is: NO. - -SHOW_GROUPED_MEMB_INC = NO - -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include -# files with double quotes in the documentation rather than with sharp brackets. -# The default value is: NO. - -FORCE_LOCAL_INCLUDES = NO - -# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the -# documentation for inline members. -# The default value is: YES. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the -# (detailed) documentation of file and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. -# The default value is: YES. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief -# descriptions of file, namespace and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. Note that -# this will also influence the order of the classes in the class list. -# The default value is: NO. - -SORT_BRIEF_DOCS = YES - -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the -# (brief and detailed) documentation of class members so that constructors and -# destructors are listed first. If set to NO the constructors will appear in the -# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. -# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief -# member documentation. -# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting -# detailed member documentation. -# The default value is: NO. - -SORT_MEMBERS_CTORS_1ST = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy -# of group names into alphabetical order. If set to NO the group names will -# appear in their defined order. -# The default value is: NO. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by -# fully-qualified names, including namespaces. If set to NO, the class list will -# be sorted only by class name, not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the alphabetical -# list. -# The default value is: NO. - -SORT_BY_SCOPE_NAME = NO - -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper -# type resolution of all parameters of a function it will reject a match between -# the prototype and the implementation of a member function even if there is -# only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still -# accept a match between prototype and implementation in such cases. -# The default value is: NO. - -STRICT_PROTO_MATCHING = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo -# list. This list is created by putting \todo commands in the documentation. -# The default value is: YES. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test -# list. This list is created by putting \test commands in the documentation. -# The default value is: YES. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug -# list. This list is created by putting \bug commands in the documentation. -# The default value is: YES. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) -# the deprecated list. This list is created by putting \deprecated commands in -# the documentation. -# The default value is: YES. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional documentation -# sections, marked by \if ... \endif and \cond -# ... \endcond blocks. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the -# initial value of a variable or macro / define can have for it to appear in the -# documentation. If the initializer consists of more lines than specified here -# it will be hidden. Use a value of 0 to hide initializers completely. The -# appearance of the value of individual variables and macros / defines can be -# controlled using \showinitializer or \hideinitializer command in the -# documentation regardless of this setting. -# Minimum value: 0, maximum value: 10000, default value: 30. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -# the bottom of the documentation of classes and structs. If set to YES, the -# list will mention the files that were used to generate the documentation. -# The default value is: YES. - -SHOW_USED_FILES = YES - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This -# will remove the Files entry from the Quick Index and from the Folder Tree View -# (if specified). -# The default value is: YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces -# page. This will remove the Namespaces entry from the Quick Index and from the -# Folder Tree View (if specified). -# The default value is: YES. - -SHOW_NAMESPACES = NO - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command command input-file, where command is the value of the -# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file -# version. For an example see the documentation. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated -# output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can -# optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. -# -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE -# tag is left empty. - -LAYOUT_FILE = docs/DoxygenLayout.xml - -# The CITE_BIB_FILES tag can be used to specify one or more bib files containing -# the reference definitions. This must be a list of .bib files. The .bib -# extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. -# For LaTeX the style of the bibliography can be controlled using -# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -# search path. See also \cite for info how to create references. - -CITE_BIB_FILES = - -#--------------------------------------------------------------------------- -# Configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the -# messages are off. -# The default value is: NO. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES -# this implies that the warnings are on. -# -# Tip: Turn warnings on while writing the documentation. -# The default value is: YES. - -WARNINGS = YES - -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate -# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag -# will automatically be disabled. -# The default value is: YES. - -WARN_IF_UNDOCUMENTED = YES - -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. -# The default value is: YES. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that -# are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. -# The default value is: NO. - -WARN_NO_PARAMDOC = NO - -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS -# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but -# at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. -# The default value is: NO. - -WARN_AS_ERROR = NO - -# The WARN_FORMAT tag determines the format of the warning messages that doxygen -# can produce. The string should contain the $file, $line, and $text tags, which -# will be replaced by the file and line number from which the warning originated -# and the warning text. Optionally the format may contain $version, which will -# be replaced by the version of the file (if it could be obtained via -# FILE_VERSION_FILTER) -# The default value is: $file:$line: $text. - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning and error -# messages should be written. If left blank the output is written to standard -# error (stderr). - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# Configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag is used to specify the files and/or directories that contain -# documented source files. You may enter file names like myfile.cpp or -# directories like /usr/src/myproject. Separate the files or directories with -# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING -# Note: If this tag is empty the current directory is searched. - -INPUT = src/pyscipopt \ - examples/finished \ - examples/tutorial \ - docs \ - README.md \ - CHANGELOG.md \ - INSTALL.md \ - CONTRIBUTING.md - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses -# libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: -# https://www.gnu.org/software/libiconv/) for the list of possible encodings. -# The default value is: UTF-8. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -# *.h) to filter out the source-files in the directories. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. -# -# Note the list of default checked file patterns might differ from the list of -# default file extension mappings. -# -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), -# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, -# *.ucf, *.qsf and *.ice. - -FILE_PATTERNS = *.pyx \ - *.pxi \ - *.py \ - *.md - -# The RECURSIVE tag can be used to specify whether or not subdirectories should -# be searched for input files as well. -# The default value is: NO. - -RECURSIVE = NO - -# The EXCLUDE tag can be used to specify files and/or directories that should be -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. -# -# Note that relative paths are relative to the directory from which doxygen is -# run. - -EXCLUDE = src/pyscipopt/__init__.py - -# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or -# directories that are symbolic links (a Unix file system feature) are excluded -# from the input. -# The default value is: NO. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* - -EXCLUDE_SYMBOLS = Conshdlr_sils \ - PY_SCIP_* - -# The EXAMPLE_PATH tag can be used to specify one or more files or directories -# that contain example code fragments that are included (see the \include -# command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and -# *.h) to filter out the source-files in the directories. If left blank all -# files are included. - -EXAMPLE_PATTERNS = - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude commands -# irrespective of the value of the RECURSIVE tag. -# The default value is: NO. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or directories -# that contain images that are to be included in the documentation (see the -# \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command: -# -# -# -# where is the value of the INPUT_FILTER tag, and is the -# name of an input file. Doxygen will then use the output that the filter -# program writes to standard output. If FILTER_PATTERNS is specified, this tag -# will be ignored. -# -# Note that the filter must not add or remove lines; it is applied before the -# code is scanned, but not when the output code is generated. If lines are added -# or removed, the anchors will not be placed correctly. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: pattern=filter -# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how -# filters are used. If the FILTER_PATTERNS tag is empty or if none of the -# patterns match the file name, INPUT_FILTER is applied. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will also be used to filter the input files that are used for -# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). -# The default value is: NO. - -FILTER_SOURCE_FILES = NO - -# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and -# it is also possible to disable source filtering for a specific pattern using -# *.ext= (so without naming a filter). -# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. - -FILTER_SOURCE_PATTERNS = - -# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that -# is part of the input, its contents will be placed on the main page -# (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. - -USE_MDFILE_AS_MAINPAGE = - -#--------------------------------------------------------------------------- -# Configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will be -# generated. Documented entities will be cross-referenced with these sources. -# -# Note: To get rid of all source code in the generated output, make sure that -# also VERBATIM_HEADERS is set to NO. -# The default value is: NO. - -SOURCE_BROWSER = YES - -# Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. -# The default value is: NO. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any -# special comment blocks from generated source code fragments. Normal C, C++ and -# Fortran comments will always remain visible. -# The default value is: YES. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# entity all documented functions referencing it will be listed. -# The default value is: NO. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES then for each documented function -# all documented entities called/used by that function will be listed. -# The default value is: NO. - -REFERENCES_RELATION = YES - -# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -# to YES then the hyperlinks from functions in REFERENCES_RELATION and -# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will -# link to the documentation. -# The default value is: YES. - -REFERENCES_LINK_SOURCE = YES - -# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the -# source code will show a tooltip with additional information such as prototype, -# brief description and links to the definition and documentation. Since this -# will make the HTML file larger and loading of large files a bit slower, you -# can opt to disable this feature. -# The default value is: YES. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -SOURCE_TOOLTIPS = YES - -# If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in -# source browser. The htags tool is part of GNU's global source tagging system -# (see https://www.gnu.org/software/global/global.html). You will need version -# 4.8.6 or higher. -# -# To use it do the following: -# - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file -# - Make sure the INPUT points to the root of the source tree -# - Run doxygen as normal -# -# Doxygen will invoke htags (and that will in turn invoke gtags), so these -# tools must be available from the command line (i.e. in the search path). -# -# The result: instead of the source browser generated by doxygen, the links to -# source code will now point to the output of htags. -# The default value is: NO. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a -# verbatim copy of the header file for each class for which an include is -# specified. Set to NO to disable this. -# See also: Section \class. -# The default value is: YES. - -VERBATIM_HEADERS = YES - -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: -# http://clang.llvm.org/) for more accurate parsing at the cost of reduced -# performance. This can be particularly helpful with template rich C++ code for -# which doxygen's built-in parser lacks the necessary type information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to -# YES then doxygen will add the directory of each input to the include path. -# The default value is: YES. - -CLANG_ADD_INC_PATHS = YES - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -# If clang assisted parsing is enabled you can provide the clang parser with the -# path to the directory containing a file called compile_commands.json. This -# file is the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the -# options used when the source files were built. This is equivalent to -# specifying the -p option to a clang tool, such as clang-check. These options -# will then be passed to the parser. Any options specified with CLANG_OPTIONS -# will be added as well. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. - -CLANG_DATABASE_PATH = - -#--------------------------------------------------------------------------- -# Configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all -# compounds will be generated. Enable this if the project contains a lot of -# classes, structs, unions or interfaces. -# The default value is: YES. - -ALPHABETICAL_INDEX = NO - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output -# The default value is: YES. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each -# generated HTML page (for example: .htm, .php, .asp). -# The default value is: .html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a -# standard header. -# -# To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. -# the setting GENERATE_TREEVIEW). It is highly recommended to start with a -# default header using -# doxygen -w html new_header.html new_footer.html new_stylesheet.css -# YourConfigFile -# and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally -# uses. -# Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description -# of the possible markers and block names see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_HEADER = docs/header.html - -# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard -# footer. See HTML_HEADER for more information on how to generate a default -# footer and what special commands can be used inside the footer. See also -# section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FOOTER = docs/footer.html - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style -# sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. -# See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. -# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as -# it is more robust and this tag (HTML_STYLESHEET) will in the future become -# obsolete. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_STYLESHEET = - -# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined -# cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. -# This is preferred over using HTML_STYLESHEET since it does not replace the -# standard style sheet and is therefore more robust against future updates. -# Doxygen will copy the style sheet files to the output directory. -# Note: The order of the extra style sheet files is of importance (e.g. the last -# style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_STYLESHEET = - -# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or -# other source files which should be copied to the HTML output directory. Note -# that these files will be copied to the base HTML output directory. Use the -# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -# files. In the HTML_STYLESHEET file, use the file name only. Also note that the -# files will be copied as-is; there are no commands or markers available. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_FILES = - -# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -# will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see -# https://en.wikipedia.org/wiki/Hue for more information. For instance the value -# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 -# purple, and 360 is red again. -# Minimum value: 0, maximum value: 359, default value: 220. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_HUE = 220 - -# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A -# value of 255 will produce the most vivid colors. -# Minimum value: 0, maximum value: 255, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_SAT = 100 - -# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the -# luminance component of the colors in the HTML output. Values below 100 -# gradually make the output lighter, whereas values above 100 make the output -# darker. The value divided by 100 is the actual gamma applied, so 80 represents -# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not -# change the gamma. -# Minimum value: 40, maximum value: 240, default value: 80. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_GAMMA = 80 - -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = YES - -# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML -# documentation will contain a main index with vertical navigation menus that -# are dynamically created via JavaScript. If disabled, the navigation index will -# consists of multiple levels of tabs that are statically embedded in every HTML -# page. Disable this option to support browsers that do not have JavaScript, -# like the Qt help browser. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_MENUS = YES - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_SECTIONS = NO - -# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries -# shown in the various tree structured indices initially; the user can expand -# and collapse entries dynamically later on. Doxygen will expand the tree to -# such a level that at most the specified number of entries are visible (unless -# a fully collapsed tree already exceeds this amount). So setting the number of -# entries 1 will produce a full collapsed tree by default. 0 is a special value -# representing an infinite number of entries and will result in a full expanded -# tree by default. -# Minimum value: 0, maximum value: 9999, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_INDEX_NUM_ENTRIES = 100 - -# If the GENERATE_DOCSET tag is set to YES, additional index files will be -# generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: -# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To -# create a documentation set, doxygen will generate a Makefile in the HTML -# output directory. Running make will produce the docset in that directory and -# running make install will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy -# genXcode/_index.html for more information. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_DOCSET = NO - -# This tag determines the name of the docset feed. A documentation feed provides -# an umbrella under which multiple documentation sets from a single provider -# (such as a company or product suite) can be grouped. -# The default value is: Doxygen generated docs. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# This tag specifies a string that should uniquely identify the documentation -# set bundle. This should be a reverse domain-name style string, e.g. -# com.mycompany.MyDocSet. Doxygen will append .docset to the name. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify -# the documentation publisher. This should be a reverse domain-name style -# string, e.g. com.mycompany.MyDocSet.documentation. -# The default value is: org.doxygen.Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_ID = org.doxygen.Publisher - -# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. -# The default value is: Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_NAME = Publisher - -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three -# additional HTML index files: index.hhp, index.hhc, and index.hhk. The -# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: -# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. -# -# The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML -# files are now used as the Windows 98 help format, and will replace the old -# Windows help format (.hlp) on all Windows platforms in the future. Compressed -# HTML files also contain an index, a table of contents, and you can search for -# words in the documentation. The HTML workshop also contains a viewer for -# compressed HTML files. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_HTMLHELP = NO - -# The CHM_FILE tag can be used to specify the file name of the resulting .chm -# file. You can add a path in front of the file if the result should not be -# written to the html output directory. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_FILE = - -# The HHC_LOCATION tag can be used to specify the location (absolute path -# including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. -# The file has to be specified with full path. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -HHC_LOCATION = - -# The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the main .chm file (NO). -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -GENERATE_CHI = NO - -# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) -# and project file content. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_INDEX_ENCODING = - -# The BINARY_TOC flag controls whether a binary table of contents is generated -# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it -# enables the Previous and Next buttons. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members to -# the table of contents of the HTML help documentation and to the tree view. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that -# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help -# (.qch) of the generated HTML documentation. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify -# the file name of the resulting .qch file. The path specified is relative to -# the HTML output folder. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help -# Project output. For more information please see Qt Help Project / Namespace -# (see: -# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_NAMESPACE = org.doxygen.Project - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt -# Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: -# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). -# The default value is: doc. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_VIRTUAL_FOLDER = doc - -# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom -# filter to add. For more information please see Qt Help Project / Custom -# Filters (see: -# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the -# custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: -# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -# project's filter section matches. Qt Help Project / Filter Attributes (see: -# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_SECT_FILTER_ATTRS = - -# The QHG_LOCATION tag can be used to specify the location (absolute path -# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to -# run qhelpgenerator on the generated .qhp file. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHG_LOCATION = - -# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be -# generated, together with the HTML files, they form an Eclipse help plugin. To -# install this plugin and make it available under the help contents menu in -# Eclipse, the contents of the directory containing the HTML and XML files needs -# to be copied into the plugins directory of eclipse. The name of the directory -# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. -# After copying Eclipse needs to be restarted before the help appears. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_ECLIPSEHELP = NO - -# A unique identifier for the Eclipse help plugin. When installing the plugin -# the directory name containing the HTML and XML files should also have this -# name. Each documentation set should have its own identifier. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. - -ECLIPSE_DOC_ID = org.doxygen.Project - -# If you want full control over the layout of the generated HTML pages it might -# be necessary to disable the index and replace it with your own. The -# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top -# of each HTML page. A value of NO enables the index and the value YES disables -# it. Since the tabs in the index contain the same information as the navigation -# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -DISABLE_INDEX = YES - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. If the tag -# value is set to YES, a side panel will be generated containing a tree-like -# index structure (just like the one that is generated for HTML Help). For this -# to work a browser that supports JavaScript, DHTML, CSS and frames is required -# (i.e. any modern browser). Windows users are probably better off using the -# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_TREEVIEW = YES - -# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. -# -# Note that a value of 0 will completely suppress the enum values from appearing -# in the overview section. -# Minimum value: 0, maximum value: 20, default value: 4. -# This tag requires that the tag GENERATE_HTML is set to YES. - -ENUM_VALUES_PER_LINE = 4 - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used -# to set the initial width (in pixels) of the frame in which the tree is shown. -# Minimum value: 0, maximum value: 1500, default value: 250. -# This tag requires that the tag GENERATE_HTML is set to YES. - -TREEVIEW_WIDTH = 250 - -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to -# external symbols imported via tag files in a separate window. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -EXT_LINKS_IN_WINDOW = NO - -# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg -# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see -# https://inkscape.org) to generate formulas as SVG images instead of PNGs for -# the HTML output. These images will generally look nicer at scaled resolutions. -# Possible values are: png (the default) and svg (looks nicer but requires the -# pdf2svg or inkscape tool). -# The default value is: png. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FORMULA_FORMAT = png - -# Use this tag to change the font size of LaTeX formulas included as images in -# the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML -# output directory to force them to be regenerated. -# Minimum value: 8, maximum value: 50, default value: 10. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_FONTSIZE = 10 - -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - -# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands -# to create new LaTeX commands to be used in formulas as building blocks. See -# the section "Including formulas" for details. - -FORMULA_MACROFILE = - -# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# https://www.mathjax.org) which uses client side JavaScript for the rendering -# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX -# installed or if you want to formulas look prettier in the HTML output. When -# enabled you may also need to install MathJax separately and configure the path -# to it using the MATHJAX_RELPATH option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -USE_MATHJAX = NO - -# When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. -# Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. -# The default value is: HTML-CSS. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_FORMAT = HTML-CSS - -# When MathJax is enabled you need to specify the location relative to the HTML -# output directory using the MATHJAX_RELPATH option. The destination directory -# should contain the MathJax.js script. For instance, if the mathjax directory -# is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -# Content Delivery Network so you can quickly see the result without installing -# MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest - -# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax -# extension names that should be enabled during MathJax rendering. For example -# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_EXTENSIONS = - -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -# of code that will be used on startup of the MathJax code. See the MathJax site -# (see: -# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an -# example see the documentation. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_CODEFILE = - -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and -# should work on any modern browser. Note that when using HTML help -# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) -# there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then -# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to -# search using the keyboard; to jump to the search box use + S -# (what the is depends on the OS and browser, but it is typically -# , /

- - - - - - diff --git a/docs/header.html b/docs/header.html deleted file mode 100644 index a3520ad21..000000000 --- a/docs/header.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - -$projectname: $title -$title - - - -$treeview -$search -$mathjax - -$extrastylesheet - - -
- - -
- - - - - - - - - - - - - - - - - - - - - -
-
$projectname -  $projectnumber -
-
$projectbrief
-
-
$projectbrief
-
$searchbox
-
- - diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..a69063218 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +######################### +PySCIPOpt Documentation +######################### + + +PySCIPOpt is the Python interface to `SCIP (Solving Constraint Integer Programs) `_. + + +******** +Contents +******** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + install + build + tutorials/index + whyscip + similarsoftware + faq + api + diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 000000000..d9cc7613b --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,47 @@ +################## +Installation Guide +################## + +This page will detail all methods for installing PySCIPOpt via package managers, +which come with their own versions of SCIP. For building PySCIPOpt against your +own custom version of SCIP, or for building PySCIPOpt from source, visit :doc:`this page `. + +.. contents:: Contents + + +PyPI (pip) +============ + +Pre-built binary wheels are uploaded to PyPI (Python Package Index) for each release. +Supported platforms are Linux (x86_64), Windows (x86_64) and MacOS (x86_64, Apple Silicon). + +To install PySCIPOpt simply run the command: + +.. code-block:: bash + + pip install pyscipopt + +.. note:: For Linux users: PySCIPOpt versions newer than 5.1.1 installed via PyPI now require glibc 2.28+ + + For our build infrastructure we use `manylinux `_. + As CentOS 7 is no longer supported, we have migrated from ``manylinux2014`` to ``manylinux_2_28``. + + TLDR: Older linux distributions may not work for newer versions of PySCIPOpt installed via pip. + +.. note:: For versions older than 4.4.0 installed via PyPI SCIP is not automatically installed. + This means that SCIP must be installed yourself. If it is not installed globally, + then the ``SCIPOPTDIR`` environment flag must be set, see :doc:`this page ` for more details. + + +Conda +===== + +It is also possible to use the Conda package manager to install PySCIPOpt. +Conda will install SCIP automatically, hence everything can be installed in a single command: + +.. code-block:: bash + + conda install --channel conda-forge pyscipopt + +.. note:: Do not use the Conda base environment to install PySCIPOpt. + diff --git a/docs/maindoc.py b/docs/maindoc.py deleted file mode 100644 index 1185910d5..000000000 --- a/docs/maindoc.py +++ /dev/null @@ -1,53 +0,0 @@ -##@file maindoc.py -#@brief Main documentation page - -## @mainpage Overview -# -# This project provides an interface from Python to the [SCIP Optimization Suite](http://scip.zib.de).
-# -# See the [web site] (https://github.com/SCIP-Interfaces/PySCIPOpt) to download PySCIPOpt. -# -# @section Changelog -# See [CHANGELOG.md](CHANGELOG.md) for added, removed or fixed functionality. -# -# @section Installation -# See [INSTALL.md](INSTALL.md) for instructions. -# -# @section TABLEOFCONTENTS Structure of this manual -# -# This documentation gives an introduction to the functionality of the Python interface of the SCIP code in the following chapters -# -# - \ref pyscipopt::scip::Model "Model" Class with the most fundamental functions to create and solve a problem -# - \ref examples/tutorial "Tutorials" and \ref examples/finished "Examples" to display some functionality of the interface -# - @subpage EXTEND Explanations on extending the PySCIPOpt interface -# -# For a more detailed description on how to create a model and how to extend the interface, please have a look at the [README.md] (README.md). -# - -##@page EXTEND Extending the interface -# PySCIPOpt already covers many of the SCIP callable library methods. You -#may also extend it to increase the functionality of this interface. The -#following will provide some directions on how this can be achieved: -# -#The two most important files in PySCIPOpt are the `scip.pxd` and -#`scip.pxi`. These two files specify the public functions of SCIP that -#can be accessed from your python code. -# -#To make PySCIPOpt aware of the public functions you would like to -#access, you must add them to `scip.pxd`. There are two things that must -#be done in order to properly add the functions: -# -# -# Ensure any `enum`s, `struct`s or SCIP variable types are included in -# `scip.pxd` -# -# Add the prototype of the public function you wish to access to -# `scip.pxd` -# -#After following the previous two steps, it is then possible to create -#functions in python that reference the SCIP public functions included in -#`scip.pxd`. This is achieved by modifying the `scip.pxi` file to add the -#functionality you require. -# -#We are always happy to accept pull request containing patches or -#extensions! -# -#Please have a look at our [contribution guidelines](CONTRIBUTING.md). diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..fe66c8814 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx-rtd-theme +sphinx-nefertiti +sphinxcontrib-bibtex \ No newline at end of file diff --git a/docs/similarsoftware.rst b/docs/similarsoftware.rst new file mode 100644 index 000000000..8802a6b53 --- /dev/null +++ b/docs/similarsoftware.rst @@ -0,0 +1,98 @@ +################# +Similar Software +################# + +.. contents:: Contents + +Alternate MIP Solvers (with Python interfaces) +============================================== + +In the following we will give a list of other mixed-integer optimizers with available python interfaces. +As each solver has its own set of problem classes that it can solve we will use a table with reference +keys to summarise these problem classes. + +.. note:: This table is by no means complete. + +.. note:: SCIP can solve of all the below problem classes (and many more). + +.. list-table:: Label Summaries + :widths: 25 25 + :align: center + :header-rows: 1 + + * - Key + - Feature + * - LP + - Linear Programs + * - MILP + - Mixed-Integer Linear Programs + * - QP + - Quadratic Programs + * - MIQP + - Mixed-Integer Quadratic Programs + * - QCP + - Quadratically Constrained Programs + * - MIQCP + - Mixed-Integer Quadratically Constrained Programs + * - MINLP + - Mixed-Integer Nonlinear Programs + * - PB + - Pseudo-Boolean Problems + * - SOCP + - Second Order Cone Programming + * - SDP + - Semidefinite Programming + * - MISDP + - Mixed-Integer Semidefinite Programming + +Open Source +*********** + +- `HiGHS `_: O, LP, MILP, QP, MIQP +- `CLP / CBC `_: O, LP, MILP +- `GLOP `_: O, LP + +Closed Source +************* + +- `CPLEX `_: LP, MILP, QP, MIQP, QCP, MIQCP, SOCP +- `Gurobi `_: LP, MILP, QP, MIQP, QCP, MIQCP, MINLP, PB, SOCP +- `Xpress `_: LP, MILP, QP, MIQP, QCP, MIQCP, MINLP, SOCP +- `COPT `_: LP, MILP, QP, MIQP, QCP, MIQCP, SOCP, SDP +- `MOSEK `_: LP, MILP, QP, MIQP, QCP, MIQCP, SOCP, SDP, MISDP + +General Modelling Frameworks (Solver Agnostic) +============================================== + +This list will contain general modelling frameworks that allow you to use swap out SCIP for other +mixed-integer optimizers. While we recommend using PySCIPOpt for the many features it provides, +which are for the most part not available via general modelling frameworks, +if you want to simply use a mixed-integer optimizer then a general modelling framework +allows you to swap out the solver in a single line. This is a big advantage if you +are uncertain of the solver that you want to use, you believe the solver might change at some point, +or you want to compare the performance of different solvers. + +- `Pyomo `_ +- `CVXPy `_ +- `LINOPy `_ +- `PULP `_ +- `PICOS `_ +- `AMPL Python API `_ + + +Software using PySCIPOpt +======================== + +This is software that is built on PySCIPOpt + +- `GeCO `_: Generators for Combinatorial Optimization +- `scip-routing `_: An exact VRPTW solver in Python +- `PySCIPOpt-ML `_: Python interface to automatically formulate Machine Learning models into Mixed-Integer Programs +- `SCIP Book `_: Mathematical Optimization: Solving Problems using SCIP and Python + +Additional SCIP Resources +========================= + +- `SCIP Website `_ +- `SCIP Documentation `_ +- `SCIP GitHub `_ diff --git a/docs/tutorials/branchrule.rst b/docs/tutorials/branchrule.rst new file mode 100644 index 000000000..89f937960 --- /dev/null +++ b/docs/tutorials/branchrule.rst @@ -0,0 +1,210 @@ +############### +Branching Rules +############### + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, Branchrule, SCIP_RESULT + + scip = Model() + +.. contents:: Contents + +What is Branching +=================== + +Branching is when an optimization problem is split into smaller subproblems. +Traditionally this is done on an integer variable with a fractional LP solution, with +two child nodes being created with constraints ``x >= ceil(frac)`` and ``x <= floor(frac)``. +In SCIP, arbitrary amount of children nodes can be created, and the constraints added the +created nodes can also be arbitrary. This is not going to be used in the examples below, but this +should be kept in mind when considering your application of your branching rule. + +Example Branching Rule +======================= + +Here we will program a most infeasible branching rule. This rule selects the integer variable +whose LP solution is most fractional. + +.. code-block:: python + + import numpy as np + + class MostInfBranchRule(Branchrule): + + def __init__(self, scip): + self.scip = scip + + def branchexeclp(self, allowaddcons): + + # Get the branching candidates. Only consider the number of priority candidates (they are sorted to be first) + # The implicit integer candidates in general shouldn't be branched on. Unless specified by the user + # npriocands and ncands are the same (npriocands are variables that have been designated as priorities) + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands() + + # Find the variable that is most fractional + best_cand_idx = 0 + best_dist = np.inf + for i in range(npriocands): + if abs(branch_cand_fracs[i] - 0.5) <= best_dist: + best_dist = abs(branch_cand_fracs[i] - 0.5) + best_cand_idx = i + + # Branch on the variable with the largest score + down_child, eq_child, up_child = self.model.branchVarVal( + branch_cands[best_cand_idx], branch_cand_sols[best_cand_idx]) + + return {"result": SCIP_RESULT.BRANCHED} + +Let's talk about some features of this branching rule. Currently we only explicitly programmed +a single function, which is called ``branchexeclp``. This is the function that gets called +when branching on an LP optimal solution. While this is the main case, it is not the only +case that SCIP handles. What if there was an LP error at the node, or you are given a set of external +candidates? For more information on this please read `this page `_. + +Now let's discuss what the function returned. We see that it returned a simple dictionary, with the +``"result"`` key and a ``SCIP_RESULT``. This is because inside the function the child nodes +have already been created, and the solver just needs to be made aware of this with the appropriate +code. + +In the case of something going wrong while branching (and you have not made the children nodes), +just simply return ``SCIP_RESULT:DIDNOTRUN``. This will then move on to the next branching rule with +the next highest priority. + +.. note:: + + Returning ``SCIP_RESULT:DIDNOTRUN`` for more complicated components of the branching rule + line in ``branchexecps`` is completely encouraged. It is even strongly suggested if you are doing + an LP specific branching rule. + +Now we will finally see how to include the branching rule. + +.. code-block:: python + + scip = Model() + + most_inf_branch_rule = MostInfBranchRule(scip) + scip.includeBranchrule(most_inf_branch_rule, "mostinf", "custom most infeasible branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + +This function ``includeBranchrule`` takes the branching rule as an argument, the name (which will +be visible in the statistics), a description, the priority (for your rule to be called by default it must +be higher than the current highest, which can be quite large), the maxdepth of the branch and bound tree +for which the rule still works (-1 for unlimited), and the maxbounddist (We recommend using 1 and to see +SCIP documentation for an explanation). + +Strong Branching Information +============================= + +Now let's look at a more complicated example, namely one where we implement our own strong branching rule. +The aim of this example is to provide a basic understanding of what functions are necessary to use +strong branching or obtain some strong branching information. + +.. note:: This example is not equivalent to the strong branching rule in SCIP. It ignores some of the + more complicated interactions in a MIP solver for information found during strong branching. + These include but are not strictly limited to: + + - What happens if a new primal solution is found and the bound is larger than the cutoff bound? + - What happens if the bound for one of the children is above a cutoff bound? + - If probing is enabled then one would need to handle new found bounds appropriately. + +.. code-block:: python + + class StrongBranchingRule(Branchrule): + + def __init__(self, scip): + self.scip = scip + + def branchexeclp(self, allowaddcons): + + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands() + + # Initialise scores for each variable + scores = [-self.scip.infinity() for _ in range(npriocands)] + down_bounds = [None for _ in range(npriocands)] + up_bounds = [None for _ in range(npriocands)] + + # Initialise placeholder values + num_nodes = self.scip.getNNodes() + lpobjval = self.scip.getLPObjVal() + lperror = False + best_cand_idx = 0 + + # Start strong branching and iterate over the branching candidates + self.scip.startStrongbranch() + for i in range(npriocands): + + # Check the case that the variable has already been strong branched on at this node. + # This case occurs when events happen in the node that should be handled immediately. + # When processing the node again (because the event did not remove it), there's no need to duplicate work. + if self.scip.getVarStrongbranchNode(branch_cands[i]) == num_nodes: + down, up, downvalid, upvalid, _, lastlpobjval = self.scip.getVarStrongbranchLast(branch_cands[i]) + if downvalid: + down_bounds[i] = down + if upvalid: + up_bounds[i] = up + downgain = max([down - lastlpobjval, 0]) + upgain = max([up - lastlpobjval, 0]) + scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain]) + continue + + # Strong branch! + down, up, downvalid, upvalid, downinf, upinf, downconflict, upconflict, lperror = self.scip.getVarStrongbranch( + branch_cands[i], 200, idempotent=False) + + # In the case of an LP error handle appropriately (for this example we just break the loop) + if lperror: + break + + # In the case of both infeasible sub-problems cutoff the node + if downinf and upinf: + return {"result": SCIP_RESULT.CUTOFF} + + # Calculate the gains for each up and down node that strong branching explored + if not downinf and downvalid: + down_bounds[i] = down + downgain = max([down - lpobjval, 0]) + else: + downgain = 0 + if not upinf and upvalid: + up_bounds[i] = up + upgain = max([up - lpobjval, 0]) + else: + upgain = 0 + + # Update the pseudo-costs + lpsol = branch_cands[i].getLPSol() + if not downinf and downvalid: + self.scip.updateVarPseudocost(branch_cands[i], -self.scip.frac(lpsol), downgain, 1) + if not upinf and upvalid: + self.scip.updateVarPseudocost(branch_cands[i], 1 - self.scip.frac(lpsol), upgain, 1) + + scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain]) + if scores[i] > scores[best_cand_idx]: + best_cand_idx = i + + # End strong branching + self.scip.endStrongbranch() + + # In the case of an LP error + if lperror: + return {"result": SCIP_RESULT.DIDNOTRUN} + + # Branch on the variable with the largest score + down_child, eq_child, up_child = self.model.branchVarVal( + branch_cands[best_cand_idx], branch_cands[best_cand_idx].getLPSol()) + + # Update the bounds of the down node and up node. Some cols might not exist due to pricing + if self.scip.allColsInLP(): + if down_child is not None and down_bounds[best_cand_idx] is not None: + self.scip.updateNodeLowerbound(down_child, down_bounds[best_cand_idx]) + if up_child is not None and up_bounds[best_cand_idx] is not None: + self.scip.updateNodeLowerbound(up_child, up_bounds[best_cand_idx]) + + return {"result": SCIP_RESULT.BRANCHED} + +.. note:: In SCIP we must call ``startStrongbranch`` + before doing any actual strong branching (which is done with the call ``getVarStrongbranch``). When we're done + with strong branching we must then also call ``endStrongbranch``. \ No newline at end of file diff --git a/docs/tutorials/constypes.rst b/docs/tutorials/constypes.rst new file mode 100644 index 000000000..ea714d112 --- /dev/null +++ b/docs/tutorials/constypes.rst @@ -0,0 +1,180 @@ +################### +Constraints in SCIP +################### + +In this overview of constraints in PySCIPOpt we'll walk through best +practices for modelling them and the various information that they +can be extracted from them. + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, quicksum + + scip = Model() + +.. note:: In general you want to keep track of your constraint objects. + They can always be obtained from the model after they are added, but then + the responsibility falls to the user to match them, e.g. by name. + +.. contents:: Contents + +What is a Constraint? +======================== + +A constraint in SCIP is likely much more broad in definition than you are familiar with. +For more information we recommend reading :doc:`this page `. + +To create a standard linear or non-linear constraint use the command: + +.. code-block:: python + + x = scip.addVar(vtype='B', name='x') + y = scip.addVar(vtype='B', name='y') + z = scip.addVar(vtype='B', name='z') + # Linear constraint + linear_cons = scip.addCons(x + y + z == 1, name="lin_cons") + # Non-linear constraint + nonlinear_cons = scip.addCons(x * y + z == 1, name="nonlinear_cons") + +What is a Row? +================ + +In a similar fashion to Variables with columns, see :doc:`this page `, +constraints bring up an interesting feature of SCIP when used in the context of an LP. +The context of an LP here means that we are after the LP relaxation of the optimization problem +at some node. Is the constraint even in the LP? +When you solve an optimization problm with SCIP, the problem is first transformed. This process is +called presolve, and is done to accelerate the subsequent solving process. Therefore a constraint +that was originally created may have been transformed entirely, as the original variables that +featured in the constraint have also been changed. Additionally, maybe the constraint was found to be redundant, +i.e., trivially true, and was removed. The constraint is also much more general +than necessary, containing information that is not strictly necessary for solving the LP, +and may not even be representable by linear constraints. +Therefore, when representing a constraint in an LP, we use Row objects. +Be warned however, that this is not necessarily a simple one-to-one matching. Some more complicated +constraints may either have no Row representation in the LP or have multiple such rows +necessary to best represent it in the LP. For a standard linear constraint the Row +that represents the constraint in the LP can be found with the code: + +.. code-block:: python + + row = scip.getRowLinear(linear_cons) + +.. note:: Remember that such a Row representation refers only to the latest LP, and is + best queried when access to the current LP is clear, e.g. when branching. + +From a Row object one can easily obtain information about the current LP. Some quick examples are +the lhs, rhs, constant shift, the columns with non-zero coefficient values, the matching +coefficient values, and the constraint handler that created the Row. + +.. code-block:: python + + lhs = row.getLhs() + rhs = row.getRhs() + constant = row.getConstant() + cols = row.getCols() + vals = row.getVals() + origin_cons_name = row.getConsOriginConshdlrtype() + + +Constraint Information +======================== + +The Constraint object can be queried like any other object. Some of the information a Constraint +object contains is the name of the constraint handler responsible for the constraint, +and many boolean properties of the constraint, e.g., is it linear. + +.. code-block:: python + + linear_conshdlr_name = linear_cons.getConshdlrName() + assert linear_cons.isLinear() + +As constraints are broader than the standard linear constraints most users are familiar with, +many of the functions that obtain constraint information are callable from the Model object. +These include the activity of the constraint, the slack of the constraint, +and adding or deleting coefficients. + +.. code-block:: python + + if scip.getNSols() >= 1: + scip_sol = scip.getBestSol() + activity = scip.getActivity(linear_cons, scip_sol) + slack = scip.getSlack(linear_cons, scip_sol) + # Check current coefficients with scip.getValsLinear(linear_cons) + scip.chgCoefLinear(linear_cons, x, 7) # Change the coefficient to 7 + +Currently not mentioned w.r.t. the constraints and rows is the dual information. +This is frustratingly complicated. SCIP has a plugin based LP solver, which offers many +choices for LP solvers, but makes getting information from them more complicated. Getting +dual values from constraints or rows will work, but to be confident that they are returning +the correct information we encourage doing three different things: + +- Disable presolving and propagation to ensure that the LP solver + - which is providing the dual information - actually solves the unmodified problem. +- Disable heuristics to avoid that the problem is solved before the LP solver is called. +- Ensure there are no bound constraints, i.e., constraints with only one variable. + +To accomplish this one can apply the following settings to the Model. + +.. code-block:: python + + from pyscipopt import SCIP_PARAMSETTING + scip.setPresolve(SCIP_PARAMSETTING.OFF) + scip.setHeuristics(SCIP_PARAMSETTING.OFF) + scip.disablePropagation() + +We stress again that when accessing such values you should be confident that you know which +LP is being referenced. This information for instance is unclear or difficult +to derive a meaningful interpretation from when the solution process has ended. +The dual value of a constraint can be obtained with the following code: + +.. code-block:: python + + dual_sol = scip.getDualsolLinear(linear_cons) + +Constraint Types +================== + +In the above we presented examples of only linear constraints and a non-linear +constraint. SCIP however can handle many different types of constraints. Some of these that are +likely familiar are SOS constraints, Indicator constraints, and AND / OR / XOR constraints. +These constraint handlers have custom methods for improving the solving process of +optimization problems that they feature in. To add such a constraint, e.g., an SOS and indicator +constraint, you'd use the code: + +.. code-block:: python + + sos_cons = scip.addConsSOS1([x, y, z], name="example_sos") + indicator_cons = scip.addConsIndicator(x + y <= 1, binvar=z, name="example_indicator") + +SCIP also allows the creation of custom constraint handlers. These could be empty and just +there to record data, there to provide custom handling of some user defined function, or they could be there to +enforce a constraint that is incredibly inefficient to enforce via linear constraints. +An example of such a constraint handler +is presented in the lazy constraint tutorial for modelling the subtour elimination +constraints :doc:`here ` + +Quicksum +======== + +It is very common that when constructing constraints one wants to use the inbuilt ``sum`` function +in Python. For example, consider the common scenario where we have a set of binary variables. + +.. code-block:: python + + x = [scip.addVar(vtype='B', name=f"x_{i}") for i in range(1000)] + +A standard constraint in this example may be that exactly one binary variable can be active. +To sum these varaibles we recommend using the custom ``quicksum`` function, as it avoids +intermediate data structure and adds terms inplace. For example: + +.. code-block:: python + + scip.addCons(quicksum(x[i] for i in range(1000)) == 1, name="sum_cons") + +.. note:: While this is often unnecessary for smaller models, for larger models it can have a substantial + improvement on time spent in model construction. + +.. note:: For ``prod`` there also exists an equivalent ``quickprod`` function. diff --git a/docs/tutorials/cutselector.rst b/docs/tutorials/cutselector.rst new file mode 100644 index 000000000..5168bf661 --- /dev/null +++ b/docs/tutorials/cutselector.rst @@ -0,0 +1,104 @@ +############ +Cut Selector +############ + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, SCIP_RESULT + from pyscipopt.scip import Cutsel + + scip = Model() + +.. contents:: Contents + +What is a Cut Selector? +======================== + +A cutting plane (cut) selector is an algorithm that selects which cuts to add to the +optimization problem. It is given a set of candidate cuts, and from the set must decide which +subset to add. + +Cut Selector Structure +======================= + +A cut selector in PySCIPOpt takes the following structure: + +.. code-block:: python + + class DummyCutsel(Cutsel): + + def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): + """ + :param cuts: the cuts which we want to select from. Is a list of scip Rows + :param forcedcuts: the cuts which we must add. Is a list of scip Rows + :param root: boolean indicating whether weare at the root node + :param maxnselectedcuts: int which is the maximum amount of cuts that can be selected + :return: sorted cuts and forcedcuts + """ + + return {'cuts': sorted_cuts, 'nselectedcuts': n, + 'result': SCIP_RESULT.SUCCESS} + +The class ``DummyCutsel`` inherits the necessary ``Cutsel`` class, and then programs +the necessary function ``cutselselect``. The docstrings of the ``cutselselect`` explain +the input to the function. It is then up to the user to create some new ordering ``cuts``, +which we have represented by ``sorted_cuts``. The returned value of ``nselectedcuts`` results in the first +``nselectedcuts`` of the ``sorted_cuts`` being added to the optimization problem. The +``SCIP_RESULT`` is there to indicate whether the algorithm was successful. See the +appropriate documentation for more potential result codes. + +To include a cut selector one would need to do something like the following code: + +.. code-block:: python + + cutsel = DummyCutsel() + scip.includeCutsel(cutsel, 'name', 'description', 5000000) + +The final argument of the ``includeCutsel`` function in the example above was the +priority. If the priority is higher than all other cut selectors then it will be called +first. In the case of some failure or non-success return code, then the second highest +priority cut selector is called and so on. + +Example Cut Selector +====================== + +In this example we will program a cut selector that selects the 10 most +efficacious cuts. Efficacy is the standard measure for cut quality and can be calcuated +via SCIP directly. + +.. code-block:: python + + class MaxEfficacyCutsel(Cutsel): + + def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): + """ + Selects the 10 cuts with largest efficacy. + """ + + scip = self.model + + scores = [0] * len(cuts) + for i in range(len(scores)): + scores[i] = scip.getCutEfficacy(cuts[i]) + + rankings = sorted(range(len(cuts)), key=lambda x: scores[x], reverse=True) + + sorted_cuts = [cuts[rank] for rank in rankings] + + assert len(sorted_cuts) == len(cuts) + + return {'cuts': sorted_cuts, 'nselectedcuts': min(maxnselectedcuts, len(cuts), 10), + 'result': SCIP_RESULT.SUCCESS} + + +Things to Keep in Mind +======================= + +Here are some things to keep in mind when programming your own custom cut selector. + +- Do not change any of the actual cut information! +- Do not reorder the ``forcedcuts``. They are provided as reference points to inform + the selection process. They should not be edited or reordered. +- Only reorder ``cuts``. Do not add any new cuts. diff --git a/docs/tutorials/expressions.rst b/docs/tutorials/expressions.rst new file mode 100644 index 000000000..71042e81f --- /dev/null +++ b/docs/tutorials/expressions.rst @@ -0,0 +1,187 @@ +####################### +Non-Linear Expressions +####################### + + +One of the big advantages of SCIP is that it handles arbitrary constraints. +Arbitrary here is not an exaggeration, see :doc:`the constraint tutorial `. +An advantage of this generality is that it has inbuilt support for many non-linear functions. +These non-linear expressions can be arbitrarily composed and SCIP will still find a globally +optimal solution within tolerances of the entire constraint. Below we will outline many of the +supported non-linear expressions. + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model + + scip = Model() + +.. contents:: Contents + +Non-Linear Objectives +====================== + +While SCIP supports general non-linearities, it only supports linear objective functions. +With some basic reformulation this is not a restriction however. Let's consider the general +optimization problem: + +.. math:: + + &\text{min} & \quad &f(x) \\ + &\text{s.t.} & & g(x) \leq b \\ + & & & x \in \mathbb{Z}^{|\mathcal{J}|} \times \mathbb{R}^{[n] / \mathcal{J}}, \quad \mathcal{J} \subseteq [n] \\ + & & & f : \mathbb{R}^{n} \rightarrow \mathbb{R}, g : \mathbb{R}^{n} \rightarrow \mathbb{R} + +Let's consider the case where ``f(x)`` is a non-linear function. This problem can be equivalently +reformulated as: + +.. math:: + + &\text{min} & \quad &y \\ + &\text{s.t.} & & g(x) \leq b \\ + & & & y \geq f(x) \\ + & & & x \in \mathbb{Z}^{|\mathcal{J}|} \times \mathbb{R}^{[n] / \mathcal{J}}, \quad \mathcal{J} \subseteq [n] \\ + & & & y \in \mathbb{R} \\ + & & & f : \mathbb{R}^{n} \rightarrow \mathbb{R}, g : \mathbb{R}^{n} \rightarrow \mathbb{R} + +We've now obtained an equivalent problem with a linear objective function! +The same process can be performed with a maximization problem, albeit by introducing +a ``<=`` constraint for the introduced variable. + +Let's see an example of how this would work when programming. Consider the simple problem: + +.. math:: + + &\text{min} & \quad &x^{2} + y \\ + &\text{s.t.} & & x + y \geq 5 \\ + & & & x + 1.3 y \leq 10 \\ + & & & (x,y) \in \mathbb{Z}^{2} + +One can program an equivalent optimization problem with linear objective function as follows: + +.. code-block:: python + + x = scip.addVar(vtype='I', name='x') + y = scip.addVar(vtype='I', name='y') + z = scip.addVar(vtype='I', name='z') # This will be our replacement objective variable + cons_1 = scip.addCons(x + y >= 5, name="cons_1") + cons_2 = scip.addCons(x + 1.3 * y <= 10, name="cons_2") + cons_3 = scip.addCons(z >= x * x + y, name="cons_3") + scip.setObjective(z) + + +Polynomials +============ + +Polynomials can be constructed directly from using Python operators on created variables. +Let's see an example of constructing the following constraint: + +.. math:: + + \frac{3x^{2} + y^{3}z^{2} + (2x + 3z)^{2}}{2(xz)} \leq xyz + +The code for the following constraint can be written as follows: + +.. code-block:: python + + x = scip.addVar(vtype='C', name='x') + y = scip.addVar(vtype='C', name='y') + z = scip.addVar(vtype='C', name='z') + # Build the expression slowly (or do it all in the addCons call) + lhs = 3 * (x ** 2) + ((y ** 3) * (z ** 2)) + ((2 * x) + (3 * z)) ** 2 + lhs = lhs / (2 * x * z) + cons_1 = scip.addCons(lhs <= x * y * z, name="poly_cons") + +Square Root (sqrt) +=================== + +There is native support for the square root function. Let's see an example for +constructing the following constraint: + +.. math:: + + \sqrt{x} \leq y + +The code for the following constraint can be written as follows: + +.. code-block:: python + + from pyscipopt import sqrt + x = scip.addVar(vtype='C', name='x') + y = scip.addVar(vtype='C', name='y') + cons_1 = scip.addCons(sqrt(x) <= y, name="sqrt_cons") + + +Absolute (Abs) +=============== + +Absolute values of expressions is supported by overloading how ``__abs__`` function of +SCIP expression objects. Therefore one does not need to import any functions. +Let's see an example for constructing the following constraint: + +.. math:: + + |x| \leq y + 5 + +The code for the following constraint can be written as follows: + +.. code-block:: python + + x = scip.addVar(vtype='C', lb=None, name='x') + y = scip.addVar(vtype='C', name='y') + cons_1 = scip.addCons(abs(x) <= y + 5, name="abs_cons") + +.. note:: In general many constraints containing ``abs`` functions can be reformulated + to linear constraints with the introduction of some binary variables. We recommend + reformulating when it is easily possible, as it will in general improve solver performance. + +Exponential (exp) and Log +========================== + +There is native support for the exp and log functions. Let's see an example for +constructing the following constraints: + +.. math:: + + \frac{1}{1 + e^{-x}} &= y \\ + & \\ + \log (x) &\leq z + +The code for the following constraint can be written as follows: + +.. code-block:: python + + from pyscipopt import exp, log + x = scip.addVar(vtype='C', name='x') + y = scip.addVar(vtype='C', name='y') + z = scip.addVar(vtype='C', name='z') + cons_1 = scip.addCons( (1 / (1 + exp(-x))) == y, name="exp_cons") + cons_2 = scip.addCons(log(x) <= z, name="log_cons) + + +Sin and Cosine (cos) +====================== + +There is native support for the sin and cos functions. Let's see an example for +constructing the following constraints: + +.. math:: + + sin(x) &= y \\ + & \\ + cos(y) & \leq 0.5 \\ + + +The code for the following constraint can be written as follows: + +.. code-block:: python + + from pyscipopt import cos, sin + x = scip.addVar(vtype='C', name='x') + y = scip.addVar(vtype='C', name='y') + cons_1 = scip.addCons(sin(x) == y, name="sin_cons") + cons_2 = scip.addCons(cos(y) <= 0.5, name="cos_cons") + + diff --git a/docs/tutorials/heuristic.rst b/docs/tutorials/heuristic.rst new file mode 100644 index 000000000..62daed049 --- /dev/null +++ b/docs/tutorials/heuristic.rst @@ -0,0 +1,106 @@ +########### +Heuristics +########### + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_HEURTIMING, SCIP_LPSOLSTAT + + scip = Model() + +.. contents:: Contents + +What is a Heuristic? +===================== + +A (primal) heuristic is an algorithm for finding a feasible solution to an optimization problem at lower +computational costs than their exact counterparts but without any optimality guarantees. +The reason that heuristics are implemented in exact optimization solvers are two-fold. It is advantageous +for certain algorithms to have a good intermediate solution, and it is helpful for users that they can +halt the solving process and access the current best solution. + +Simple Rounding Heuristic Example +================================= + +In this example we show how to implement a simple rounding heuristic in SCIP. The rounding heuristic +will take all the fractional variables with integer requirements from the current relaxation solution, +and attempt to round them to their nearest integer values. + +.. code-block:: python + + class SimpleRoundingHeuristic(Heur): + + def heurexec(self, heurtiming, nodeinfeasible): + + scip = self.model + result = SCIP_RESULT.DIDNOTRUN + + # This heuristic does not run if the LP status is not optimal + lpsolstat = scip.getLPSolstat() + if lpsolstat != SCIP_LPSOLSTAT.OPTIMAL: + return {"result": result} + + # We haven't added handling of implicit integers to this heuristic + if scip.getNImplVars() > 0: + return {"result": result} + + # Get the current branching candidate, i.e., the current fractional variables with integer requirements + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = scip.getLPBranchCands() + + # Ignore if there are no branching candidates + if ncands == 0: + return {"result": result} + + # Create a solution that is initialised to the LP values + sol = scip.createSol(self, initlp=True) + + # Now round the variables that can be rounded + for i in range(ncands): + old_sol_val = branch_cand_sols[i] + scip_var = branch_cands[i] + may_round_up = scip_var.varMayRound(direction="up") + may_round_down = scip_var.varMayRound(direction="down") + # If we can round in both directions then round in objective function direction + if may_round_up and may_round_down: + if scip_var.getObj() >= 0.0: + new_sol_val = scip.feasFloor(old_sol_val) + else: + new_sol_val = scip.feasCeil(old_sol_val) + elif may_round_down: + new_sol_val = scip.feasFloor(old_sol_val) + elif may_round_up: + new_sol_val = scip.feasCeil(old_sol_val) + else: + # The variable cannot be rounded. The heuristic will fail. + continue + + # Set the rounded new solution value + scip.setSolVal(sol, scip_var, new_sol_val) + + # Now try the solution. Note: This will free the solution afterwards by default. + stored = scip.trySol(sol) + + if stored: + return {"result": SCIP_RESULT.FOUNDSOL} + else: + return {"result": SCIP_RESULT.DIDNOTFIND} + +To include the heuristic in the SCIP model one would use the following code: + +.. code-block:: python + + heuristic = SimpleRoundingHeuristic() + scip.includeHeur(heuristic, "SimpleRounding", "custom heuristic implemented in python", "Y", + timingmask=SCIP_HEURTIMING.DURINGLPLOOP) + +.. note:: The ``timingmask`` is especially important when programming your own heuristic. See + `here `_ for information on timing options and how the affect + when the heuristic can be called. Note also that heuristic are as other plugins, called in order of + their priorities. + +.. note:: When you create a SCIP solution object it is important that you eventually free the object. + This is done by calling ``scip.freeSol(sol)``, although this is not necessary when the solution has been + passed to ``scip.trySol(sol)`` with ``free=True`` (default behaviour). + diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 000000000..a19d6cd3e --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,24 @@ +###################### +User Guide (Tutorials) +###################### + +This section contains official tutorials (examples) for PySCIPOpt. Please keep in mind +that PySCIPOpt's primary purpose is as a wrapper for SCIP. Therefore, for sometimes +more detailed information see `this page `_. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + model + logfile + expressions + readwrite + vartypes + constypes + branchrule + cutselector + separator + heuristic + nodeselector + lazycons \ No newline at end of file diff --git a/docs/tutorials/lazycons.rst b/docs/tutorials/lazycons.rst new file mode 100644 index 000000000..11c156c43 --- /dev/null +++ b/docs/tutorials/lazycons.rst @@ -0,0 +1,182 @@ +######################################### +Lazy Constraints (via Constraint Handler) +######################################### + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, quicksum, Conshdlr, SCIP_RESULT + + scip = Model() + +.. contents:: Contents + +What are Lazy Constraints? +========================== + +A lazy constraint is a constraint that the user believes improves computational times when removed from the +optimization problem until it is violated. Generally, lazy constraints exist in an incredibly large amount, +with most of them being trivially satisfied. + + +Why use a Constraint Handler? +============================= + +SCIP does not have a lazy constraint plug-in, rather its definition of constraint is broad enough to +naturally encompass lazy constraints already. Therefore the user must simply create an appropriate +constraint handler. + + +TSP Subtour Elimination Constraint Example +========================================== + +In this example we will examine a basic TSP integer programming formulation, where the exponential +amount of subtour elimination constraints are treated as lazy constraints. + +TSP (short for travelling salesman problem) is a classic optimization problem. Let :math:`n \in \mathbb{N}` +be the number of nodes in a graph with vertex set :math:`\mathcal{V} = \{1,...,n\}` and assume that +the graph is complete, i.e., any two vertices are connected by an edge. Each edge :math:`(i,j)` has an associated cost +:math:`c_{i,j} \in \mathbb{R}` and an associated binary variable :math:`x_{i,j}`. A standard +integer programming formulation for the problem is: + +.. math:: + + &\text{min} & & \sum_{i=1}^{n} \sum_{j=1}^{n} c_{i,j} x_{i,j} \\ + &\text{s.t.} & & \sum_{i=1}^{n} x_{i,j} = 2, \quad \forall j \in \mathcal{V} \\ + & & & \sum_{i,j \in \mathcal{S}} x_{i,j} \leq |\mathcal{S}| - 1, \quad \forall \mathcal{S} \subset \mathcal{V}, |\mathcal{S}| \geq 2 \quad (*) \\ + & & & x_{i,j} \in \{0,1\}, \quad \forall (i,j) \in \mathcal{V} \times \mathcal{V} + +In the above formulation, the second set of constraints (marked with an \*) are called subtour elimination constraints. +They are called such as a valid solution in absense of those constraints might consist of a collection +of smaller cycles instead of a single large cycle. As the constraint set requires checking every subset of nodes +there are exponentially many. Moreover, we know that most of the constraints are probably unnecessary, +because it is clear from the objective that a minimum tour does not exist with a mini-cycle of nodes that are +extremely far away from each other. Therefore, we want to model them as lazy constraints! + +For modelling these constraints using a constraint handler, the constraint handler needs to +be able to answer the following questions: + +- Is a given solution feasible? +- If the given solution is not feasible, can you do something to forbid this solution from further consideration? + +We will now create the basic model containing all information aside from the constraint handler + +.. code-block:: python + + import numpy as np + import networkx + n = 300 + x = {} + c = {} + for i in range(n): + x[i] = {} + c[i] = {} + for j in range(i + 1, n): + x[i][j] = scip.addVar(vtype='B', name=f"x_{i}_{j}") + c[i][j] = np.random.uniform(10) + scip.setObjective(quicksum(quicksum(c[i][j]*x[i][j] for j in range(i + 1, n)) for i in range(n)), "minimize") + for i in range(n): + scip.addCons(quicksum(x[i][j] for j in range(i + 1, n)) + quicksum(x[j][i] for j in range(i-1, 0, -1)) == 2, + name=f"sum_in_out_{i}") + + +Now we will create the code on how to implement such a constraint handler. + +.. code-block:: python + + # subtour elimination constraint handler + class SEC(Conshdlr): + + # method for creating a constraint of this constraint handler type + def createCons(self, name, variables): + model = self.model + cons = model.createCons(self, name) + + # data relevant for the constraint; in this case we only need to know which + # variables cannot form a subtour + cons.data = {} + cons.data['vars'] = variables + return cons + + + # find subtours in the graph induced by the edges {i,j} for which x[i][j] is positive + # at the given solution; when solution is None, the LP solution is used + def find_subtours(self, cons, solution = None): + edges = [] + x = cons.data['vars'] + + for i in list(x.keys()): + for j in list(x[i].keys()): + if self.model.getSolVal(solution, x[i][j]) > 0.5: + edges.append((i, j)) + + G = networkx.Graph() + G.add_edges_from(edges) + components = list(networkx.connected_components(G)) + + if len(components) == 1: + return [] + else: + return components + + # checks whether solution is feasible + def conscheck(self, constraints, solution, check_integrality, + check_lp_rows, print_reason, completely, **results): + + # check if there is a violated subtour elimination constraint + for cons in constraints: + if self.find_subtours(cons, solution): + return {"result": SCIP_RESULT.INFEASIBLE} + + # no violated constriant found -> feasible + return {"result": SCIP_RESULT.FEASIBLE} + + + # enforces the LP solution: searches for subtours in the solution and adds + # adds constraints forbidding all the found subtours + def consenfolp(self, constraints, n_useful_conss, sol_infeasible): + consadded = False + + for cons in constraints: + subtours = self.find_subtours(cons) + + # if there are subtours + if subtours: + x = cons.data['vars'] + + # add subtour elimination constraint for each subtour + for S in subtours: + print("Constraint added!) + self.model.addCons(quicksum(x[i][j] for i in S for j in S if j>i) <= len(S)-1) + consadded = True + + if consadded: + return {"result": SCIP_RESULT.CONSADDED} + else: + return {"result": SCIP_RESULT.FEASIBLE} + + + # this is rather technical and not relevant for the exercise. to learn more see + # https://scipopt.org/doc/html/CONS.php#CONS_FUNDAMENTALCALLBACKS + def conslock(self, constraint, locktype, nlockspos, nlocksneg): + pass + +In the above we've created our problem and custom constraint handler! We now need to actually +add the constraint handler to the problem. After that, we can simply call ``optimize`` whenever we are ready. +To add the costraint handler use something along the lines of the following: + +.. code-block:: python + + # create the constraint handler + conshdlr = SEC() + + # Add the constraint handler to SCIP. We set check priority < 0 so that only integer feasible solutions + # are passed to the conscheck callback + scip.includeConshdlr(conshdlr, "TSP", "TSP subtour eliminator", chckpriority = -10, enfopriority = -10) + + # create a subtour elimination constraint + cons = conshdlr.createCons("no_subtour_cons", x) + + # add constraint to SCIP + scip.addPyCons(cons) \ No newline at end of file diff --git a/docs/tutorials/logfile.rst b/docs/tutorials/logfile.rst new file mode 100644 index 000000000..59d408525 --- /dev/null +++ b/docs/tutorials/logfile.rst @@ -0,0 +1,283 @@ +############## +SCIP Log Files +############## + +For the following let us assume that we have called ``optimize()`` on a SCIP Model. +When running, SCIP outputs a constant stream of information on the current state of the +optimization process. + +.. contents:: Contents + +How to Read SCIP Output +======================= + +Let's consider the example complete output below: + +.. code-block:: RST + + presolving: + (round 1, fast) 136 del vars, 0 del conss, 2 add conss, 0 chg bounds, 0 chg sides, 0 chg coeffs, 0 upgd conss, 0 impls, 0 clqs + (round 2, fast) 136 del vars, 1 del conss, 2 add conss, 0 chg bounds, 132 chg sides, 0 chg coeffs, 0 upgd conss, 0 impls, 0 clqs + (round 3, exhaustive) 136 del vars, 2 del conss, 2 add conss, 0 chg bounds, 133 chg sides, 0 chg coeffs, 0 upgd conss, 0 impls, 0 clqs + (round 4, exhaustive) 136 del vars, 2 del conss, 2 add conss, 0 chg bounds, 133 chg sides, 0 chg coeffs, 131 upgd conss, 0 impls, 0 clqs + (0.0s) probing cycle finished: starting next cycle + (0.0s) symmetry computation started: requiring (bin +, int +, cont +), (fixed: bin -, int -, cont -) + (0.0s) no symmetry present (symcode time: 0.00) + presolving (5 rounds: 5 fast, 3 medium, 3 exhaustive): + 136 deleted vars, 2 deleted constraints, 2 added constraints, 0 tightened bounds, 0 added holes, 133 changed sides, 0 changed coefficients + 231 implications, 0 cliques + presolved problem has 232 variables (231 bin, 0 int, 1 impl, 0 cont) and 137 constraints + 53 constraints of type + 6 constraints of type + 78 constraints of type + transformed objective value is always integral (scale: 1) + Presolving Time: 0.01 + + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 0.0s| 1 | 0 | 409 | - | 5350k | 0 | 232 | 156 | 137 | 0 | 0 | 19 | 0 | 7.649866e+03 | -- | Inf | unknown + o 0.0s| 1 | 0 | 1064 | - |feaspump| 0 | 232 | 156 | 137 | 0 | 0 | 19 | 0 | 7.650000e+03 | 8.267000e+03 | 8.07%| unknown + 0.0s| 1 | 0 | 1064 | - | 5368k | 0 | 232 | 156 | 137 | 0 | 0 | 19 | 0 | 7.650000e+03 | 8.267000e+03 | 8.07%| unknown + 0.0s| 1 | 0 | 1064 | - | 5422k | 0 | 232 | 156 | 137 | 0 | 0 | 19 | 0 | 7.650000e+03 | 8.267000e+03 | 8.07%| unknown + 0.0s| 1 | 0 | 1067 | - | 5422k | 0 | 232 | 156 | 137 | 0 | 0 | 19 | 0 | 7.650000e+03 | 8.267000e+03 | 8.07%| unknown + 0.1s| 1 | 0 | 1132 | - | 9912k | 0 | 232 | 156 | 138 | 1 | 1 | 19 | 0 | 7.659730e+03 | 8.267000e+03 | 7.93%| unknown + 0.1s| 1 | 0 | 1133 | - | 9924k | 0 | 232 | 157 | 138 | 1 | 1 | 20 | 0 | 7.660000e+03 | 8.267000e+03 | 7.92%| unknown + 0.1s| 1 | 0 | 1134 | - | 9924k | 0 | 232 | 157 | 138 | 1 | 1 | 20 | 0 | 7.660000e+03 | 8.267000e+03 | 7.92%| unknown + 0.1s| 1 | 0 | 1210 | - | 15M | 0 | 232 | 157 | 141 | 4 | 2 | 20 | 0 | 7.671939e+03 | 8.267000e+03 | 7.76%| unknown + 0.1s| 1 | 0 | 1213 | - | 15M | 0 | 232 | 159 | 141 | 4 | 2 | 22 | 0 | 7.672000e+03 | 8.267000e+03 | 7.76%| unknown + 0.1s| 1 | 0 | 1280 | - | 18M | 0 | 232 | 157 | 143 | 6 | 3 | 22 | 0 | 7.685974e+03 | 8.267000e+03 | 7.56%| unknown + 0.1s| 1 | 0 | 1282 | - | 18M | 0 | 232 | 157 | 143 | 6 | 3 | 22 | 0 | 7.686000e+03 | 8.267000e+03 | 7.56%| unknown + 0.2s| 1 | 0 | 1353 | - | 21M | 0 | 232 | 156 | 145 | 8 | 4 | 22 | 0 | 7.701524e+03 | 8.267000e+03 | 7.34%| unknown + 0.2s| 1 | 0 | 1355 | - | 21M | 0 | 232 | 156 | 145 | 8 | 4 | 22 | 0 | 7.702000e+03 | 8.267000e+03 | 7.34%| unknown + 0.2s| 1 | 0 | 1435 | - | 24M | 0 | 232 | 156 | 147 | 10 | 5 | 22 | 0 | 7.706318e+03 | 8.267000e+03 | 7.28%| unknown + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 0.2s| 1 | 0 | 1438 | - | 24M | 0 | 232 | 158 | 147 | 10 | 5 | 24 | 0 | 7.707000e+03 | 8.267000e+03 | 7.27%| unknown + 0.2s| 1 | 0 | 1520 | - | 30M | 0 | 232 | 158 | 149 | 12 | 6 | 24 | 0 | 7.711108e+03 | 8.267000e+03 | 7.21%| unknown + 0.2s| 1 | 0 | 1521 | - | 30M | 0 | 232 | 158 | 149 | 12 | 6 | 24 | 0 | 7.712000e+03 | 8.267000e+03 | 7.20%| unknown + 0.2s| 1 | 0 | 1658 | - | 34M | 0 | 232 | 158 | 151 | 14 | 7 | 24 | 0 | 7.715238e+03 | 8.267000e+03 | 7.15%| unknown + 0.2s| 1 | 0 | 1659 | - | 34M | 0 | 232 | 158 | 151 | 14 | 7 | 24 | 0 | 7.716000e+03 | 8.267000e+03 | 7.14%| unknown + 0.3s| 1 | 0 | 1770 | - | 40M | 0 | 232 | 158 | 153 | 16 | 8 | 24 | 0 | 7.717854e+03 | 8.267000e+03 | 7.12%| unknown + 0.3s| 1 | 0 | 1771 | - | 40M | 0 | 232 | 158 | 153 | 16 | 8 | 24 | 0 | 7.718000e+03 | 8.267000e+03 | 7.11%| unknown + 0.3s| 1 | 0 | 1883 | - | 40M | 0 | 232 | 157 | 154 | 17 | 9 | 24 | 0 | 7.730185e+03 | 8.267000e+03 | 6.94%| unknown + 0.3s| 1 | 0 | 1884 | - | 40M | 0 | 232 | 157 | 154 | 17 | 9 | 24 | 0 | 7.731000e+03 | 8.267000e+03 | 6.93%| unknown + 0.3s| 1 | 0 | 1925 | - | 46M | 0 | 232 | 157 | 156 | 19 | 10 | 24 | 0 | 7.734301e+03 | 8.267000e+03 | 6.89%| unknown + 0.3s| 1 | 0 | 1926 | - | 46M | 0 | 232 | 157 | 152 | 19 | 10 | 24 | 0 | 7.735000e+03 | 8.267000e+03 | 6.88%| unknown + 0.3s| 1 | 0 | 1946 | - | 46M | 0 | 232 | 157 | 154 | 21 | 11 | 24 | 0 | 7.735000e+03 | 8.267000e+03 | 6.88%| unknown + 0.4s| 1 | 0 | 1972 | - | 46M | 0 | 232 | 157 | 156 | 23 | 12 | 24 | 0 | 7.735275e+03 | 8.267000e+03 | 6.87%| unknown + 0.4s| 1 | 0 | 1973 | - | 46M | 0 | 232 | 158 | 156 | 23 | 12 | 25 | 0 | 7.736000e+03 | 8.267000e+03 | 6.86%| unknown + 0.4s| 1 | 0 | 2007 | - | 46M | 0 | 232 | 158 | 157 | 24 | 13 | 25 | 0 | 7.736000e+03 | 8.267000e+03 | 6.86%| unknown + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 0.4s| 1 | 0 | 2057 | - | 46M | 0 | 232 | 155 | 158 | 25 | 14 | 27 | 0 | 7.737403e+03 | 8.267000e+03 | 6.84%| unknown + 0.4s| 1 | 0 | 2058 | - | 46M | 0 | 232 | 155 | 153 | 25 | 14 | 27 | 0 | 7.738000e+03 | 8.267000e+03 | 6.84%| unknown + 0.4s| 1 | 0 | 2086 | - | 46M | 0 | 232 | 155 | 154 | 26 | 15 | 27 | 0 | 7.738004e+03 | 8.267000e+03 | 6.84%| unknown + 0.4s| 1 | 0 | 2093 | - | 46M | 0 | 232 | 155 | 156 | 28 | 16 | 27 | 0 | 7.738165e+03 | 8.267000e+03 | 6.83%| unknown + 0.4s| 1 | 0 | 2094 | - | 46M | 0 | 232 | 156 | 156 | 28 | 16 | 28 | 0 | 7.739000e+03 | 8.267000e+03 | 6.82%| unknown + 0.5s| 1 | 0 | 2146 | - | 46M | 0 | 232 | 156 | 157 | 29 | 17 | 28 | 0 | 7.739168e+03 | 8.267000e+03 | 6.82%| unknown + 0.5s| 1 | 0 | 2147 | - | 46M | 0 | 232 | 156 | 157 | 29 | 17 | 28 | 0 | 7.740000e+03 | 8.267000e+03 | 6.81%| unknown + 0.5s| 1 | 0 | 2178 | - | 46M | 0 | 232 | 156 | 157 | 30 | 18 | 28 | 0 | 7.740000e+03 | 8.267000e+03 | 6.81%| unknown + 0.5s| 1 | 0 | 2223 | - | 46M | 0 | 232 | 157 | 159 | 32 | 19 | 29 | 0 | 7.740575e+03 | 8.267000e+03 | 6.80%| unknown + 0.5s| 1 | 0 | 2224 | - | 46M | 0 | 232 | 157 | 159 | 32 | 19 | 29 | 0 | 7.741000e+03 | 8.267000e+03 | 6.79%| unknown + 0.5s| 1 | 0 | 2259 | - | 46M | 0 | 232 | 157 | 160 | 33 | 20 | 29 | 0 | 7.741000e+03 | 8.267000e+03 | 6.79%| unknown + 0.5s| 1 | 0 | 2282 | - | 46M | 0 | 232 | 157 | 161 | 34 | 21 | 29 | 0 | 7.741495e+03 | 8.267000e+03 | 6.79%| unknown + 0.5s| 1 | 0 | 2283 | - | 46M | 0 | 232 | 157 | 161 | 34 | 21 | 29 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + 0.5s| 1 | 0 | 2300 | - | 46M | 0 | 232 | 157 | 159 | 35 | 22 | 29 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + 0.6s| 1 | 0 | 2342 | - | 46M | 0 | 232 | 157 | 160 | 36 | 23 | 29 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 0.6s| 1 | 0 | 2355 | - | 46M | 0 | 232 | 159 | 162 | 38 | 24 | 31 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + 0.6s| 1 | 0 | 2368 | - | 46M | 0 | 232 | 159 | 163 | 39 | 25 | 31 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + 0.6s| 1 | 0 | 2376 | - | 46M | 0 | 232 | 160 | 164 | 40 | 26 | 32 | 0 | 7.742000e+03 | 8.267000e+03 | 6.78%| unknown + L 0.8s| 1 | 0 | 2713 | - | rens| 0 | 232 | 165 | 164 | 40 | 27 | 37 | 0 | 7.742000e+03 | 8.135000e+03 | 5.08%| unknown + 0.8s| 1 | 0 | 2713 | - | 46M | 0 | 232 | 165 | 164 | 40 | 27 | 37 | 0 | 7.742000e+03 | 8.135000e+03 | 5.08%| unknown + 0.8s| 1 | 0 | 2713 | - | 46M | 0 | 232 | 165 | 164 | 40 | 27 | 37 | 0 | 7.742000e+03 | 8.135000e+03 | 5.08%| unknown + (run 1, node 1) restarting after 26 global fixings of integer variables + + (restart) converted 26 cuts from the global cut pool into linear constraints + + presolving: + (round 1, fast) 26 del vars, 9 del conss, 1 add conss, 0 chg bounds, 0 chg sides, 4 chg coeffs, 0 upgd conss, 231 impls, 0 clqs + (round 2, fast) 26 del vars, 9 del conss, 1 add conss, 0 chg bounds, 119 chg sides, 123 chg coeffs, 0 upgd conss, 231 impls, 0 clqs + (round 3, exhaustive) 26 del vars, 9 del conss, 1 add conss, 0 chg bounds, 119 chg sides, 123 chg coeffs, 13 upgd conss, 231 impls, 0 clqs + (round 4, exhaustive) 26 del vars, 9 del conss, 1 add conss, 0 chg bounds, 119 chg sides, 143 chg coeffs, 13 upgd conss, 231 impls, 0 clqs + presolving (5 rounds: 5 fast, 3 medium, 3 exhaustive): + 26 deleted vars, 9 deleted constraints, 1 added constraints, 0 tightened bounds, 0 added holes, 119 changed sides, 143 changed coefficients + 231 implications, 0 cliques + presolved problem has 206 variables (205 bin, 0 int, 1 impl, 0 cont) and 182 constraints + 65 constraints of type + 20 constraints of type + 97 constraints of type + transformed objective value is always integral (scale: 1) + Presolving Time: 0.02 + + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 0.9s| 1 | 0 | 3076 | - | 42M | 0 | 206 | 182 | 164 | 0 | 0 | 37 | 0 | 7.742000e+03 | 8.135000e+03 | 5.08%| unknown + 0.9s| 1 | 0 | 3101 | - | 42M | 0 | 206 | 182 | 166 | 2 | 1 | 38 | 0 | 7.742241e+03 | 8.135000e+03 | 5.07%| unknown + 0.9s| 1 | 0 | 3102 | - | 42M | 0 | 206 | 183 | 165 | 2 | 1 | 39 | 0 | 7.743000e+03 | 8.135000e+03 | 5.06%| unknown + 0.9s| 1 | 0 | 3103 | - | 42M | 0 | 206 | 184 | 165 | 2 | 1 | 40 | 0 | 7.743000e+03 | 8.135000e+03 | 5.06%| unknown + 0.9s| 1 | 0 | 3184 | - | 43M | 0 | 206 | 184 | 167 | 4 | 2 | 40 | 0 | 7.744329e+03 | 8.135000e+03 | 5.04%| unknown + 0.9s| 1 | 0 | 3186 | - | 43M | 0 | 206 | 187 | 167 | 4 | 2 | 43 | 0 | 7.745000e+03 | 8.135000e+03 | 5.04%| unknown + 1.0s| 1 | 0 | 3233 | - | 43M | 0 | 206 | 187 | 169 | 6 | 3 | 43 | 0 | 7.745000e+03 | 8.135000e+03 | 5.04%| unknown + 1.0s| 1 | 0 | 3252 | - | 45M | 0 | 206 | 187 | 171 | 8 | 4 | 43 | 0 | 7.745123e+03 | 8.135000e+03 | 5.03%| unknown + 1.0s| 1 | 0 | 3255 | - | 45M | 0 | 206 | 192 | 171 | 8 | 4 | 48 | 0 | 7.746000e+03 | 8.135000e+03 | 5.02%| unknown + 1.0s| 1 | 0 | 3290 | - | 45M | 0 | 206 | 192 | 173 | 10 | 5 | 48 | 0 | 7.746000e+03 | 8.135000e+03 | 5.02%| unknown + 1.1s| 1 | 0 | 3434 | - | 46M | 0 | 206 | 192 | 166 | 12 | 6 | 49 | 0 | 7.746946e+03 | 8.135000e+03 | 5.01%| unknown + 1.1s| 1 | 0 | 3435 | - | 46M | 0 | 206 | 192 | 166 | 12 | 6 | 49 | 0 | 7.747000e+03 | 8.135000e+03 | 5.01%| unknown + 1.1s| 1 | 0 | 3459 | - | 46M | 0 | 206 | 192 | 167 | 13 | 7 | 49 | 0 | 7.747318e+03 | 8.135000e+03 | 5.00%| unknown + 1.1s| 1 | 0 | 3460 | - | 46M | 0 | 206 | 192 | 167 | 13 | 7 | 49 | 0 | 7.748000e+03 | 8.135000e+03 | 4.99%| unknown + 1.2s| 1 | 0 | 3588 | - | 47M | 0 | 206 | 192 | 168 | 14 | 8 | 49 | 0 | 7.748652e+03 | 8.135000e+03 | 4.99%| unknown + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 1.2s| 1 | 0 | 3589 | - | 47M | 0 | 206 | 193 | 168 | 14 | 8 | 50 | 0 | 7.749000e+03 | 8.135000e+03 | 4.98%| unknown + 1.2s| 1 | 0 | 3622 | - | 55M | 0 | 206 | 193 | 165 | 15 | 9 | 50 | 0 | 7.749000e+03 | 8.135000e+03 | 4.98%| unknown + 1.2s| 1 | 0 | 3736 | - | 55M | 0 | 206 | 193 | 166 | 16 | 10 | 50 | 0 | 7.750062e+03 | 8.135000e+03 | 4.97%| unknown + 1.2s| 1 | 0 | 3737 | - | 55M | 0 | 206 | 193 | 166 | 16 | 10 | 50 | 0 | 7.751000e+03 | 8.135000e+03 | 4.95%| unknown + 1.2s| 1 | 0 | 3759 | - | 55M | 0 | 206 | 193 | 167 | 17 | 11 | 50 | 0 | 7.751000e+03 | 8.135000e+03 | 4.95%| unknown + 1.3s| 1 | 0 | 3823 | - | 55M | 0 | 206 | 193 | 168 | 18 | 12 | 50 | 0 | 7.751152e+03 | 8.135000e+03 | 4.95%| unknown + 1.3s| 1 | 0 | 3824 | - | 55M | 0 | 206 | 193 | 168 | 18 | 12 | 50 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 1.3s| 1 | 0 | 3829 | - | 55M | 0 | 206 | 193 | 161 | 19 | 13 | 50 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 1.3s| 1 | 0 | 3836 | - | 55M | 0 | 206 | 193 | 162 | 20 | 14 | 50 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 1.3s| 1 | 0 | 3838 | - | 55M | 0 | 206 | 193 | 163 | 21 | 15 | 50 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 1.3s| 1 | 0 | 3874 | - | 55M | 0 | 206 | 195 | 165 | 23 | 16 | 52 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 1.3s| 1 | 0 | 3878 | - | 55M | 0 | 206 | 195 | 166 | 24 | 17 | 53 | 0 | 7.752000e+03 | 8.135000e+03 | 4.94%| unknown + 2.0s| 1 | 2 | 4001 | - | 55M | 0 | 206 | 200 | 166 | 24 | 18 | 59 | 71 | 7.784907e+03 | 8.135000e+03 | 4.50%| unknown + * 2.9s| 59 | 21 | 6175 | 44.6 |strongbr| 11 | 206 | 251 | 158 | 45 | 1 | 110 | 494 | 7.846000e+03 | 8.099000e+03 | 3.22%| 17.26% + * 3.0s| 94 | 26 | 6897 | 35.7 | LP | 18 | 206 | 262 | 151 | 54 | 2 | 121 | 508 | 7.846000e+03 | 8.090000e+03 | 3.11%| 20.71% + time | node | left |LP iter|LP it/n|mem/heur|mdpt |vars |cons |rows |cuts |sepa|confs|strbr| dualbound | primalbound | gap | compl. + 3.0s| 100 | 24 | 7010 | 34.7 | 85M | 18 | 206 | 268 | 146 | 54 | 0 | 127 | 511 | 7.846000e+03 | 8.090000e+03 | 3.11%| 22.99% + 3.3s| 200 | 34 | 9281 | 28.7 | 109M | 18 | 206 | 294 | 156 | 104 | 1 | 153 | 539 | 7.868560e+03 | 8.090000e+03 | 2.81%| 35.07% + 3.5s| 300 | 32 | 10971 | 24.8 | 109M | 18 | 206 | 307 | 146 | 134 | 0 | 166 | 546 | 7.905000e+03 | 8.090000e+03 | 2.34%| 47.01% + 3.8s| 400 | 28 | 12714 | 22.9 | 109M | 18 | 206 | 322 | 146 | 159 | 0 | 181 | 557 | 7.927000e+03 | 8.090000e+03 | 2.06%| 58.37% + 4.0s| 500 | 16 | 14489 | 21.9 | 109M | 18 | 206 | 328 | 148 | 196 | 0 | 187 | 565 | 7.955492e+03 | 8.090000e+03 | 1.69%| 80.54% + + SCIP Status : problem is solved [optimal solution found] + Solving Time (sec) : 4.10 + Solving Nodes : 584 (total of 585 nodes in 2 runs) + Primal Bound : +8.09000000000000e+03 (4 solutions) + Dual Bound : +8.09000000000000e+03 + Gap : 0.00 % + +Let's now walk through information that the log provides us. We'll break down this information into +smaller bits. + +Presolve Information +******************** + +At the beginning of the run presolve information is output. The most important component is likely the final +few lines of this portion of output. For the log above, from those lines we know that our problem after presolve +has 232 variables and 137 constraints. 231 of those variables are binary with one variable being an implicit integer. +53 constraints are type knapsack, 6 are linear, and 78 are type logicor. The presolving time was 0.01s. + +Branch-and-Bound Information +**************************** + +This section has the bulk of the solve information, and comes directly after the presolve section. It can +easily be identified by it's table like content that makes it easily machine readable. The columns of +the output above are information on the following + +.. list-table:: Label Summaries + :widths: 25 25 + :align: center + :header-rows: 1 + + * - Key + - Full Description + * - time + - total solution time + * - node + - number of processed nodes + * - left + - number of unprocessed nodes + * - LP iter + - number of simplex iterations (see statistics for more accurate numbers) + * - LP it/n + - average number of LP iterations since the last output line + * - mem/heur + - total number of bytes in block memory or the creator name when a new incumbent solution was found + * - mdpt + - maximal depth of all processed nodes + * - vars + - number of variables in the problem + * - cons + - number of globally valid constraints in the problem + * - rows + - number of LP rows in current node + * - cuts + - total number of cuts applied to the LPs + * - sepa + - number of separation rounds performed at the current node + * - confs + - total number of conflicts found in conflict analysis + * - strbr + - total number of strong branching calls + * - dualbound + - current global dual bound + * - primalbound + - current primal bound + * - gap + - current (relative) gap using \|primal-dual\| / MIN(\|dual\| , \|primal\|) + * - compl. + - completion of search in percent (based on tree size estimation) + +.. note:: When a new primal solution is found a letter or asterisk appears on the left side of the current row. + An asterisk indicates that a primal solution has been found during the tree search, and a letter indicates that + a primal heuristic has found a solution (letter maps to a specific heuristic) + +The table shows the progress of the solver as it optimizes the problem. Each line snapshots a state of the +optimization process, and thereby gives users frequent updates on the quality of the current best solution, +how much memory is being used, and predicted amount of the tree search completed. + +It should be mentioned that in the log file above there is a pause in between the branch-and-bound +output and SCIP provides more presolve information. This is due to SCIP identifying that +it is beneficial to start the branch-and-bound tree again but this time applying information +it has learnt to the beginning of the search process. In the example above this is explained by the lines: + +.. code-block:: RST + + (run 1, node 1) restarting after 26 global fixings of integer variables + + (restart) converted 26 cuts from the global cut pool into linear constraints + +Final Summarised Information +**************************** + +After the branch-and-bound search is complete, SCIP provides a small amount of summarised information +that is most important for the majority of users. This includes the status (was the problem proven optimal, +or was it infeasible, did we hit a time limit, etc), the total solving time, the amount of nodes explored +in the tree (if restarts were used then nodes of the current tree differ from total nodes of all trees), +the final primal bound (objective value of the best solution), the dual bound (the strongest valid bound +at the end of the solving process), +and finally the gap (the relative difference between the primal and dual bound). + +How to Redirect SCIP Output +=========================== + +If you do not want this information output to your terminal than before calling ``optimize`` one can +call the following function: + +.. code-block:: python + + scip.hideOutput() + +If you want to redirect your output to Python instead of terminal then one can use the function: + +.. code-block:: python + + scip.redirectOutput() + +Finally, if you'd like to write the log to a file while optimizing, then one can use the function: + +.. code-block:: python + + scip.setLogfile(path_to_file) + + +SCIP Statistics +=============== + +While much information is available from the log file or can be easily queried from the Model object, +more specific information is often difficult to find, e.g., how many cuts of a certain type were applied? +For this information one must use the statistics of SCIP. The statistics can be directly printed to terminal +or can be written to a file with the following commands: + +.. code-block:: python + + scip.printStatistics() + scip.writeStatistics(filename=path_to_file) + diff --git a/docs/tutorials/model.rst b/docs/tutorials/model.rst new file mode 100644 index 000000000..57c25bd17 --- /dev/null +++ b/docs/tutorials/model.rst @@ -0,0 +1,145 @@ +##################################################################### +Introduction (Model Object, Solution Information, Parameter Settings) +##################################################################### + + +The ``Model`` object is the central Python object that you will interact with. To use the ``Model`` object +simply import it from the package directly. + +.. code-block:: python + + from pyscipopt import Model + + scip = Model() + +.. contents:: Contents + + +Create a Model, Variables, and Constraints +============================================== + +While an empty Model is still something, we ultimately want a non-empty optimization problem. Let's +consider the basic optimization problem: + +.. math:: + + &\text{min} & \quad &2x + 3y -5z \\ + &\text{s.t.} & &x + y \leq 5\\ + & & &x+z \geq 3\\ + & & &y + z = 4\\ + & & &(x,y,z) \in \mathbb{R}_{\geq 0} + +We can construct the optimization problem as follows: + +.. code-block:: python + + scip = Model() + x = scip.addVar(vtype='C', lb=0, ub=None, name='x') + y = scip.addVar(vtype='C', lb=0, ub=None, name='y') + z = scip.addVar(vtype='C', lb=0, ub=None, name='z') + cons_1 = scip.addCons(x + y <= 5, name="cons_1") + cons_1 = scip.addCons(y + z >= 3, name="cons_2") + cons_1 = scip.addCons(x + y == 5, name="cons_3") + scip.setObjective(2 * x + 3 * y - 5 * z, sense="minimize") + scip.optimize() + +That's it! We've built the optimization problem defined above and we've optimized it. +For how to read a Model from file see :doc:`this page ` and for best practices +on how to create more variables see :doc:`this page `. + +.. note:: ``vtype='C'`` here refers to a continuous variables. + Providing the lb, ub was not necessary as they default to (0, None) for continuous variables. + Providing the name attribute is not necessary either but is good practice. + Providing the objective sense was also not necessary as it defaults to "minimize". + +.. note:: An advantage of SCIP is that it can handle general non-linearities. See + :doc:`this page ` for more information on this. + +Query the Model for Solution Information +========================================= + +Now that we have successfully optimized our model, let's see some examples +of what information we can query. For example, the solving time, number of nodes, +optimal objective value, and the variable solution values in the optimal solution. + +.. code-block:: python + + solve_time = scip.getSolvingTime() + num_nodes = scip.getNTotalNodes() # Note that getNNodes() is only the number of nodes for the current run (resets at restart) + obj_val = scip.getObjVal() + for scip_var in [x, y, z]: + print(f"Variable {scip_var.name} has value {scip.getVal(scip_var)}) + +Set / Get a Parameter +===================== + +SCIP has an absolutely giant amount of parameters (see `here `_). +There is one easily accessible function for setting individual parameters. For example, +if we want to set a time limit of 20s on the solving process then we would execute the following code: + +.. code-block:: python + + scip.setParam("limits/time", 20) + +To get the value of a parameter there is also one easily accessible function. For instance, we could +now check if the time limit has been set correctly with the following code. + +.. code-block:: python + + time_limit = scip.getParam("limits/time") + +A user can set multiple parameters at once by creating a dictionary with keys corresponding to the +parameter names and values corresponding to the desired parameter values. + +.. code-block:: python + + param_dict = {"limits/time": 20} + scip.setParams(param_dict) + +To get the values of all parameters in a dictionary use the following command: + +.. code-block:: python + + param_dict = scip.getParams() + +Finally, if you have a ``.set`` file (common for using SCIP via the command-line) that contains +all the parameter values that you wish to set, then one can use the command: + +.. code-block:: python + + scip.readParams(path_to_file) + +Copy a SCIP Model +================== + +A SCIP Model can also be copied. This can be done with the following logic: + +.. code-block:: python + + scip_alternate_model = Model(sourceModel=scip) # Assuming scip is a pyscipopt Model + +This model is completely independent from the source model. The data has been duplicated. +That is, calling ``scip.optimize()`` at this point will have no effect on ``scip_alternate_model``. + +.. note:: After optimizing users often struggle with reoptimization. To make changes to an + already optimized model, one must first fo the following: + + .. code-block:: python + + scip.freeTransform() + + Without calling this function the Model can only be queried in its post optimized state. + This is because the transformed problem and all the previous solving information + is not automatically deleted, and thus stops a new optimization call. + +.. note:: To completely remove the SCIP model from memory use the following command: + + .. code-block:: python + + scip.freeProb() + + This command is potentially useful if there are memory concerns and one is creating a large amount + of different SCIP models. + + + diff --git a/docs/tutorials/nodeselector.rst b/docs/tutorials/nodeselector.rst new file mode 100644 index 000000000..37fae7398 --- /dev/null +++ b/docs/tutorials/nodeselector.rst @@ -0,0 +1,92 @@ +############# +Node Selector +############# + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model + from pyscipopt.scip import Nodesel + + scip = Model() + +.. contents:: Contents + +What is a Node Selector? +======================== + +In the branch-and-bound tree an important question that must be answered is which node should currently +be processed. That is, given a branch-and-bound tree in an intermediate state, select a a leaf node of the tree +that will be processed next (most likely branched on). In SCIP this problem has its own plug-in, +and thus custom algorithms can easily be included into the solving process! + +Example Node Selector +===================== + +In this example we are going to implement a depth first search node selection strategy. +There are two functions that we need to code ourselves when adding such a rule from python. +The first is the strategy on which node to select from all the current leaves, and the other +is a comparison function that decides which node is preferred from two candidates. + +.. code-block:: python + + # Depth First Search Node Selector + class DFS(Nodesel): + + def __init__(self, scip, *args, **kwargs): + super().__init__(*args, **kwargs) + self.scip = scip + + def nodeselect(self): + """Decide which of the leaves from the branching tree to process next""" + selnode = self.scip.getPrioChild() + if selnode is None: + selnode = self.scip.getPrioSibling() + if selnode is None: + selnode = self.scip.getBestLeaf() + + return {"selnode": selnode} + + def nodecomp(self, node1, node2): + """ + compare two leaves of the current branching tree + + It should return the following values: + + value < 0, if node 1 comes before (is better than) node 2 + value = 0, if both nodes are equally good + value > 0, if node 1 comes after (is worse than) node 2. + """ + depth_1 = node1.getDepth() + depth_2 = node2.getDepth() + if depth_1 > depth_2: + return -1 + elif depth_1 < depth_2: + return 1 + else: + lb_1 = node1.getLowerbound() + lb_2 = node2.getLowerbound() + if lb_1 < lb_2: + return -1 + elif lb_1 > lb_2: + return 1 + else: + return 0 + +.. note:: In general when implementing a node selection rule you will commonly use either ``getPrioChild`` + or ``getBestChild``. The first returns the child of the current node with + the largest node selection priority, as assigned by the branching rule. The second + returns the best child of the current node with respect to the node selector's ordering relation as defined + in ``nodecomp``. + +To include the node selector in your SCIP Model one would use the following code: + +.. code-block:: python + + dfs_node_sel = DFS(scip) + scip.includeNodesel(dfs_node_sel, "DFS", "Depth First Search Nodesel.", 1000000, 1000000) + + + + diff --git a/docs/tutorials/readwrite.rst b/docs/tutorials/readwrite.rst new file mode 100644 index 000000000..a288c2bfb --- /dev/null +++ b/docs/tutorials/readwrite.rst @@ -0,0 +1,91 @@ +##################### +Read and Write Files +##################### + +While building your own optimization problem is fun, at some point you often need to share it, or +read an optimization problem written by someone else. For this you're going to need to +write the optimization problem to some file or read it from some file. + +.. contents:: Contents + +Model File Formats +===================== + +SCIP has extensive support for a wide variety of file formats. The table below outlines +what formats those are and the model types they're associated with. + +.. list-table:: Supported File Formats + :widths: 25 25 + :align: center + :header-rows: 1 + + * - Extension + - Model Type + * - CIP + - SCIP's constraint integer programming format + * - CNF + - DIMACS CNF (conjunctive normal form) format used for example for SAT problems + * - DIFF + - reading a new objective function for mixed-integer programs + * - FZN + - FlatZinc is a low-level solver input language that is the target language for MiniZinc + * - GMS + - mixed-integer nonlinear programs (GAMS) [reading requires compilation with GAMS=true and a working GAMS system] + * - LP + - mixed-integer (quadratically constrained quadratic) programs (CPLEX) + * - MPS + - mixed-integer (quadratically constrained quadratic) programs + * - OPB + - pseudo-Boolean optimization instances + * - OSiL + - mixed-integer nonlinear programs + * - PIP + - mixed-integer polynomial programming problems + * - SOL + - solutions; XML-format (read-only) or raw SCIP format + * - WBO + - weighted pseudo-Boolean optimization instances + * - ZPL + - ZIMPL models, i.e., mixed-integer linear and nonlinear programming problems [read only] + + +.. note:: In general we recommend sharing files using the ``.mps`` extension when possible. + + For a more human readable format for equivalent problems we recommend the ``.lp`` extension. + + For general non-linearities that are to be shared with others we recommend the ``.osil`` extension. + + For general constraint types that will only be used by other SCIP users we recommend the ``.cip`` extension. + +.. note:: Some of these file formats may only have a reader programmed and not a writer. Additionally, + some of these readers may require external plug-ins that are not shipped by default via PyPI. + +Write a Model +================ + +To write a SCIP Model to a file one simply needs to run the command: + +.. code-block:: python + + from pyscipopt import Model + scip = Model() + scip.writeProblem(filename="example_file.mps", trans=False, genericnames=False) + +.. note:: Both ``trans`` and ``genericnames`` are there as their default values. The ``trans`` + option is available if you want to print the transformed problem (post presolve) instead + of the model originally created. The ``genericnames`` option is there if you want to overwrite + the variable and constraint names provided. + +Read a Model +=============== + +To read in a file to a SCIP model one simply needs to run the command: + +.. code-block:: python + + from pyscipopt import Model + scip = Model() + scip.readProblem(filename="example_file.mps") + +This will read in the file and you will now have a SCIP model that matches the file. +Variables and constraints can be queried, with their names matching those in the file. diff --git a/docs/tutorials/separator.rst b/docs/tutorials/separator.rst new file mode 100644 index 000000000..dabd46d5b --- /dev/null +++ b/docs/tutorials/separator.rst @@ -0,0 +1,311 @@ +########################## +Seperator (Cutting Planes) +########################## + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, Sepa, SCIP_RESULT + + scip = Model() + +.. contents:: Contents + + +What is a Separator? +===================== + +A separator is an algorithm for generating cutting planes (often abbreviated as cuts). +A cut is an inequality that does not remove any feasible solutions of the optimization problem but is intended +to remove some fractional solutions from the relaxation. For the purpose of this introduction we restrict ourselves +to linear cuts in this paper. A cut would then be denoted by a an array of coefficients +(:math:`\boldsymbol{\alpha} \in \mathbb{R}^{n}`) on each variable and a right-hand-side value +(:math:`\beta \in \mathbb{R}`). + +.. math:: + + \boldsymbol{\alpha}^{T}\mathbf{x} \leq \beta + +The purpose of a separator is to find cuts that tighten the relaxation of an optimization problem. +Most commonly, cuts are found that separate the current fractional feasible solution to the relaxation, +thereby making a tighter relaxation. For this reason algorithms that find cuts are often called separators. + + +Gomory Mixed-Integer Cut Example +================================ + +In this example we show to generate one of the most prolific class of cutting planes, the +Gomory mixed-integer (GMI) cut. This example looks complicated, and that's simply because it is. +While in theory a GMI cut can be quickly written down, that assumes that your basis information is nicely +accessible in the form that you want and that your problem is in standard form. For this reason the code +needs to be quite substantial to construct the cuts from scratch. + +.. note:: Separators are one of the most difficult components of a MIP solver to program due to their correctness + being difficult to verify, and numerics being a constant issue. + +.. code-block:: python + + class GMI(Sepa): + + def __init__(self): + self.ncuts = 0 + + def getGMIFromRow(self, cols, rows, binvrow, binvarow, primsol): + """ Given the row (binvarow, binvrow) of the tableau, computes gomory cut + + :param primsol: is the rhs of the tableau row. + :param cols: are the variables + :param rows: are the slack variables + :param binvrow: components of the tableau row associated to the basis inverse + :param binvarow: components of the tableau row associated to the basis inverse * A + + The GMI is given by + sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + where J_I are the integer non-basic variables and J_C are the continuous. + f_0 is the fractional part of primsol + a_j is the j-th coefficient of the row and f_j its fractional part + Note: we create -% <= -f_0 !! + Note: this formula is valid for a problem of the form Ax = b, x>= 0. Since we do not have + such problem structure in general, we have to (implicitely) transform whatever we are given + to that form. Specifically, non-basic variables at their lower bound are shifted so that the lower + bound is 0 and non-basic at their upper bound are complemented. + """ + + # initialize + cutcoefs = [0] * len(cols) + cutrhs = 0 + + # get scip + scip = self.model + + # Compute cut fractionality f0 and f0/(1-f0) + f0 = scip.frac(primsol) + ratiof0compl = f0/(1-f0) + + # rhs of the cut is the fractional part of the LP solution for the basic variable + cutrhs = -f0 + + # Generate cut coefficients for the original variables + for c in range(len(cols)): + col = cols[c] + assert col is not None # is this the equivalent of col != NULL? does it even make sense to have this assert? + status = col.getBasisStatus() + + # Get simplex tableau coefficient + if status == "lower": + # Take coefficient if nonbasic at lower bound + rowelem = binvarow[c] + elif status == "upper": + # Flip coefficient if nonbasic at upper bound: x --> u - x + rowelem = -binvarow[c] + else: + # variable is nonbasic free at zero -> cut coefficient is zero, skip OR + # variable is basic, skip + assert status == "zero" or status == "basic" + continue + + # Integer variables + if col.isIntegral(): + # warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive) + # However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true, so we ignore + # the coefficient (see below) + cutelem = scip.frac(rowelem) + + if cutelem > f0: + # sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + cutelem = -((1.0 - cutelem) * ratiof0compl) + else: + # sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + cutelem = -cutelem + else: + # Continuous variables + if rowelem < 0.0: + # -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + cutelem = rowelem * ratiof0compl + else: + # sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + cutelem = -rowelem + + # cut is define when variables are in [0, infty). Translate to general bounds + if not scip.isZero(cutelem): + if col.getBasisStatus() == "upper": + cutelem = -cutelem + cutrhs += cutelem * col.getUb() + else: + cutrhs += cutelem * col.getLb() + # Add coefficient to cut in dense form + cutcoefs[col.getLPPos()] = cutelem + + # Generate cut coefficients for the slack variables; skip basic ones + for c in range(len(rows)): + row = rows[c] + assert row != None + status = row.getBasisStatus() + + # free slack variable shouldn't appear + assert status != "zero" + + # Get simplex tableau coefficient + if status == "lower": + # Take coefficient if nonbasic at lower bound + rowelem = binvrow[row.getLPPos()] + # But if this is a >= or ranged constraint at the lower bound, we have to flip the row element + if not scip.isInfinity(-row.getLhs()): + rowelem = -rowelem + elif status == "upper": + # Take element if nonbasic at upper bound - see notes at beginning of file: only nonpositive slack variables + # can be nonbasic at upper, therefore they should be flipped twice and we can take the element directly. + rowelem = binvrow[row.getLPPos()] + else: + assert status == "basic" + continue + + # if row is integral we can strengthen the cut coefficient + if row.isIntegral() and not row.isModifiable(): + # warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive) + # However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true (see later) + cutelem = scip.frac(rowelem) + + if cutelem > f0: + # sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + cutelem = -((1.0 - cutelem) * ratiof0compl) + else: + # sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + cutelem = -cutelem + else: + # Continuous variables + if rowelem < 0.0: + # -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + cutelem = rowelem * ratiof0compl + else: + # sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + cutelem = -rowelem + + # cut is define in original variables, so we replace slack by its definition + if not scip.isZero(cutelem): + # get lhs/rhs + rlhs = row.getLhs() + rrhs = row.getRhs() + assert scip.isLE(rlhs, rrhs) + assert not scip.isInfinity(rlhs) or not scip.isInfinity(rrhs) + + # If the slack variable is fixed, we can ignore this cut coefficient + if scip.isFeasZero(rrhs - rlhs): + continue + + # Unflip slack variable and adjust rhs if necessary: row at lower means the slack variable is at its upper bound. + # Since SCIP adds +1 slacks, this can only happen when constraints have a finite lhs + if row.getBasisStatus() == "lower": + assert not scip.isInfinity(-rlhs) + cutelem = -cutelem + + rowcols = row.getCols() + rowvals = row.getVals() + + assert len(rowcols) == len(rowvals) + + # Eliminate slack variable: rowcols is sorted: [columns in LP, columns not in LP] + for i in range(row.getNLPNonz()): + cutcoefs[rowcols[i].getLPPos()] -= cutelem * rowvals[i] + + act = scip.getRowLPActivity(row) + rhsslack = rrhs - act + if scip.isFeasZero(rhsslack): + assert row.getBasisStatus() == "upper" # cutelem != 0 and row active at upper bound -> slack at lower, row at upper + cutrhs -= cutelem * (rrhs - row.getConstant()) + else: + assert scip.isFeasZero(act - rlhs) + cutrhs -= cutelem * (rlhs - row.getConstant()) + + return cutcoefs, cutrhs + + def sepaexeclp(self): + result = SCIP_RESULT.DIDNOTRUN + scip = self.model + + if not scip.isLPSolBasic(): + return {"result": result} + + # get LP data + cols = scip.getLPColsData() + rows = scip.getLPRowsData() + + # exit if LP is trivial + if len(cols) == 0 or len(rows) == 0: + return {"result": result} + + result = SCIP_RESULT.DIDNOTFIND + + # get basis indices + basisind = scip.getLPBasisInd() + + # For all basic columns (not slacks) belonging to integer variables, try to generate a gomory cut + for i in range(len(rows)): + tryrow = False + c = basisind[i] + + if c >= 0: + assert c < len(cols) + var = cols[c].getVar() + + if var.vtype() != "CONTINUOUS": + primsol = cols[c].getPrimsol() + assert scip.getSolVal(None, var) == primsol + + if 0.005 <= scip.frac(primsol) <= 1 - 0.005: + tryrow = True + + # generate the cut! + if tryrow: + # get the row of B^-1 for this basic integer variable with fractional solution value + binvrow = scip.getLPBInvRow(i) + + # get the tableau row for this basic integer variable with fractional solution value + binvarow = scip.getLPBInvARow(i) + + # get cut's coefficients + cutcoefs, cutrhs = self.getGMIFromRow(cols, rows, binvrow, binvarow, primsol) + + # add cut + cut = scip.createEmptyRowSepa(self, "gmi%d_x%d"%(self.ncuts,c if c >= 0 else -c-1), lhs = None, rhs = cutrhs) + scip.cacheRowExtensions(cut) + + for j in range(len(cutcoefs)): + if scip.isZero(cutcoefs[j]): # maybe here we need isFeasZero + continue + scip.addVarToRow(cut, cols[j].getVar(), cutcoefs[j]) + + if cut.getNNonz() == 0: + assert scip.isFeasNegative(cutrhs) + return {"result": SCIP_RESULT.CUTOFF} + + + # Only take efficacious cuts, except for cuts with one non-zero coefficient (= bound changes) + # the latter cuts will be handeled internally in sepastore. + if cut.getNNonz() == 1 or scip.isCutEfficacious(cut): + + # flush all changes before adding the cut + scip.flushRowExtensions(cut) + + infeasible = scip.addCut(cut, forcecut=True) + self.ncuts += 1 + + if infeasible: + result = SCIP_RESULT.CUTOFF + else: + result = SCIP_RESULT.SEPARATED + scip.releaseRow(cut) + + return {"result": result} + +The GMI separator can then be included using the following code: + +.. code-block:: python + + sepa = GMI() + scip.includeSepa(sepa, "python_gmi", "generates gomory mixed integer cuts", priorityS=1000, freq=1) + diff --git a/docs/tutorials/vartypes.rst b/docs/tutorials/vartypes.rst new file mode 100644 index 000000000..d49beb4ac --- /dev/null +++ b/docs/tutorials/vartypes.rst @@ -0,0 +1,232 @@ +#################### +Variables in SCIP +#################### + +In this overview of variables in PySCIPOpt we'll walk through best +practices for modelling them and the various information that +can be extracted from them. + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model, quicksum + + scip = Model() + +.. note:: In general you want to keep track of your variable objects. + They can always be obtained from the model after they are added, but then + the responsibility falls to the user to match them, e.g. by name or constraints + they feature in. + +.. contents:: Contents + +Dictionary of Variables +========================= + +Here we will store PySCIPOpt variables in a standard Python dictionary + +.. code-block:: python + + var_dict = {} + n = 5 + m = 5 + for i in range(n): + var_dict[i] = {} + for j in range(m): + var_dict[i][j] = scip.addVar(vtype='B', name=f"x_{i}_{j}") + + example_cons = {} + for i in range(n): + example_cons[i] = scip.addCons(quicksum(var_dict[i][j] for j in range(m)) == 1, name=f"cons_{i}") + +List of Variables +=================== + +Here we will store PySCIPOpt variables in a standard Python list + +.. code-block:: python + + n, m = 5, 5 + var_list = [[None for i in range(m)] for i in range(n)] + for i in range(n): + for j in range(m): + var_list[i][j] = scip.addVar(vtype='B', name=f"x_{i}_{j}") + + example_cons = [] + for i in range(n): + example_cons.append(scip.addCons(quicksum(var_list[i][j] for j in range(m)) == 1, name=f"cons_{i}")) + + +Numpy array of Variables +========================= + +Here we will store PySCIPOpt variables in a numpy ndarray + +.. code-block:: python + + import numpy as np + n, m = 5, 5 + var_array = np.zeros((n, m), dtype=object) # dtype object allows arbitrary storage + for i in range(n): + for j in range(m): + var_array[i][j] = scip.addVar(vtype='B', name=f"x_{i}_{j}") + + example_cons = np.zeros((n,), dtype=object) + for i in range(n): + example_cons[i] = scip.addCons(quicksum(var_dict[i][j] for j in range(m)) == 1, name=f"cons_{i}") + +.. note:: An advantage of using numpy array storage is that you can then use numpy operators on + the array of variables, e.g. reshape and stacking functions. It also means that you + can form PySCIPOpt expressions in bulk, similar to matrix variables in some other + packages. That is something like: + + .. code-block:: python + + a = np.random.uniform(size=(n,m)) + c = a @ var_array + +Get Variables +============= + +Given a Model object, all added variables can be retrieved with the function: + +.. code-block:: python + + scip_vars = scip.getVars() + +Variable Types +================= + +SCIP has four different types of variables: + +.. list-table:: Variable Types + :widths: 25 25 25 + :align: center + :header-rows: 1 + + * - Variable Type + - Abbreviation + - Description + * - Continuous + - C + - A continuous variable belonging to the reals with some lower and upper bound + * - Integer + - I + - An integer variable unable to take fractional values in a solution with some lower and upper bound + * - Binary + - B + - A variable restricted to the values 0 or 1. + * - Implicit Integer + - M + - A variable that is continuous but can be inferred to be integer in any valid solution + +The variable type can be queried from the Variable object. + +.. code-block:: python + + x = scip.addVar(vtype='C', name='x') + assert x.vtype() == "CONTINUOUS" + +Variable Information +======================= + +In this subsection we'll walk through some functionality that is possible with the variable +objects. + +First, we can easily obtain the objective coefficient of a variable. + +.. code-block:: python + + scip.setObjective(2 * x) + assert x.getObj() == 2.0 + +Assuming we have a solution to our problem, we can obtain the variable solution value +in the current best solution with the command: + +.. code-block:: python + + var_val = scip.getVal(x) + +An alternate way to obtain the variable solution value (can be done from whatever solution you wish) is +to query the solution object with the SCIP expression (potentially just the variable) + +.. code-block:: python + + if scip.getNSols() >= 1: + scip_sol = scip.getBestSol() + var_val = scip_sol[x] + +What is a Column? +================= + +We can also obtain the LP solution of a variable. This would be used when you have included your own +plugin, and are querying specific information for a given LP relaxation at some node. This is not the +variable solution value in the final optimal solution! + +The LP solution value brings up an interesting feature of SCIP. Is the variable even in the LP? +We can easily check this. + +.. code-block:: python + + is_in_lp = x.isInLP() + if is_in_lp: + print("Variable is in LP!") + print(f"Variable value in LP is {x.getLPSol()}") + else: + print("Variable is not in LP!") + +When you solve an optimization problem with SCIP, the problem is first transformed. This process is +called presolve, and is done to accelerate the subsequent solving process. Therefore a variable +that was originally created may have been transformed to another variable, or may have just been removed +from the transformed problem entirely. The variable may also not exist because you +are currently doing some pricing, and the LP only contains a subset of the variables. The summary is: +It should not be taken for granted that your originally created variable is in an LP. + +Now to some additional confusion. When you're solving an LP do you actually want a variable object? +The variable object contains a lot of unnecessary information that is not needed to strictly +solve the LP. This information will also have to be sent to the LP solver because SCIP is a plugin +based solver and can use many different LP solvers. Therefore, if the variable is in the LP, +it is represented by a column. The column object is the object that is actually used when solving the LP. +The column for a variable can be found with the following code: + +.. code-block:: python + + col = x.getCol() + +Information that is LP specific can be queried by the column directly. This includes the +objective value coefficient, the LP solution value, lower and upper bounds, +and of course the variable that it represents. + +.. code-block:: python + + obj_coeff = col.getObjCoeff() + lp_val = col.getPrimsol() + lb = col.getLb() + ub = col.getUb() + x = col.getVar() + +What is a Transformed Variable? +=============================== + +In the explanation of a column we touched on the transformed problem. +Naturally, in the transformed space we now have transformed variables instead of the original variables. +To access the transformed variables one can use the command: + +.. code-block:: python + + scip_vars = scip.getVars(transformed=True) + +A variable can be checked for whether it belongs to the original space or the transformed space +with the command: + + .. code-block:: python + + is_original = scip_vars[0].isOriginal() + +This difference is often important and should be kept in mind. For instance, in general the user is not interested +in the solution values of the transformed variables at the end of the solving process, rather they are interested +in the solution values of the original variables. This is because they can be interpreted easily as they +belong to some user defined formulation. + +.. note:: By default SCIP places a ``t_`` in front of all transformed variable names. diff --git a/docs/whyscip.rst b/docs/whyscip.rst new file mode 100644 index 000000000..c3ea2ae60 --- /dev/null +++ b/docs/whyscip.rst @@ -0,0 +1,138 @@ +########### +Why SCIP? +########### + +.. note:: This page is written for a user that is primarily MILP focused. + +"Why SCIP?" is an important question, and one that is in general answered by performance claims. +To be clear, SCIP is performant. It is one of the leading open-source solvers. +It manages to be competitive on a huge array of benchmarks, which include but are not limited to, +mixed-integer linear programming, mixed-integer quadratic programming, mixed-integer semidefinite +programming, mixed-integer non-linear programming, and pseudo-boolean optimization. +This page will attempt to answer the question "Why SCIP?" without relying on a performance comparison. +It will convey the scope of SCIP, how the general structure of SCIP works, +and the natural advantages (also weaknesses) SCIP has compared to other mixed-integer optimizers. + +So, why SCIP? SCIP (Solving Constraint Integer Programs). The two main points are that SCIP is likely +much more general than you expect, and it also likely provides easy to use functionality +that you didn't know was possible. + +Differences to Standard MILP Solvers +==================================== + +SCIP (Solving Constraint Integer Programs) is a constraint based solver, it is not a pure MILP solver. +This claim is thrown around frequently, but it is difficult to actually understand the difference and +the positive + negative outcomes this has on the solving process. +Let's consider a stock standard MILP definition. + +.. math:: + + &\text{min} & \quad &\mathbf{c}^{t}x \\ + &\text{s.t.} & & \mathbf{A}x \leq \mathbf{b} \\ + & & & x \in \mathbb{Z}^{|\mathcal{J}|} \times \mathbb{R}^{[n] / \mathcal{J}}, \quad \mathcal{J} \subseteq [n] + +When looking at such a problem, one probably thinks of matrices, e.g., the coefficient matrix :math:`\mathbf{A}`. +When thinking of the solving process, one probably then thinks of adding cuts and branching. Adding cuts could then +be represented by adding additional rows to the problem, and branching represented by creating two subproblems +with different variable bounds (the bounds also maybe represented as rows). SCIP's design is inherently different +from such an approach, and does not take a matrix view. +SCIP considers how to handle each constraint individually by asking the corresponding constraint handler. + +What are the ramifications of this choice? A clear negative ramification is that some matrix operations, many of which +are important for quick MILP solving, are less clear on how they'd be implemented and are likely to be less efficient. +A clear positive ramification is that now the jump to handling non-linear constraints is easier. +If there is no restriction to such a matrix representation, and each constraint is handled individually, +then why can the constraints not actually be arbitrary? That's exactly one of the core strengths of SCIP! +This can be seen in the :doc:`lazy constraint example `, where a single constraint +is constructed that will ultimately create up to exponentially many additional linear constraints +dynamically in the solving process. To emphasise this point further, one of the most common +constraint handlers in SCIP is the integrality constraint handler. +It is responsible for telling the solver how to enforce integrality for integer variables with currently +fractional solutions. That is, a constraint handler enforces integrality, and therefore it can be transparently +decided when to enforce integrality during the solving process. + +Most probably, when reading the above, an assumption has been made that when solving MILPs one needs access +to an LP solver for solving the LP relaxations. SCIP is unusual here on two points. Firstly, SCIP +does not have a built-in LP solver, but rather it has an LP interfaces that works with a huge variety of +other external LP solvers. This has the huge benefit of separating the reliance on a single LP solver, +and allows user to change the underlying LP solver if they wish. This also has a huge downside however, +which is that communication with the LP solver is now more computationally costly, and some information from the +LP solver may be inaccessible. The second unusual point is that SCIP does not need an LP solver to +prove globally optimal solutions. There is an entire field of study parallel to MILP, namely CP +(constraint programming). In CP, the problems are solved using propagation to tighten variable domains, +and the splitting of the original problem into subproblems, i.e., branching. +That is not to say that one should avoid LPs when solving MILPs, rather we claim that in the vast majority of cases +the use of LP relaxations is extremely important for good computational performance. +It is rather to say that SCIP gives the user a choice on the solving techniques used! + +Now on to the final assumption. Above we mentioned that SCIP does not need to rely on an LP solver, and that +it can use techniques from the CP community. What if we wanted to use another relaxation technique however? +For instance some convex relaxation, but necessarily a linear relaxation? This can be done in SCIP! +The functionality exists to write your own relaxator, and decide on what the relaxation of the problem should +look like at each node of the tree. + +SCIP is therefore a software at the intersection of mixed-integer programming and constraint programming. +Its technique of treating the optimization problem as a set of constraints as opposed to an array of +inequalities makes it able to naturally handle a wider class of optimization problems. + + +Modular Design and Plug-Ins +=========================== + +Maybe the biggest advantage of SCIP over other solvers, is for users in the MILP community that +want to influence the default solving process in some way. Let's take a look at the following graphic: + +.. image:: _static/scip_structure_landscape-compressed.png + :width: 600 + :align: center + :alt: SCIP Plug-In Flower structure. Each flower is a plugin, with specific algorithms being petals. + +This graphic shows the plug-in structure of SCIP. In the center we have the core of SCIP. Ignore this for now +however, and let's look at one of the various flowers of the graph. For instance, let's take +the primal heuristic flower, i.e., the yellow nodes on the right. Let's then take a look at the petals +(nodes). These have names corresponding to different heuristics. For example, there's feaspump, rins, +random rounding, and fracdiving. The beauty of the structure of SCIP is that all these primal heuristics +share a common interface. Why is this important? Because writing and including your own heuristic means +you just need to implement a small and well-defined set of functions! These functions have plenty of +documentation online, examples in Python are given in this website, and the design insulates you +from having to actually dive into the core. It is therefore incredibly accessible for MIP practitioners +and researchers to use SCIP for their own custom algorithms. + +The same logic is true for other plug-ins. These include branching rules, node selection rules, +separators (cutting planes), cut selection rules, event handlers, and constraint handlers! +Therefore if a user wants to include their own custom algorithm for one specific aspect of the solving +process, SCIP is a perfectly designed tool to do so. It's design let's users easily test the +performance impact of one rule compared to another. + +Another advantage of SCIP is in how each plug-in is called. Let's consider primal heuristics again. +When SCIP decides to run primal heuristics (a decision that is often done in the core of SCIP), +it does not need to reference the individual primal heuristics themselves. +Rather SCIP gives a command to run primal heuristics, which then iterates over +the individual primal heuristics ordered by their priorities. This is done until some stopping criteria +is met, e.g., time limit or no more heuristics with positive priority left. +This is an incredibly flexible framework! It means that we can be confident exactly when our +heuristics are being called, the order in which they're being called, and can dynamically +change the order in which they're called throughout the solving process (adjust the priorities). +This priority concept extends to all plug-ins. That is, when including your own rule (e.g. a custom branching rule) +or when setting a priority parameter of some already included rule (e.g. the ``relpscost`` branching rule), +the priority dictates the order in which the individual rules are called for that plug-in. +For example, when branching the branching rules are called in order of their priority until some +``SCIP_RESULT.BRANCHED`` status is returned. + +The design philosophy of SCIP is extremely powerful and beautiful. Each plug-in is self standing, +and does not depend on the other plug-ins. The core of SCIP therefore is heavily focused +on deciding when to call each plug-in and how to handle the information returned by each plug-in. +This philosophy does have one difficulty from a design point of view. Different rules are not +supposed to communicate each other. All communication from one rule to another rule or from plug-in to +another plug-in must go through the core of SCIP, and thereby have available functionality for all such rules +of that plug-in. Much of the most common information is available and some level of communication +happens constantly, but the result for users is that if they write two custom rules themselves +(maybe for different plug-ins), these rules should not communicate unless general functions for all +such rules are written. This is less of an issue for Python users (see the note below). + +.. note:: Because we are using Python, communication between different self-written plug-ins is easy. + Let's consider a branching plug-in and a node selection plug-in. + If the branching plug-in in Python has access to the node selector object, then it can query information + as desired. + +The structure of SCIP is therefore extremely modular, extendable, and user friendly. diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 4dbe9247f..093d805b4 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -754,6 +754,8 @@ cdef extern from "scip/scip.h": int SCIPgetNOrigVars(SCIP* scip) int SCIPgetNIntVars(SCIP* scip) int SCIPgetNBinVars(SCIP* scip) + int SCIPgetNImplVars(SCIP* scip) + int SCIPgetNContVars(SCIP* scip) SCIP_VARTYPE SCIPvarGetType(SCIP_VAR* var) SCIP_Bool SCIPvarIsOriginal(SCIP_VAR* var) SCIP_Bool SCIPvarIsTransformed(SCIP_VAR* var) @@ -773,6 +775,8 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetVarPseudocost(SCIP* scip, SCIP_VAR *var, SCIP_BRANCHDIR dir) SCIP_Real SCIPvarGetCutoffSum(SCIP_VAR* var, SCIP_BRANCHDIR dir) SCIP_Longint SCIPvarGetNBranchings(SCIP_VAR* var, SCIP_BRANCHDIR dir) + SCIP_Bool SCIPvarMayRoundUp(SCIP_VAR* var) + SCIP_Bool SCIPvarMayRoundDown(SCIP_VAR * var) # LP Methods SCIP_RETCODE SCIPgetLPColsData(SCIP* scip, SCIP_COL*** cols, int* ncols) @@ -846,7 +850,8 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPgetSolTransObj(SCIP* scip, SCIP_SOL* sol) SCIP_RETCODE SCIPcreateSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) SCIP_RETCODE SCIPcreatePartialSol(SCIP* scip, SCIP_SOL** sol,SCIP_HEUR* heur) - SCIP_RETCODE SCIPcreateOrigSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) + SCIP_RETCODE SCIPcreateOrigSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) + SCIP_RETCODE SCIPcreateLPSol(SCIP* scip, SCIP_SOL** sol, SCIP_HEUR* heur) SCIP_RETCODE SCIPsetSolVal(SCIP* scip, SCIP_SOL* sol, SCIP_VAR* var, SCIP_Real val) SCIP_RETCODE SCIPtrySolFree(SCIP* scip, SCIP_SOL** sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* stored) SCIP_RETCODE SCIPtrySol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* stored) @@ -1273,6 +1278,9 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPinfinity(SCIP* scip) SCIP_Real SCIPfrac(SCIP* scip, SCIP_Real val) SCIP_Real SCIPfeasFrac(SCIP* scip, SCIP_Real val) + SCIP_Real SCIPfeasFloor(SCIP* scip, SCIP_Real val) + SCIP_Real SCIPfeasCeil(SCIP* scip, SCIP_Real val) + SCIP_Real SCIPfeasRound(SCIP* scip, SCIP_Real val) SCIP_Bool SCIPisZero(SCIP* scip, SCIP_Real val) SCIP_Bool SCIPisFeasIntegral(SCIP* scip, SCIP_Real val) SCIP_Bool SCIPisFeasZero(SCIP* scip, SCIP_Real val) @@ -1862,6 +1870,8 @@ cdef extern from "scip/scip_tree.h": SCIP_NODE* SCIPgetBestChild(SCIP* scip) SCIP_NODE* SCIPgetBestSibling(SCIP* scip) SCIP_NODE* SCIPgetBestLeaf(SCIP* scip) + SCIP_NODE* SCIPgetPrioChild(SCIP* scip) + SCIP_NODE* SCIPgetPrioSibling(SCIP* scip) SCIP_NODE* SCIPgetBestNode(SCIP* scip) SCIP_NODE* SCIPgetBestboundNode(SCIP* scip) SCIP_RETCODE SCIPrepropagateNode(SCIP* scip, SCIP_NODE* node) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d0e223485..c164b190a 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -904,6 +904,22 @@ cdef class Variable(Expr): """Get the weighted average solution of variable in all feasible primal solutions found""" return SCIPvarGetAvgSol(self.scip_var) + def varMayRound(self, direction="down"): + """Checks whether its it possible, to round variable up and stay feasible for the relaxation + + :param direction: direction in which the variable will be rounded + + """ + if direction not in ("down", "up"): + raise Warning(f"Unrecognized direction for rounding: {direction}") + cdef SCIP_Bool mayround + if direction == "down": + mayround = SCIPvarMayRoundDown(self.scip_var) + else: + mayround = SCIPvarMayRoundUp(self.scip_var) + + return mayround + cdef class Constraint: """Base class holding a pointer to corresponding SCIP_CONS""" @@ -1247,6 +1263,18 @@ cdef class Model: """returns fractional part of value, i.e. x - floor(x) in epsilon tolerance: x - floor(x+eps)""" return SCIPfrac(self._scip, value) + def feasFloor(self, value): + """ rounds value + feasibility tolerance down to the next integer""" + return SCIPfeasFloor(self._scip, value) + + def feasCeil(self, value): + """rounds value - feasibility tolerance up to the next integer""" + return SCIPfeasCeil(self._scip, value) + + def feasRound(self, value): + """rounds value to the nearest integer in feasibility tolerance""" + return SCIPfeasRound(self._scip, value) + def isZero(self, value): """returns whether abs(value) < eps""" return SCIPisZero(self._scip, value) @@ -1748,6 +1776,7 @@ cdef class Model: ub = SCIPinfinity(self._scip) PY_SCIP_CALL(SCIPchgVarUbNode(self._scip, node.scip_node, var.scip_var, ub)) + def chgVarType(self, Variable var, vtype): """Changes the type of a variable @@ -1819,6 +1848,14 @@ cdef class Model: def getNBinVars(self): """gets number of binary active problem variables""" return SCIPgetNBinVars(self._scip) + + def getNImplVars(self): + """gets number of implicit integer active problem variables""" + return SCIPgetNImplVars(self._scip) + + def getNContVars(self): + """gets number of continuous active problem variables""" + return SCIPgetNContVars(self._scip) def getVarDict(self): """gets dictionary with variables names as keys and current variable values as items""" @@ -1854,6 +1891,14 @@ cdef class Model: """gets the best sibling of the focus node w.r.t. the node selection strategy.""" return Node.create(SCIPgetBestSibling(self._scip)) + def getPrioChild(self): + """gets the best child of the focus node w.r.t. the node selection priority assigned by the branching rule.""" + return Node.create(SCIPgetPrioChild(self._scip)) + + def getPrioSibling(self): + """gets the best sibling of the focus node w.r.t. the node selection priority assigned by the branching rule.""" + return Node.create(SCIPgetPrioSibling(self._scip)) + def getBestLeaf(self): """gets the best leaf from the node queue w.r.t. the node selection strategy.""" return Node.create(SCIPgetBestLeaf(self._scip)) @@ -4547,10 +4592,11 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) - def createSol(self, Heur heur = None): + def createSol(self, Heur heur = None, initlp=False): """Create a new primal solution in the transformed space. :param Heur heur: heuristic that found the solution (Default value = None) + :param initlp: Should the created solution be initialised to the current LP solution instead of all zeros """ cdef SCIP_HEUR* _heur @@ -4561,7 +4607,10 @@ cdef class Model: _heur = SCIPfindHeur(self._scip, n) else: _heur = NULL - PY_SCIP_CALL(SCIPcreateSol(self._scip, &_sol, _heur)) + if not initlp: + PY_SCIP_CALL(SCIPcreateSol(self._scip, &_sol, _heur)) + else: + PY_SCIP_CALL(SCIPcreateLPSol(self._scip, &_sol, _heur)) solution = Solution.create(self._scip, _sol) return solution diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index f6b5946fa..0eaff8c2f 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -1,7 +1,15 @@ from pyscipopt import Model, quicksum, SCIP_PARAMSETTING, exp, log, sqrt, sin from typing import List -def random_MIP_1(): +from pyscipopt.scip import is_memory_freed + + +def is_optimized_mode(): + model = Model() + return is_memory_freed() + + +def random_mip_1(disable_sepa=True, disable_huer=True, disable_presolve=True, node_lim=2000, small=False): model = Model() x0 = model.addVar(lb=-2, ub=4) @@ -13,13 +21,20 @@ def random_MIP_1(): u = model.addVar(vtype="I", lb=-3, ub=99) more_vars = [] - for i in range(100): + if small: + n = 100 + else: + n = 500 + for i in range(n): more_vars.append(model.addVar(vtype="I", lb=-12, ub=40)) model.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) for i in range(100): more_vars.append(model.addVar(vtype="I", lb=-52, ub=10)) - model.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) + if small: + model.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[65::2])) + else: + model.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[405::2])) model.addCons(r1 >= x0) model.addCons(r2 >= -x0) @@ -32,10 +47,20 @@ def random_MIP_1(): model.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) + if disable_sepa: + model.setSeparating(SCIP_PARAMSETTING.OFF) + if disable_huer: + model.setHeuristics(SCIP_PARAMSETTING.OFF) + if disable_presolve: + model.setPresolve(SCIP_PARAMSETTING.OFF) + model.setParam("limits/nodes", node_lim) + return model + def random_lp_1(): - return random_MIP_1().relax() + return random_mip_1().relax() + def random_nlp_1(): model = Model() @@ -46,12 +71,13 @@ def random_nlp_1(): y = model.addVar() z = model.addVar() - model.addCons(exp(v)+log(w)+sqrt(x)+sin(y)+z**3 * y <= 5) + model.addCons(exp(v) + log(w) + sqrt(x) + sin(y) + z ** 3 * y <= 5) model.setObjective(v + w + x + y + z, sense='maximize') return model -def knapsack_model(weights = [4, 2, 6, 3, 7, 5], costs = [7, 2, 5, 4, 3, 4]): + +def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4]): # create solver instance s = Model("Knapsack") s.hideOutput() @@ -72,12 +98,12 @@ def knapsack_model(weights = [4, 2, 6, 3, 7, 5], costs = [7, 2, 5, 4, 3, 4]): varNames.append(varBaseName + "_" + str(i)) knapsackVars.append(s.addVar(varNames[i], vtype='I', obj=costs[i], ub=1.0)) - # adding a linear constraint for the knapsack constraint - s.addCons(quicksum(w*v for (w, v) in zip(weights, knapsackVars)) <= knapsackSize) + s.addCons(quicksum(w * v for (w, v) in zip(weights, knapsackVars)) <= knapsackSize) return s + def bin_packing_model(sizes: List[int], capacity: int) -> Model: model = Model("Binpacking") n = len(sizes) @@ -86,23 +112,24 @@ def bin_packing_model(sizes: List[int], capacity: int) -> Model: for j in range(n): x[i, j] = model.addVar(vtype="B", name=f"x{i}_{j}") y = [model.addVar(vtype="B", name=f"y{i}") for i in range(n)] - + for i in range(n): model.addCons( quicksum(x[i, j] for j in range(n)) == 1 ) - + for j in range(n): model.addCons( quicksum(sizes[i] * x[i, j] for i in range(n)) <= capacity * y[j] ) - + model.setObjective( quicksum(y[j] for j in range(n)), "minimize" ) - + return model + # test gastrans: see example in /examples/CallableLibrary/src/gastrans.c # of course there is a more pythonic/elegant way of implementing this, probably # starting by using a proper graph structure @@ -112,98 +139,99 @@ def gastrans_model(): DENSITY = 0.616 COMPRESSIBILITY = 0.8 nodes = [ - # name supplylo supplyup pressurelo pressureup cost - ("Anderlues", 0.0, 1.2, 0.0, 66.2, 0.0 ), # 0 - ("Antwerpen", None, -4.034, 30.0, 80.0, 0.0 ), # 1 - ("Arlon", None, -0.222, 0.0, 66.2, 0.0 ), # 2 - ("Berneau", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 3 - ("Blaregnies", None, -15.616, 50.0, 66.2, 0.0 ), # 4 - ("Brugge", None, -3.918, 30.0, 80.0, 0.0 ), # 5 - ("Dudzele", 0.0, 8.4, 0.0, 77.0, 2.28 ), # 6 - ("Gent", None, -5.256, 30.0, 80.0, 0.0 ), # 7 - ("Liege", None, -6.385, 30.0, 66.2, 0.0 ), # 8 - ("Loenhout", 0.0, 4.8, 0.0, 77.0, 2.28 ), # 9 - ("Mons", None, -6.848, 0.0, 66.2, 0.0 ), # 10 - ("Namur", None, -2.120, 0.0, 66.2, 0.0 ), # 11 - ("Petange", None, -1.919, 25.0, 66.2, 0.0 ), # 12 - ("Peronnes", 0.0, 0.96, 0.0, 66.2, 1.68 ), # 13 - ("Sinsin", 0.0, 0.0, 0.0, 63.0, 0.0 ), # 14 - ("Voeren", 20.344, 22.012, 50.0, 66.2, 1.68 ), # 15 - ("Wanze", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 16 - ("Warnand", 0.0, 0.0, 0.0, 66.2, 0.0 ), # 17 - ("Zeebrugge", 8.87, 11.594, 0.0, 77.0, 2.28 ), # 18 - ("Zomergem", 0.0, 0.0, 0.0, 80.0, 0.0 ) # 19 - ] + # name supplylo supplyup pressurelo pressureup cost + ("Anderlues", 0.0, 1.2, 0.0, 66.2, 0.0), # 0 + ("Antwerpen", None, -4.034, 30.0, 80.0, 0.0), # 1 + ("Arlon", None, -0.222, 0.0, 66.2, 0.0), # 2 + ("Berneau", 0.0, 0.0, 0.0, 66.2, 0.0), # 3 + ("Blaregnies", None, -15.616, 50.0, 66.2, 0.0), # 4 + ("Brugge", None, -3.918, 30.0, 80.0, 0.0), # 5 + ("Dudzele", 0.0, 8.4, 0.0, 77.0, 2.28), # 6 + ("Gent", None, -5.256, 30.0, 80.0, 0.0), # 7 + ("Liege", None, -6.385, 30.0, 66.2, 0.0), # 8 + ("Loenhout", 0.0, 4.8, 0.0, 77.0, 2.28), # 9 + ("Mons", None, -6.848, 0.0, 66.2, 0.0), # 10 + ("Namur", None, -2.120, 0.0, 66.2, 0.0), # 11 + ("Petange", None, -1.919, 25.0, 66.2, 0.0), # 12 + ("Peronnes", 0.0, 0.96, 0.0, 66.2, 1.68), # 13 + ("Sinsin", 0.0, 0.0, 0.0, 63.0, 0.0), # 14 + ("Voeren", 20.344, 22.012, 50.0, 66.2, 1.68), # 15 + ("Wanze", 0.0, 0.0, 0.0, 66.2, 0.0), # 16 + ("Warnand", 0.0, 0.0, 0.0, 66.2, 0.0), # 17 + ("Zeebrugge", 8.87, 11.594, 0.0, 77.0, 2.28), # 18 + ("Zomergem", 0.0, 0.0, 0.0, 80.0, 0.0) # 19 + ] arcs = [ - # node1 node2 diameter length active */ - ( 18, 6, 890.0, 4.0, False ), - ( 18, 6, 890.0, 4.0, False ), - ( 6, 5, 890.0, 6.0, False ), - ( 6, 5, 890.0, 6.0, False ), - ( 5, 19, 890.0, 26.0, False ), - ( 9, 1, 590.1, 43.0, False ), - ( 1, 7, 590.1, 29.0, False ), - ( 7, 19, 590.1, 19.0, False ), - ( 19, 13, 890.0, 55.0, False ), - ( 15, 3, 890.0, 5.0, True ), - ( 15, 3, 395.0, 5.0, True ), - ( 3, 8, 890.0, 20.0, False ), - ( 3, 8, 395.0, 20.0, False ), - ( 8, 17, 890.0, 25.0, False ), - ( 8, 17, 395.0, 25.0, False ), - ( 17, 11, 890.0, 42.0, False ), - ( 11, 0, 890.0, 40.0, False ), - ( 0, 13, 890.0, 5.0, False ), - ( 13, 10, 890.0, 10.0, False ), - ( 10, 4, 890.0, 25.0, False ), - ( 17, 16, 395.5, 10.5, False ), - ( 16, 14, 315.5, 26.0, True ), - ( 14, 2, 315.5, 98.0, False ), - ( 2, 12, 315.5, 6.0, False ) - ] + # node1 node2 diameter length active */ + (18, 6, 890.0, 4.0, False), + (18, 6, 890.0, 4.0, False), + (6, 5, 890.0, 6.0, False), + (6, 5, 890.0, 6.0, False), + (5, 19, 890.0, 26.0, False), + (9, 1, 590.1, 43.0, False), + (1, 7, 590.1, 29.0, False), + (7, 19, 590.1, 19.0, False), + (19, 13, 890.0, 55.0, False), + (15, 3, 890.0, 5.0, True), + (15, 3, 395.0, 5.0, True), + (3, 8, 890.0, 20.0, False), + (3, 8, 395.0, 20.0, False), + (8, 17, 890.0, 25.0, False), + (8, 17, 395.0, 25.0, False), + (17, 11, 890.0, 42.0, False), + (11, 0, 890.0, 40.0, False), + (0, 13, 890.0, 5.0, False), + (13, 10, 890.0, 10.0, False), + (10, 4, 890.0, 25.0, False), + (17, 16, 395.5, 10.5, False), + (16, 14, 315.5, 26.0, True), + (14, 2, 315.5, 98.0, False), + (2, 12, 315.5, 6.0, False) + ] model = Model() # create flow variables flow = {} for arc in arcs: - flow[arc] = model.addVar("flow_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0]), # names of nodes in arc - lb = 0.0 if arc[4] else None) # no lower bound if not active + flow[arc] = model.addVar("flow_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), # names of nodes in arc + lb=0.0 if arc[4] else None) # no lower bound if not active # pressure difference variables pressurediff = {} for arc in arcs: - pressurediff[arc] = model.addVar("pressurediff_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0]), # names of nodes in arc - lb = None) + pressurediff[arc] = model.addVar("pressurediff_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), + # names of nodes in arc + lb=None) # supply variables supply = {} for node in nodes: - supply[node] = model.addVar("supply_%s"%(node[0]), lb = node[1], ub = node[2], obj = node[5]) + supply[node] = model.addVar("supply_%s" % (node[0]), lb=node[1], ub=node[2], obj=node[5]) # square pressure variables pressure = {} for node in nodes: - pressure[node] = model.addVar("pressure_%s"%(node[0]), lb = node[3]**2, ub = node[4]**2) - + pressure[node] = model.addVar("pressure_%s" % (node[0]), lb=node[3] ** 2, ub=node[4] ** 2) # node balance constrains, for each node i: outflows - inflows = supply for nid, node in enumerate(nodes): # find arcs that go or end at this node flowbalance = 0 for arc in arcs: - if arc[0] == nid: # arc is outgoing + if arc[0] == nid: # arc is outgoing flowbalance += flow[arc] - elif arc[1] == nid: # arc is incoming + elif arc[1] == nid: # arc is incoming flowbalance -= flow[arc] else: continue - model.addCons(flowbalance == supply[node], name="flowbalance%s"%node[0]) + model.addCons(flowbalance == supply[node], name="flowbalance%s" % node[0]) # pressure difference constraints: pressurediff[node1 to node2] = pressure[node1] - pressure[node2] for arc in arcs: - model.addCons(pressurediff[arc] == pressure[nodes[arc[0]]] - pressure[nodes[arc[1]]], "pressurediffcons_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + model.addCons(pressurediff[arc] == pressure[nodes[arc[0]]] - pressure[nodes[arc[1]]], + "pressurediffcons_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) # pressure loss constraints: # active arc: flow[arc]^2 + coef * pressurediff[arc] <= 0.0 @@ -212,19 +240,25 @@ def gastrans_model(): # lambda = (2*log10(3.7*diameter(i)/rugosity))^(-2) from math import log10 for arc in arcs: - coef = 96.074830e-15 * arc[2]**5 * (2.0*log10(3.7*arc[2]/RUGOSITY))**2 / COMPRESSIBILITY / GASTEMP / arc[3] / DENSITY - if arc[4]: # active - model.addCons(flow[arc]**2 + coef * pressurediff[arc] <= 0.0, "pressureloss_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + coef = 96.074830e-15 * arc[2] ** 5 * (2.0 * log10(3.7 * arc[2] / RUGOSITY)) ** 2 / COMPRESSIBILITY / GASTEMP / \ + arc[3] / DENSITY + if arc[4]: # active + model.addCons(flow[arc] ** 2 + coef * pressurediff[arc] <= 0.0, + "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) else: - model.addCons(flow[arc]*abs(flow[arc]) - coef * pressurediff[arc] == 0.0, "pressureloss_%s_%s"%(nodes[arc[0]][0],nodes[arc[1]][0])) + model.addCons(flow[arc] * abs(flow[arc]) - coef * pressurediff[arc] == 0.0, + "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) return model + def knapsack_lp(weights, costs): return knapsack_model(weights, costs).relax() + def bin_packing_lp(sizes, capacity): return bin_packing_model(sizes, capacity).relax() + def gastrans_lp(): - return gastrans_model().relax() \ No newline at end of file + return gastrans_model().relax() diff --git a/tests/test_branch_incomplete.py b/tests/test_branch_incomplete.py deleted file mode 100644 index 28bcf0e6d..000000000 --- a/tests/test_branch_incomplete.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -import os -from pyscipopt import Model, Branchrule, SCIP_PARAMSETTING - -@pytest.mark.skip(reason="fix later") -def test_incomplete_branchrule(): - class IncompleteBranchrule(Branchrule): - pass - - branchrule = IncompleteBranchrule() - model = Model() - model.setPresolve(SCIP_PARAMSETTING.OFF) - model.setSeparating(SCIP_PARAMSETTING.OFF) - model.setHeuristics(SCIP_PARAMSETTING.OFF) - model.includeBranchrule(branchrule, "", "", priority=10000000, maxdepth=-1, maxbounddist=1) - model.readProblem(os.path.join("tests", "data", "10teams.mps")) - - with pytest.raises(Exception): - model.optimize() diff --git a/tests/test_branch_mostinfeas.py b/tests/test_branch_mostinfeas.py new file mode 100644 index 000000000..9d80d4cd6 --- /dev/null +++ b/tests/test_branch_mostinfeas.py @@ -0,0 +1,37 @@ +from pyscipopt import Branchrule, SCIP_RESULT +from helpers.utils import random_mip_1 + + +class MostInfBranchRule(Branchrule): + + def __init__(self, scip): + self.scip = scip + + def branchexeclp(self, allowaddcons): + + # Get the branching candidates. Only consider the number of priority candidates (they are sorted to be first) + # The implicit integer candidates in general shouldn't be branched on. Unless specified by the user + # npriocands and ncands are the same (npriocands are variables that have been designated as priorities) + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands() + + # Find the variable that is most fractional + best_cand_idx = 0 + best_dist = float('inf') + for i in range(npriocands): + if abs(branch_cand_fracs[i] - 0.5) <= best_dist: + best_dist = abs(branch_cand_fracs[i] - 0.5) + best_cand_idx = i + + # Branch on the variable with the largest score + down_child, eq_child, up_child = self.model.branchVarVal( + branch_cands[best_cand_idx], branch_cand_sols[best_cand_idx]) + + return {"result": SCIP_RESULT.BRANCHED} + + +def test_branch_mostinfeas(): + scip = random_mip_1(node_lim=1000, small=True) + most_inf_branch_rule = MostInfBranchRule(scip) + scip.includeBranchrule(most_inf_branch_rule, "python-mostinf", "custom most infeasible branching rule", + priority=10000000, maxdepth=-1, maxbounddist=1) + scip.optimize() diff --git a/tests/test_heur.py b/tests/test_heur.py index 3ec3d62ba..4af260e5b 100644 --- a/tests/test_heur.py +++ b/tests/test_heur.py @@ -3,10 +3,10 @@ import pytest -from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_PARAMSETTING, SCIP_HEURTIMING +from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_PARAMSETTING, SCIP_HEURTIMING, SCIP_LPSOLSTAT from pyscipopt.scip import is_memory_freed -from util import is_optimized_mode +from helpers.utils import random_mip_1, is_optimized_mode class MyHeur(Heur): @@ -25,6 +25,63 @@ def heurexec(self, heurtiming, nodeinfeasible): else: return {"result": SCIP_RESULT.DIDNOTFIND} +class SimpleRoundingHeuristic(Heur): + + def heurexec(self, heurtiming, nodeinfeasible): + + scip = self.model + result = SCIP_RESULT.DIDNOTRUN + + # This heuristic does not run if the LP status is not optimal + lpsolstat = scip.getLPSolstat() + if lpsolstat != SCIP_LPSOLSTAT.OPTIMAL: + return {"result": result} + + # We haven't added handling of implicit integers to this heuristic + if scip.getNImplVars() > 0: + return {"result": result} + + # Get the current branching candidate, i.e., the current fractional variables with integer requirements + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = scip.getLPBranchCands() + + # Ignore if there are no branching candidates + if ncands == 0: + return {"result": result} + + # Create a solution that is initialised to the LP values + sol = scip.createSol(self, initlp=True) + + # Now round the variables that can be rounded + for i in range(ncands): + old_sol_val = branch_cand_sols[i] + scip_var = branch_cands[i] + may_round_up = scip_var.varMayRound(direction="up") + may_round_down = scip_var.varMayRound(direction="down") + # If we can round in both directions then round in objective function direction + if may_round_up and may_round_down: + if scip_var.getObj() >= 0.0: + new_sol_val = scip.feasFloor(old_sol_val) + else: + new_sol_val = scip.feasCeil(old_sol_val) + elif may_round_down: + new_sol_val = scip.feasFloor(old_sol_val) + elif may_round_up: + new_sol_val = scip.feasCeil(old_sol_val) + else: + # The variable cannot be rounded. The heuristic will fail. + continue + + # Set the rounded new solution value + scip.setSolVal(sol, scip_var, new_sol_val) + + # Now try the solution. Note: This will free the solution afterwards by default. + stored = scip.trySol(sol) + + if stored: + return {"result": SCIP_RESULT.FOUNDSOL} + else: + return {"result": SCIP_RESULT.DIDNOTFIND} + def test_heur(): # create solver instance s = Model() @@ -64,3 +121,12 @@ def inner(): heur_prox.name assert is_memory_freed() + +def test_simple_round_heur(): + # create solver instance + s = random_mip_1(disable_sepa=False, disable_huer=False, node_lim=1) + heuristic = SimpleRoundingHeuristic() + s.includeHeur(heuristic, "SimpleRounding", "simple rounding heuristic implemented in python", "Y", + timingmask=SCIP_HEURTIMING.DURINGLPLOOP) + # solve problem + s.optimize() diff --git a/tests/test_memory.py b/tests/test_memory.py index c599f72df..2433c2c6a 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -1,6 +1,6 @@ import pytest from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use -from util import is_optimized_mode +from helpers.utils import is_optimized_mode def test_not_freed(): if is_optimized_mode(): diff --git a/tests/test_nodesel.py b/tests/test_nodesel.py index 2fa4254e9..634426c4f 100644 --- a/tests/test_nodesel.py +++ b/tests/test_nodesel.py @@ -1,44 +1,98 @@ -from pyscipopt import Model +from pyscipopt import Model, SCIP_PARAMSETTING from pyscipopt.scip import Nodesel +from helpers.utils import random_mip_1 class FiFo(Nodesel): - def nodeselect(self): - '''first method called in each iteration in the main solving loop. ''' + def nodeselect(self): + """first method called in each iteration in the main solving loop.""" - leaves, children, siblings = self.model.getOpenNodes() - nodes = leaves + children + siblings + leaves, children, siblings = self.model.getOpenNodes() + nodes = leaves + children + siblings - return {"selnode" : nodes[0]} if len(nodes) > 0 else {} + return {"selnode": nodes[0]} if len(nodes) > 0 else {} - def nodecomp(self, node1, node2): - ''' - compare two leaves of the current branching tree + def nodecomp(self, node1, node2): + """ + compare two leaves of the current branching tree - It should return the following values: + It should return the following values: - value < 0, if node 1 comes before (is better than) node 2 - value = 0, if both nodes are equally good - value > 0, if node 1 comes after (is worse than) node 2. - ''' - return 0 + value < 0, if node 1 comes before (is better than) node 2 + value = 0, if both nodes are equally good + value > 0, if node 1 comes after (is worse than) node 2. + """ + return 0 -def test_nodesel(): + +# Depth First Search Node Selector +class DFS(Nodesel): + + def __init__(self, scip, *args, **kwargs): + super().__init__(*args, **kwargs) + self.scip = scip + + def nodeselect(self): + + selnode = self.scip.getPrioChild() + if selnode is None: + selnode = self.scip.getPrioSibling() + if selnode is None: + selnode = self.scip.getBestLeaf() + + return {"selnode": selnode} + + def nodecomp(self, node1, node2): + """ + compare two leaves of the current branching tree + + It should return the following values: + + value < 0, if node 1 comes before (is better than) node 2 + value = 0, if both nodes are equally good + value > 0, if node 1 comes after (is worse than) node 2. + """ + depth_1 = node1.getDepth() + depth_2 = node2.getDepth() + if depth_1 > depth_2: + return -1 + elif depth_1 < depth_2: + return 1 + else: + lb_1 = node1.getLowerbound() + lb_2 = node2.getLowerbound() + if lb_1 < lb_2: + return -1 + elif lb_1 > lb_2: + return 1 + else: + return 0 + + +def test_nodesel_fifo(): m = Model() - m.hideOutput() # include node selector m.includeNodesel(FiFo(), "testnodeselector", "Testing a node selector.", 1073741823, 536870911) # add Variables - x0 = m.addVar(vtype = "C", name = "x0", obj=-1) - x1 = m.addVar(vtype = "C", name = "x1", obj=-1) - x2 = m.addVar(vtype = "C", name = "x2", obj=-1) + x0 = m.addVar(vtype="C", name="x0", obj=-1) + x1 = m.addVar(vtype="C", name="x1", obj=-1) + x2 = m.addVar(vtype="C", name="x2", obj=-1) # add constraints m.addCons(x0 >= 2) - m.addCons(x0**2 <= x1) + m.addCons(x0 ** 2 <= x1) m.addCons(x1 * x2 >= x0) m.setObjective(x1 + x0) + m.optimize() + +def test_nodesel_dfs(): + m = random_mip_1(node_lim=500) + + # include node selector + dfs_node_sel = DFS(m) + m.includeNodesel(dfs_node_sel, "DFS", "Depth First Search Nodesel.", 1000000, 1000000) + m.optimize() \ No newline at end of file diff --git a/tests/test_relax.py b/tests/test_relax.py index 588067a2e..400e4ec0d 100644 --- a/tests/test_relax.py +++ b/tests/test_relax.py @@ -1,7 +1,7 @@ from pyscipopt import Model, SCIP_RESULT from pyscipopt.scip import Relax import pytest -from helpers.utils import random_MIP_1 +from helpers.utils import random_mip_1 calls = [] @@ -65,7 +65,7 @@ def test_empty_relaxator(): m.optimize() def test_relax(): - model = random_MIP_1() + model = random_mip_1() x = model.addVar(vtype="B") diff --git a/tests/test_strong_branching.py b/tests/test_strong_branching.py index 8cdc6c2d7..fc538df75 100644 --- a/tests/test_strong_branching.py +++ b/tests/test_strong_branching.py @@ -1,4 +1,5 @@ from pyscipopt import Model, Branchrule, SCIP_RESULT, quicksum, SCIP_PARAMSETTING +from helpers.utils import random_mip_1 """ This is a test for strong branching. It also gives a basic outline of what a function that imitates strong @@ -131,49 +132,15 @@ def branchexeclp(self, allowaddcons): avg_sol = cols[0].getVar().getAvgSol() - return {"result": SCIP_RESULT.DIDNOTRUN} - - -def create_model(): - scip = Model() - # Disable separating and heuristics as we want to branch on the problem many times before reaching optimality. - scip.setHeuristics(SCIP_PARAMSETTING.OFF) - scip.setSeparating(SCIP_PARAMSETTING.OFF) - scip.setLongintParam("limits/nodes", 2000) - - x0 = scip.addVar(lb=-2, ub=4) - r1 = scip.addVar() - r2 = scip.addVar() - y0 = scip.addVar(lb=3) - t = scip.addVar(lb=None) - l = scip.addVar(vtype="I", lb=-9, ub=18) - u = scip.addVar(vtype="I", lb=-3, ub=99) - - more_vars = [] - for i in range(100): - more_vars.append(scip.addVar(vtype="I", lb=-12, ub=40)) - scip.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + # While branching let's check some other functionality + branch_cands, _, _, _, npriocands, _ = self.scip.getLPBranchCands() - for i in range(100): - more_vars.append(scip.addVar(vtype="I", lb=-52, ub=10)) - scip.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[200::2])) - scip.addCons(r1 >= x0) - scip.addCons(r2 >= -x0) - scip.addCons(y0 == r1 + r2) - scip.addCons(t + l + 7 * u <= 300) - scip.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5 * more_vars[9]) - scip.addCons(more_vars[3] >= l + 2) - scip.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) - scip.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) - - scip.setObjective(t - quicksum(j * v for j, v in enumerate(more_vars[20:-40]))) - - return scip + return {"result": SCIP_RESULT.DIDNOTRUN} def test_strong_branching(): - scip = create_model() + scip = random_mip_1(disable_presolve=False, disable_huer=False, small=True, node_lim=500) strong_branch_rule = StrongBranchingRule(scip, idempotent=False) scip.includeBranchrule(strong_branch_rule, "strong branch rule", "custom strong branching rule", @@ -181,13 +148,14 @@ def test_strong_branching(): scip.optimize() if scip.getStatus() == "optimal": - assert scip.isEQ(-112196, scip.getObjVal()) + assert scip.isEQ(-45308, scip.getObjVal()) else: - assert -112196 <= scip.getObjVal() + if scip.getNSols() >= 1: + assert -45308 <= scip.getObjVal() def test_strong_branching_idempotent(): - scip = create_model() + scip = random_mip_1(disable_presolve=False, disable_huer=False, small=True, node_lim=500) strong_branch_rule = StrongBranchingRule(scip, idempotent=True) scip.includeBranchrule(strong_branch_rule, "strong branch rule", "custom strong branching rule", @@ -195,16 +163,17 @@ def test_strong_branching_idempotent(): scip.optimize() if scip.getStatus() == "optimal": - assert scip.isEQ(-112196, scip.getObjVal()) + assert scip.isEQ(-45308, scip.getObjVal()) else: - assert -112196 <= scip.getObjVal() + if scip.getNSols() >= 1: + assert -45308 <= scip.getObjVal() def test_dummy_feature_selector(): - scip = create_model() + scip = random_mip_1(disable_presolve=False, disable_huer=False, small=True, node_lim=300) feature_dummy_branch_rule = FeatureSelectorBranchingRule(scip) scip.includeBranchrule(feature_dummy_branch_rule, "dummy branch rule", "custom feature creation branching rule", priority=10000000, maxdepth=-1, maxbounddist=1) - scip.optimize() \ No newline at end of file + scip.optimize() diff --git a/tests/util.py b/tests/util.py deleted file mode 100644 index 8c7787eb2..000000000 --- a/tests/util.py +++ /dev/null @@ -1,7 +0,0 @@ -from pyscipopt.scip import Model, is_memory_freed - -def is_optimized_mode(): - s = Model() - return is_memory_freed() - - From aa352450406ce8f50ba31e8e3a851355a118e365 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:02:59 +0200 Subject: [PATCH 065/135] Add readthedocs.yaml (#891) --- .readthedocs.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..a5c45b9ba --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +sphinx: + builder: html + configuration: docs/conf.py + +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + +formats: all + +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file From faefd4bfe98f5e4cfb7088a2574925c7eccbba8e Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:03:38 +0200 Subject: [PATCH 066/135] Remove import pyscipopt (#892) --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0e3d665ab..e48892520 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,6 @@ # import os import sys -import pyscipopt # sys.path.insert(0, os.path.abspath('.')) From 2995f56d2d893d17ec1f26e9e9f867ee5500b29f Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:10:50 +0200 Subject: [PATCH 067/135] Add pyscipopt as docu requirement (#893) --- docs/conf.py | 1 + docs/requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index e48892520..0e3d665ab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ # import os import sys +import pyscipopt # sys.path.insert(0, os.path.abspath('.')) diff --git a/docs/requirements.txt b/docs/requirements.txt index fe66c8814..569b1214a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ sphinx sphinx-rtd-theme sphinx-nefertiti -sphinxcontrib-bibtex \ No newline at end of file +sphinxcontrib-bibtex +pyscipopt \ No newline at end of file From 86c1ec538db986c6d75448369d2e5f846289612c Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:21:12 +0200 Subject: [PATCH 068/135] Update README with new docs. Move CONTRIBUTING to docs (#894) * Update README with new docs. Move CONTRIBUTING to docs * Fix typo * Add line linking to the old documentation for API --- CONTRIBUTING.md | 78 ---------------------------- README.md | 131 +++-------------------------------------------- docs/build.rst | 9 ++++ docs/extend.rst | 89 ++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/install.rst | 2 + 6 files changed, 109 insertions(+), 201 deletions(-) delete mode 100644 CONTRIBUTING.md create mode 100644 docs/extend.rst diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f996ce613..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,78 +0,0 @@ -Contributing to PySCIPOpt -========================= - -Code contributions are very welcome and should comply to a few rules: - -1. Read [Design principles of PySCIPOpt](#design-principles-of-pyscipopt). -2. New features should be covered by tests *and* examples. Please - extend [tests](tests) and [examples](examples). Tests uses pytest - and examples are meant to be accessible for PySCIPOpt newcomers - (even advanced examples). -3. New code should be documented in the same style as the rest of - the code. -4. New features or bugfixes have to be documented in the CHANGELOG. -5. New code should be - [pep8-compliant](https://www.python.org/dev/peps/pep-0008/). Help - yourself with the [style guide - checker](https://pypi.org/project/pep8/). -6. Before implementing a new PySCIPOpt feature, check whether the - feature exists in SCIP. If so, implement it as a pure wrapper, - mimicking SCIP whenever possible. If the new feature does not exist - in SCIP but it is close to an existing one, consider if implementing - that way is substantially convenient (e.g. Pythonic). If it does - something completely different, you are welcome to pull your request - and discuss the implementation. -7. PySCIPOpt uses [semantic versioning](https://semver.org/). Version - number increase only happens on master and must be tagged to build a - new PyPI release. - -For general reference, we suggest: - -- [PySCIPOpt README](README.md); -- [SCIP documentation](http://scip.zib.de/doc/html/); -- [SCIP mailing list](https://listserv.zib.de/mailman/listinfo/scip/) - which can be easily searched with search engines (e.g. - [Google](http://www.google.com/#q=site:listserv.zib.de%2Fpipermail%2Fscip)); -- [open and closed PySCIPOpt - issues](https://github.com/SCIP-Interfaces/PySCIPOpt/issues?utf8=%E2%9C%93&q=is%3Aissue); -- [SCIP/PySCIPOpt Stack - Exchange](https://stackoverflow.com/questions/tagged/scip). - -If you find this contributing guide unclear, please open an issue! :) - -How to craft a release ----------------------- - -Moved to [RELEASE.md](RELEASE.md). - -Design principles of PySCIPOpt -============================== - -PySCIPOpt is meant to be a fast-prototyping interface of the pure SCIP C -API. By design, we distinguish different functions in PySCIPOPT: - -- pure wrapping functions of SCIP; -- convenience functions. - -**PySCIPOpt wrappers of SCIP functions** should act: - -- with an expected behavior - and parameters, returns, attributes, ... - - as close to SCIP as possible -- without *"breaking"* Python and the purpose for what the language it - is meant. - -Ideally speaking, we want every SCIP function to be wrapped in -PySCIPOpt. - -**Convenience functions** are additional, non-detrimental features meant -to help prototyping the Python way. Since these functions are not in -SCIP, we wish to limit them to prevent difference in features between -SCIP and PySCIPOPT, which are always difficult to maintain. A few -convenience functions survive in PySCIPOpt when keeping them is -doubtless beneficial. - -Admittedly, *there is a middle ground where functions are not completely -wrappers or just convenient*. That is the case, for instance, of -fundamental `Model`{.sourceCode} methods like `addCons`{.sourceCode} or -`writeProblem`{.sourceCode}. We want to leave their development to -negotiation. diff --git a/README.md b/README.md index 55985f631..b612c064d 100644 --- a/README.md +++ b/README.md @@ -13,54 +13,23 @@ This project provides an interface from Python to the [SCIP Optimization Suite]( Documentation ------------- -Please consult the [online documentation](https://scipopt.github.io/PySCIPOpt/docs/html) or use the `help()` function directly in Python or `?` in IPython/Jupyter. +Please consult the [online documentation](https://pyscipopt.readthedocs.io/en/latest/) or use the `help()` function directly in Python or `?` in IPython/Jupyter. -See [CHANGELOG.md](https://github.com/scipopt/PySCIPOpt/blob/master/CHANGELOG.md) for added, removed or fixed functionality. +The old documentation, which we are in the process of migrating from, +is still more complete w.r.t. the API, and can be found [here](https://scipopt.github.io/PySCIPOpt/docs/html/index.html) -You can also build the documentation locally with the command -```shell -pip install -r docs/requirements.txt -sphinx-build docs docs/_build -``` -Às the documentation requires additional python packages, one should run the following command -before building the documentation for the first time: -```shell -(venv) pip install -r docs/requirements.txt -``` +See [CHANGELOG.md](https://github.com/scipopt/PySCIPOpt/blob/master/CHANGELOG.md) for added, removed or fixed functionality. Installation ------------ -**Using Conda** - -[![Conda version](https://img.shields.io/conda/vn/conda-forge/pyscipopt?logo=conda-forge)](https://anaconda.org/conda-forge/pyscipopt) -[![Conda platforms](https://img.shields.io/conda/pn/conda-forge/pyscipopt?logo=conda-forge)](https://anaconda.org/conda-forge/pyscipopt) - -***DO NOT USE THE CONDA BASE ENVIRONMENT TO INSTALL PYSCIPOPT.*** - -Conda will install SCIP automatically, hence everything can be installed in a single command: +The recommended installation method is via PyPI ```bash -conda install --channel conda-forge pyscipopt +pip install pyscipopt ``` -**Using PyPI and from Source** - -See [INSTALL.md](https://github.com/scipopt/PySCIPOpt/blob/master/INSTALL.md) for instructions. -Please note that the latest PySCIPOpt version is usually only compatible with the latest major release of the SCIP Optimization Suite. -The following table summarizes which version of PySCIPOpt is required for a given SCIP version: - -|SCIP| PySCIPOpt | -|----|----| -9.1 | 5.1+ -9.0 | 5.0.x -8.0 | 4.x -7.0 | 3.x -6.0 | 2.x -5.0 | 1.4, 1.3 -4.0 | 1.2, 1.1 -3.2 | 1.0 - -Information which version of PySCIPOpt is required for a given SCIP version can also be found in [INSTALL.md](https://github.com/scipopt/PySCIPOpt/blob/master/INSTALL.md). +For information on specific versions, installation via Conda, and guides for building from source, +please see the online documentation. Building and solving a model ---------------------------- @@ -113,90 +82,6 @@ examples. Please notice that in most cases one needs to use a `dictionary` to specify the return values needed by SCIP. -Extending the interface ------------------------ - -PySCIPOpt already covers many of the SCIP callable library methods. You -may also extend it to increase the functionality of this interface. The -following will provide some directions on how this can be achieved: - -The two most important files in PySCIPOpt are the `scip.pxd` and -`scip.pxi`. These two files specify the public functions of SCIP that -can be accessed from your python code. - -To make PySCIPOpt aware of the public functions you would like to -access, you must add them to `scip.pxd`. There are two things that must -be done in order to properly add the functions: - -1) Ensure any `enum`s, `struct`s or SCIP variable types are included in - `scip.pxd`
-2) Add the prototype of the public function you wish to access to - `scip.pxd` - -After following the previous two steps, it is then possible to create -functions in python that reference the SCIP public functions included in -`scip.pxd`. This is achieved by modifying the `scip.pxi` file to add the -functionality you require. - -We are always happy to accept pull request containing patches or -extensions! - -Please have a look at our [contribution guidelines](https://github.com/scipopt/PySCIPOpt/blob/master/CONTRIBUTING.md). - -Gotchas -------- - -### Ranged constraints - -While ranged constraints of the form - -``` {.sourceCode .} -lhs <= expression <= rhs -``` - -are supported, the Python syntax for [chained -comparisons](https://docs.python.org/3.5/reference/expressions.html#comparisons) -can't be hijacked with operator overloading. Instead, parenthesis must -be used, e.g., - -``` {.sourceCode .} -lhs <= (expression <= rhs) -``` - -Alternatively, you may call `model.chgRhs(cons, newrhs)` or -`model.chgLhs(cons, newlhs)` after the single-sided constraint has been -created. - -### Variable objects - -You can't use `Variable` objects as elements of `set`s or as keys of -`dict`s. They are not hashable and comparable. The issue is that -comparisons such as `x == y` will be interpreted as linear constraints, -since `Variable`s are also `Expr` objects. - -### Dual values - -While PySCIPOpt supports access to the dual values of a solution, there -are some limitations involved: - -- Can only be used when presolving and propagation is disabled to - ensure that the LP solver - which is providing the dual - information - actually solves the unmodified problem. -- Heuristics should also be disabled to avoid that the problem is - solved before the LP solver is called. -- There should be no bound constraints, i.e., constraints with only - one variable. This can cause incorrect values as explained in - [\#136](https://github.com/scipopt/PySCIPOpt/issues/136) - -Therefore, you should use the following settings when trying to work -with dual information: - -``` {.sourceCode .python} -model.setPresolve(pyscipopt.SCIP_PARAMSETTING.OFF) -model.setHeuristics(pyscipopt.SCIP_PARAMSETTING.OFF) -model.disablePropagation() -``` - Citing PySCIPOpt ---------------- diff --git a/docs/build.rst b/docs/build.rst index 9a18b51e1..b18fe0bda 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -184,3 +184,12 @@ Ideally, the status of your tests must be passed or skipped. Running tests with pytest creates the __pycache__ directory in tests and, occasionally, a model file in the working directory. They can be removed harmlessly. +Building Documentation Locally +=============================== + +You can build the documentation locally with the command: + +.. code-block:: bash + + pip install -r docs/requirements.txt + sphinx-build docs docs/_build diff --git a/docs/extend.rst b/docs/extend.rst new file mode 100644 index 000000000..396afbcbd --- /dev/null +++ b/docs/extend.rst @@ -0,0 +1,89 @@ +######################## +Extending the Interface +######################## + +PySCIPOpt covers many of the SCIP callable library methods, however it is not complete. +One can extend the interface to encompass even more functionality that is possible with +SCIP. The following will provide some directions on how this can be achieved. + +.. contents:: Contents + +General File Structure +====================== + +The two most important files in PySCIPOpt are the ``scip.pxd`` and +``scip.pxi``. These two files specify the public functions of SCIP that +can be accessed from your python code. + +To make PySCIPOpt aware of the public functions that one can access, +one must add them to ``scip.pxd``. There are two things that must +be done in order to properly add the functions: + +- Ensure any ``enum``, ``struct`` or SCIP variable types are included in ``scip.pxd`` +- Add the prototype of the public function you wish to access to ``scip.pxd`` + +After following the previous two steps, it is then possible to create +functions in python that reference the SCIP public functions included in +``scip.pxd``. This is achieved by modifying the `scip.pxi` file to add the +functionality you require. + +Contribution Guidelines +======================= + +Code contributions are very welcome and should comply to a few rules: + +- Read Design Principles of PySCIPOpt in the section below +- New features should be covered by tests and examples. Please extend + the tests and example appropriately. Tests uses pytest + and examples are meant to be accessible for PySCIPOpt newcomers + (even advanced examples). +- New code should be documented in the same style as the rest of the code. +- New features or bugfixes have to be documented in the CHANGELOG. +- New code should be `pep8-compliant `_. Help + yourself with the `style guide checker `_. +- Before implementing a new PySCIPOpt feature, check whether the + feature exists in SCIP. If so, implement it as a pure wrapper, + mimicking SCIP whenever possible. If the new feature does not exist + in SCIP but it is close to an existing one, consider if implementing + that way is substantially convenient (e.g. Pythonic). If it does + something completely different, you are welcome to pull your request + and discuss the implementation. +- PySCIPOpt uses `semantic versioning `_. Version + number increase only happens on master and must be tagged to build a new PyPI release. + +For general reference, we suggest: + +- `SCIP documentation `_ +- `SCIP mailing list `_ +- `open and closed PySCIPOpt issues `_ +- `SCIP/PySCIPOpt Stack Exchange `_ + +If you find this contributing guide unclear, please open an issue! :) + +Design Principles of PySCIPOpt +============================== + +PySCIPOpt is meant to be a fast-prototyping interface of the pure SCIP C +API. By design, we distinguish different functions in PySCIPOPT: + +- pure wrapping functions of SCIP; +- convenience functions. + +**PySCIPOpt wrappers of SCIP functions** should act: + +- with an expected behavior - and parameters, returns, attributes, ... - as close to SCIP as possible +- without **"breaking"** Python and the purpose for what the language it is meant. + +Ideally speaking, we want every SCIP function to be wrapped in PySCIPOpt. + +**Convenience functions** are additional, non-detrimental features meant +to help prototyping the Python way. Since these functions are not in +SCIP, we wish to limit them to prevent difference in features between +SCIP and PySCIPOPT, which are always difficult to maintain. A few +convenience functions survive in PySCIPOpt when keeping them is +doubtless beneficial. + +Admittedly, there is a middle ground where functions are not completely +wrappers or just convenient. That is the case, for instance, of +fundamental ``Model`` methods like ``addCons`` or +``writeProblem``. We want to leave their development to negotiation. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a69063218..1a62ca3ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ Contents build tutorials/index whyscip + extend similarsoftware faq api diff --git a/docs/install.rst b/docs/install.rst index d9cc7613b..b66ee7c26 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -2,6 +2,8 @@ Installation Guide ################## +**This file is deprecated and will be removed soon. Please see the online documentation.** + This page will detail all methods for installing PySCIPOpt via package managers, which come with their own versions of SCIP. For building PySCIPOpt against your own custom version of SCIP, or for building PySCIPOpt from source, visit :doc:`this page `. From fb385631c7bad18de9700472901d1bbb14bc3099 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 9 Sep 2024 14:34:03 +0200 Subject: [PATCH 069/135] Easier way to attach event handlers (#895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Easier way to attach event handlers * Test new event handler callback * Update CHANGELOG --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 2 ++ src/pyscipopt/scip.pxi | 51 ++++++++++++++++++++++++++++++++++++++++++ tests/test_event.py | 21 ++++++++++++++++- 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afd5e9eb..514a8cad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ SCIPgetPrioSibling - Added additional tests to test_nodesel, test_heur, and test_strong_branching - Migrated documentation to Readthedocs +- `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers ### Fixed - Fixed too strict getObjVal, getVal check ### Changed diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 093d805b4..b0ca1ecf2 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1977,6 +1977,8 @@ cdef class Model: cdef SCIP_Bool _freescip # map to store python variables cdef _modelvars + # used to keep track of the number of event handlers generated + cdef int _generated_event_handlers_count @staticmethod cdef create(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index c164b190a..9a17d6f7d 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1043,6 +1043,7 @@ cdef class Model: self._freescip = True self._modelvars = {} + self._generated_event_handlers_count = 0 if not createscip: # if no SCIP instance should be created, then an empty Model object is created. @@ -1064,6 +1065,56 @@ cdef class Model: else: PY_SCIP_CALL(SCIPcopy(sourceModel._scip, self._scip, NULL, NULL, n, globalcopy, enablepricing, threadsafe, True, self._valid)) + + def attachEventHandlerCallback(self, + callback, + events: List[SCIP_EVENTTYPE], + name="eventhandler", + description="" + ): + """Attach an event handler to the model using a callback function. + + Parameters + ---------- + callback : callable + The callback function to be called when an event occurs. + The callback function should have the following signature: + callback(model, event) + events : list of SCIP_EVENTTYPE + List of event types to attach the event handler to. + name : str, optional + Name of the event handler. If not provided, a unique default name will be generated. + description : str, optional + Description of the event handler. If not provided, an empty string will be used. + """ + + self._generated_event_handlers_count += 1 + model = self + + class EventHandler(Eventhdlr): + def __init__(self, callback): + super(EventHandler, self).__init__() + self.callback = callback + + def eventinit(self): + for event in events: + self.model.catchEvent(event, self) + + def eventexit(self): + for event in events: + self.model.dropEvent(event, self) + + def eventexec(self, event): + self.callback(model, event) + + event_handler = EventHandler(callback) + + if name == "eventhandler": + name = f"eventhandler_{self._generated_event_handlers_count}" + + self.includeEventhdlr(event_handler, name, description) + + def __dealloc__(self): # call C function directly, because we can no longer call this object's methods, according to # http://docs.cython.org/src/reference/extension_types.html#finalization-dealloc diff --git a/tests/test_event.py b/tests/test_event.py index f07d4b384..1cd4c59d6 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -81,4 +81,23 @@ def test_event(): for j in range(1,20): s.addCons(quicksum(x[i] for i in range(100) if i%j==0) >= random.randint(10,100)) - s.optimize() \ No newline at end of file + s.optimize() + + + +def test_event_handler_callback(): + m = Model() + m.hideOutput() + + number_of_calls = 0 + + def callback(model, event): + nonlocal number_of_calls + number_of_calls += 1 + + m.attachEventHandlerCallback(callback, [SCIP_EVENTTYPE.BESTSOLFOUND]) + m.attachEventHandlerCallback(callback, [SCIP_EVENTTYPE.BESTSOLFOUND]) + + m.optimize() + + assert number_of_calls == 2 \ No newline at end of file From bdbbabbaad9ea0cc576c27da9a477c994b0131cd Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:05:12 +0200 Subject: [PATCH 070/135] Add list of contributors (#897) --- docs/contributors.rst | 39 +++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 40 insertions(+) create mode 100644 docs/contributors.rst diff --git a/docs/contributors.rst b/docs/contributors.rst new file mode 100644 index 000000000..51408494b --- /dev/null +++ b/docs/contributors.rst @@ -0,0 +1,39 @@ +############ +Contributors +############ + +.. contents:: Contents + +List of SCIP Authors +==================== + +A list of current and past SCIP developers can be found `here `__. + + +List of PySCIPOpt Contributors +============================== + +A list of all contributors to PySCIPOpt can be found +`here `__. + + +How to Cite PySCIPOpt / SCIP +============================ + +When using PySCIPOpt as part of your research, please cite the following paper: + +.. code-block:: RST + + @incollection{MaherMiltenbergerPedrosoRehfeldtSchwarzSerrano2016, + author = {Stephen Maher and Matthias Miltenberger and Jo{\~{a}}o Pedro Pedroso and Daniel Rehfeldt and Robert Schwarz and Felipe Serrano}, + title = {{PySCIPOpt}: Mathematical Programming in Python with the {SCIP} Optimization Suite}, + booktitle = {Mathematical Software {\textendash} {ICMS} 2016}, + publisher = {Springer International Publishing}, + pages = {301--307}, + year = {2016}, + doi = {10.1007/978-3-319-42432-3_37}, + } + +Additionally, please cite the corresponding SCIP Optimization Suite report, which can be found +`here `__. + diff --git a/docs/index.rst b/docs/index.rst index 1a62ca3ef..d59f04638 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,5 +21,6 @@ Contents extend similarsoftware faq + contributors api From d0b701f02ecd5ed2ffcc005348e6367db9fd0f27 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Wed, 11 Sep 2024 15:31:00 +0200 Subject: [PATCH 071/135] Add documentation for event handlers --- docs/tutorials/eventhandler.rst | 75 +++++++++++++++++++++++++++++++++ docs/tutorials/index.rst | 3 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/eventhandler.rst diff --git a/docs/tutorials/eventhandler.rst b/docs/tutorials/eventhandler.rst new file mode 100644 index 000000000..1f6051ea8 --- /dev/null +++ b/docs/tutorials/eventhandler.rst @@ -0,0 +1,75 @@ +############### +Event Handlers +############### + +For the following let us assume that a Model object is available, which is created as follows: + +.. code-block:: python + + from pyscipopt import Model + + scip = Model() + +.. contents:: Contents + +SCIP Events +=========== +SCIP provides a number of events that can be used to interact with the solver. These events would describe a change in the model state before or during the solving process; For example a variable/constraint were added or a new best solution is found. +The enum :code:`pyscipopt.SCIP_EVENTTYPE` provides a list of all available events. + + +Adding Event Handlers +============== +Event handlers are used to react to events that occur during the solving process. +They are registered with the solver and are called whenever an event occurs. +The event handler can then react to the event by performing some action. +For example, an event handler can be used to update the incumbent solution whenever a new best solution is found. + +The easiest way to create an event handler is providing a callback function to the model using the :code:`Model.attachEventHandlerCallback` method. +The following is an example the prints the value of the objective function whenever a new best solution is found: + +.. code-block:: python + + from pyscipopt import Model, SCIP_EVENTTYPE + + def print_obj_value(model, event): + print("New best solution found with objective value: {}".format(model.getObjVal())) + + m = Model() + m.attachEventHandlerCallback(print_obj_value, [SCIP_EVENTTYPE.BESTSOLFOUND]) + m.optimize() + + +The callback function should have the following signature: :code:`def callback(model, event)`. +The first argument is the model object and the second argument is the event that occurred. + + +If you need to store additional data in the event handler, you can create a custom event handler class that inherits from :code:`pyscipopt.Eventhdlr`. +and then include it in the model using the :code:`Model.includeEventHandler` method. The following is an example that stores the number of best solutions found: + +.. code-block:: python + from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE + + + class BestSolCounter(Eventhdlr): + def __init__(self, model): + Eventhdlr.__init__(model) + self.count = 0 + + def eventinit(self): + self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + + def eventexit(self): + self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + + def eventexec(self, event): + self.count += 1 + print("!!!![@BestSolCounter] New best solution found. Total best solutions found: {}".format(self.count)) + + + m = Model() + best_sol_counter = BestSolCounter(m) + m.includeEventhdlr(best_sol_counter, "best_sol_event_handler", "Event handler that counts the number of best solutions found") + m.optimize() + assert best_sol_counter.count == 1 + diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index a19d6cd3e..1ebe56198 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -21,4 +21,5 @@ more detailed information see `this page Date: Wed, 11 Sep 2024 17:39:42 +0200 Subject: [PATCH 072/135] Improvements to the event handler docs --- docs/tutorials/eventhandler.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/eventhandler.rst b/docs/tutorials/eventhandler.rst index 1f6051ea8..b9f6bcf54 100644 --- a/docs/tutorials/eventhandler.rst +++ b/docs/tutorials/eventhandler.rst @@ -18,13 +18,17 @@ SCIP provides a number of events that can be used to interact with the solver. T The enum :code:`pyscipopt.SCIP_EVENTTYPE` provides a list of all available events. -Adding Event Handlers +What's an Event Handler? ============== Event handlers are used to react to events that occur during the solving process. They are registered with the solver and are called whenever an event occurs. The event handler can then react to the event by performing some action. For example, an event handler can be used to update the incumbent solution whenever a new best solution is found. + +Adding Event Handlers with Callbacks +==================================== + The easiest way to create an event handler is providing a callback function to the model using the :code:`Model.attachEventHandlerCallback` method. The following is an example the prints the value of the objective function whenever a new best solution is found: @@ -44,10 +48,14 @@ The callback function should have the following signature: :code:`def callback(m The first argument is the model object and the second argument is the event that occurred. +Adding Event Handlers with Classes +================================== + If you need to store additional data in the event handler, you can create a custom event handler class that inherits from :code:`pyscipopt.Eventhdlr`. and then include it in the model using the :code:`Model.includeEventHandler` method. The following is an example that stores the number of best solutions found: .. code-block:: python + from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE From 6d6e27c96529625847b1a80222e2aa1c5e973701 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Wed, 11 Sep 2024 17:49:39 +0200 Subject: [PATCH 073/135] Add more equals --- docs/tutorials/eventhandler.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/eventhandler.rst b/docs/tutorials/eventhandler.rst index b9f6bcf54..57bb1080a 100644 --- a/docs/tutorials/eventhandler.rst +++ b/docs/tutorials/eventhandler.rst @@ -19,7 +19,7 @@ The enum :code:`pyscipopt.SCIP_EVENTTYPE` provides a list of all available event What's an Event Handler? -============== +======================== Event handlers are used to react to events that occur during the solving process. They are registered with the solver and are called whenever an event occurs. The event handler can then react to the event by performing some action. From de5da01354e375bc9b5fb92fdbbe1df197b7e451 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 13 Sep 2024 11:10:48 +0200 Subject: [PATCH 074/135] Update recommended version. We're currently on 9.1.0 --- src/pyscipopt/scip.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9a17d6f7d..9d727b224 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -38,7 +38,7 @@ include "nodesel.pxi" # recommended SCIP version; major version is required MAJOR = 9 -MINOR = 0 +MINOR = 1 PATCH = 0 # for external user functions use def; for functions used only inside the interface (starting with _) use cdef From a061b767f688e977fbd0648d5441ea52ca065a39 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:43:40 +0200 Subject: [PATCH 075/135] Change min requirement again (#900) SCIP version is returning conflicting information. Update to 9.0.1 requirement (identical to 9.1.0) --- src/pyscipopt/scip.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9d727b224..3854d2956 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -38,8 +38,8 @@ include "nodesel.pxi" # recommended SCIP version; major version is required MAJOR = 9 -MINOR = 1 -PATCH = 0 +MINOR = 0 +PATCH = 1 # for external user functions use def; for functions used only inside the interface (starting with _) use cdef # todo: check whether this is currently done like this From 729420c7da5f65091e58528fd4d5d30d031e232b Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:14:35 +0200 Subject: [PATCH 076/135] Change docstring style to numpydocs (#901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update half the docstrings to numpy style * Fix minor change bug * Introduce numpy style * Add new api reference style * Remove older style @p references * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Update src/pyscipopt/scip.pxi * Apply suggestions from code review * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update some comments from Joao * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Update src/pyscipopt/scip.pxi --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/api.rst | 83 +- docs/api/column.rst | 6 + docs/api/constraint.rst | 6 + docs/api/event.rst | 6 + docs/api/model.rst | 6 + docs/api/node.rst | 6 + docs/api/row.rst | 6 + docs/api/variable.rst | 6 + docs/conf.py | 5 +- src/pyscipopt/scip.pxi | 5795 +++++++++++++++++++++++++++++++-------- 11 files changed, 4767 insertions(+), 1159 deletions(-) create mode 100644 docs/api/column.rst create mode 100644 docs/api/constraint.rst create mode 100644 docs/api/event.rst create mode 100644 docs/api/model.rst create mode 100644 docs/api/node.rst create mode 100644 docs/api/row.rst create mode 100644 docs/api/variable.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 514a8cad3..7dd4af49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Fixed too strict getObjVal, getVal check ### Changed - Changed createSol to now have an option of initialising at the current LP solution +- Unified documentation style of scip.pxi to numpydocs ### Removed ## 5.1.1 - 2024-06-22 diff --git a/docs/api.rst b/docs/api.rst index 1aeae47b4..ec750ddd7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,79 +1,86 @@ ############# -API reference +API Reference ############# This page provides an auto-generated summary of PySCIPOpt's API. +.. automodule:: pyscipopt + SCIP Model ========== -.. autosummary:: - :toctree: _autosummary - :recursive: +This is the main class of PySCIPOpt. Most functionality is accessible through functions +of this class. All functions that require the SCIP object belong to this class. + +.. toctree:: + :maxdepth: 1 - pyscipopt.Model + api/model SCIP Constraint =============== -.. autosummary:: - :toctree: _autosummary - :recursive: +This class wraps a SCIP constraint object. It contains functions that can retrieve basic information +that is entirely contained within the constraint object. + +.. toctree:: + :maxdepth: 1 - pyscipopt.Constraint + api/constraint SCIP Variable ============= -.. autosummary:: - :toctree: _autosummary - :recursive: +This class wraps a SCIP variable object. It contains functions that can retrieve basic information +that is entirely contained within the variable object. - pyscipopt.Variable +.. toctree:: + :maxdepth: 1 + + api/variable SCIP Row ======== -.. autosummary:: - :toctree: _autosummary - :recursive: +This class wraps a SCIP row object. It contains functions that can retrieve basic information +that is entirely contained within the row object. + +.. toctree:: + :maxdepth: 1 - pyscipopt.scip.Row + api/row SCIP Column =========== -.. autosummary:: - :toctree: _autosummary - :recursive: +This class wraps a SCIP column object. It contains functions that can retrieve basic information +that is entirely contained within the column object. + +.. toctree:: + :maxdepth: 1 - pyscipopt.scip.Column + api/column SCIP Node ========= -.. autosummary:: - :toctree: _autosummary - :recursive: +This class wraps a SCIP node object. It contains functions that can retrieve basic information +that is entirely contained within the node object. - pyscipopt.scip.Node +.. toctree:: + :maxdepth: 1 -SCIP Solution -============= - -.. autosummary:: - :toctree: _autosummary - :recursive: - - pyscipopt.scip.Solution + api/node SCIP Event -=========== +========== + +This class wraps a SCIP event object. It contains functions that can retrieve basic information +that is entirely contained within the event object. -.. autosummary:: - :toctree: _autosummary - :recursive: +.. toctree:: + :maxdepth: 1 - pyscipopt.scip.Event + api/event diff --git a/docs/api/column.rst b/docs/api/column.rst new file mode 100644 index 000000000..938e11cd1 --- /dev/null +++ b/docs/api/column.rst @@ -0,0 +1,6 @@ +########## +Column API +########## + +.. autoclass:: pyscipopt.scip.Column + :members: \ No newline at end of file diff --git a/docs/api/constraint.rst b/docs/api/constraint.rst new file mode 100644 index 000000000..ecddfde16 --- /dev/null +++ b/docs/api/constraint.rst @@ -0,0 +1,6 @@ +############## +Constraint API +############## + +.. autoclass:: pyscipopt.Constraint + :members: \ No newline at end of file diff --git a/docs/api/event.rst b/docs/api/event.rst new file mode 100644 index 000000000..b138a5ed1 --- /dev/null +++ b/docs/api/event.rst @@ -0,0 +1,6 @@ +########## +Event API +########## + +.. autoclass:: pyscipopt.scip.Event + :members: \ No newline at end of file diff --git a/docs/api/model.rst b/docs/api/model.rst new file mode 100644 index 000000000..ee1521a6d --- /dev/null +++ b/docs/api/model.rst @@ -0,0 +1,6 @@ +######### +Model API +######### + +.. autoclass:: pyscipopt.Model + :members: \ No newline at end of file diff --git a/docs/api/node.rst b/docs/api/node.rst new file mode 100644 index 000000000..548e79e0c --- /dev/null +++ b/docs/api/node.rst @@ -0,0 +1,6 @@ +######## +Node API +######## + +.. autoclass:: pyscipopt.scip.Node + :members: \ No newline at end of file diff --git a/docs/api/row.rst b/docs/api/row.rst new file mode 100644 index 000000000..0a8ac06f4 --- /dev/null +++ b/docs/api/row.rst @@ -0,0 +1,6 @@ +####### +Row API +####### + +.. autoclass:: pyscipopt.scip.Row + :members: \ No newline at end of file diff --git a/docs/api/variable.rst b/docs/api/variable.rst new file mode 100644 index 000000000..8e7bbdd0c --- /dev/null +++ b/docs/api/variable.rst @@ -0,0 +1,6 @@ +############ +Variable API +############ + +.. autoclass:: pyscipopt.Variable + :members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 0e3d665ab..b69be4972 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,8 +82,9 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -autosummary_generate = True -autoclass_content = "class" +autosummary_generate = False +napoleon_numpy_docstring = True +napoleon_google_docstring = False pygments_style = "sphinx" diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 3854d2956..a57e322e7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -300,10 +300,24 @@ def PY_SCIP_CALL(SCIP_RETCODE rc): raise Exception('SCIP: unknown return code!') cdef class Event: - """Base class holding a pointer to corresponding SCIP_EVENT""" + """Base class holding a pointer to corresponding SCIP_EVENT.""" @staticmethod cdef create(SCIP_EVENT* scip_event): + """ + Main method for creating an Event class. Is used instead of __init__. + + Parameters + ---------- + scip_event : SCIP_EVENT* + A pointer to the SCIP_EVENT + + Returns + ------- + event : Event + The Python representative of the SCIP_EVENT + + """ if scip_event == NULL: raise Warning("cannot create Event with SCIP_EVENT* == NULL") event = Event() @@ -311,17 +325,31 @@ cdef class Event: return event def getType(self): - """gets type of event""" + """ + Gets type of event. + + Returns + ------- + PY_SCIP_EVENTTYPE + + """ return SCIPeventGetType(self.event) def getName(self): - """gets name of event""" + """ + Gets name of event. + + Returns + ------- + str + + """ if not EventNames: self._getEventNames() return EventNames[self.getType()] def _getEventNames(self): - """gets event names""" + """Gets event names.""" for name in dir(PY_SCIP_EVENTTYPE): attr = getattr(PY_SCIP_EVENTTYPE, name) if isinstance(attr, int): @@ -334,25 +362,61 @@ cdef class Event: return self.getName() def getNewBound(self): - """gets new bound for a bound change event""" + """ + Gets new bound for a bound change event. + + Returns + ------- + float + + """ return SCIPeventGetNewbound(self.event) def getOldBound(self): - """gets old bound for a bound change event""" + """ + Gets old bound for a bound change event. + + Returns + ------- + float + + """ return SCIPeventGetOldbound(self.event) def getVar(self): - """gets variable for a variable event (var added, var deleted, var fixed, objective value or domain change, domain hole added or removed)""" + """ + Gets variable for a variable event (var added, var deleted, var fixed, + objective value or domain change, domain hole added or removed). + + Returns + ------- + Variable + + """ cdef SCIP_VAR* var = SCIPeventGetVar(self.event) return Variable.create(var) def getNode(self): - """gets node for a node or LP event""" + """ + Gets node for a node or LP event. + + Returns + ------- + Node + + """ cdef SCIP_NODE* node = SCIPeventGetNode(self.event) return Node.create(node) def getRow(self): - """gets row for a row event""" + """ + Gets row for a row event. + + Returns + ------- + Row + + """ cdef SCIP_ROW* row = SCIPeventGetRow(self.event) return Row.create(row) @@ -364,10 +428,24 @@ cdef class Event: and self.event == (other).event) cdef class Column: - """Base class holding a pointer to corresponding SCIP_COL""" + """Base class holding a pointer to corresponding SCIP_COL.""" @staticmethod cdef create(SCIP_COL* scipcol): + """ + Main method for creating a Column class. Is used instead of __init__. + + Parameters + ---------- + scipcol : SCIP_COL* + A pointer to the SCIP_COL + + Returns + ------- + col : Column + The Python representative of the SCIP_COL + + """ if scipcol == NULL: raise Warning("cannot create Column with SCIP_COL* == NULL") col = Column() @@ -375,11 +453,35 @@ cdef class Column: return col def getLPPos(self): - """gets position of column in current LP, or -1 if it is not in LP""" + """ + Gets position of column in current LP, or -1 if it is not in LP. + + Returns + ------- + int + + """ return SCIPcolGetLPPos(self.scip_col) def getBasisStatus(self): - """gets the basis status of a column in the LP solution, Note: returns basis status `zero` for columns not in the current SCIP LP""" + """ + Gets the basis status of a column in the LP solution + + Returns + ------- + str + Possible values are "lower", "basic", "upper", and "zero" + + Raises + ------ + Exception + If SCIP returns an unknown basis status + + Notes + ----- + Returns basis status "zero" for columns not in the current SCIP LP. + + """ cdef SCIP_BASESTAT stat = SCIPcolGetBasisStatus(self.scip_col) if stat == SCIP_BASESTAT_LOWER: return "lower" @@ -393,33 +495,82 @@ cdef class Column: raise Exception('SCIP returned unknown base status!') def isIntegral(self): - """returns whether the associated variable is of integral type (binary, integer, implicit integer)""" + """ + Returns whether the associated variable is of integral type (binary, integer, implicit integer). + + Returns + ------- + bool + + """ return SCIPcolIsIntegral(self.scip_col) def getVar(self): - """gets variable this column represents""" + """ + Gets variable this column represents. + + Returns + ------- + Variable + + """ cdef SCIP_VAR* var = SCIPcolGetVar(self.scip_col) return Variable.create(var) def getPrimsol(self): - """gets the primal LP solution of a column""" + """ + Gets the primal LP solution of a column. + + Returns + ------- + float + + """ return SCIPcolGetPrimsol(self.scip_col) def getLb(self): - """gets lower bound of column""" + """ + Gets lower bound of column. + + Returns + ------- + float + + """ return SCIPcolGetLb(self.scip_col) def getUb(self): - """gets upper bound of column""" + """ + Gets upper bound of column. + + Returns + ------- + float + + """ return SCIPcolGetUb(self.scip_col) def getObjCoeff(self): - """gets objective value coefficient of a column""" + """ + Gets objective value coefficient of a column. + + Returns + ------- + float + + """ return SCIPcolGetObj(self.scip_col) def getAge(self): - """Gets the age of the column, i.e., the total number of successive times a column was in the LP - and was 0.0 in the solution""" + """ + Gets the age of the column, i.e., the total number of successive times a column was in the LP + and was 0.0 in the solution. + + Returns + ------- + int + + """ return SCIPcolGetAge(self.scip_col) def __hash__(self): @@ -430,10 +581,24 @@ cdef class Column: and self.scip_col == (other).scip_col) cdef class Row: - """Base class holding a pointer to corresponding SCIP_ROW""" + """Base class holding a pointer to corresponding SCIP_ROW.""" @staticmethod cdef create(SCIP_ROW* sciprow): + """ + Main method for creating a Row class. Is used instead of __init__. + + Parameters + ---------- + sciprow : SCIP_ROW* + A pointer to the SCIP_ROW + + Returns + ------- + row : Row + The Python representative of the SCIP_ROW + + """ if sciprow == NULL: raise Warning("cannot create Row with SCIP_ROW* == NULL") row = Row() @@ -446,23 +611,68 @@ cdef class Row: return cname.decode('utf-8') def getLhs(self): - """returns the left hand side of row""" + """ + Returns the left hand side of row. + + Returns + ------- + float + + """ return SCIProwGetLhs(self.scip_row) def getRhs(self): - """returns the right hand side of row""" + """ + Returns the right hand side of row. + + Returns + ------- + float + + """ return SCIProwGetRhs(self.scip_row) def getConstant(self): - """gets constant shift of row""" + """ + Gets constant shift of row. + + Returns + ------- + float + + """ return SCIProwGetConstant(self.scip_row) def getLPPos(self): - """gets position of row in current LP, or -1 if it is not in LP""" + """ + Gets position of row in current LP, or -1 if it is not in LP. + + Returns + ------- + int + + """ return SCIProwGetLPPos(self.scip_row) def getBasisStatus(self): - """gets the basis status of a row in the LP solution, Note: returns basis status `basic` for rows not in the current SCIP LP""" + """ + Gets the basis status of a row in the LP solution. + + Returns + ------- + str + Possible values are "lower", "basic", and "upper" + + Raises + ------ + Exception + If SCIP returns an unknown or "zero" basis status + + Notes + ----- + Returns basis status "basic" for rows not in the current SCIP LP. + + """ cdef SCIP_BASESTAT stat = SCIProwGetBasisStatus(self.scip_row) if stat == SCIP_BASESTAT_LOWER: return "lower" @@ -477,58 +687,150 @@ cdef class Row: raise Exception('SCIP returned unknown base status!') def isIntegral(self): - """returns TRUE iff the activity of the row (without the row's constant) is always integral in a feasible solution """ + """ + Returns TRUE iff the activity of the row (without the row's constant) + is always integral in a feasible solution. + + Returns + ------- + bool + + """ return SCIProwIsIntegral(self.scip_row) def isLocal(self): - """returns TRUE iff the row is only valid locally """ + """ + Returns TRUE iff the row is only valid locally. + + Returns + ------- + bool + + """ return SCIProwIsLocal(self.scip_row) def isModifiable(self): - """returns TRUE iff row is modifiable during node processing (subject to column generation) """ + """ + Returns TRUE iff row is modifiable during node processing (subject to column generation). + + Returns + ------- + bool + + """ return SCIProwIsModifiable(self.scip_row) def isRemovable(self): - """returns TRUE iff row is removable from the LP (due to aging or cleanup)""" + """ + Returns TRUE iff row is removable from the LP (due to aging or cleanup). + + Returns + ------- + bool + + """ return SCIProwIsRemovable(self.scip_row) def isInGlobalCutpool(self): - """return TRUE iff row is a member of the global cut pool""" + """ + Return TRUE iff row is a member of the global cut pool. + + Returns + ------- + bool + + """ return SCIProwIsInGlobalCutpool(self.scip_row) def getOrigintype(self): - """returns type of origin that created the row""" + """ + Returns type of origin that created the row. + + Returns + ------- + PY_SCIP_ROWORIGINTYPE + + """ return SCIProwGetOrigintype(self.scip_row) def getConsOriginConshdlrtype(self): - """returns type of constraint handler that created the row""" + """ + Returns type of constraint handler that created the row. + + Returns + ------- + str + + """ cdef SCIP_CONS* scip_con = SCIProwGetOriginCons(self.scip_row) return bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(scip_con))).decode('UTF-8') def getNNonz(self): - """get number of nonzero entries in row vector""" + """ + Get number of nonzero entries in row vector. + + Returns + ------- + int + + """ return SCIProwGetNNonz(self.scip_row) def getNLPNonz(self): - """get number of nonzero entries in row vector that correspond to columns currently in the SCIP LP""" + """ + Get number of nonzero entries in row vector that correspond to columns currently in the SCIP LP. + + Returns + ------- + int + + """ return SCIProwGetNLPNonz(self.scip_row) def getCols(self): - """gets list with columns of nonzero entries""" + """ + Gets list with columns of nonzero entries + + Returns + ------- + list of Column + + """ cdef SCIP_COL** cols = SCIProwGetCols(self.scip_row) return [Column.create(cols[i]) for i in range(self.getNNonz())] def getVals(self): - """gets list with coefficients of nonzero entries""" + """ + Gets list with coefficients of nonzero entries. + + Returns + ------- + list of int + + """ cdef SCIP_Real* vals = SCIProwGetVals(self.scip_row) return [vals[i] for i in range(self.getNNonz())] def getAge(self): - """Gets the age of the row. (The consecutive times the row has been non-active in the LP)""" + """ + Gets the age of the row. (The consecutive times the row has been non-active in the LP). + + Returns + ------- + int + + """ return SCIProwGetAge(self.scip_row) def getNorm(self): - """gets Euclidean norm of row vector """ + """ + Gets Euclidean norm of row vector. + + Returns + ------- + float + + """ return SCIProwGetNorm(self.scip_row) def __hash__(self): @@ -539,10 +841,24 @@ cdef class Row: and self.scip_row == (other).scip_row) cdef class NLRow: - """Base class holding a pointer to corresponding SCIP_NLROW""" + """Base class holding a pointer to corresponding SCIP_NLROW.""" @staticmethod cdef create(SCIP_NLROW* scipnlrow): + """ + Main method for creating a NLRow class. Is used instead of __init__. + + Parameters + ---------- + scipnlrow : SCIP_NLROW* + A pointer to the SCIP_NLROW + + Returns + ------- + nlrow : NLRow + The Python representative of the SCIP_NLROW + + """ if scipnlrow == NULL: raise Warning("cannot create NLRow with SCIP_NLROW* == NULL") nlrow = NLRow() @@ -555,26 +871,61 @@ cdef class NLRow: return cname.decode('utf-8') def getConstant(self): - """returns the constant of a nonlinear row""" + """ + Returns the constant of a nonlinear row. + + Returns + ------- + float + + """ return SCIPnlrowGetConstant(self.scip_nlrow) def getLinearTerms(self): - """returns a list of tuples (var, coef) representing the linear part of a nonlinear row""" + """ + Returns a list of tuples (var, coef) representing the linear part of a nonlinear row. + + Returns + ------- + list of tuple + + """ cdef SCIP_VAR** linvars = SCIPnlrowGetLinearVars(self.scip_nlrow) cdef SCIP_Real* lincoefs = SCIPnlrowGetLinearCoefs(self.scip_nlrow) cdef int nlinvars = SCIPnlrowGetNLinearVars(self.scip_nlrow) return [(Variable.create(linvars[i]), lincoefs[i]) for i in range(nlinvars)] def getLhs(self): - """returns the left hand side of a nonlinear row""" + """ + Returns the left hand side of a nonlinear row. + + Returns + ------- + float + + """ return SCIPnlrowGetLhs(self.scip_nlrow) def getRhs(self): - """returns the right hand side of a nonlinear row""" + """ + Returns the right hand side of a nonlinear row. + + Returns + ------- + float + + """ return SCIPnlrowGetRhs(self.scip_nlrow) def getDualsol(self): - """gets the dual NLP solution of a nonlinear row""" + """ + Gets the dual NLP solution of a nonlinear row. + + Returns + ------- + float + + """ return SCIPnlrowGetDualsol(self.scip_nlrow) def __hash__(self): @@ -585,7 +936,7 @@ cdef class NLRow: and self.scip_nlrow == (other).scip_nlrow) cdef class Solution: - """Base class holding a pointer to corresponding SCIP_SOL""" + """Base class holding a pointer to corresponding SCIP_SOL.""" # We are raising an error here to avoid creating a solution without an associated model. See Issue #625 def __init__(self, raise_error = False): @@ -594,6 +945,23 @@ cdef class Solution: @staticmethod cdef create(SCIP* scip, SCIP_SOL* scip_sol): + """ + Main method for creating a Solution class. Please use createSol method of the Model class + when wanting to create a Solution as a user. + + Parameters + ---------- + scip : SCIP* + A pointer to the SCIP object + scip_sol : SCIP_SOL* + A pointer to the SCIP_SOL + + Returns + ------- + sol : Solution + The Python representative of the SCIP_SOL + + """ if scip == NULL: raise Warning("cannot create Solution with SCIP* == NULL") sol = Solution(True) @@ -647,6 +1015,20 @@ cdef class BoundChange: @staticmethod cdef create(SCIP_BOUNDCHG* scip_boundchg): + """ + Main method for creating a BoundChange class. Is used instead of __init__. + + Parameters + ---------- + scip_boundchg : SCIP_BOUNDCHG* + A pointer to the SCIP_BOUNDCHG + + Returns + ------- + boundchg : BoundChange + The Python representative of the SCIP_BOUNDCHG + + """ if scip_boundchg == NULL: raise Warning("cannot create BoundChange with SCIP_BOUNDCHG* == NULL") boundchg = BoundChange() @@ -654,23 +1036,60 @@ cdef class BoundChange: return boundchg def getNewBound(self): - """Returns the new value of the bound in the bound change.""" + """ + Returns the new value of the bound in the bound change. + + Returns + ------- + float + + """ return SCIPboundchgGetNewbound(self.scip_boundchg) def getVar(self): - """Returns the variable of the bound change.""" + """ + Returns the variable of the bound change. + + Returns + ------- + Variable + + """ return Variable.create(SCIPboundchgGetVar(self.scip_boundchg)) def getBoundchgtype(self): - """Returns the bound change type of the bound change.""" + """ + Returns the bound change type of the bound change. + + Returns + ------- + int + (0 = branching, 1 = consinfer, 2 = propinfer) + + """ return SCIPboundchgGetBoundchgtype(self.scip_boundchg) def getBoundtype(self): - """Returns the bound type of the bound change.""" + """ + Returns the bound type of the bound change. + + Returns + ------- + int + (0 = lower, 1 = upper) + + """ return SCIPboundchgGetBoundtype(self.scip_boundchg) def isRedundant(self): - """Returns whether the bound change is redundant due to a more global bound that is at least as strong.""" + """ + Returns whether the bound change is redundant due to a more global bound that is at least as strong. + + Returns + ------- + bool + + """ return SCIPboundchgIsRedundant(self.scip_boundchg) def __repr__(self): @@ -683,6 +1102,20 @@ cdef class DomainChanges: @staticmethod cdef create(SCIP_DOMCHG* scip_domchg): + """ + Main method for creating a DomainChanges class. Is used instead of __init__. + + Parameters + ---------- + scip_domchg : SCIP_DOMCHG* + A pointer to the SCIP_DOMCHG + + Returns + ------- + domchg : DomainChanges + The Python representative of the SCIP_DOMCHG + + """ if scip_domchg == NULL: raise Warning("cannot create DomainChanges with SCIP_DOMCHG* == NULL") domchg = DomainChanges() @@ -690,7 +1123,14 @@ cdef class DomainChanges: return domchg def getBoundchgs(self): - """Returns the bound changes in the domain change.""" + """ + Returns the bound changes in the domain change. + + Returns + ------- + list of BoundChange + + """ nboundchgs = SCIPdomchgGetNBoundchgs(self.scip_domchg) return [BoundChange.create(SCIPdomchgGetBoundchg(self.scip_domchg, i)) for i in range(nboundchgs)] @@ -700,6 +1140,20 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode): + """ + Main method for creating a Node class. Is used instead of __init__. + + Parameters + ---------- + scipnode : SCIP_NODE* + A pointer to the SCIP_NODE + + Returns + ------- + node : Node + The Python representative of the SCIP_NODE + + """ if scipnode == NULL: return None node = Node() @@ -707,31 +1161,80 @@ cdef class Node: return node def getParent(self): - """Retrieve parent node (or None if the node has no parent node).""" + """ + Retrieve parent node (or None if the node has no parent node). + + Returns + ------- + Node + + """ return Node.create(SCIPnodeGetParent(self.scip_node)) def getNumber(self): - """Retrieve number of node.""" + """ + Retrieve number of node. + + Returns + ------- + int + + """ return SCIPnodeGetNumber(self.scip_node) def getDepth(self): - """Retrieve depth of node.""" + """ + Retrieve depth of node. + + Returns + ------- + int + + """ return SCIPnodeGetDepth(self.scip_node) def getType(self): - """Retrieve type of node.""" + """ + Retrieve type of node. + + Returns + ------- + PY_SCIP_NODETYPE + + """ return SCIPnodeGetType(self.scip_node) def getLowerbound(self): - """Retrieve lower bound of node.""" + """ + Retrieve lower bound of node. + + Returns + ------- + float + + """ return SCIPnodeGetLowerbound(self.scip_node) def getEstimate(self): - """Retrieve the estimated value of the best feasible solution in subtree of the node""" + """ + Retrieve the estimated value of the best feasible solution in subtree of the node. + + Returns + ------- + float + + """ return SCIPnodeGetEstimate(self.scip_node) def getAddedConss(self): - """Retrieve all constraints added at this node.""" + """ + Retrieve all constraints added at this node. + + Returns + ------- + list of Constraint + + """ cdef int addedconsssize = SCIPnodeGetNAddedConss(self.scip_node) if addedconsssize == 0: return [] @@ -744,19 +1247,47 @@ cdef class Node: return constraints def getNAddedConss(self): - """Retrieve number of added constraints at this node""" + """ + Retrieve number of added constraints at this node. + + Returns + ------- + int + + """ return SCIPnodeGetNAddedConss(self.scip_node) def isActive(self): - """Is the node in the path to the current node?""" + """ + Is the node in the path to the current node? + + Returns + ------- + bool + + """ return SCIPnodeIsActive(self.scip_node) def isPropagatedAgain(self): - """Is the node marked to be propagated again?""" + """ + Is the node marked to be propagated again? + + Returns + ------- + bool + + """ return SCIPnodeIsPropagatedAgain(self.scip_node) def getNParentBranchings(self): - """Retrieve the number of variable branchings that were performed in the parent node to create this node.""" + """ + Retrieve the number of variable branchings that were performed in the parent node to create this node. + + Returns + ------- + int + + """ cdef SCIP_VAR* dummy_branchvars cdef SCIP_Real dummy_branchbounds cdef SCIP_BOUNDTYPE dummy_boundtypes @@ -770,7 +1301,16 @@ cdef class Node: return nbranchvars def getParentBranchings(self): - """Retrieve the set of variable branchings that were performed in the parent node to create this node.""" + """ + Retrieve the set of variable branchings that were performed in the parent node to create this node. + + Returns + ------- + list of Variable + list of float + list of int + + """ cdef int nbranchvars = self.getNParentBranchings() if nbranchvars == 0: return None @@ -792,7 +1332,16 @@ cdef class Node: return py_variables, py_branchbounds, py_boundtypes def getNDomchg(self): - """Retrieve the number of bound changes due to branching, constraint propagation, and propagation.""" + """ + Retrieve the number of bound changes due to branching, constraint propagation, and propagation. + + Returns + ------- + nbranchings : int + nconsprop : int + nprop : int + + """ cdef int nbranchings cdef int nconsprop cdef int nprop @@ -800,7 +1349,14 @@ cdef class Node: return nbranchings, nconsprop, nprop def getDomchg(self): - """Retrieve domain changes for this node.""" + """ + Retrieve domain changes for this node. + + Returns + ------- + DomainChanges + + """ cdef SCIP_DOMCHG* domchg = SCIPnodeGetDomchg(self.scip_node) if domchg == NULL: return None @@ -818,6 +1374,20 @@ cdef class Variable(Expr): @staticmethod cdef create(SCIP_VAR* scipvar): + """ + Main method for creating a Variable class. Is used instead of __init__. + + Parameters + ---------- + scipvar : SCIP_VAR* + A pointer to the SCIP_VAR + + Returns + ------- + var : Variable + The Python representative of the SCIP_VAR + + """ if scipvar == NULL: raise Warning("cannot create Variable with SCIP_VAR* == NULL") var = Variable() @@ -838,7 +1408,15 @@ cdef class Variable(Expr): return self.name def vtype(self): - """Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS)""" + """ + Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) + + Returns + ------- + str + "BINARY", "INTEGER", "CONTINUOUS", or "IMPLINT" + + """ vartype = SCIPvarGetType(self.scip_var) if vartype == SCIP_VARTYPE_BINARY: return "BINARY" @@ -850,64 +1428,163 @@ cdef class Variable(Expr): return "IMPLINT" def isOriginal(self): - """Retrieve whether the variable belongs to the original problem""" + """ + Retrieve whether the variable belongs to the original problem + + Returns + ------- + bool + + """ return SCIPvarIsOriginal(self.scip_var) def isInLP(self): - """Retrieve whether the variable is a COLUMN variable that is member of the current LP""" + """ + Retrieve whether the variable is a COLUMN variable that is member of the current LP. + + Returns + ------- + bool + + """ return SCIPvarIsInLP(self.scip_var) def getIndex(self): - """Retrieve the unique index of the variable.""" + """ + Retrieve the unique index of the variable. + + Returns + ------- + int + + """ return SCIPvarGetIndex(self.scip_var) def getCol(self): - """Retrieve column of COLUMN variable""" + """ + Retrieve column of COLUMN variable. + + Returns + ------- + Column + + """ cdef SCIP_COL* scip_col scip_col = SCIPvarGetCol(self.scip_var) return Column.create(scip_col) def getLbOriginal(self): - """Retrieve original lower bound of variable""" + """ + Retrieve original lower bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetLbOriginal(self.scip_var) def getUbOriginal(self): - """Retrieve original upper bound of variable""" + """ + Retrieve original upper bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetUbOriginal(self.scip_var) def getLbGlobal(self): - """Retrieve global lower bound of variable""" + """ + Retrieve global lower bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetLbGlobal(self.scip_var) def getUbGlobal(self): - """Retrieve global upper bound of variable""" + """ + Retrieve global upper bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetUbGlobal(self.scip_var) def getLbLocal(self): - """Retrieve current lower bound of variable""" + """ + Retrieve current lower bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetLbLocal(self.scip_var) def getUbLocal(self): - """Retrieve current upper bound of variable""" + """ + Retrieve current upper bound of variable. + + Returns + ------- + float + + """ return SCIPvarGetUbLocal(self.scip_var) def getObj(self): - """Retrieve current objective value of variable""" + """ + Retrieve current objective value of variable. + + Returns + ------- + float + + """ return SCIPvarGetObj(self.scip_var) def getLPSol(self): - """Retrieve the current LP solution value of variable""" + """ + Retrieve the current LP solution value of variable. + + Returns + ------- + float + + """ return SCIPvarGetLPSol(self.scip_var) def getAvgSol(self): - """Get the weighted average solution of variable in all feasible primal solutions found""" + """ + Get the weighted average solution of variable in all feasible primal solutions found. + + Returns + ------- + float + + """ return SCIPvarGetAvgSol(self.scip_var) def varMayRound(self, direction="down"): - """Checks whether its it possible, to round variable up and stay feasible for the relaxation + """ + Checks whether it is possible to round variable up / down and stay feasible for the relaxation. - :param direction: direction in which the variable will be rounded + Parameters + ---------- + direction : str + "up" or "down" + + Returns + ------- + bool """ if direction not in ("down", "up"): @@ -925,6 +1602,20 @@ cdef class Constraint: @staticmethod cdef create(SCIP_CONS* scipcons): + """ + Main method for creating a Constraint class. Is used instead of __init__. + + Parameters + ---------- + scipcons : SCIP_CONS* + A pointer to the SCIP_CONS + + Returns + ------- + cons : Constraint + The Python representative of the SCIP_CONS + + """ if scipcons == NULL: raise Warning("cannot create Constraint with SCIP_CONS* == NULL") cons = Constraint() @@ -940,65 +1631,170 @@ cdef class Constraint: return self.name def isOriginal(self): - """Retrieve whether the constraint belongs to the original problem""" + """ + Retrieve whether the constraint belongs to the original problem. + + Returns + ------- + bool + + """ return SCIPconsIsOriginal(self.scip_cons) def isInitial(self): - """Retrieve True if the relaxation of the constraint should be in the initial LP""" + """ + Returns True if the relaxation of the constraint should be in the initial LP. + + Returns + ------- + bool + + """ return SCIPconsIsInitial(self.scip_cons) def isSeparated(self): - """Retrieve True if constraint should be separated during LP processing""" + """ + Returns True if constraint should be separated during LP processing. + + Returns + ------- + bool + + """ return SCIPconsIsSeparated(self.scip_cons) def isEnforced(self): - """Retrieve True if constraint should be enforced during node processing""" + """ + Returns True if constraint should be enforced during node processing. + + Returns + ------- + bool + + """ return SCIPconsIsEnforced(self.scip_cons) def isChecked(self): - """Retrieve True if constraint should be checked for feasibility""" + """ + Returns True if constraint should be checked for feasibility. + + Returns + ------- + bool + + """ return SCIPconsIsChecked(self.scip_cons) def isPropagated(self): - """Retrieve True if constraint should be propagated during node processing""" + """ + Returns True if constraint should be propagated during node processing. + + Returns + ------- + bool + + """ return SCIPconsIsPropagated(self.scip_cons) def isLocal(self): - """Retrieve True if constraint is only locally valid or not added to any (sub)problem""" + """ + Returns True if constraint is only locally valid or not added to any (sub)problem. + + Returns + ------- + bool + + """ return SCIPconsIsLocal(self.scip_cons) def isModifiable(self): - """Retrieve True if constraint is modifiable (subject to column generation)""" + """ + Returns True if constraint is modifiable (subject to column generation). + + Returns + ------- + bool + + """ return SCIPconsIsModifiable(self.scip_cons) def isDynamic(self): - """Retrieve True if constraint is subject to aging""" + """ + Returns True if constraint is subject to aging. + + Returns + ------- + bool + + """ return SCIPconsIsDynamic(self.scip_cons) def isRemovable(self): - """Retrieve True if constraint's relaxation should be removed from the LP due to aging or cleanup""" + """ + Returns True if constraint's relaxation should be removed from the LP due to aging or cleanup. + + Returns + ------- + bool + + """ return SCIPconsIsRemovable(self.scip_cons) def isStickingAtNode(self): - """Retrieve True if constraint is only locally valid or not added to any (sub)problem""" + """ + Returns True if constraint is only locally valid or not added to any (sub)problem. + + Returns + ------- + bool + + """ return SCIPconsIsStickingAtNode(self.scip_cons) def isActive(self): - """returns True iff constraint is active in the current node""" + """ + Returns True iff constraint is active in the current node. + + Returns + ------- + bool + + """ return SCIPconsIsActive(self.scip_cons) def isLinear(self): - """Retrieve True if constraint is linear""" + """ + Returns True if constraint is linear + + Returns + ------- + bool + + """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype == 'linear' def isNonlinear(self): - """Retrieve True if constraint is nonlinear""" + """ + Returns True if constraint is nonlinear. + + Returns + ------- + bool + + """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype == 'nonlinear' def getConshdlrName(self): - """Return the constraint handler's name""" + """ + Return the constraint handler's name. + + Returns + ------- + str + + """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype @@ -1023,18 +1819,30 @@ cdef void relayErrorMessage(void *messagehdlr, FILE *file, const char *msg) noex #@anchor Model ## cdef class Model: - """Main class holding a pointer to SCIP for managing most interactions""" def __init__(self, problemName='model', defaultPlugins=True, Model sourceModel=None, origcopy=False, globalcopy=True, enablepricing=False, createscip=True, threadsafe=False): """ - :param problemName: name of the problem (default 'model') - :param defaultPlugins: use default plugins? (default True) - :param sourceModel: create a copy of the given Model instance (default None) - :param origcopy: whether to call copy or copyOrig (default False) - :param globalcopy: whether to create a global or a local copy (default True) - :param enablepricing: whether to enable pricing in copy (default False) - :param createscip: initialize the Model object and creates a SCIP instance - :param threadsafe: False if data can be safely shared between the source and target problem + Main class holding a pointer to SCIP for managing most interactions + + Parameters + ---------- + problemName : str, optional + name of the problem (default 'model') + defaultPlugins : bool, optional + use default plugins? (default True) + sourceModel : Model or None, optional + create a copy of the given Model instance (default None) + origcopy : bool, optional + whether to call copy or copyOrig (default False) + globalcopy : bool, optional + whether to create a global or a local copy (default True) + enablepricing : bool, optional + whether to enable pricing in copy (default False) + createscip : bool, optional + initialize the Model object and creates a SCIP instance (default True) + threadsafe : bool, optional + False if data can be safely shared between the source and target problem (default False) + """ if self.version() < MAJOR: raise Exception("linked SCIP is not compatible to this version of PySCIPOpt - use at least version", MAJOR) @@ -1068,11 +1876,12 @@ cdef class Model: def attachEventHandlerCallback(self, callback, - events: List[SCIP_EVENTTYPE], + events, name="eventhandler", description="" ): - """Attach an event handler to the model using a callback function. + """ + Attach an event handler to the model using a callback function. Parameters ---------- @@ -1130,7 +1939,18 @@ cdef class Model: @staticmethod cdef create(SCIP* scip): - """Creates a model and appropriately assigns the scip and bestsol parameters + """ + Creates a model and appropriately assigns the scip and bestsol parameters. + + Parameters + ---------- + scip : SCIP* + A pointer to a SCIP object + + Returns + ------- + Model + """ if scip == NULL: raise Warning("cannot create Model with SCIP* == NULL") @@ -1141,26 +1961,48 @@ cdef class Model: @property def _freescip(self): - """Return whether the underlying Scip pointer gets deallocted when the current + """ + Return whether the underlying Scip pointer gets deallocted when the current object is deleted. + + Returns + ------- + bool + """ return self._freescip @_freescip.setter def _freescip(self, val): - """Set whether the underlying Scip pointer gets deallocted when the current + """ + Set whether the underlying Scip pointer gets deallocted when the current object is deleted. + + Parameters + ---------- + val : bool + """ self._freescip = val @cython.always_allow_keywords(True) @staticmethod def from_ptr(capsule, take_ownership): - """Create a Model from a given pointer. + """ + Create a Model from a given pointer. + + Parameters + ---------- + capsule + The PyCapsule containing the SCIP pointer under the name "scip" + take_ownership : bool + Whether the newly created Model assumes ownership of the + underlying Scip pointer (see ``_freescip``) + + Returns + ------- + Model - :param cpasule: The PyCapsule containing the SCIP pointer under the name "scip". - :param take_ownership: Whether the newly created Model assumes ownership of the - underlying Scip pointer (see `_freescip`). """ if not PyCapsule_IsValid(capsule, "scip"): raise ValueError("The given capsule does not contain a valid scip pointer") @@ -1170,12 +2012,21 @@ cdef class Model: @cython.always_allow_keywords(True) def to_ptr(self, give_ownership): - """Return the underlying Scip pointer to the current Model. + """ + Return the underlying Scip pointer to the current Model. + + Parameters + ---------- + give_ownership : bool + Whether the current Model gives away ownership of the + underlying Scip pointer (see ``_freescip``) + + Returns + ------- + capsule + The underlying pointer to the current Model, wrapped in a + PyCapsule under the name "scip". - :param give_ownership: Whether the current Model gives away ownership of the - underlying Scip pointer (see `_freescip`). - :return capsule: The underlying pointer to the current Model, wrapped in a - PyCapsule under the name "scip". """ capsule = PyCapsule_New(self._scip, "scip", NULL) if give_ownership: @@ -1183,24 +2034,29 @@ cdef class Model: return capsule def includeDefaultPlugins(self): - """Includes all default plug-ins into SCIP""" + """Includes all default plug-ins into SCIP.""" PY_SCIP_CALL(SCIPincludeDefaultPlugins(self._scip)) def createProbBasic(self, problemName='model'): - """Create new problem instance with given name + """ + Create new problem instance with given name. - :param problemName: name of model or problem (Default value = 'model') + Parameters + ---------- + problemName : str, optional + name of model or problem (Default value = 'model') """ n = str_conversion(problemName) PY_SCIP_CALL(SCIPcreateProbBasic(self._scip, n)) def freeProb(self): - """Frees problem and solution process data""" + """Frees problem and solution process data.""" PY_SCIP_CALL(SCIPfreeProb(self._scip)) def freeTransform(self): - """Frees all solution process data including presolving and transformed problem, only original problem is kept""" + """Frees all solution process data including presolving and + transformed problem, only original problem is kept.""" self._modelvars = { var: value for var, value in self._modelvars.items() @@ -1209,11 +2065,18 @@ cdef class Model: PY_SCIP_CALL(SCIPfreeTransform(self._scip)) def version(self): - """Retrieve SCIP version""" + """ + Retrieve SCIP version. + + Returns + ------- + float + + """ return SCIPversion() def printVersion(self): - """Print version, copyright information and compile mode""" + """Print version, copyright information and compile mode.""" user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -1222,7 +2085,7 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def printExternalCodeVersions(self): - """Print external code versions, e.g. symmetry, non-linear solver, lp solver""" + """Print external code versions, e.g. symmetry, non-linear solver, lp solver.""" user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -1231,149 +2094,474 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def getProbName(self): - """Retrieve problem name""" + """ + Retrieve problem name. + + Returns + ------- + str + + """ return bytes(SCIPgetProbName(self._scip)).decode('UTF-8') def getTotalTime(self): - """Retrieve the current total SCIP time in seconds, i.e. the total time since the SCIP instance has been created""" + """ + Retrieve the current total SCIP time in seconds, + i.e. the total time since the SCIP instance has been created. + + Returns + ------- + float + + """ return SCIPgetTotalTime(self._scip) def getSolvingTime(self): - """Retrieve the current solving time in seconds""" + """ + Retrieve the current solving time in seconds. + + Returns + ------- + float + + """ return SCIPgetSolvingTime(self._scip) def getReadingTime(self): - """Retrieve the current reading time in seconds""" + """ + Retrieve the current reading time in seconds. + + Returns + ------- + float + + """ return SCIPgetReadingTime(self._scip) def getPresolvingTime(self): - """Retrieve the curernt presolving time in seconds""" + """ + Returns the current presolving time in seconds. + + Returns + ------- + float + + """ return SCIPgetPresolvingTime(self._scip) def getNLPIterations(self): - """Retrieve the total number of LP iterations so far.""" + """ + Returns the total number of LP iterations so far. + + Returns + ------- + int + + """ return SCIPgetNLPIterations(self._scip) def getNNodes(self): - """gets number of processed nodes in current run, including the focus node.""" + """ + Gets number of processed nodes in current run, including the focus node. + + Returns + ------- + int + + """ return SCIPgetNNodes(self._scip) def getNTotalNodes(self): - """gets number of processed nodes in all runs, including the focus node.""" + """ + Gets number of processed nodes in all runs, including the focus node. + + Returns + ------- + int + + """ return SCIPgetNTotalNodes(self._scip) def getNFeasibleLeaves(self): - """Retrieve number of leaf nodes processed with feasible relaxation solution.""" + """ + Retrieve number of leaf nodes processed with feasible relaxation solution. + + Returns + ------- + int + + """ return SCIPgetNFeasibleLeaves(self._scip) def getNInfeasibleLeaves(self): - """gets number of infeasible leaf nodes processed.""" + """ + Gets number of infeasible leaf nodes processed. + + Returns + ------- + int + + """ return SCIPgetNInfeasibleLeaves(self._scip) def getNLeaves(self): - """gets number of leaves in the tree.""" + """ + Gets number of leaves in the tree. + + Returns + ------- + int + + """ return SCIPgetNLeaves(self._scip) def getNChildren(self): - """gets number of children of focus node.""" + """ + Gets number of children of focus node. + + Returns + ------- + int + + """ return SCIPgetNChildren(self._scip) def getNSiblings(self): - """gets number of siblings of focus node.""" + """ + Gets number of siblings of focus node. + + Returns + ------- + int + + """ return SCIPgetNSiblings(self._scip) def getCurrentNode(self): - """Retrieve current node.""" + """ + Retrieve current node. + + Returns + ------- + Node + + """ return Node.create(SCIPgetCurrentNode(self._scip)) def getGap(self): - """Retrieve the gap, i.e. |(primalbound - dualbound)/min(|primalbound|,|dualbound|)|.""" + """ + Retrieve the gap, + i.e. abs((primalbound - dualbound)/min(abs(primalbound),abs(dualbound))) + + Returns + ------- + float + + """ return SCIPgetGap(self._scip) def getDepth(self): - """Retrieve the depth of the current node""" + """ + Retrieve the depth of the current node. + + Returns + ------- + int + + """ return SCIPgetDepth(self._scip) def infinity(self): - """Retrieve SCIP's infinity value""" + """ + Retrieve SCIP's infinity value. + + Returns + ------- + int + + """ return SCIPinfinity(self._scip) def epsilon(self): - """Retrieve epsilon for e.g. equality checks""" + """ + Retrieve epsilon for e.g. equality checks. + + Returns + ------- + float + + """ return SCIPepsilon(self._scip) def feastol(self): - """Retrieve feasibility tolerance""" + """ + Retrieve feasibility tolerance. + + Returns + ------- + float + + """ return SCIPfeastol(self._scip) def feasFrac(self, value): - """returns fractional part of value, i.e. x - floor(x) in feasible tolerance: x - floor(x+feastol)""" + """ + Returns fractional part of value, i.e. x - floor(x) in feasible tolerance: x - floor(x+feastol). + + Parameters + ---------- + value : float + + Returns + ------- + float + + """ return SCIPfeasFrac(self._scip, value) def frac(self, value): - """returns fractional part of value, i.e. x - floor(x) in epsilon tolerance: x - floor(x+eps)""" + """ + Returns fractional part of value, i.e. x - floor(x) in epsilon tolerance: x - floor(x+eps). + + Parameters + ---------- + value : float + + Returns + ------- + float + + """ return SCIPfrac(self._scip, value) def feasFloor(self, value): - """ rounds value + feasibility tolerance down to the next integer""" + """ + Rounds value + feasibility tolerance down to the next integer. + + Parameters + ---------- + value : float + + Returns + ------- + float + + """ return SCIPfeasFloor(self._scip, value) def feasCeil(self, value): - """rounds value - feasibility tolerance up to the next integer""" + """ + Rounds value - feasibility tolerance up to the next integer. + + Parameters + ---------- + value : float + + Returns + ------- + float + + """ return SCIPfeasCeil(self._scip, value) def feasRound(self, value): - """rounds value to the nearest integer in feasibility tolerance""" + """ + Rounds value to the nearest integer in feasibility tolerance. + + Parameters + ---------- + value : float + + Returns + ------- + float + + """ return SCIPfeasRound(self._scip, value) def isZero(self, value): - """returns whether abs(value) < eps""" + """ + Returns whether abs(value) < eps. + + Parameters + ---------- + value : float + + Returns + ------- + bool + + """ return SCIPisZero(self._scip, value) def isFeasZero(self, value): - """returns whether abs(value) < feastol""" + """ + Returns whether abs(value) < feastol. + + Parameters + ---------- + value : float + + Returns + ------- + bool + + """ return SCIPisFeasZero(self._scip, value) def isInfinity(self, value): - """returns whether value is SCIP's infinity""" + """ + Returns whether value is SCIP's infinity. + + Parameters + ---------- + value : float + + Returns + ------- + bool + + """ return SCIPisInfinity(self._scip, value) def isFeasNegative(self, value): - """returns whether value < -feastol""" + """ + Returns whether value < -feastol. + + Parameters + ---------- + value : float + + Returns + ------- + bool + + """ return SCIPisFeasNegative(self._scip, value) def isFeasIntegral(self, value): - """returns whether value is integral within the LP feasibility bounds""" + """ + Returns whether value is integral within the LP feasibility bounds. + + Parameters + ---------- + value : float + + Returns + ------- + bool + + """ return SCIPisFeasIntegral(self._scip, value) def isEQ(self, val1, val2): - """checks, if values are in range of epsilon""" + """ + Checks, if values are in range of epsilon. + + Parameters + ---------- + val1 : float + val2 : float + + Returns + ------- + bool + + """ return SCIPisEQ(self._scip, val1, val2) def isFeasEQ(self, val1, val2): - """checks, if relative difference of values is in range of feasibility tolerance""" + """ + Checks, if relative difference of values is in range of feasibility tolerance. + + Parameters + ---------- + val1 : float + val2 : float + + Returns + ------- + bool + + """ return SCIPisFeasEQ(self._scip, val1, val2) def isLE(self, val1, val2): - """returns whether val1 <= val2 + eps""" + """ + Returns whether val1 <= val2 + eps. + + Parameters + ---------- + val1 : float + val2 : float + + Returns + ------- + bool + + """ return SCIPisLE(self._scip, val1, val2) def isLT(self, val1, val2): - """returns whether val1 < val2 - eps""" + """ + Returns whether val1 < val2 - eps. + + Parameters + ---------- + val1 : float + val2 : float + + Returns + ------- + bool + + """ return SCIPisLT(self._scip, val1, val2) def isGE(self, val1, val2): - """returns whether val1 >= val2 - eps""" + """ + Returns whether val1 >= val2 - eps. + + Parameters + ---------- + val1 : float + val2 : float + + Returns + ------- + bool + + """ return SCIPisGE(self._scip, val1, val2) def isGT(self, val1, val2): - """returns whether val1 > val2 + eps""" + """ + Returns whether val1 > val2 + eps. + + Parameters + ---------- + val1 : float + val2 : foat + + Returns + ------- + bool + + """ return SCIPisGT(self._scip, val1, val2) - def getCondition(self, exact=False): - """Get the current LP's condition number + def getCondition(self, exact=False): + """ + Get the current LP's condition number. + + Parameters + ---------- + exact : bool, optional + whether to get an estimate or the exact value (Default value = False) - :param exact: whether to get an estimate or the exact value (Default value = False) + Returns + ------- + float """ cdef SCIP_LPI* lpi @@ -1387,11 +2575,26 @@ cdef class Model: return quality def enableReoptimization(self, enable=True): - """include specific heuristics and branching rules for reoptimization""" + """ + Include specific heuristics and branching rules for reoptimization. + + Parameters + ---------- + enable : bool, optional + True to enable and False to disable + + """ PY_SCIP_CALL(SCIPenableReoptimization(self._scip, enable)) def lpiGetIterations(self): - """Get the iteration count of the last solved LP""" + """ + Get the iteration count of the last solved LP. + + Returns + ------- + int + + """ cdef SCIP_LPI* lpi PY_SCIP_CALL(SCIPgetLPI(self._scip, &lpi)) cdef int iters = 0 @@ -1409,24 +2612,41 @@ cdef class Model: PY_SCIP_CALL(SCIPsetObjsense(self._scip, SCIP_OBJSENSE_MAXIMIZE)) def setObjlimit(self, objlimit): - """Set a limit on the objective function. + """ + Set a limit on the objective function. Only solutions with objective value better than this limit are accepted. - :param objlimit: limit on the objective function + Parameters + ---------- + objlimit : float + limit on the objective function """ PY_SCIP_CALL(SCIPsetObjlimit(self._scip, objlimit)) def getObjlimit(self): - """returns current limit on objective function.""" + """ + Returns current limit on objective function. + + Returns + ------- + float + + """ return SCIPgetObjlimit(self._scip) def setObjective(self, expr, sense = 'minimize', clear = 'true'): - """Establish the objective function as a linear expression. + """ + Establish the objective function as a linear expression. - :param expr: the objective function SCIP Expr, or constant value - :param sense: the objective sense (Default value = 'minimize') - :param clear: set all other variables objective coefficient to zero (Default value = 'true') + Parameters + ---------- + expr : Expr or float + the objective function SCIP Expr, or constant value + sense : str, optional + the objective sense ("minimize" or "maximize") (Default value = 'minimize') + clear : bool, optional + set all other variables objective coefficient to zero (Default value = 'true') """ @@ -1467,7 +2687,14 @@ cdef class Model: raise Warning("unrecognized optimization sense: %s" % sense) def getObjective(self): - """Retrieve objective function as Expr""" + """ + Retrieve objective function as Expr. + + Returns + ------- + Expr + + """ variables = self.getVars() objective = Expr() for var in variables: @@ -1478,10 +2705,15 @@ cdef class Model: return objective def addObjoffset(self, offset, solutions = False): - """Add constant offset to objective + """ + Add constant offset to objective. - :param offset: offset to add - :param solutions: add offset also to existing solutions (Default value = False) + Parameters + ---------- + offset : float + offset to add + solutions : bool, optional + add offset also to existing solutions (Default value = False) """ if solutions: @@ -1490,9 +2722,17 @@ cdef class Model: PY_SCIP_CALL(SCIPaddOrigObjoffset(self._scip, offset)) def getObjoffset(self, original = True): - """Retrieve constant objective offset + """ + Retrieve constant objective offset + + Parameters + ---------- + original : bool, optional + offset of original or transformed problem (Default value = True) - :param original: offset of original or transformed problem (Default value = True) + Returns + ------- + float """ if original: @@ -1501,19 +2741,31 @@ cdef class Model: return SCIPgetTransObjoffset(self._scip) def setObjIntegral(self): - """informs SCIP that the objective value is always integral in every feasible solution - Note: This function should be used to inform SCIP that the objective function is integral, helping to improve the - performance. This is useful when using column generation. If no column generation (pricing) is used, SCIP - automatically detects whether the objective function is integral or can be scaled to be integral. However, in - any case, the user has to make sure that no variable is added during the solving process that destroys this - property. + """Informs SCIP that the objective value is always integral in every feasible solution. + + Notes + ----- + This function should be used to inform SCIP that the objective function is integral, + helping to improve the performance. This is useful when using column generation. + If no column generation (pricing) is used, SCIP automatically detects whether the objective + function is integral or can be scaled to be integral. However, in any case, the user has to + make sure that no variable is added during the solving process that destroys this property. """ PY_SCIP_CALL(SCIPsetObjIntegral(self._scip)) def getLocalEstimate(self, original = False): - """gets estimate of best primal solution w.r.t. original or transformed problem contained in current subtree + """ + Gets estimate of best primal solution w.r.t. original or transformed problem contained in current subtree. + + Parameters + ---------- + original : bool, optional + get estimate of original or transformed problem (Default value = False) + + Returns + ------- + float - :param original: estimate of original or transformed problem (Default value = False) """ if original: return SCIPgetLocalOrigEstimate(self._scip) @@ -1522,38 +2774,62 @@ cdef class Model: # Setting parameters def setPresolve(self, setting): - """Set presolving parameter settings. + """ + Set presolving parameter settings. + - :param setting: the parameter settings (SCIP_PARAMSETTING) + Parameters + ---------- + setting : SCIP_PARAMSETTING + the parameter settings, e.g. SCIP_PARAMSETTING.OFF """ PY_SCIP_CALL(SCIPsetPresolving(self._scip, setting, True)) def setProbName(self, name): - """Set problem name""" + """ + Set problem name. + + Parameters + ---------- + name : str + + """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetProbName(self._scip, n)) def setSeparating(self, setting): - """Set separating parameter settings. + """ + Set separating parameter settings. - :param setting: the parameter settings (SCIP_PARAMSETTING) + Parameters + ---------- + setting : SCIP_PARAMSETTING + the parameter settings, e.g. SCIP_PARAMSETTING.OFF """ PY_SCIP_CALL(SCIPsetSeparating(self._scip, setting, True)) def setHeuristics(self, setting): - """Set heuristics parameter settings. + """ + Set heuristics parameter settings. - :param setting: the parameter setting (SCIP_PARAMSETTING) + Parameters + ---------- + setting : SCIP_PARAMSETTING + the parameter settings, e.g. SCIP_PARAMSETTING.OFF """ PY_SCIP_CALL(SCIPsetHeuristics(self._scip, setting, True)) def disablePropagation(self, onlyroot=False): - """Disables propagation in SCIP to avoid modifying the original problem during transformation. + """ + Disables propagation in SCIP to avoid modifying the original problem during transformation. - :param onlyroot: use propagation when root processing is finished (Default value = False) + Parameters + ---------- + onlyroot : bool, optional + use propagation when root processing is finished (Default value = False) """ self.setIntParam("propagating/maxroundsroot", 0) @@ -1561,12 +2837,23 @@ cdef class Model: self.setIntParam("propagating/maxrounds", 0) def writeProblem(self, filename='model.cip', trans=False, genericnames=False, verbose=True): - """Write current model/problem to a file. + """ + Write current model/problem to a file. + + Parameters + ---------- + filename : str, optional + the name of the file to be used (Default value = 'model.cip'). + Should have an extension corresponding to one of the readable file formats, + described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. + trans : bool, optional + indicates whether the transformed problem is written to file (Default value = False) + genericnames : bool, optional + indicates whether the problem should be written with generic variable + and constraint names (Default value = False) + verbose : bool, optional + indicates whether a success message should be printed - :param filename: the name of the file to be used (Default value = 'model.cip'). Should have an extension corresponding to one of the readable file formats, described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. - :param trans: indicates whether the transformed problem is written to file (Default value = False) - :param genericnames: indicates whether the problem should be written with generic variable and constraint names (Default value = False) - :param verbose: indicates whether a success message should be printed """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -1593,16 +2880,30 @@ cdef class Model: # Variable Functions def addVar(self, name='', vtype='C', lb=0.0, ub=None, obj=0.0, pricedVar=False, pricedVarScore=1.0): - """Create a new variable. Default variable is non-negative and continuous. + """ + Create a new variable. Default variable is non-negative and continuous. - :param name: name of the variable, generic if empty (Default value = '') - :param vtype: type of the variable: 'C' continuous, 'I' integer, 'B' binary, and 'M' implicit integer - (see https://www.scipopt.org/doc/html/FAQ.php#implicitinteger) (Default value = 'C') - :param lb: lower bound of the variable, use None for -infinity (Default value = 0.0) - :param ub: upper bound of the variable, use None for +infinity (Default value = None) - :param obj: objective value of variable (Default value = 0.0) - :param pricedVar: is the variable a pricing candidate? (Default value = False) - :param pricedVarScore: score of variable in case it is priced, the higher the better (Default value = 1.0) + Parameters + ---------- + name : str, optional + name of the variable, generic if empty (Default value = '') + vtype : str, optional + type of the variable: 'C' continuous, 'I' integer, 'B' binary, and 'M' implicit integer + (Default value = 'C') + lb : float or None, optional + lower bound of the variable, use None for -infinity (Default value = 0.0) + ub : float or None, optional + upper bound of the variable, use None for +infinity (Default value = None) + obj : float, optional + objective value of variable (Default value = 0.0) + pricedVar : bool, optional + is the variable a pricing candidate? (Default value = False) + pricedVarScore : float, optional + score of variable in case it is priced, the higher the better (Default value = 1.0) + + Returns + ------- + Variable """ cdef SCIP_VAR* scip_var @@ -1651,9 +2952,17 @@ cdef class Model: return pyVar def getTransformedVar(self, Variable var): - """Retrieve the transformed variable. + """ + Retrieve the transformed variable. - :param Variable var: original variable to get the transformed of + Parameters + ---------- + var : Variable + original variable to get the transformed of + + Returns + ------- + Variable """ cdef SCIP_VAR* _tvar @@ -1662,21 +2971,38 @@ cdef class Model: return Variable.create(_tvar) def addVarLocks(self, Variable var, nlocksdown, nlocksup): - """adds given values to lock numbers of variable for rounding + """ + Adds given values to lock numbers of variable for rounding. - :param Variable var: variable to adjust the locks for - :param nlocksdown: new number of down locks - :param nlocksup: new number of up locks + Parameters + ---------- + var : Variable + variable to adjust the locks for + nlocksdown : int + new number of down locks + nlocksup : int + new number of up locks """ PY_SCIP_CALL(SCIPaddVarLocks(self._scip, var.scip_var, nlocksdown, nlocksup)) def fixVar(self, Variable var, val): - """Fixes the variable var to the value val if possible. + """ + Fixes the variable var to the value val if possible. + + Parameters + ---------- + var : Variable + variable to fix + val : float + the fix value - :param Variable var: variable to fix - :param val: float, the fix value - :return: tuple (infeasible, fixed) of booleans + Returns + ------- + infeasible : bool + Is the fixing infeasible? + fixed : bool + Was the fixing performed? """ cdef SCIP_Bool infeasible @@ -1685,10 +3011,18 @@ cdef class Model: return infeasible, fixed def delVar(self, Variable var): - """Delete a variable. + """ + Delete a variable. + + Parameters + ---------- + var : Variable + the variable which shall be deleted - :param var: the variable which shall be deleted - :return: bool, was deleting succesful + Returns + ------- + deleted : bool + Whether deleting was successfull """ cdef SCIP_Bool deleted @@ -1698,14 +3032,24 @@ cdef class Model: return deleted def tightenVarLb(self, Variable var, lb, force=False): - """Tighten the lower bound in preprocessing or current node, if the bound is tighter. + """ + Tighten the lower bound in preprocessing or current node, if the bound is tighter. + + Parameters + ---------- + var : Variable + SCIP variable + lb : float + possible new lower bound + force : bool, optional + force tightening even if below bound strengthening tolerance (default = False) - :param var: SCIP variable - :param lb: possible new lower bound - :param force: force tightening even if below bound strengthening tolerance - :return: tuple of bools, (infeasible, tightened) - infeasible: whether new domain is empty - tightened: whether the bound was tightened + Returns + ------- + infeasible : bool + Whether new domain is empty + tightened : bool + Whether the bound was tightened """ cdef SCIP_Bool infeasible @@ -1714,14 +3058,24 @@ cdef class Model: return infeasible, tightened def tightenVarUb(self, Variable var, ub, force=False): - """Tighten the upper bound in preprocessing or current node, if the bound is tighter. + """ + Tighten the upper bound in preprocessing or current node, if the bound is tighter. + + Parameters + ---------- + var : Variable + SCIP variable + ub : float + possible new upper bound + force : bool, optional + force tightening even if below bound strengthening tolerance - :param var: SCIP variable - :param ub: possible new upper bound - :param force: force tightening even if below bound strengthening tolerance - :return: tuple of bools, (infeasible, tightened) - infeasible: whether new domain is empty - tightened: whether the bound was tightened + Returns + ------- + infeasible : bool + Whether new domain is empty + tightened : bool + Whether the bound was tightened """ cdef SCIP_Bool infeasible @@ -1730,14 +3084,24 @@ cdef class Model: return infeasible, tightened def tightenVarUbGlobal(self, Variable var, ub, force=False): - """Tighten the global upper bound, if the bound is tighter. + """ + Tighten the global upper bound, if the bound is tighter. + + Parameters + ---------- + var : Variable + SCIP variable + ub : float + possible new upper bound + force : bool, optional + force tightening even if below bound strengthening tolerance - :param var: SCIP variable - :param ub: possible new upper bound - :param force: force tightening even if below bound strengthening tolerance - :return: tuple of bools, (infeasible, tightened) - infeasible: whether new domain is empty - tightened: whether the bound was tightened + Returns + ------- + infeasible : bool + Whether new domain is empty + tightened : bool + Whether the bound was tightened """ cdef SCIP_Bool infeasible @@ -1746,14 +3110,23 @@ cdef class Model: return infeasible, tightened def tightenVarLbGlobal(self, Variable var, lb, force=False): - """Tighten the global upper bound, if the bound is tighter. + """Tighten the global lower bound, if the bound is tighter. + + Parameters + ---------- + var : Variable + SCIP variable + lb : float + possible new lower bound + force : bool, optional + force tightening even if below bound strengthening tolerance - :param var: SCIP variable - :param lb: possible new upper bound - :param force: force tightening even if below bound strengthening tolerance - :return: tuple of bools, (infeasible, tightened) - infeasible: whether new domain is empty - tightened: whether the bound was tightened + Returns + ------- + infeasible : bool + Whether new domain is empty + tightened : bool + Whether the bound was tightened """ cdef SCIP_Bool infeasible @@ -1762,10 +3135,15 @@ cdef class Model: return infeasible, tightened def chgVarLb(self, Variable var, lb): - """Changes the lower bound of the specified variable. + """ + Changes the lower bound of the specified variable. - :param Variable var: variable to change bound of - :param lb: new lower bound (set to None for -infinity) + Parameters + ---------- + var : Variable + variable to change bound of + lb : float or None + new lower bound (set to None for -infinity) """ if lb is None: @@ -1775,8 +3153,12 @@ cdef class Model: def chgVarUb(self, Variable var, ub): """Changes the upper bound of the specified variable. - :param Variable var: variable to change bound of - :param ub: new upper bound (set to None for +infinity) + Parameters + ---------- + var : Variable + variable to change bound of + lb : float or None + new upper bound (set to None for +infinity) """ if ub is None: @@ -1786,8 +3168,12 @@ cdef class Model: def chgVarLbGlobal(self, Variable var, lb): """Changes the global lower bound of the specified variable. - :param Variable var: variable to change bound of - :param lb: new lower bound (set to None for -infinity) + Parameters + ---------- + var : Variable + variable to change bound of + lb : float or None + new lower bound (set to None for -infinity) """ if lb is None: @@ -1797,8 +3183,12 @@ cdef class Model: def chgVarUbGlobal(self, Variable var, ub): """Changes the global upper bound of the specified variable. - :param Variable var: variable to change bound of - :param ub: new upper bound (set to None for +infinity) + Parameters + ---------- + var : Variable + variable to change bound of + lb : float or None + new upper bound (set to None for +infinity) """ if ub is None: @@ -1808,8 +3198,15 @@ cdef class Model: def chgVarLbNode(self, Node node, Variable var, lb): """Changes the lower bound of the specified variable at the given node. - :param Variable var: variable to change bound of - :param lb: new lower bound (set to None for -infinity) + Parameters + ---------- + node : Node + Node at which the variable bound will be changed + var : Variable + variable to change bound of + lb : float or None + new lower bound (set to None for -infinity) + """ if lb is None: @@ -1819,8 +3216,14 @@ cdef class Model: def chgVarUbNode(self, Node node, Variable var, ub): """Changes the upper bound of the specified variable at the given node. - :param Variable var: variable to change bound of - :param ub: new upper bound (set to None for +infinity) + Parameters + ---------- + node : Node + Node at which the variable bound will be changed + var : Variable + variable to change bound of + lb : float or None + new upper bound (set to None for +infinity) """ if ub is None: @@ -1829,10 +3232,16 @@ cdef class Model: def chgVarType(self, Variable var, vtype): - """Changes the type of a variable + """ + Changes the type of a variable. - :param Variable var: variable to change type of - :param vtype: new variable type + Parameters + ---------- + var : Variable + variable to change type of + vtype : str + new variable type. 'C' or "CONTINUOUS", 'I' or "INTEGER", + 'B' or "BINARY", and 'M' "IMPLINT". """ cdef SCIP_Bool infeasible @@ -1850,9 +3259,17 @@ cdef class Model: print('could not change variable type of variable %s' % var) def getVars(self, transformed=False): - """Retrieve all variables. + """ + Retrieve all variables. + + Parameters + ---------- + transformed : bool, optional + Get transformed variables instead of original (Default value = False) - :param transformed: get transformed variables instead of original (Default value = False) + Returns + ------- + list of Variable """ cdef SCIP_VAR** _vars @@ -1883,9 +3300,18 @@ cdef class Model: return vars def getNVars(self, transformed=True): - """Retrieve number of variables in the problems. - - :param transformed: get transformed variables instead of original (Default value = True) + """ + Retrieve number of variables in the problems. + + Parameters + ---------- + transformed : bool, optional + Get transformed variables instead of original (Default value = True) + + Returns + ------- + int + """ if transformed: return SCIPgetNVars(self._scip) @@ -1893,40 +3319,80 @@ cdef class Model: return SCIPgetNOrigVars(self._scip) def getNIntVars(self): - """gets number of integer active problem variables""" + """ + Gets number of integer active problem variables. + + Returns + ------- + int + + """ return SCIPgetNIntVars(self._scip) def getNBinVars(self): - """gets number of binary active problem variables""" + """ + Gets number of binary active problem variables. + + Returns + ------- + int + + """ return SCIPgetNBinVars(self._scip) def getNImplVars(self): - """gets number of implicit integer active problem variables""" + """ + Gets number of implicit integer active problem variables. + + Returns + ------- + int + + """ return SCIPgetNImplVars(self._scip) def getNContVars(self): - """gets number of continuous active problem variables""" + """ + Gets number of continuous active problem variables. + + Returns + ------- + int + + """ return SCIPgetNContVars(self._scip) def getVarDict(self): - """gets dictionary with variables names as keys and current variable values as items""" + """ + Gets dictionary with variables names as keys and current variable values as items. + + Returns + ------- + dict of str to float + + """ var_dict = {} for var in self.getVars(): var_dict[var.name] = self.getVal(var) return var_dict def updateNodeLowerbound(self, Node node, lb): - """if given value is larger than the node's lower bound (in transformed problem), - sets the node's lower bound to the new value + """ + If given value is larger than the node's lower bound (in transformed problem), + sets the node's lower bound to the new value. - :param node: Node, the node to update - :param newbound: float, new bound (if greater) for the node + Parameters + ---------- + node : Node + the node to update + lb : float + new bound (if greater) for the node """ PY_SCIP_CALL(SCIPupdateNodeLowerbound(self._scip, node.scip_node, lb)) def relax(self): - """Relaxes the integrality restrictions of the model""" + """Relaxes the integrality restrictions of the model.""" if self.getStage() != SCIP_STAGE_PROBLEM: raise Warning("method can only be called in stage PROBLEM") @@ -1935,37 +3401,93 @@ cdef class Model: # Node methods def getBestChild(self): - """gets the best child of the focus node w.r.t. the node selection strategy.""" + """ + Gets the best child of the focus node w.r.t. the node selection strategy. + + Returns + ------- + Node + + """ return Node.create(SCIPgetBestChild(self._scip)) def getBestSibling(self): - """gets the best sibling of the focus node w.r.t. the node selection strategy.""" + """ + Gets the best sibling of the focus node w.r.t. the node selection strategy. + + Returns + ------- + Node + + """ return Node.create(SCIPgetBestSibling(self._scip)) def getPrioChild(self): - """gets the best child of the focus node w.r.t. the node selection priority assigned by the branching rule.""" + """ + Gets the best child of the focus node w.r.t. the node selection priority + assigned by the branching rule. + + Returns + ------- + Node + + """ return Node.create(SCIPgetPrioChild(self._scip)) def getPrioSibling(self): - """gets the best sibling of the focus node w.r.t. the node selection priority assigned by the branching rule.""" + """Gets the best sibling of the focus node w.r.t. + the node selection priority assigned by the branching rule. + + Returns + ------- + Node + + """ return Node.create(SCIPgetPrioSibling(self._scip)) def getBestLeaf(self): - """gets the best leaf from the node queue w.r.t. the node selection strategy.""" + """Gets the best leaf from the node queue w.r.t. the node selection strategy. + + Returns + ------- + Node + + """ return Node.create(SCIPgetBestLeaf(self._scip)) def getBestNode(self): - """gets the best node from the tree (child, sibling, or leaf) w.r.t. the node selection strategy.""" + """Gets the best node from the tree (child, sibling, or leaf) w.r.t. the node selection strategy. + + Returns + ------- + Node + + """ return Node.create(SCIPgetBestNode(self._scip)) def getBestboundNode(self): - """gets the node with smallest lower bound from the tree (child, sibling, or leaf).""" + """Gets the node with smallest lower bound from the tree (child, sibling, or leaf). + + Returns + ------- + Node + + """ return Node.create(SCIPgetBestboundNode(self._scip)) def getOpenNodes(self): - """access to all data of open nodes (leaves, children, and siblings) + """ + Access to all data of open nodes (leaves, children, and siblings). + + Returns + ------- + leaves : list of Node + list of all open leaf nodes + children : list of Node + list of all open children nodes + siblings : list of Node + list of all open sibling nodes - :return: three lists containing open leaves, children, siblings """ cdef SCIP_NODE** _leaves cdef SCIP_NODE** _children @@ -1983,21 +3505,33 @@ cdef class Model: return leaves, children, siblings def repropagateNode(self, Node node): - """marks the given node to be propagated again the next time a node of its subtree is processed""" + """Marks the given node to be propagated again the next time a node of its subtree is processed.""" PY_SCIP_CALL(SCIPrepropagateNode(self._scip, node.scip_node)) # LP Methods def getLPSolstat(self): - """Gets solution status of current LP""" + """ + Gets solution status of current LP. + + Returns + ------- + SCIP_LPSOLSTAT + + """ return SCIPgetLPSolstat(self._scip) def constructLP(self): - """makes sure that the LP of the current node is loaded and - may be accessed through the LP information methods + """ + Makes sure that the LP of the current node is loaded and + may be accessed through the LP information methods. + - :return: bool cutoff, i.e. can the node be cut off? + Returns + ------- + cutoff : bool + Can the node be cutoff? """ cdef SCIP_Bool cutoff @@ -2005,12 +3539,26 @@ cdef class Model: return cutoff def getLPObjVal(self): - """gets objective value of current LP (which is the sum of column and loose objective value)""" + """ + Gets objective value of current LP (which is the sum of column and loose objective value). + + Returns + ------- + float + + """ return SCIPgetLPObjval(self._scip) def getLPColsData(self): - """Retrieve current LP columns""" + """ + Retrieve current LP columns. + + Returns + ------- + list of Column + + """ cdef SCIP_COL** cols cdef int ncols @@ -2018,7 +3566,14 @@ cdef class Model: return [Column.create(cols[i]) for i in range(ncols)] def getLPRowsData(self): - """Retrieve current LP rows""" + """ + Retrieve current LP rows. + + Returns + ------- + list of Row + + """ cdef SCIP_ROW** rows cdef int nrows @@ -2026,15 +3581,37 @@ cdef class Model: return [Row.create(rows[i]) for i in range(nrows)] def getNLPRows(self): - """Retrieve the number of rows currently in the LP""" + """ + Retrieve the number of rows currently in the LP. + + Returns + ------- + int + + """ return SCIPgetNLPRows(self._scip) def getNLPCols(self): - """Retrieve the number of cols currently in the LP""" + """ + Retrieve the number of columns currently in the LP. + + Returns + ------- + int + + """ return SCIPgetNLPCols(self._scip) def getLPBasisInd(self): - """Gets all indices of basic columns and rows: index i >= 0 corresponds to column i, index i < 0 to row -i-1""" + """ + Gets all indices of basic columns and rows: + index i >= 0 corresponds to column i, index i < 0 to row -i-1 + + Returns + ------- + list of int + + """ cdef int nrows = SCIPgetNLPRows(self._scip) cdef int* inds = malloc(nrows * sizeof(int)) @@ -2044,7 +3621,19 @@ cdef class Model: return result def getLPBInvRow(self, row): - """gets a row from the inverse basis matrix B^-1""" + """ + Gets a row from the inverse basis matrix B^-1 + + Parameters + ---------- + row : int + The row index of the inverse basis matrix + + Returns + ------- + list of float + + """ # TODO: sparsity information cdef int nrows = SCIPgetNLPRows(self._scip) cdef SCIP_Real* coefs = malloc(nrows * sizeof(SCIP_Real)) @@ -2055,7 +3644,19 @@ cdef class Model: return result def getLPBInvARow(self, row): - """gets a row from B^-1 * A""" + """ + Gets a row from B^-1 * A. + + Parameters + ---------- + row : int + The row index of the inverse basis matrix multiplied by the coefficient matrix + + Returns + ------- + list of float + + """ # TODO: sparsity information cdef int ncols = SCIPgetNLPCols(self._scip) cdef SCIP_Real* coefs = malloc(ncols * sizeof(SCIP_Real)) @@ -2066,35 +3667,72 @@ cdef class Model: return result def isLPSolBasic(self): - """returns whether the current LP solution is basic, i.e. is defined by a valid simplex basis""" + """ + Returns whether the current LP solution is basic, i.e. is defined by a valid simplex basis. + + Returns + ------- + bool + + """ return SCIPisLPSolBasic(self._scip) def allColsInLP(self): - """checks if all columns, i.e. every variable with non-empty column is present in the LP. - This is not True when performing pricing for instance.""" + """ + Checks if all columns, i.e. every variable with non-empty column is present in the LP. + This is not True when performing pricing for instance. + + Returns + ------- + bool + + """ return SCIPallColsInLP(self._scip) # LP Col Methods def getColRedCost(self, Column col): - """gets the reduced cost of the column in the current LP + """ + Gets the reduced cost of the column in the current LP. + + Parameters + ---------- + col : Column + + Returns + ------- + float - :param Column col: the column of the LP for which the reduced cost will be retrieved """ return SCIPgetColRedcost(self._scip, col.scip_col) #TODO: documentation!! # LP Row Methods def createEmptyRowSepa(self, Sepa sepa, name="row", lhs = 0.0, rhs = None, local = True, modifiable = False, removable = True): - """creates and captures an LP row without any coefficients from a separator + """ + Creates and captures an LP row without any coefficients from a separator. + + Parameters + ---------- + sepa : Sepa + separator that creates the row + name : str, optional + name of row (Default value = "row") + lhs : float or None, optional + left hand side of row (Default value = 0) + rhs : float or None, optional + right hand side of row (Default value = None) + local : bool, optional + is row only valid locally? (Default value = True) + modifiable : bool, optional + is row modifiable during node processing (subject to column generation)? (Default value = False) + removable : bool, optional + should the row be removed from the LP due to aging or cleanup? (Default value = True) + + Returns + ------- + Row - :param sepa: separator that creates the row - :param name: name of row (Default value = "row") - :param lhs: left hand side of row (Default value = 0) - :param rhs: right hand side of row (Default value = None) - :param local: is row only valid locally? (Default value = True) - :param modifiable: is row modifiable during node processing (subject to column generation)? (Default value = False) - :param removable: should the row be removed from the LP due to aging or cleanup? (Default value = True) """ cdef SCIP_ROW* row lhs = -SCIPinfinity(self._scip) if lhs is None else lhs @@ -2105,14 +3743,28 @@ cdef class Model: return PyRow def createEmptyRowUnspec(self, name="row", lhs = 0.0, rhs = None, local = True, modifiable = False, removable = True): - """creates and captures an LP row without any coefficients from an unspecified source + """ + Creates and captures an LP row without any coefficients from an unspecified source. + + Parameters + ---------- + name : str, optional + name of row (Default value = "row") + lhs : float or None, optional + left hand side of row (Default value = 0) + rhs : float or None, optional + right hand side of row (Default value = None) + local : bool, optional + is row only valid locally? (Default value = True) + modifiable : bool, optional + is row modifiable during node processing (subject to column generation)? (Default value = False) + removable : bool, optional + should the row be removed from the LP due to aging or cleanup? (Default value = True) + + Returns + ------- + Row - :param name: name of row (Default value = "row") - :param lhs: left hand side of row (Default value = 0) - :param rhs: right hand side of row (Default value = None) - :param local: is row only valid locally? (Default value = True) - :param modifiable: is row modifiable during node processing (subject to column generation)? (Default value = False) - :param removable: should the row be removed from the LP due to aging or cleanup? (Default value = True) """ cdef SCIP_ROW* row lhs = -SCIPinfinity(self._scip) if lhs is None else lhs @@ -2121,108 +3773,313 @@ cdef class Model: PyRow = Row.create(row) return PyRow - def getRowActivity(self, Row row): - """returns the activity of a row in the last LP or pseudo solution""" - return SCIPgetRowActivity(self._scip, row.scip_row) + def getRowActivity(self, Row row): + """ + Returns the activity of a row in the last LP or pseudo solution. + + Parameters + ---------- + row : Row + + Returns + ------- + float + + """ + return SCIPgetRowActivity(self._scip, row.scip_row) + + def getRowLPActivity(self, Row row): + """ + Returns the activity of a row in the last LP solution. + + Parameters + ---------- + row : Row + + Returns + ------- + float - def getRowLPActivity(self, Row row): - """returns the activity of a row in the last LP solution""" + """ return SCIPgetRowLPActivity(self._scip, row.scip_row) # TODO: do we need this? (also do we need release var??) def releaseRow(self, Row row not None): - """decreases usage counter of LP row, and frees memory if necessary""" + """ + Decreases usage counter of LP row, and frees memory if necessary. + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPreleaseRow(self._scip, &row.scip_row)) def cacheRowExtensions(self, Row row not None): - """informs row, that all subsequent additions of variables to the row should be cached and not directly applied; + """ + Informs row that all subsequent additions of variables to the row + should be cached and not directly applied; after all additions were applied, flushRowExtensions() must be called; - while the caching of row extensions is activated, information methods of the row give invalid results; - caching should be used, if a row is build with addVarToRow() calls variable by variable to increase the performance""" + while the caching of row extensions is activated, information methods of the + row give invalid results; caching should be used, if a row is build with addVarToRow() + calls variable by variable to increase the performance. + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPcacheRowExtensions(self._scip, row.scip_row)) def flushRowExtensions(self, Row row not None): - """flushes all cached row extensions after a call of cacheRowExtensions() and merges coefficients with equal columns into a single coefficient""" + """ + Flushes all cached row extensions after a call of cacheRowExtensions() + and merges coefficients with equal columns into a single coefficient + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPflushRowExtensions(self._scip, row.scip_row)) def addVarToRow(self, Row row not None, Variable var not None, value): - """resolves variable to columns and adds them with the coefficient to the row""" + """ + Resolves variable to columns and adds them with the coefficient to the row. + + Parameters + ---------- + row : Row + Row in which the variable will be added + var : Variable + Variable which will be added to the row + value : float + Coefficient on the variable when placed in the row + + """ PY_SCIP_CALL(SCIPaddVarToRow(self._scip, row.scip_row, var.scip_var, value)) def printRow(self, Row row not None): - """Prints row.""" + """ + Prints row. + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPprintRow(self._scip, row.scip_row, NULL)) def getRowNumIntCols(self, Row row): - """Returns number of intergal columns in the row""" + """ + Returns number of intergal columns in the row. + + Parameters + ---------- + row : Row + + Returns + ------- + int + + """ return SCIPgetRowNumIntCols(self._scip, row.scip_row) def getRowObjParallelism(self, Row row): - """Returns 1 if the row is parallel, and 0 if orthogonal""" + """ + Returns 1 if the row is parallel, and 0 if orthogonal. + + Parameters + ---------- + row : Row + + Returns + ------- + float + + """ return SCIPgetRowObjParallelism(self._scip, row.scip_row) def getRowParallelism(self, Row row1, Row row2, orthofunc=101): - """Returns the degree of parallelism between hyplerplanes. 1 if perfectly parallel, 0 if orthogonal. - For two row vectors v, w the parallelism is calculated as: |v*w|/(|v|*|w|). - 101 in this case is an 'e' (euclidean) in ASCII. The other acceptable input is 100 (d for discrete).""" + """ + Returns the degree of parallelism between hyplerplanes. 1 if perfectly parallel, 0 if orthogonal. + For two row vectors v, w the parallelism is calculated as: abs(v*w)/(abs(v)*abs(w)). + 101 in this case is an 'e' (euclidean) in ASCII. The other acceptable input is 100 (d for discrete). + + Parameters + ---------- + row1 : Row + row2 : Row + orthofunc : int, optional + 101 (default) is an 'e' (euclidean) in ASCII. Alternate value is 100 (d for discrete) + + Returns + ------- + float + + """ return SCIProwGetParallelism(row1.scip_row, row2.scip_row, orthofunc) def getRowDualSol(self, Row row): - """Gets the dual LP solution of a row""" + """ + Gets the dual LP solution of a row. + + Parameters + ---------- + row : Row + + Returns + ------- + float + + """ return SCIProwGetDualsol(row.scip_row) # Cutting Plane Methods def addPoolCut(self, Row row not None): - """if not already existing, adds row to global cut pool""" + """ + If not already existing, adds row to global cut pool. + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPaddPoolCut(self._scip, row.scip_row)) def getCutEfficacy(self, Row cut not None, Solution sol = None): - """returns efficacy of the cut with respect to the given primal solution or the current LP solution: e = -feasibility/norm""" + """ + Returns efficacy of the cut with respect to the given primal solution or the + current LP solution: e = -feasibility/norm + + Parameters + ---------- + cut : Row + sol : Solution or None, optional + + Returns + ------- + float + + """ return SCIPgetCutEfficacy(self._scip, NULL if sol is None else sol.sol, cut.scip_row) def isCutEfficacious(self, Row cut not None, Solution sol = None): - """ returns whether the cut's efficacy with respect to the given primal solution or the current LP solution is greater than the minimal cut efficacy""" + """ + Returns whether the cut's efficacy with respect to the given primal solution or the + current LP solution is greater than the minimal cut efficacy. + + Parameters + ---------- + cut : Row + sol : Solution or None, optional + + Returns + ------- + float + + """ return SCIPisCutEfficacious(self._scip, NULL if sol is None else sol.sol, cut.scip_row) def getCutLPSolCutoffDistance(self, Row cut not None, Solution sol not None): - """ returns row's cutoff distance in the direction of the given primal solution""" + """ + Returns row's cutoff distance in the direction of the given primal solution. + + Parameters + ---------- + cut : Row + sol : Solution + + Returns + ------- + float + + """ return SCIPgetCutLPSolCutoffDistance(self._scip, sol.sol, cut.scip_row) def addCut(self, Row cut not None, forcecut = False): - """adds cut to separation storage and returns whether cut has been detected to be infeasible for local bounds""" + """ + Adds cut to separation storage and returns whether cut has been detected to be infeasible for local bounds. + + Parameters + ---------- + cut : Row + The cut that will be added + forcecut : bool, optional + Whether the cut should be forced or not, i.e., selected no matter what + + Returns + ------- + infeasible : bool + Whether the cut has been detected to be infeasible from local bounds + + """ cdef SCIP_Bool infeasible PY_SCIP_CALL(SCIPaddRow(self._scip, cut.scip_row, forcecut, &infeasible)) return infeasible def getNCuts(self): - """Retrieve total number of cuts in storage""" + """ + Retrieve total number of cuts in storage. + + Returns + ------- + int + + """ return SCIPgetNCuts(self._scip) def getNCutsApplied(self): - """Retrieve number of currently applied cuts""" + """ + Retrieve number of currently applied cuts. + + Returns + ------- + int + + """ return SCIPgetNCutsApplied(self._scip) def getNSepaRounds(self): - """Retrieve the number of separation rounds that have been performed - at the current node""" + """ + Retrieve the number of separation rounds that have been performed + at the current node. + + Returns + ------- + int + + """ return SCIPgetNSepaRounds(self._scip) def separateSol(self, Solution sol = None, pretendroot = False, allowlocal = True, onlydelayed = False): - """separates the given primal solution or the current LP solution by calling the separators and constraint handlers' - separation methods; - the generated cuts are stored in the separation storage and can be accessed with the methods SCIPgetCuts() and - SCIPgetNCuts(); + """ + Separates the given primal solution or the current LP solution by calling + the separators and constraint handlers' separation methods; + the generated cuts are stored in the separation storage and can be accessed + with the methods SCIPgetCuts() and SCIPgetNCuts(); after evaluating the cuts, you have to call SCIPclearCuts() in order to remove the cuts from the - separation storage; - it is possible to call SCIPseparateSol() multiple times with different solutions and evaluate the found cuts - afterwards - :param Solution sol: solution to separate, None to use current lp solution (Default value = None) - :param pretendroot: should the cut separators be called as if we are at the root node? (Default value = "False") - :param allowlocal: should the separator be asked to separate local cuts (Default value = True) - :param onlydelayed: should only separators be called that were delayed in the previous round? (Default value = False) - returns - delayed -- whether a separator was delayed - cutoff -- whether the node can be cut off + separation storage; it is possible to call SCIPseparateSol() multiple times with + different solutions and evaluate the found cuts afterwards. + + Parameters + ---------- + sol : Solution or None, optional + solution to separate, None to use current lp solution (Default value = None) + pretendroot : bool, optional + should the cut separators be called as if we are at the root node? (Default value = "False") + allowlocal : bool, optional + should the separator be asked to separate local cuts (Default value = True) + onlydelayed : bool, optional + should only separators be called that were delayed in the previous round? (Default value = False) + + Returns + ------- + delayed : bool + whether a separator was delayed + cutoff : bool + whether the node can be cut off + """ cdef SCIP_Bool delayed cdef SCIP_Bool cutoff @@ -2231,6 +4088,20 @@ cdef class Model: return delayed, cutoff def _createConsLinear(self, ExprCons lincons, **kwargs): + """ + The function for creating a linear constraint, but not adding it to the Model. + Please do not use this function directly, but rather use createConsFromExpr + + Parameters + ---------- + lincons : ExprCons + kwargs : dict, optional + + Returns + ------- + Constraint + + """ assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() @@ -2261,6 +4132,20 @@ cdef class Model: return PyCons def _createConsQuadratic(self, ExprCons quadcons, **kwargs): + """ + The function for creating a quadratic constraint, but not adding it to the Model. + Please do not use this function directly, but rather use createConsFromExpr + + Parameters + ---------- + quadcons : ExprCons + kwargs : dict, optional + + Returns + ------- + Constraint + + """ terms = quadcons.expr.terms assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() @@ -2300,6 +4185,20 @@ cdef class Model: return PyCons def _createConsNonlinear(self, cons, **kwargs): + """ + The function for creating a non-linear constraint, but not adding it to the Model. + Please do not use this function directly, but rather use createConsFromExpr + + Parameters + ---------- + cons : ExprCons + kwargs : dict, optional + + Returns + ------- + Constraint + + """ cdef SCIP_EXPR* expr cdef SCIP_EXPR** varexprs cdef SCIP_EXPR** monomials @@ -2355,6 +4254,20 @@ cdef class Model: return PyCons def _createConsGenNonlinear(self, cons, **kwargs): + """ + The function for creating a general non-linear constraint, but not adding it to the Model. + Please do not use this function directly, but rather use createConsFromExpr + + Parameters + ---------- + cons : ExprCons + kwargs : dict, optional + + Returns + ------- + Constraint + + """ cdef SCIP_EXPR** childrenexpr cdef SCIP_EXPR** scipexprs cdef SCIP_CONS* scip_cons @@ -2479,23 +4392,44 @@ cdef class Model: enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): - """Create a linear or nonlinear constraint without adding it to the SCIP problem. This is useful for creating disjunction constraints - without also enforcing the individual constituents. Currently, this can only be used as an argument to `.addConsElemDisjunction`. To add + """ + Create a linear or nonlinear constraint without adding it to the SCIP problem. + This is useful for creating disjunction constraints without also enforcing the individual constituents. + Currently, this can only be used as an argument to `.addConsElemDisjunction`. To add an individual linear/nonlinear constraint, prefer `.addCons()`. - :param cons: constraint object - :param name: the name of the constraint, generic name if empty (Default value = '') - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) - :return The created @ref scip#Constraint "Constraint" object. + Parameters + ---------- + cons : ExprCons + The expression constraint that is not yet an actual constraint + name : str, optional + the name of the constraint, generic name if empty (Default value = '') + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The created Constraint object. """ if name == '': @@ -2525,21 +4459,41 @@ cdef class Model: enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): - """Add a linear or nonlinear constraint. + """ + Add a linear or nonlinear constraint. - :param cons: constraint object - :param name: the name of the constraint, generic name if empty (Default value = '') - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) - :return The added @ref scip#Constraint "Constraint" object. + Parameters + ---------- + cons : ExprCons + The expression constraint that is not yet an actual constraint + name : str, optional + the name of the constraint, generic name if empty (Default value = "") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraints always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The created and added Constraint object. """ assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ @@ -2573,25 +4527,45 @@ cdef class Model: Each of the constraints is added to the model using Model.addCons(). - For all parameters, except @p conss, this method behaves differently depending on the type of the passed argument: - 1. If the value is iterable, it must be of the same length as @p conss. For each constraint, Model.addCons() will be called with the value at the corresponding index. - 2. Else, the (default) value will be applied to all of the constraints. - - :param conss An iterable of constraint objects. Any iterable will be converted into a list before further processing. - :param name: the names of the constraints, generic name if empty (Default value = ''). If a single string is passed, it will be suffixed by an underscore and the enumerated index of the constraint (starting with 0). - :param initial: should the LP relaxation of constraints be in the initial LP? (Default value = True) - :param separate: should the constraints be separated during LP processing? (Default value = True) - :param enforce: should the constraints be enforced during node processing? (Default value = True) - :param check: should the constraints be checked for feasibility? (Default value = True) - :param propagate: should the constraints be propagated during node processing? (Default value = True) - :param local: are the constraints only valid locally? (Default value = False) - :param modifiable: are the constraints modifiable (subject to column generation)? (Default value = False) - :param dynamic: are the constraints subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraints always be kept at the node where it was added, even if it may be @oved to a more global node? (Default value = False) - :return A list of added @ref scip#Constraint "Constraint" objects. + For all parameters, except `conss`, this method behaves differently depending on the + type of the passed argument: + 1. If the value is iterable, it must be of the same length as `conss`. For each + constraint, Model.addCons() will be called with the value at the corresponding index. + 2. Else, the (default) value will be applied to all of the constraints. + + Parameters + ---------- + conss : iterable of ExprCons + An iterable of constraint objects. Any iterable will be converted into a list before further processing. + name : str or iterable of str, optional + the name of the constraint, generic name if empty (Default value = '') + initial : bool or iterable of bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool or iterable of bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool or iterable of bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool or iterable of bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool or iterable of bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool or iterable of bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool or iterable of bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool or iterable of bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool or iterable of bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool or iterable of bool, optional + should the constraints always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + list of Constraint + The created and added Constraint objects. - :see addCons() """ def ensure_iterable(elem, length): if isinstance(elem, Iterable): @@ -2633,18 +4607,37 @@ cdef class Model: def addConsDisjunction(self, conss, name = '', initial = True, relaxcons = None, enforce=True, check =True, local=False, modifiable = False, dynamic = False): - """Add a disjunction constraint. + """ + Add a disjunction constraint. + + Parameters + ---------- + conss : iterable of ExprCons + An iterable of constraint objects to be included initially in the disjunction. + Currently, these must be expressions. + name : str, optional + the name of the disjunction constraint. + initial : bool, optional + should the LP relaxation of disjunction constraint be in the initial LP? (Default value = True) + relaxcons : None, optional + a conjunction constraint containing the linear relaxation of the disjunction constraint, or None. + NOT YET SUPPORTED. (Default value = None) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + + Returns + ------- + Constraint + The created disjunction constraint - :param Iterable[Constraint] conss: An iterable of constraint objects to be included initially in the disjunction. Currently, these must be expressions. - :param name: the name of the disjunction constraint. - :param initial: should the LP relaxation of disjunction constraint be in the initial LP? (Default value = True) - :param relaxcons: a conjunction constraint containing the linear relaxation of the disjunction constraint, or None. (Default value = None) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :return The added @ref scip#Constraint "Constraint" object. """ def ensure_iterable(elem, length): if isinstance(elem, Iterable): @@ -2682,11 +4675,21 @@ cdef class Model: return PyCons def addConsElemDisjunction(self, Constraint disj_cons, Constraint cons): - """Appends a constraint to a disjunction. + """ + Appends a constraint to a disjunction. + + Parameters + ---------- + disj_cons : Constraint + the disjunction constraint to append to. + cons : Constraint + the constraint to append + + Returns + ------- + disj_cons : Constraint + The disjunction constraint with `cons` appended. - :param Constraint disj_cons: the disjunction constraint to append to. - :param Constraint cons: the Constraint to append - :return The disjunction constraint with added @ref scip#Constraint object. """ PY_SCIP_CALL(SCIPaddConsElemDisjunction(self._scip, (disj_cons).scip_cons, (cons).scip_cons)) PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(cons).scip_cons)) @@ -2696,7 +4699,20 @@ cdef class Model: """ Gets number of variables in a constraint. - :param constraint: Constraint to get the number of variables from. + Parameters + ---------- + constraint : Constraint + Constraint to get the number of variables from. + + Returns + ------- + int + + Raises + ------ + TypeError + If the associated constraint handler does not have this functionality + """ cdef int nvars cdef SCIP_Bool success @@ -2714,7 +4730,15 @@ cdef class Model: """ Gets variables in a constraint. - :param constraint: Constraint to get the variables from. + Parameters + ---------- + constraint : Constraint + Constraint to get the variables from. + + Returns + ------- + list of Variable + """ cdef SCIP_Bool success cdef int _nvars @@ -2739,13 +4763,26 @@ cdef class Model: return vars def printCons(self, Constraint constraint): + """ + Print the constraint + + Parameters + ---------- + constraint : Constraint + + """ return PY_SCIP_CALL(SCIPprintCons(self._scip, constraint.scip_cons, NULL)) - # TODO Find a better way to retrieve a scip expression from a python expression. Consider making GenExpr include Expr, to avoid using Union. See PR #760. - from typing import Union - def addExprNonlinear(self, Constraint cons, expr: Union[Expr,GenExpr], float coef): + def addExprNonlinear(self, Constraint cons, expr, coef): """ Add coef*expr to nonlinear constraint. + + Parameters + ---------- + cons : Constraint + expr : Expr or GenExpr + coef : float + """ assert self.getStage() == 1, "addExprNonlinear cannot be called in stage %i." % self.getStage() assert cons.isNonlinear(), "addExprNonlinear can only be called with nonlinear constraints." @@ -2760,21 +4797,33 @@ cdef class Model: self.delCons(temp_cons) def addConsCoeff(self, Constraint cons, Variable var, coeff): - """Add coefficient to the linear constraint (if non-zero). + """ + Add coefficient to the linear constraint (if non-zero). - :param Constraint cons: constraint to be changed - :param Variable var: variable to be added - :param coeff: coefficient of new variable + Parameters + ---------- + cons : Constraint + Constraint to be changed + var : Variable + variable to be added + coeff : float + coefficient of new variable """ PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, coeff)) def addConsNode(self, Node node, Constraint cons, Node validnode=None): - """Add a constraint to the given node + """ + Add a constraint to the given node. - :param Node node: node to add the constraint to - :param Constraint cons: constraint to add - :param Node validnode: more global node where cons is also valid + Parameters + ---------- + node : Node + node at which the constraint will be added + cons : Constraint + the constraint to add to the node + validnode : Node or None, optional + more global node where cons is also valid. (Default=None) """ if isinstance(validnode, Node): @@ -2784,10 +4833,15 @@ cdef class Model: Py_INCREF(cons) def addConsLocal(self, Constraint cons, Node validnode=None): - """Add a constraint to the current node + """ + Add a constraint to the current node. - :param Constraint cons: constraint to add - :param Node validnode: more global node where cons is also valid + Parameters + ---------- + cons : Constraint + the constraint to add to the current node + validnode : Node or None, optional + more global node where cons is also valid. (Default=None) """ if isinstance(validnode, Node): @@ -2800,7 +4854,8 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): - """Add an SOS1 constraint. + """ + Add an SOS1 constraint. :param vars: list of variables to be included :param weights: list of weights (Default value = None) @@ -2815,6 +4870,40 @@ cdef class Model: :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + + Parameters + ---------- + vars : list of Variable + list of variables to be included + weights : list of float or None, optional + list of weights (Default value = None) + name : str, optional + name of the constraint (Default value = "SOS1cons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created SOS1 constraint + """ cdef SCIP_CONS* scip_cons cdef int _nvars @@ -2839,20 +4928,41 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): - """Add an SOS2 constraint. + """ + Add an SOS2 constraint. - :param vars: list of variables to be included - :param weights: list of weights (Default value = None) - :param name: name of the constraint (Default value = "SOS2cons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: is the constraint only valid locally? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + Parameters + ---------- + vars : list of Variable + list of variables to be included + weights : list of float or None, optional + list of weights (Default value = None) + name : str, optional + name of the constraint (Default value = "SOS2cons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created SOS2 constraint """ cdef SCIP_CONS* scip_cons @@ -2878,20 +4988,42 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): - """Add an AND-constraint. - :param vars: list of BINARY variables to be included (operators) - :param resvar: BINARY variable (resultant) - :param name: name of the constraint (Default value = "ANDcons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + """ + Add an AND-constraint. + + Parameters + ---------- + vars : list of Variable + list of BINARY variables to be included (operators) + resvar : Variable + BINARY variable (resultant) + name : str, optional + name of the constraint (Default value = "ANDcons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created AND constraint + """ cdef SCIP_CONS* scip_cons @@ -2913,24 +5045,46 @@ cdef class Model: return pyCons - def addConsOr(self, vars, resvar, name="ORcons", - initial=True, separate=True, enforce=True, check=True, - propagate=True, local=False, modifiable=False, dynamic=False, - removable=False, stickingatnode=False): - """Add an OR-constraint. - :param vars: list of BINARY variables to be included (operators) - :param resvar: BINARY variable (resultant) - :param name: name of the constraint (Default value = "ORcons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + def addConsOr(self, vars, resvar, name="ORcons", + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=False, modifiable=False, dynamic=False, + removable=False, stickingatnode=False): + """ + Add an OR-constraint. + + Parameters + ---------- + vars : list of Variable + list of BINARY variables to be included (operators) + resvar : Variable + BINARY variable (resultant) + name : str, optional + name of the constraint (Default value = "ORcons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created OR constraint + """ cdef SCIP_CONS* scip_cons @@ -2956,20 +5110,42 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): - """Add a XOR-constraint. - :param vars: list of BINARY variables to be included (operators) - :param rhsvar: BOOLEAN value, explicit True, False or bool(obj) is needed (right-hand side) - :param name: name of the constraint (Default value = "XORcons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + """ + Add a XOR-constraint. + + Parameters + ---------- + vars : list of Variable + list of binary variables to be included (operators) + rhsvar : bool + BOOLEAN value, explicit True, False or bool(obj) is needed (right-hand side) + name : str, optional + name of the constraint (Default value = "XORcons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created XOR constraint + """ cdef SCIP_CONS* scip_cons @@ -2995,22 +5171,48 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): - """Add a cardinality constraint that allows at most 'cardval' many nonzero variables. + """ + Add a cardinality constraint that allows at most 'cardval' many nonzero variables. - :param consvars: list of variables to be included - :param cardval: nonnegative integer - :param indvars: indicator variables indicating which variables may be treated as nonzero in cardinality constraint, or None if new indicator variables should be introduced automatically (Default value = None) - :param weights: weights determining the variable order, or None if variables should be ordered in the same way they were added to the constraint (Default value = None) - :param name: name of the constraint (Default value = "CardinalityCons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + Parameters + ---------- + consvars : list of Variable + list of variables to be included + cardval : int + nonnegative integer + indvars : list of Variable or None, optional + indicator variables indicating which variables may be treated as nonzero in + cardinality constraint, or None if new indicator variables should be + introduced automatically (Default value = None) + weights : list of float or None, optional + weights determining the variable order, or None if variables should be ordered + in the same way they were added to the constraint (Default value = None) + name : str, optional + name of the constraint (Default value = "CardinalityCons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created Cardinality constraint """ cdef SCIP_CONS* scip_cons @@ -3045,24 +5247,45 @@ cdef class Model: initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): - """Add an indicator constraint for the linear inequality 'cons'. + """Add an indicator constraint for the linear inequality `cons`. - The 'binvar' argument models the redundancy of the linear constraint. A solution for which - 'binvar' is 1 must satisfy the constraint. + The `binvar` argument models the redundancy of the linear constraint. A solution for which + `binvar` is 1 must satisfy the constraint. - :param cons: a linear inequality of the form "<=" - :param binvar: binary indicator variable, or None if it should be created (Default value = None) - :param activeone: constraint should active if binvar is 1 (0 if activeone = False) - :param name: name of the constraint (Default value = "IndicatorCons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + Parameters + ---------- + cons : ExprCons + a linear inequality of the form "<=" + binvar : Variable, optional + binary indicator variable, or None if it should be created (Default value = None) + activeone : bool, optional + constraint should active if binvar is 1 (0 if activeone = False) + name : str, optional + name of the constraint (Default value = "IndicatorCons") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added, + even if it may be moved to a more global node? (Default value = False) + + Returns + ------- + Constraint + The newly created Indicator constraint """ assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ @@ -3106,102 +5329,154 @@ cdef class Model: return pyCons def getSlackVarIndicator(self, Constraint cons): - """Get slack variable of an indicator constraint. + """ + Get slack variable of an indicator constraint. + - :param Constraint cons: indicator constraint + Parameters + ---------- + cons : Constraint + The indicator constraint + + Returns + ------- + Variable """ cdef SCIP_VAR* var = SCIPgetSlackVarIndicator(cons.scip_cons); return Variable.create(var) def addPyCons(self, Constraint cons): - """Adds a customly created cons. + """ + Adds a customly created cons. - :param Constraint cons: constraint to add + Parameters + ---------- + cons : Constraint + constraint to add """ PY_SCIP_CALL(SCIPaddCons(self._scip, cons.scip_cons)) Py_INCREF(cons) def addVarSOS1(self, Constraint cons, Variable var, weight): - """Add variable to SOS1 constraint. + """ + Add variable to SOS1 constraint. - :param Constraint cons: SOS1 constraint - :param Variable var: new variable - :param weight: weight of new variable + Parameters + ---------- + cons : Constraint + SOS1 constraint + var : Variable + new variable + weight : weight + weight of new variable """ PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, cons.scip_cons, var.scip_var, weight)) def appendVarSOS1(self, Constraint cons, Variable var): - """Append variable to SOS1 constraint. + """ + Append variable to SOS1 constraint. - :param Constraint cons: SOS1 constraint - :param Variable var: variable to append + Parameters + ---------- + cons : Constraint + SOS1 constraint + var : Variable + variable to append """ PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, cons.scip_cons, var.scip_var)) def addVarSOS2(self, Constraint cons, Variable var, weight): - """Add variable to SOS2 constraint. + """ + Add variable to SOS2 constraint. - :param Constraint cons: SOS2 constraint - :param Variable var: new variable - :param weight: weight of new variable + Parameters + ---------- + cons : Constraint + SOS2 constraint + var : Variable + new variable + weight : weight + weight of new variable """ PY_SCIP_CALL(SCIPaddVarSOS2(self._scip, cons.scip_cons, var.scip_var, weight)) def appendVarSOS2(self, Constraint cons, Variable var): - """Append variable to SOS2 constraint. + """ + Append variable to SOS2 constraint. - :param Constraint cons: SOS2 constraint - :param Variable var: variable to append + Parameters + ---------- + cons : Constraint + SOS2 constraint + var : Variable + variable to append """ PY_SCIP_CALL(SCIPappendVarSOS2(self._scip, cons.scip_cons, var.scip_var)) def setInitial(self, Constraint cons, newInit): - """Set "initial" flag of a constraint. + """ + Set "initial" flag of a constraint. + + Parameters + ---------- + cons : Constraint + newInit : bool - Keyword arguments: - cons -- constraint - newInit -- new initial value """ PY_SCIP_CALL(SCIPsetConsInitial(self._scip, cons.scip_cons, newInit)) def setRemovable(self, Constraint cons, newRem): - """Set "removable" flag of a constraint. + """ + Set "removable" flag of a constraint. + + Parameters + ---------- + cons : Constraint + newRem : bool - Keyword arguments: - cons -- constraint - newRem -- new removable value """ PY_SCIP_CALL(SCIPsetConsRemovable(self._scip, cons.scip_cons, newRem)) def setEnforced(self, Constraint cons, newEnf): - """Set "enforced" flag of a constraint. + """ + Set "enforced" flag of a constraint. + + Parameters + ---------- + cons : Constraint + newEnf : bool - Keyword arguments: - cons -- constraint - newEnf -- new enforced value """ PY_SCIP_CALL(SCIPsetConsEnforced(self._scip, cons.scip_cons, newEnf)) def setCheck(self, Constraint cons, newCheck): - """Set "check" flag of a constraint. + """ + Set "check" flag of a constraint. + + Parameters + ---------- + cons : Constraint + newCheck : bool - Keyword arguments: - cons -- constraint - newCheck -- new check value """ PY_SCIP_CALL(SCIPsetConsChecked(self._scip, cons.scip_cons, newCheck)) def chgRhs(self, Constraint cons, rhs): - """Change right hand side value of a constraint. + """ + Change right-hand side value of a constraint. - :param Constraint cons: linear or quadratic constraint - :param rhs: new right hand side (set to None for +infinity) + Parameters + ---------- + cons : Constraint + linear or quadratic constraint + rhs : float or None + new right-hand side (set to None for +infinity) """ @@ -3217,10 +5492,15 @@ cdef class Model: raise Warning("method cannot be called for constraints of type " + constype) def chgLhs(self, Constraint cons, lhs): - """Change left hand side value of a constraint. + """ + Change left-hand side value of a constraint. - :param Constraint cons: linear or quadratic constraint - :param lhs: new left hand side (set to None for -infinity) + Parameters + ---------- + cons : Constraint + linear or quadratic constraint + lhs : float or None + new left-hand side (set to None for -infinity) """ @@ -3236,9 +5516,17 @@ cdef class Model: raise Warning("method cannot be called for constraints of type " + constype) def getRhs(self, Constraint cons): - """Retrieve right hand side value of a constraint. + """ + Retrieve right-hand side value of a constraint. + + Parameters + ---------- + cons : Constraint + linear or quadratic constraint - :param Constraint cons: linear or quadratic constraint + Returns + ------- + float """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') @@ -3250,9 +5538,17 @@ cdef class Model: raise Warning("method cannot be called for constraints of type " + constype) def getLhs(self, Constraint cons): - """Retrieve left hand side value of a constraint. + """ + Retrieve left-hand side value of a constraint. + + Parameters + ---------- + cons : Constraint + linear or quadratic constraint - :param Constraint cons: linear or quadratic constraint + Returns + ------- + float """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') @@ -3264,47 +5560,73 @@ cdef class Model: raise Warning("method cannot be called for constraints of type " + constype) def chgCoefLinear(self, Constraint cons, Variable var, value): - """Changes coefficient of variable in linear constraint; + """ + Changes coefficient of variable in linear constraint; deletes the variable if coefficient is zero; adds variable if not yet contained in the constraint This method may only be called during problem creation stage for an original constraint and variable. This method requires linear time to search for occurences of the variable in the constraint data. - :param Constraint cons: linear constraint - :param Variable var: variable of constraint entry - :param value: new coefficient of constraint entry + Parameters + ---------- + cons : Constraint + linear constraint + var : Variable + variable of constraint entry + value : float + new coefficient of constraint entry """ PY_SCIP_CALL( SCIPchgCoefLinear(self._scip, cons.scip_cons, var.scip_var, value) ) def delCoefLinear(self, Constraint cons, Variable var): - """Deletes variable from linear constraint + """ + Deletes variable from linear constraint This method may only be called during problem creation stage for an original constraint and variable. - This method requires linear time to search for occurences of the variable in the constraint data. + This method requires linear time to search for occurrences of the variable in the constraint data. - :param Constraint cons: linear constraint - :param Variable var: variable of constraint entry + Parameters + ---------- + cons : Constraint + linear constraint + var : Variable + variable of constraint entry """ PY_SCIP_CALL( SCIPdelCoefLinear(self._scip, cons.scip_cons, var.scip_var) ) def addCoefLinear(self, Constraint cons, Variable var, value): - """Adds coefficient to linear constraint (if it is not zero) + """ + Adds coefficient to linear constraint (if it is not zero) - :param Constraint cons: linear constraint - :param Variable var: variable of constraint entry - :param value: coefficient of constraint entry + Parameters + ---------- + cons : Constraint + linear constraint + var : Variable + variable of constraint entry + value : float + coefficient of constraint entry """ PY_SCIP_CALL( SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, value) ) def getActivity(self, Constraint cons, Solution sol = None): - """Retrieve activity of given constraint. + """ + Retrieve activity of given constraint. Can only be called after solving is completed. - :param Constraint cons: linear or quadratic constraint - :param Solution sol: solution to compute activity of, None to use current node's solution (Default value = None) + Parameters + ---------- + cons : Constraint + linear or quadratic constraint + sol : Solution or None, optional + solution to compute activity of, None to use current node's solution (Default value = None) + + Returns + ------- + float """ cdef SCIP_Real activity @@ -3328,13 +5650,22 @@ cdef class Model: def getSlack(self, Constraint cons, Solution sol = None, side = None): - """Retrieve slack of given constraint. + """ + Retrieve slack of given constraint. Can only be called after solving is completed. + Parameters + ---------- + cons : Constraint + linear or quadratic constraint + sol : Solution or None, optional + solution to compute slack of, None to use current node's solution (Default value = None) + side : str or None, optional + whether to use 'lhs' or 'rhs' for ranged constraints, None to return minimum (Default value = None) - :param Constraint cons: linear or quadratic constraint - :param Solution sol: solution to compute slack of, None to use current node's solution (Default value = None) - :param side: whether to use 'lhs' or 'rhs' for ranged constraints, None to return minimum (Default value = None) + Returns + ------- + float """ cdef SCIP_Real activity @@ -3368,9 +5699,16 @@ cdef class Model: return min(lhsslack, rhsslack) def getTransformedCons(self, Constraint cons): - """Retrieve transformed constraint. + """ + Retrieve transformed constraint. + + Parameters + ---------- + cons : Constraint - :param Constraint cons: constraint + Returns + ------- + Constraint """ cdef SCIP_CONS* transcons @@ -3378,25 +5716,55 @@ cdef class Model: return Constraint.create(transcons) def isNLPConstructed(self): - """returns whether SCIP's internal NLP has been constructed""" + """ + Returns whether SCIP's internal NLP has been constructed. + + Returns + ------- + bool + + """ return SCIPisNLPConstructed(self._scip) def getNNlRows(self): - """gets current number of nonlinear rows in SCIP's internal NLP""" + """ + Gets current number of nonlinear rows in SCIP's internal NLP. + + Returns + ------- + int + + """ return SCIPgetNNLPNlRows(self._scip) def getNlRows(self): - """returns a list with the nonlinear rows in SCIP's internal NLP""" + """ + Returns a list with the nonlinear rows in SCIP's internal NLP. + + Returns + ------- + list of NLRow + + """ cdef SCIP_NLROW** nlrows nlrows = SCIPgetNLPNlRows(self._scip) return [NLRow.create(nlrows[i]) for i in range(self.getNNlRows())] def getNlRowSolActivity(self, NLRow nlrow, Solution sol = None): - """gives the activity of a nonlinear row for a given primal solution - Keyword arguments: - nlrow -- nonlinear row - solution -- a primal solution, if None, then the current LP solution is used + """ + Gives the activity of a nonlinear row for a given primal solution. + + Parameters + ---------- + nlrow : NLRow + sol : Solution or None, optional + a primal solution, if None, then the current LP solution is used + + Returns + ------- + float + """ cdef SCIP_Real activity cdef SCIP_SOL* solptr @@ -3406,10 +5774,19 @@ cdef class Model: return activity def getNlRowSolFeasibility(self, NLRow nlrow, Solution sol = None): - """gives the feasibility of a nonlinear row for a given primal solution - Keyword arguments: - nlrow -- nonlinear row - solution -- a primal solution, if None, then the current LP solution is used + """ + Gives the feasibility of a nonlinear row for a given primal solution + + Parameters + ---------- + nlrow : NLRow + sol : Solution or None, optional + a primal solution, if None, then the current LP solution is used + + Returns + ------- + bool + """ cdef SCIP_Real feasibility cdef SCIP_SOL* solptr @@ -3419,7 +5796,18 @@ cdef class Model: return feasibility def getNlRowActivityBounds(self, NLRow nlrow): - """gives the minimal and maximal activity of a nonlinear row w.r.t. the variable's bounds""" + """ + Gives the minimal and maximal activity of a nonlinear row w.r.t. the variable's bounds. + + Parameters + ---------- + nlrow : NLRow + + Returns + ------- + tuple of float + + """ cdef SCIP_Real minactivity cdef SCIP_Real maxactivity @@ -3427,13 +5815,27 @@ cdef class Model: return (minactivity, maxactivity) def printNlRow(self, NLRow nlrow): - """prints nonlinear row""" + """ + Prints nonlinear row. + + Parameters + ---------- + nlrow : NLRow + + """ PY_SCIP_CALL( SCIPprintNlRow(self._scip, nlrow.scip_nlrow, NULL) ) def checkQuadraticNonlinear(self, Constraint cons): - """returns if the given constraint is quadratic + """ + Returns if the given constraint is quadratic. + + Parameters + ---------- + cons : Constraint - :param Constraint cons: constraint + Returns + ------- + bool """ cdef SCIP_Bool isquadratic @@ -3441,9 +5843,18 @@ cdef class Model: return isquadratic def getTermsQuadratic(self, Constraint cons): - """Retrieve bilinear, quadratic, and linear terms of a quadratic constraint. + """ + Retrieve bilinear, quadratic, and linear terms of a quadratic constraint. + + Parameters + ---------- + cons : Constraint - :param Constraint cons: constraint + Returns + ------- + bilinterms : list of tuple + quadterms : list of tuple + linterms : list of tuple """ cdef SCIP_EXPR* expr @@ -3504,13 +5915,30 @@ cdef class Model: return (bilinterms, quadterms, linterms) def setRelaxSolVal(self, Variable var, val): - """sets the value of the given variable in the global relaxation solution""" + """ + Sets the value of the given variable in the global relaxation solution. + + Parameters + ---------- + var : Variable + val : float + + """ PY_SCIP_CALL(SCIPsetRelaxSolVal(self._scip, NULL, var.scip_var, val)) def getConss(self, transformed=True): - """Retrieve all constraints. - - :param transformed: get transformed variables instead of original (Default value = True) + """ + Retrieve all constraints. + + Parameters + ---------- + transformed : bool, optional + get transformed variables instead of original (Default value = True) + + Returns + ------- + list of Constraint + """ cdef SCIP_CONS** _conss cdef int _nconss @@ -3525,32 +5953,60 @@ cdef class Model: return [Constraint.create(_conss[i]) for i in range(_nconss)] def getNConss(self, transformed=True): - """Retrieve number of all constraints""" + """ + Retrieve number of all constraints. + + Parameters + ---------- + transformed : bool, optional + get number of transformed variables instead of original (Default value = True) + + Returns + ------- + int + + """ if transformed: return SCIPgetNConss(self._scip) else: return SCIPgetNOrigConss(self._scip) def delCons(self, Constraint cons): - """Delete constraint from the model + """ + Delete constraint from the model - :param Constraint cons: constraint to be deleted + Parameters + ---------- + cons : Constraint + constraint to be deleted """ PY_SCIP_CALL(SCIPdelCons(self._scip, cons.scip_cons)) def delConsLocal(self, Constraint cons): - """Delete constraint from the current node and it's children + """ + Delete constraint from the current node and its children. - :param Constraint cons: constraint to be deleted + Parameters + ---------- + cons : Constraint + constraint to be deleted """ PY_SCIP_CALL(SCIPdelConsLocal(self._scip, cons.scip_cons)) def getValsLinear(self, Constraint cons): - """Retrieve the coefficients of a linear constraint + """ + Retrieve the coefficients of a linear constraint + + Parameters + ---------- + cons : Constraint + linear constraint to get the coefficients of - :param Constraint cons: linear constraint to get the coefficients of + Returns + ------- + dict of str to float """ cdef SCIP_Real* _vals @@ -3569,10 +6025,18 @@ cdef class Model: return valsdict def getRowLinear(self, Constraint cons): - """Retrieve the linear relaxation of the given linear constraint as a row. - may return NULL if no LP row was yet created; the user must not modify the row! + """ + Retrieve the linear relaxation of the given linear constraint as a row. + may return NULL if no LP row was yet created; the user must not modify the row! + + Parameters + ---------- + cons : Constraint + linear constraint to get the coefficients of - :param Constraint cons: linear constraint to get the coefficients of + Returns + ------- + Row """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') @@ -3583,9 +6047,17 @@ cdef class Model: return Row.create(row) def getDualsolLinear(self, Constraint cons): - """Retrieve the dual solution to a linear constraint. + """ + Retrieve the dual solution to a linear constraint. + + Parameters + ---------- + cons : Constraint + linear constraint - :param Constraint cons: linear constraint + Returns + ------- + float """ constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') @@ -3598,18 +6070,34 @@ cdef class Model: return SCIPgetDualsolLinear(self._scip, transcons.scip_cons) def getDualMultiplier(self, Constraint cons): - """DEPRECATED: Retrieve the dual solution to a linear constraint. + """ + DEPRECATED: Retrieve the dual solution to a linear constraint. + + Parameters + ---------- + cons : Constraint + linear constraint - :param Constraint cons: linear constraint + Returns + ------- + float """ raise Warning("model.getDualMultiplier(cons) is deprecated: please use model.getDualsolLinear(cons)") return self.getDualsolLinear(cons) def getDualfarkasLinear(self, Constraint cons): - """Retrieve the dual farkas value to a linear constraint. + """ + Retrieve the dual farkas value to a linear constraint. + + Parameters + ---------- + cons : Constraint + linear constraint - :param Constraint cons: linear constraint + Returns + ------- + float """ # TODO this should ideally be handled on the SCIP side @@ -3620,9 +6108,17 @@ cdef class Model: return SCIPgetDualfarkasLinear(self._scip, cons.scip_cons) def getVarRedcost(self, Variable var): - """Retrieve the reduced cost of a variable. + """ + Retrieve the reduced cost of a variable. + + Parameters + ---------- + var : Variable + variable to get the reduced cost of - :param Variable var: variable to get the reduced cost of + Returns + ------- + float """ redcost = None @@ -3635,10 +6131,20 @@ cdef class Model: return redcost def getDualSolVal(self, Constraint cons, boundconstraint=False): - """Retrieve returns dual solution value of a constraint. + """ + Returns dual solution value of a constraint. + + Parameters + ---------- + cons : Constraint + constraint to get the dual solution value of + boundconstraint : bool, optional + Decides whether to store a bool if the constraint is a bound constraint + (default = False) - :param Constraint cons: constraint to get the dual solution value of - :param boundconstraint bool: Decides whether to store a bool if the constraint is a bound constraint + Returns + ------- + float """ cdef SCIP_Real _dualsol @@ -3673,10 +6179,14 @@ cdef class Model: # Benders' decomposition methods def initBendersDefault(self, subproblems): - """initialises the default Benders' decomposition with a dictionary of subproblems + """ + Initialises the default Benders' decomposition with a dictionary of subproblems. + + Parameters + ---------- + subproblems : Model or dict of object to Model + a single Model instance or dictionary of Model instances - Keyword arguments: - subproblems -- a single Model instance or dictionary of Model instances """ cdef SCIP** subprobs cdef SCIP_BENDERS* benders @@ -3712,7 +6222,6 @@ cdef class Model: """Solves the subproblems with the best solution to the master problem. Afterwards, the best solution from each subproblem can be queried to get the solution to the original problem. - If the user wants to resolve the subproblems, they must free them by calling freeBendersSubproblems() """ @@ -3737,8 +6246,7 @@ cdef class Model: def freeBendersSubproblems(self): """Calls the free subproblem function for the Benders' decomposition. - This will free all subproblems for all decompositions. - """ + This will free all subproblems for all decompositions. """ cdef SCIP_BENDERS** _benders cdef int nbenders cdef int nsubproblems @@ -3754,9 +6262,16 @@ cdef class Model: j)) def updateBendersLowerbounds(self, lowerbounds, Benders benders=None): - """"updates the subproblem lower bounds for benders using + """ + Updates the subproblem lower bounds for benders using the lowerbounds dict. If benders is None, then the default - Benders' decomposition is updated + Benders' decomposition is updated. + + Parameters + ---------- + lowerbounds : dict of int to float + benders : Benders or None, optional + """ cdef SCIP_BENDERS* _benders @@ -3771,44 +6286,66 @@ cdef class Model: SCIPbendersUpdateSubproblemLowerbound(_benders, d, lowerbounds[d]) def activateBenders(self, Benders benders, int nsubproblems): - """Activates the Benders' decomposition plugin with the input name + """ + Activates the Benders' decomposition plugin with the input name. + + Parameters + ---------- + benders : Benders + the Benders' decomposition to which the subproblem belongs to + nsubproblems : int + the number of subproblems in the Benders' decomposition - Keyword arguments: - benders -- the Benders' decomposition to which the subproblem belongs to - nsubproblems -- the number of subproblems in the Benders' decomposition """ PY_SCIP_CALL(SCIPactivateBenders(self._scip, benders._benders, nsubproblems)) def addBendersSubproblem(self, Benders benders, subproblem): - """adds a subproblem to the Benders' decomposition given by the input + """ + Adds a subproblem to the Benders' decomposition given by the input name. - Keyword arguments: - benders -- the Benders' decomposition to which the subproblem belongs to - subproblem -- the subproblem to add to the decomposition - isconvex -- can be used to specify whether the subproblem is convex + Parameters + ---------- + benders : Benders + the Benders' decomposition to which the subproblem belongs to + subproblem : Model + the subproblem to add to the decomposition + """ PY_SCIP_CALL(SCIPaddBendersSubproblem(self._scip, benders._benders, (subproblem)._scip)) def setBendersSubproblemIsConvex(self, Benders benders, probnumber, isconvex = True): - """sets a flag indicating whether the subproblem is convex + """ + Sets a flag indicating whether the subproblem is convex. + + Parameters + ---------- + benders : Benders + the Benders' decomposition which contains the subproblem + probnumber : int + the problem number of the subproblem that the convexity will be set for + isconvex : bool, optional + flag to indicate whether the subproblem is convex (default=True) - Keyword arguments: - benders -- the Benders' decomposition which contains the subproblem - probnumber -- the problem number of the subproblem that the convexity will be set for - isconvex -- flag to indicate whether the subproblem is convex """ SCIPbendersSetSubproblemIsConvex(benders._benders, probnumber, isconvex) def setupBendersSubproblem(self, probnumber, Benders benders = None, Solution solution = None, checktype = PY_SCIP_BENDERSENFOTYPE.LP): - """ sets up the Benders' subproblem given the master problem solution + """ + Sets up the Benders' subproblem given the master problem solution. + + Parameters + ---------- + probnumber : int + the index of the problem that is to be set up + benders : Benders or None, optional + the Benders' decomposition to which the subproblem belongs to + solution : Solution or None, optional + the master problem solution that is used for the set up, if None, then the LP solution is used + checktype : PY_SCIP_BENDERSENFOTYPE + the type of solution check that prompted the solving of the Benders' subproblems, either + PY_SCIP_BENDERSENFOTYPE: LP, RELAX, PSEUDO or CHECK. Default is LP. - Keyword arguments: - probnumber -- the index of the problem that is to be set up - benders -- the Benders' decomposition to which the subproblem belongs to - solution -- the master problem solution that is used for the set up, if None, then the LP solution is used - checktype -- the type of solution check that prompted the solving of the Benders' subproblems, either - PY_SCIP_BENDERSENFOTYPE: LP, RELAX, PSEUDO or CHECK. Default is LP """ cdef SCIP_BENDERS* scip_benders cdef SCIP_SOL* scip_sol @@ -3828,14 +6365,28 @@ cdef class Model: PY_SCIP_CALL(retcode) def solveBendersSubproblem(self, probnumber, solvecip, Benders benders = None, Solution solution = None): - """ solves the Benders' decomposition subproblem. The convex relaxation will be solved unless + """ + Solves the Benders' decomposition subproblem. The convex relaxation will be solved unless the parameter solvecip is set to True. - Keyword arguments: - probnumber -- the index of the problem that is to be set up - solvecip -- should the CIP of the subproblem be solved, if False, then only the convex relaxation is solved - benders -- the Benders' decomposition to which the subproblem belongs to - solution -- the master problem solution that is used for the set up, if None, then the LP solution is used + Parameters + ---------- + probnumber : int + the index of the problem that is to be set up + solvecip : bool + whether the CIP of the subproblem should be solved. If False, then only the convex relaxation is solved. + benders : Benders or None, optional + the Benders' decomposition to which the subproblem belongs to + solution : Solution or None, optional + the master problem solution that is used for the set up, if None, then the LP solution is used + + Returns + ------- + infeasible : bool + returns whether the current subproblem is infeasible + objective : float or None + the objective function value of the subproblem, can be None + """ cdef SCIP_BENDERS* scip_benders @@ -3859,12 +6410,22 @@ cdef class Model: return infeasible, objective def getBendersSubproblem(self, probnumber, Benders benders = None): - """Returns a Model object that wraps around the SCIP instance of the subproblem. - NOTE: This Model object is just a place holder and SCIP instance will not be freed when the object is destroyed. + """ + Returns a Model object that wraps around the SCIP instance of the subproblem. + NOTE: This Model object is just a place holder and SCIP instance will not be + freed when the object is destroyed. + + Parameters + ---------- + probnumber : int + the problem number for subproblem that is required + benders : Benders or None, optional + the Benders' decomposition object that the subproblem belongs to (Default = None) + + Returns + ------- + Model - Keyword arguments: - probnumber -- the problem number for subproblem that is required - benders -- the Benders' decomposition object for the that the subproblem belongs to (Default = None) """ cdef SCIP_BENDERS* scip_benders cdef SCIP* scip_subprob @@ -3879,13 +6440,23 @@ cdef class Model: return Model.create(scip_subprob) def getBendersVar(self, Variable var, Benders benders = None, probnumber = -1): - """Returns the variable for the subproblem or master problem - depending on the input probnumber + """ + Returns the variable for the subproblem or master problem + depending on the input probnumber. + + Parameters + ---------- + var : Variable + the source variable for which the target variable is requested + benders : Benders or None, optional + the Benders' decomposition to which the subproblem variables belong to + probnumber : int, optional + the problem number for which the target variable belongs, -1 for master problem + + Returns + ------- + Variable or None - Keyword arguments: - var -- the source variable for which the target variable is requested - benders -- the Benders' decomposition to which the subproblem variables belong to - probnumber -- the problem number for which the target variable belongs, -1 for master problem """ cdef SCIP_BENDERS* _benders cdef SCIP_VAR* _mappedvar @@ -3908,11 +6479,20 @@ cdef class Model: return mappedvar def getBendersAuxiliaryVar(self, probnumber, Benders benders = None): - """Returns the auxiliary variable that is associated with the input problem number + """ + Returns the auxiliary variable that is associated with the input problem number + + Parameters + ---------- + probnumber : int + the problem number for which the target variable belongs, -1 for master problem + benders : Benders or None, optional + the Benders' decomposition to which the subproblem variables belong to + + Returns + ------- + Variable - Keyword arguments: - probnumber -- the problem number for which the target variable belongs, -1 for master problem - benders -- the Benders' decomposition to which the subproblem variables belong to """ cdef SCIP_BENDERS* _benders cdef SCIP_VAR* _auxvar @@ -3928,12 +6508,23 @@ cdef class Model: return auxvar def checkBendersSubproblemOptimality(self, Solution solution, probnumber, Benders benders = None): - """Returns whether the subproblem is optimal w.r.t the master problem auxiliary variables. + """ + Returns whether the subproblem is optimal w.r.t the master problem auxiliary variables. + + Parameters + ---------- + solution : Solution + the master problem solution that is being checked for optimamlity + probnumber : int + the problem number for which optimality is being checked + benders : Benders or None, optional + the Benders' decomposition to which the subproblem belongs to + + Returns + ------- + optimal : bool + flag to indicate whether the current subproblem is optimal for the master - Keyword arguments: - solution -- the master problem solution that is being checked for optimamlity - probnumber -- the problem number for which optimality is being checked - benders -- the Benders' decomposition to which the subproblem belongs to """ cdef SCIP_BENDERS* _benders cdef SCIP_SOL* scip_sol @@ -3955,21 +6546,31 @@ cdef class Model: return optimal def includeBendersDefaultCuts(self, Benders benders): - """includes the default Benders' decomposition cuts to the custom Benders' decomposition plugin + """ + Includes the default Benders' decomposition cuts to the custom Benders' decomposition plugin. + + Parameters + ---------- + benders : Benders + the Benders' decomposition that the default cuts will be applied to - Keyword arguments: - benders -- the Benders' decomposition that the default cuts will be applied to """ PY_SCIP_CALL( SCIPincludeBendersDefaultCuts(self._scip, benders._benders) ) def includeEventhdlr(self, Eventhdlr eventhdlr, name, desc): - """Include an event handler. + """ + Include an event handler. + + Parameters + ---------- + eventhdlr : Eventhdlr + event handler + name : str + name of event handler + desc : str + description of event handler - Keyword arguments: - eventhdlr -- event handler - name -- name of event handler - desc -- description of event handler """ n = str_conversion(name) d = str_conversion(desc) @@ -3988,13 +6589,22 @@ cdef class Model: Py_INCREF(eventhdlr) def includePricer(self, Pricer pricer, name, desc, priority=1, delay=True): - """Include a pricer. + """ + Include a pricer. - :param Pricer pricer: pricer - :param name: name of pricer - :param desc: description of pricer - :param priority: priority of pricer (Default value = 1) - :param delay: should the pricer be delayed until no other pricers or already existing problem variables with negative reduced costs are found? (Default value = True) + Parameters + ---------- + pricer : Pricer + pricer + name : str + name of pricer + desc : str + description of pricer + priority : int, optional + priority of pricer (Default value = 1) + delay : bool, optional + should the pricer be delayed until no other pricers or already existing problem variables + with negative reduced costs are found? (Default value = True) """ n = str_conversion(name) @@ -4015,23 +6625,44 @@ cdef class Model: delayprop=False, needscons=True, proptiming=PY_SCIP_PROPTIMING.BEFORELP, presoltiming=PY_SCIP_PRESOLTIMING.MEDIUM): - """Include a constraint handler - - :param Conshdlr conshdlr: constraint handler - :param name: name of constraint handler - :param desc: description of constraint handler - :param sepapriority: priority for separation (Default value = 0) - :param enfopriority: priority for constraint enforcing (Default value = 0) - :param chckpriority: priority for checking feasibility (Default value = 0) - :param sepafreq: frequency for separating cuts; 0 = only at root node (Default value = -1) - :param propfreq: frequency for propagating domains; 0 = only preprocessing propagation (Default value = -1) - :param eagerfreq: frequency for using all instead of only the useful constraints in separation, propagation and enforcement; -1 = no eager evaluations, 0 = first only (Default value = 100) - :param maxprerounds: maximal number of presolving rounds the constraint handler participates in (Default value = -1) - :param delaysepa: should separation method be delayed, if other separators found cuts? (Default value = False) - :param delayprop: should propagation method be delayed, if other propagators found reductions? (Default value = False) - :param needscons: should the constraint handler be skipped, if no constraints are available? (Default value = True) - :param proptiming: positions in the node solving loop where propagation method of constraint handlers should be executed (Default value = SCIP_PROPTIMING.BEFORELP) - :param presoltiming: timing mask of the constraint handler's presolving method (Default value = SCIP_PRESOLTIMING.MEDIUM) + """ + Include a constraint handler. + + Parameters + ---------- + conshdlr : Conshdlr + constraint handler + name : str + name of constraint handler + desc : str + description of constraint handler + sepapriority : int, optional + priority for separation (Default value = 0) + enfopriority : int, optional + priority for constraint enforcing (Default value = 0) + chckpriority : int, optional + priority for checking feasibility (Default value = 0) + sepafreq : int, optional + frequency for separating cuts; 0 = only at root node (Default value = -1) + propfreq : int, optional + frequency for propagating domains; 0 = only preprocessing propagation (Default value = -1) + eagerfreq : int, optional + frequency for using all instead of only the useful constraints in separation, + propagation and enforcement; -1 = no eager evaluations, 0 = first only + (Default value = 100) + maxprerounds : int, optional + maximal number of presolving rounds the constraint handler participates in (Default value = -1) + delaysepa : bool, optional + should separation method be delayed, if other separators found cuts? (Default value = False) + delayprop : bool, optional + should propagation method be delayed, if other propagators found reductions? (Default value = False) + needscons : bool, optional + should the constraint handler be skipped, if no constraints are available? (Default value = True) + proptiming : PY_SCIP_PROPTIMING + positions in the node solving loop where propagation method of constraint handlers + should be executed (Default value = SCIP_PROPTIMING.BEFORELP) + presoltiming : PY_SCIP_PRESOLTIMING + timing mask of the constraint handler's presolving method (Default value = SCIP_PRESOLTIMING.MEDIUM) """ n = str_conversion(name) @@ -4050,20 +6681,39 @@ cdef class Model: def createCons(self, Conshdlr conshdlr, name, initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): - """Create a constraint of a custom constraint handler + """ + Create a constraint of a custom constraint handler. - :param Conshdlr conshdlr: constraint handler - :param name: name of constraint - :param initial: (Default value = True) - :param separate: (Default value = True) - :param enforce: (Default value = True) - :param check: (Default value = True) - :param propagate: (Default value = True) - :param local: (Default value = False) - :param modifiable: (Default value = False) - :param dynamic: (Default value = False) - :param removable: (Default value = False) - :param stickingatnode: (Default value = False) + Parameters + ---------- + conshdlr : Conshdlr + constraint handler + name : str + name of constraint handler + initial : bool, optional + (Default value = True) + separate : bool, optional + (Default value = True) + enforce : bool, optional + (Default value = True) + check : bool, optional + (Default value = True) + propagate : bool, optional + (Default value = True) + local : bool, optional + (Default value = False) + modifiable : bool, optional + (Default value = False) + dynamic : bool, optional + (Default value = False) + removable : bool, optional + (Default value = False) + stickingatnode : bool, optional + (Default value = False) + + Returns + ------- + Constraint """ @@ -4076,14 +6726,23 @@ cdef class Model: return constraint def includePresol(self, Presol presol, name, desc, priority, maxrounds, timing=SCIP_PRESOLTIMING_FAST): - """Include a presolver + """ + Include a presolver. - :param Presol presol: presolver - :param name: name of presolver - :param desc: description of presolver - :param priority: priority of the presolver (>= 0: before, < 0: after constraint handlers) - :param maxrounds: maximal number of presolving rounds the presolver participates in (-1: no limit) - :param timing: timing mask of presolver (Default value = SCIP_PRESOLTIMING_FAST) + Parameters + ---------- + presol : Presol + presolver + name : str + name of presolver + desc : str + description of presolver + priority : int + priority of the presolver (>= 0: before, < 0: after constraint handlers) + maxrounds : int + maximal number of presolving rounds the presolver participates in (-1: no limit) + timing : PY_SCIP_PRESOLTIMING, optional + timing mask of presolver (Default value = SCIP_PRESOLTIMING_FAST) """ n = str_conversion(name) @@ -4094,7 +6753,8 @@ cdef class Model: Py_INCREF(presol) def includeSepa(self, Sepa sepa, name, desc, priority=0, freq=10, maxbounddist=1.0, usessubscip=False, delay=False): - """Include a separator + """ + Include a separator :param Sepa sepa: separator :param name: name of separator @@ -4105,6 +6765,28 @@ cdef class Model: :param usessubscip: does the separator use a secondary SCIP instance? (Default value = False) :param delay: should separator be delayed, if other separators found cuts? (Default value = False) + + Parameters + ---------- + sepa : Sepa + separator + name : str + name of separator + desc : str + description of separator + priority : int, optional + priority of separator (>= 0: before, < 0: after constraint handlers) (default=0) + freq : int, optional + frequency for calling separator (default=10) + maxbounddist : float, optional + maximal relative distance from current node's dual bound to primal + bound compared to best node's dual bound for applying separation. + (default = 1.0) + usessubscip : bool, optional + does the separator use a secondary SCIP instance? (Default value = False) + delay : bool, optional + should separator be delayed if other separators found cuts? (Default value = False) + """ n = str_conversion(name) d = str_conversion(desc) @@ -4115,12 +6797,19 @@ cdef class Model: Py_INCREF(sepa) def includeReader(self, Reader reader, name, desc, ext): - """Include a reader + """ + Include a reader. - :param Reader reader: reader - :param name: name of reader - :param desc: description of reader - :param ext: file extension of reader + Parameters + ---------- + reader : Reader + reader + name : str + name of reader + desc : str + description of reader + ext : str + file extension of reader """ n = str_conversion(name) @@ -4134,18 +6823,31 @@ cdef class Model: def includeProp(self, Prop prop, name, desc, presolpriority, presolmaxrounds, proptiming, presoltiming=SCIP_PRESOLTIMING_FAST, priority=1, freq=1, delay=True): - """Include a propagator. + """ + Include a propagator. - :param Prop prop: propagator - :param name: name of propagator - :param desc: description of propagator - :param presolpriority: presolving priority of the propgator (>= 0: before, < 0: after constraint handlers) - :param presolmaxrounds: maximal number of presolving rounds the propagator participates in (-1: no limit) - :param proptiming: positions in the node solving loop where propagation method of constraint handlers should be executed - :param presoltiming: timing mask of the constraint handler's presolving method (Default value = SCIP_PRESOLTIMING_FAST) - :param priority: priority of the propagator (Default value = 1) - :param freq: frequency for calling propagator (Default value = 1) - :param delay: should propagator be delayed if other propagators have found reductions? (Default value = True) + Parameters + ---------- + prop : Prop + propagator + name : str + name of propagator + desc : str + description of propagator + presolpriority : int + presolving priority of the propgator (>= 0: before, < 0: after constraint handlers) + presolmaxrounds : int + maximal number of presolving rounds the propagator participates in (-1: no limit) + proptiming : SCIP_PROPTIMING + positions in the node solving loop where propagation method of constraint handlers should be executed + presoltiming : PY_SCIP_PRESOLTIMING, optional + timing mask of the constraint handler's presolving method (Default value = SCIP_PRESOLTIMING_FAST) + priority : int, optional + priority of the propagator (Default value = 1) + freq : int, optional + frequency for calling propagator (Default value = 1) + delay : bool, optional + should propagator be delayed if other propagators have found reductions? (Default value = True) """ n = str_conversion(name) @@ -4162,18 +6864,32 @@ cdef class Model: def includeHeur(self, Heur heur, name, desc, dispchar, priority=10000, freq=1, freqofs=0, maxdepth=-1, timingmask=SCIP_HEURTIMING_BEFORENODE, usessubscip=False): - """Include a primal heuristic. + """ + Include a primal heuristic. - :param Heur heur: heuristic - :param name: name of heuristic - :param desc: description of heuristic - :param dispchar: display character of heuristic - :param priority: priority of the heuristic (Default value = 10000) - :param freq: frequency for calling heuristic (Default value = 1) - :param freqofs: frequency offset for calling heuristic (Default value = 0) - :param maxdepth: maximal depth level to call heuristic at (Default value = -1) - :param timingmask: positions in the node solving loop where heuristic should be executed (Default value = SCIP_HEURTIMING_BEFORENODE) - :param usessubscip: does the heuristic use a secondary SCIP instance? (Default value = False) + Parameters + ---------- + heur : Heur + heuristic + name : str + name of heuristic + desc : str + description of heuristic + dispchar : str + display character of heuristic. Please use a single length string. + priority : int. optional + priority of the heuristic (Default value = 10000) + freq : int, optional + frequency for calling heuristic (Default value = 1) + freqofs : int. optional + frequency offset for calling heuristic (Default value = 0) + maxdepth : int, optional + maximal depth level to call heuristic at (Default value = -1) + timingmask : PY_SCIP_HEURTIMING, optional + positions in the node solving loop where heuristic should be executed + (Default value = SCIP_HEURTIMING_BEFORENODE) + usessubscip : bool, optional + does the heuristic use a secondary SCIP instance? (Default value = False) """ nam = str_conversion(name) @@ -4190,13 +6906,21 @@ cdef class Model: Py_INCREF(heur) def includeRelax(self, Relax relax, name, desc, priority=10000, freq=1): - """Include a relaxation handler. + """ + Include a relaxation handler. - :param Relax relax: relaxation handler - :param name: name of relaxation handler - :param desc: description of relaxation handler - :param priority: priority of the relaxation handler (negative: after LP, non-negative: before LP, Default value = 10000) - :param freq: frequency for calling relaxation handler + Parameters + ---------- + relax : Relax + relaxation handler + name : str + name of relaxation handler + desc : str + description of relaxation handler + priority : int, optional + priority of the relaxation handler (negative: after LP, non-negative: before LP, Default value = 10000) + freq : int, optional + frequency for calling relaxation handler """ nam = str_conversion(name) @@ -4209,12 +6933,20 @@ cdef class Model: Py_INCREF(relax) def includeCutsel(self, Cutsel cutsel, name, desc, priority): - """include a cut selector + """ + Include a cut selector. + + Parameters + ---------- + cutsel : Cutsel + cut selector + name : str + name of cut selector + desc : str + description of cut selector + priority : int + priority of the cut selector - :param Cutsel cutsel: cut selector - :param name: name of cut selector - :param desc: description of cut selector - :param priority: priority of the cut selector """ nam = str_conversion(name) @@ -4227,14 +6959,25 @@ cdef class Model: Py_INCREF(cutsel) def includeBranchrule(self, Branchrule branchrule, name, desc, priority, maxdepth, maxbounddist): - """Include a branching rule. + """ + Include a branching rule. - :param Branchrule branchrule: branching rule - :param name: name of branching rule - :param desc: description of branching rule - :param priority: priority of branching rule - :param maxdepth: maximal depth level up to which this branching rule should be used (or -1) - :param maxbounddist: maximal relative distance from current node's dual bound to primal bound compared to best node's dual bound for applying branching rule (0.0: only on current best node, 1.0: on all nodes) + Parameters + ---------- + branchrule : Branchrule + branching rule + name : str + name of branching rule + desc : str + description of branching rule + priority : int + priority of branching rule + maxdepth : int + maximal depth level up to which this branching rule should be used (or -1) + maxbounddist : float + maximal relative distance from current node's dual bound to primal bound + compared to best node's dual bound for applying branching rule + (0.0: only on current best node, 1.0: on all nodes) """ nam = str_conversion(name) @@ -4248,13 +6991,21 @@ cdef class Model: Py_INCREF(branchrule) def includeNodesel(self, Nodesel nodesel, name, desc, stdpriority, memsavepriority): - """Include a node selector. + """ + Include a node selector. - :param Nodesel nodesel: node selector - :param name: name of node selector - :param desc: description of node selector - :param stdpriority: priority of the node selector in standard mode - :param memsavepriority: priority of the node selector in memory saving mode + Parameters + ---------- + nodesel : Nodesel + node selector + name : str + name of node selector + desc : str + description of node selector + stdpriority : int + priority of the node selector in standard mode + memsavepriority : int + priority of the node selector in memory saving mode """ nam = str_conversion(name) @@ -4269,17 +7020,29 @@ cdef class Model: def includeBenders(self, Benders benders, name, desc, priority=1, cutlp=True, cutpseudo=True, cutrelax=True, shareaux=False): - """Include a Benders' decomposition. - - Keyword arguments: - benders -- the Benders decomposition - name -- the name - desc -- the description - priority -- priority of the Benders' decomposition - cutlp -- should Benders' cuts be generated from LP solutions - cutpseudo -- should Benders' cuts be generated from pseudo solutions - cutrelax -- should Benders' cuts be generated from relaxation solutions - shareaux -- should the Benders' decomposition share the auxiliary variables of the highest priority Benders' decomposition + """ + Include a Benders' decomposition. + + Parameters + ---------- + benders : Benders + the Benders decomposition + name : str + the name + desc : str + the description + priority : int, optional + priority of the Benders' decomposition + cutlp : bool, optional + should Benders' cuts be generated from LP solutions + cutpseudo : bool, optional + should Benders' cuts be generated from pseudo solutions + cutrelax : bool, optional + should Benders' cuts be generated from relaxation solutions + shareaux : bool, optional + should the Benders' decomposition share the auxiliary variables of the + highest priority Benders' decomposition + """ n = str_conversion(name) d = str_conversion(desc) @@ -4298,15 +7061,25 @@ cdef class Model: Py_INCREF(benders) def includeBenderscut(self, Benders benders, Benderscut benderscut, name, desc, priority=1, islpcut=True): - """ Include a Benders' decomposition cutting method + """ + Include a Benders' decomposition cutting method + + Parameters + ---------- + benders : Benders + the Benders' decomposition that this cutting method is attached to + benderscut : Benderscut + the Benders' decomposition cutting method + name : str + the name + desc : str + the description + priority : int. optional + priority of the Benders' decomposition (Default = 1) + islpcut : bool, optional + is this cutting method suitable for generating cuts for convex relaxations? + (Default = True) - Keyword arguments: - benders -- the Benders' decomposition that this cutting method is attached to - benderscut --- the Benders' decomposition cutting method - name -- the name - desc -- the description - priority -- priority of the Benders' decomposition - islpcut -- is this cutting method suitable for generating cuts for convex relaxations? """ cdef SCIP_BENDERS* _benders @@ -4329,20 +7102,27 @@ cdef class Model: def getLPBranchCands(self): - """gets branching candidates for LP solution branching (fractional variables) along with solution values, + """ + Gets branching candidates for LP solution branching (fractional variables) along with solution values, fractionalities, and number of branching candidates; The number of branching candidates does NOT account for fractional implicit integer variables which should not be used for branching decisions. Fractional - implicit integer variables are stored at the positions *nlpcands to *nlpcands + *nfracimplvars - 1 + implicit integer variables are stored at the positions nlpcands to nlpcands + nfracimplvars - 1 branching rules should always select the branching candidate among the first npriolpcands of the candidate list - :return tuple (lpcands, lpcandssol, lpcadsfrac, nlpcands, npriolpcands, nfracimplvars) where - - lpcands: list of variables of LP branching candidates - lpcandssol: list of LP candidate solution values - lpcandsfrac list of LP candidate fractionalities - nlpcands: number of LP branching candidates - npriolpcands: number of candidates with maximal priority - nfracimplvars: number of fractional implicit integer variables + Returns + ------- + list of Variable + list of variables of LP branching candidates + list of float + list of LP candidate solution values + list of float + list of LP candidate fractionalities + int + number of LP branching candidates + int + number of candidates with maximal priority + int + number of fractional implicit integer variables """ cdef int ncands @@ -4361,14 +7141,18 @@ cdef class Model: [lpcandsfrac[i] for i in range(nlpcands)], nlpcands, npriolpcands, nfracimplvars) def getPseudoBranchCands(self): - """gets branching candidates for pseudo solution branching (non-fixed variables) + """ + Gets branching candidates for pseudo solution branching (non-fixed variables) along with the number of candidates. - :return tuple (pseudocands, npseudocands, npriopseudocands) where - - pseudocands: list of variables of pseudo branching candidates - npseudocands: number of pseudo branching candidates - npriopseudocands: number of candidates with maximal priority + Returns + ------- + list of Variable + list of variables of pseudo branching candidates + int + number of pseudo branching candidates + int + number of candidates with maximal priority """ cdef int npseudocands @@ -4381,11 +7165,22 @@ cdef class Model: return ([Variable.create(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) def branchVar(self, Variable variable): - """Branch on a non-continuous variable. + """ + Branch on a non-continuous variable. + + Parameters + ---------- + variable : Variable + Variable to branch on - :param variable: Variable to branch on - :return: tuple(downchild, eqchild, upchild) of Nodes of the left, middle and right child. Middle child only exists - if branch variable is integer (it is None otherwise) + Returns + ------- + Node + Node created for the down (left) branch + Node or None + Node created for the equal child (middle child). Only exists if branch variable is integer + Node + Node created for the up (right) branch """ cdef SCIP_NODE* downchild @@ -4397,12 +7192,24 @@ cdef class Model: def branchVarVal(self, variable, value): - """Branches on variable using a value which separates the domain of the variable. + """ + Branches on variable using a value which separates the domain of the variable. + + Parameters + ---------- + variable : Variable + Variable to branch on + value : float + value to branch on - :param variable: Variable to branch on - :param value: float, value to branch on - :return: tuple(downchild, eqchild, upchild) of Nodes of the left, middle and right child. Middle child only exists - if branch variable is integer (it is None otherwise) + Returns + ------- + Node + Node created for the down (left) branch + Node or None + Node created for the equal child (middle child). Only exists if the branch variable is integer + Node + Node created for the up (right) branch """ cdef SCIP_NODE* downchild @@ -4414,36 +7221,64 @@ cdef class Model: return Node.create(downchild), Node.create(eqchild), Node.create(upchild) def calcNodeselPriority(self, Variable variable, branchdir, targetvalue): - """calculates the node selection priority for moving the given variable's LP value + """ + Calculates the node selection priority for moving the given variable's LP value to the given target value; - this node selection priority can be given to the SCIPcreateChild() call + this node selection priority can be given to the SCIPcreateChild() call. + + Parameters + ---------- + variable : Variable + variable on which the branching is applied + branchdir : PY_SCIP_BRANCHDIR + type of branching that was performed + targetvalue : float + new value of the variable in the child node - :param variable: variable on which the branching is applied - :param branchdir: type of branching that was performed - :param targetvalue: new value of the variable in the child node - :return: node selection priority for moving the given variable's LP value to the given target value + Returns + ------- + int + node selection priority for moving the given variable's LP value to the given target value """ return SCIPcalcNodeselPriority(self._scip, variable.scip_var, branchdir, targetvalue) def calcChildEstimate(self, Variable variable, targetvalue): - """Calculates an estimate for the objective of the best feasible solution + """ + Calculates an estimate for the objective of the best feasible solution contained in the subtree after applying the given branching; - this estimate can be given to the SCIPcreateChild() call + this estimate can be given to the SCIPcreateChild() call. + + Parameters + ---------- + variable : Variable + Variable to compute the estimate for + targetvalue : float + new value of the variable in the child node - :param variable: Variable to compute the estimate for - :param targetvalue: new value of the variable in the child node - :return: objective estimate of the best solution in the subtree after applying the given branching + Returns + ------- + float + objective estimate of the best solution in the subtree after applying the given branching """ return SCIPcalcChildEstimate(self._scip, variable.scip_var, targetvalue) def createChild(self, nodeselprio, estimate): - """Create a child node of the focus node. + """ + Create a child node of the focus node. + + Parameters + ---------- + nodeselprio : int + node selection priority of new node + estimate : float + estimate for (transformed) objective value of best feasible solution in subtree - :param nodeselprio: float, node selection priority of new node - :param estimate: float, estimate for(transformed) objective value of best feasible solution in subtree - :return: Node, the child which was created + Returns + ------- + Node + the child which was created """ cdef SCIP_NODE* child @@ -4452,59 +7287,135 @@ cdef class Model: # Diving methods (Diving is LP related) def startDive(self): - """Initiates LP diving - It allows the user to change the LP in several ways, solve, change again, etc, without affecting the actual LP that has. When endDive() is called, - SCIP will undo all changes done and recover the LP it had before startDive - """ + """Initiates LP diving. + It allows the user to change the LP in several ways, solve, change again, etc, + without affecting the actual LP. When endDive() is called, + SCIP will undo all changes done and recover the LP it had before startDive.""" PY_SCIP_CALL(SCIPstartDive(self._scip)) def endDive(self): - """Quits probing and resets bounds and constraints to the focus node's environment""" + """Quits probing and resets bounds and constraints to the focus node's environment.""" PY_SCIP_CALL(SCIPendDive(self._scip)) def chgVarObjDive(self, Variable var, newobj): - """changes (column) variable's objective value in current dive""" + """ + Changes (column) variable's objective value in current dive. + + Parameters + ---------- + var : Variable + newobj : float + + """ PY_SCIP_CALL(SCIPchgVarObjDive(self._scip, var.scip_var, newobj)) def chgVarLbDive(self, Variable var, newbound): - """changes variable's current lb in current dive""" + """ + Changes variable's current lb in current dive. + + Parameters + ---------- + var : Variable + newbound : float + + """ PY_SCIP_CALL(SCIPchgVarLbDive(self._scip, var.scip_var, newbound)) def chgVarUbDive(self, Variable var, newbound): - """changes variable's current ub in current dive""" + """ + Changes variable's current ub in current dive. + + Parameters + ---------- + var : Variable + newbound : float + + """ PY_SCIP_CALL(SCIPchgVarUbDive(self._scip, var.scip_var, newbound)) def getVarLbDive(self, Variable var): - """returns variable's current lb in current dive""" + """ + Returns variable's current lb in current dive. + + Parameters + ---------- + var : Variable + + Returns + ------- + float + + """ return SCIPgetVarLbDive(self._scip, var.scip_var) def getVarUbDive(self, Variable var): - """returns variable's current ub in current dive""" + """ + Returns variable's current ub in current dive. + + Parameters + ---------- + var : Variable + + Returns + ------- + float + + """ return SCIPgetVarUbDive(self._scip, var.scip_var) def chgRowLhsDive(self, Row row, newlhs): - """changes row lhs in current dive, change will be undone after diving - ends, for permanent changes use SCIPchgRowLhs() + """ + Changes row lhs in current dive, change will be undone after diving + ends, for permanent changes use SCIPchgRowLhs(). + + Parameters + ---------- + row : Row + newlhs : float + """ PY_SCIP_CALL(SCIPchgRowLhsDive(self._scip, row.scip_row, newlhs)) def chgRowRhsDive(self, Row row, newrhs): - """changes row rhs in current dive, change will be undone after diving - ends, for permanent changes use SCIPchgRowLhs() + """ + Changes row rhs in current dive, change will be undone after diving + ends. For permanent changes use SCIPchgRowRhs(). + + Parameters + ---------- + row : Row + newrhs : float + """ PY_SCIP_CALL(SCIPchgRowRhsDive(self._scip, row.scip_row, newrhs)) def addRowDive(self, Row row): - """adds a row to the LP in current dive""" + """ + Adds a row to the LP in current dive. + + Parameters + ---------- + row : Row + + """ PY_SCIP_CALL(SCIPaddRowDive(self._scip, row.scip_row)) def solveDiveLP(self, itlim = -1): - """solves the LP of the current dive no separation or pricing is applied - no separation or pricing is applied - :param itlim: maximal number of LP iterations to perform (Default value = -1, that is, no limit) - returns two booleans: - lperror -- if an unresolved lp error occured - cutoff -- whether the LP was infeasible or the objective limit was reached + """ + Solves the LP of the current dive. No separation or pricing is applied. + + Parameters + ---------- + itlim : int, optional + maximal number of LP iterations to perform (Default value = -1, that is, no limit) + + Returns + ------- + lperror : bool + whether an unresolved lp error occured + cutoff : bool + whether the LP was infeasible or the objective limit was reached + """ cdef SCIP_Bool lperror cdef SCIP_Bool cutoff @@ -4513,79 +7424,137 @@ cdef class Model: return lperror, cutoff def inRepropagation(self): - """returns if the current node is already solved and only propagated again.""" + """ + Returns if the current node is already solved and only propagated again. + + Returns + ------- + bool + + """ return SCIPinRepropagation(self._scip) # Probing methods (Probing is tree based) def startProbing(self): """Initiates probing, making methods SCIPnewProbingNode(), SCIPbacktrackProbing(), SCIPchgVarLbProbing(), - SCIPchgVarUbProbing(), SCIPfixVarProbing(), SCIPpropagateProbing(), SCIPsolveProbingLP(), etc available + SCIPchgVarUbProbing(), SCIPfixVarProbing(), SCIPpropagateProbing(), SCIPsolveProbingLP(), etc available. """ PY_SCIP_CALL( SCIPstartProbing(self._scip) ) def endProbing(self): - """Quits probing and resets bounds and constraints to the focus node's environment""" + """Quits probing and resets bounds and constraints to the focus node's environment.""" PY_SCIP_CALL( SCIPendProbing(self._scip) ) def newProbingNode(self): - """creates a new probing sub node, whose changes can be undone by backtracking to a higher node in the - probing path with a call to backtrackProbing() + """Creates a new probing sub node, whose changes can be undone by backtracking to a higher node in the + probing path with a call to backtrackProbing(). """ PY_SCIP_CALL( SCIPnewProbingNode(self._scip) ) def backtrackProbing(self, probingdepth): - """undoes all changes to the problem applied in probing up to the given probing depth - :param probingdepth: probing depth of the node in the probing path that should be reactivated + """ + Undoes all changes to the problem applied in probing up to the given probing depth. + + Parameters + ---------- + probingdepth : int + probing depth of the node in the probing path that should be reactivated + """ PY_SCIP_CALL( SCIPbacktrackProbing(self._scip, probingdepth) ) def getProbingDepth(self): - """returns the current probing depth""" + """Returns the current probing depth.""" return SCIPgetProbingDepth(self._scip) def chgVarObjProbing(self, Variable var, newobj): - """changes (column) variable's objective value during probing mode""" + """Changes (column) variable's objective value during probing mode.""" PY_SCIP_CALL( SCIPchgVarObjProbing(self._scip, var.scip_var, newobj) ) def chgVarLbProbing(self, Variable var, lb): - """changes the variable lower bound during probing mode + """ + Changes the variable lower bound during probing mode. + + Parameters + ---------- + var : Variable + variable to change bound of + lb : float or None + new lower bound (set to None for -infinity) - :param Variable var: variable to change bound of - :param lb: new lower bound (set to None for -infinity) """ if lb is None: lb = -SCIPinfinity(self._scip) PY_SCIP_CALL(SCIPchgVarLbProbing(self._scip, var.scip_var, lb)) def chgVarUbProbing(self, Variable var, ub): - """changes the variable upper bound during probing mode + """ + Changes the variable upper bound during probing mode. + + Parameters + ---------- + var : Variable + variable to change bound of + ub : float or None + new upper bound (set to None for +infinity) - :param Variable var: variable to change bound of - :param ub: new upper bound (set to None for +infinity) """ if ub is None: ub = SCIPinfinity(self._scip) PY_SCIP_CALL(SCIPchgVarUbProbing(self._scip, var.scip_var, ub)) def fixVarProbing(self, Variable var, fixedval): - """Fixes a variable at the current probing node.""" + """ + Fixes a variable at the current probing node. + + Parameters + ---------- + var : Variable + fixedval : float + + """ PY_SCIP_CALL( SCIPfixVarProbing(self._scip, var.scip_var, fixedval) ) def isObjChangedProbing(self): - """returns whether the objective function has changed during probing mode""" + """ + Returns whether the objective function has changed during probing mode. + + Returns + ------- + bool + + """ return SCIPisObjChangedProbing(self._scip) def inProbing(self): - """returns whether we are in probing mode; probing mode is activated via startProbing() and stopped via endProbing()""" + """ + Returns whether we are in probing mode; + probing mode is activated via startProbing() and stopped via endProbing(). + + Returns + ------- + bool + + """ return SCIPinProbing(self._scip) def solveProbingLP(self, itlim = -1): - """solves the LP at the current probing node (cannot be applied at preprocessing stage) - no separation or pricing is applied - :param itlim: maximal number of LP iterations to perform (Default value = -1, that is, no limit) - returns two booleans: - lperror -- if an unresolved lp error occured - cutoff -- whether the LP was infeasible or the objective limit was reached + """ + Solves the LP at the current probing node (cannot be applied at preprocessing stage) + no separation or pricing is applied. + + Parameters + ---------- + itlim : int + maximal number of LP iterations to perform (Default value = -1, that is, no limit) + + Returns + ------- + lperror : bool + if an unresolved lp error occured + cutoff : bool + whether the LP was infeasible or the objective limit was reached + """ cdef SCIP_Bool lperror cdef SCIP_Bool cutoff @@ -4594,11 +7563,17 @@ cdef class Model: return lperror, cutoff def applyCutsProbing(self): - """applies the cuts in the separation storage to the LP and clears the storage afterwards; + """ + Applies the cuts in the separation storage to the LP and clears the storage afterwards; this method can only be applied during probing; the user should resolve the probing LP afterwards - in order to get a new solution + in order to get a new solution. returns: - cutoff -- whether an empty domain was created + + Returns + ------- + cutoff : bool + whether an empty domain was created + """ cdef SCIP_Bool cutoff @@ -4606,14 +7581,24 @@ cdef class Model: return cutoff def propagateProbing(self, maxproprounds): - """applies domain propagation on the probing sub problem, that was changed after SCIPstartProbing() was called; + """ + Applies domain propagation on the probing sub problem, that was changed after SCIPstartProbing() was called; the propagated domains of the variables can be accessed with the usual bound accessing calls SCIPvarGetLbLocal() and SCIPvarGetUbLocal(); the propagation is only valid locally, i.e. the local bounds as well as the changed - bounds due to SCIPchgVarLbProbing(), SCIPchgVarUbProbing(), and SCIPfixVarProbing() are used for propagation - :param maxproprounds: maximal number of propagation rounds (Default value = -1, that is, no limit) - returns: - cutoff -- whether the probing node can be cutoff - ndomredsfound -- number of domain reductions found + bounds due to SCIPchgVarLbProbing(), SCIPchgVarUbProbing(), and SCIPfixVarProbing() are used for propagation. + + Parameters + ---------- + maxproprounds : int + maximal number of propagation rounds (Default value = -1, that is, no limit) + + Returns + ------- + cutoff : bool + whether the probing node can be cutoff + ndomredsfound : int + number of domain reductions found + """ cdef SCIP_Bool cutoff cdef SCIP_Longint ndomredsfound @@ -4632,8 +7617,14 @@ cdef class Model: # Solution functions def writeLP(self, filename="LP.lp"): - """writes current LP to a file - :param filename: file name (Default value = "LP.lp") + """ + Writes current LP to a file. + + Parameters + ---------- + filename : str, optional + file name (Default value = "LP.lp") + """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4644,10 +7635,19 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def createSol(self, Heur heur = None, initlp=False): - """Create a new primal solution in the transformed space. + """ + Create a new primal solution in the transformed space. + + Parameters + ---------- + heur : Heur or None, optional + heuristic that found the solution (Default value = None) + initlp : bool, optional + Should the created solution be initialised to the current LP solution instead of all zeros - :param Heur heur: heuristic that found the solution (Default value = None) - :param initlp: Should the created solution be initialised to the current LP solution instead of all zeros + Returns + ------- + Solution """ cdef SCIP_HEUR* _heur @@ -4666,8 +7666,17 @@ cdef class Model: return solution def createPartialSol(self, Heur heur = None): - """Create a partial primal solution, initialized to unknown values. - :param Heur heur: heuristic that found the solution (Default value = None) + """ + Create a partial primal solution, initialized to unknown values. + + Parameters + ---------- + heur : Heur or None, optional + heuristic that found the solution (Default value = None) + + Returns + ------- + Solution """ cdef SCIP_HEUR* _heur @@ -4683,9 +7692,17 @@ cdef class Model: return partialsolution def createOrigSol(self, Heur heur = None): - """Create a new primal solution in the original space. + """ + Create a new primal solution in the original space. + + Parameters + ---------- + heur : Heur or None, optional + heuristic that found the solution (Default value = None) - :param Heur heur: heuristic that found the solution (Default value = None) + Returns + ------- + Solution """ cdef SCIP_HEUR* _heur @@ -4702,7 +7719,15 @@ cdef class Model: return solution def printBestSol(self, write_zeros=False): - """Prints the best feasible primal solution.""" + """ + Prints the best feasible primal solution. + + Parameters + ---------- + write_zeros : bool, optional + include variables that are set to zero (Default = False) + + """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4711,11 +7736,16 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def printSol(self, Solution solution=None, write_zeros=False): - """Print the given primal solution. + """ + Print the given primal solution. + + Parameters + ---------- + solution : Solution or None, optional + solution to print (default = None) + write_zeros : bool, optional + include variables that are set to zero (Default=False) - Keyword arguments: - solution -- solution to print - write_zeros -- include variables that are set to zero """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) @@ -4729,11 +7759,16 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def writeBestSol(self, filename="origprob.sol", write_zeros=False): - """Write the best feasible primal solution to a file. + """ + Write the best feasible primal solution to a file. + + Parameters + ---------- + filename : str, optional + name of the output file (Default="origprob.sol") + write_zeros : bool, optional + include variables that are set to zero (Default=False) - Keyword arguments: - filename -- name of the output file - write_zeros -- include variables that are set to zero """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) @@ -4748,11 +7783,16 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def writeBestTransSol(self, filename="transprob.sol", write_zeros=False): - """Write the best feasible primal solution for the transformed problem to a file. + """ + Write the best feasible primal solution for the transformed problem to a file. + + Parameters + ---------- + filename : str, optional + name of the output file (Default="transprob.sol") + write_zeros : bool, optional + include variables that are set to zero (Default=False) - Keyword arguments: - filename -- name of the output file - write_zeros -- include variables that are set to zero """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4766,12 +7806,18 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def writeSol(self, Solution solution, filename="origprob.sol", write_zeros=False): - """Write the given primal solution to a file. + """ + Write the given primal solution to a file. + + Parameters + ---------- + solution : Solution + solution to write + filename : str, optional + name of the output file (Default="origprob.sol") + write_zeros : bool, optional + include variables that are set to zero (Default=False) - Keyword arguments: - solution -- solution to write - filename -- name of the output file - write_zeros -- include variables that are set to zero """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4785,12 +7831,18 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def writeTransSol(self, Solution solution, filename="transprob.sol", write_zeros=False): - """Write the given transformed primal solution to a file. + """ + Write the given transformed primal solution to a file. + + Parameters + ---------- + solution : Solution + transformed solution to write + filename : str, optional + name of the output file (Default="transprob.sol") + write_zeros : bool, optional + include variables that are set to zero (Default=False) - Keyword arguments: - solution -- transformed solution to write - filename -- name of the output file - write_zeros -- include variables that are set to zero """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4806,10 +7858,14 @@ cdef class Model: # perhaps this should not be included as it implements duplicated functionality # (as does it's namesake in SCIP) def readSol(self, filename): - """Reads a given solution file, problem has to be transformed in advance. + """ + Reads a given solution file, problem has to be transformed in advance. + + Parameters + ---------- + filename : str + name of the input file - Keyword arguments: - filename -- name of the input file """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -4820,13 +7876,21 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC, user_locale) def readSolFile(self, filename): - """Reads a given solution file. + """ + Reads a given solution file. Solution is created but not added to storage/the model. Use 'addSol' OR 'trySol' to add it. - Keyword arguments: - filename -- name of the input file + Parameters + ---------- + filename : str + name of the input file + + Returns + ------- + Solution + """ cdef SCIP_Bool partial cdef SCIP_Bool error @@ -4850,11 +7914,17 @@ cdef class Model: return solution def setSolVal(self, Solution solution, Variable var, val): - """Set a variable in a solution. + """ + Set a variable in a solution. - :param Solution solution: solution to be modified - :param Variable var: variable in the solution - :param val: value of the specified variable + Parameters + ---------- + solution : Solution + solution to be modified + var : Variable + variable in the solution + val : float + value of the specified variable """ cdef SCIP_SOL* _sol @@ -4864,15 +7934,30 @@ cdef class Model: PY_SCIP_CALL(SCIPsetSolVal(self._scip, _sol, var.scip_var, val)) def trySol(self, Solution solution, printreason=True, completely=False, checkbounds=True, checkintegrality=True, checklprows=True, free=True): - """Check given primal solution for feasibility and try to add it to the storage. + """ + Check given primal solution for feasibility and try to add it to the storage. - :param Solution solution: solution to store - :param printreason: should all reasons of violations be printed? (Default value = True) - :param completely: should all violation be checked? (Default value = False) - :param checkbounds: should the bounds of the variables be checked? (Default value = True) - :param checkintegrality: has integrality to be checked? (Default value = True) - :param checklprows: have current LP rows (both local and global) to be checked? (Default value = True) - :param free: should solution be freed? (Default value = True) + Parameters + ---------- + solution : Solution + solution to store + printreason : bool, optional + should all reasons of violations be printed? (Default value = True) + completely : bool, optional + should all violation be checked? (Default value = False) + checkbounds : bool, optional + should the bounds of the variables be checked? (Default value = True) + checkintegrality : bool, optional + does integrality have to be checked? (Default value = True) + checklprows : bool, optional + do current LP rows (both local and global) have to be checked? (Default value = True) + free : bool, optional + should solution be freed? (Default value = True) + + Returns + ------- + stored : bool + whether given solution was feasible and good enough to keep """ cdef SCIP_Bool stored @@ -4883,15 +7968,30 @@ cdef class Model: return stored def checkSol(self, Solution solution, printreason=True, completely=False, checkbounds=True, checkintegrality=True, checklprows=True, original=False): - """Check given primal solution for feasibility without adding it to the storage. + """ + Check given primal solution for feasibility without adding it to the storage. - :param Solution solution: solution to store - :param printreason: should all reasons of violations be printed? (Default value = True) - :param completely: should all violation be checked? (Default value = False) - :param checkbounds: should the bounds of the variables be checked? (Default value = True) - :param checkintegrality: has integrality to be checked? (Default value = True) - :param checklprows: have current LP rows (both local and global) to be checked? (Default value = True) - :param original: must the solution be checked against the original problem (Default value = False) + Parameters + ---------- + solution : Solution + solution to store + printreason : bool, optional + should all reasons of violations be printed? (Default value = True) + completely : bool, optional + should all violation be checked? (Default value = False) + checkbounds : bool, optional + should the bounds of the variables be checked? (Default value = True) + checkintegrality : bool, optional + has integrality to be checked? (Default value = True) + checklprows : bool, optional + have current LP rows (both local and global) to be checked? (Default value = True) + original : bool, optional + must the solution be checked against the original problem (Default value = False) + + Returns + ------- + feasible : bool + whether the given solution was feasible or not """ cdef SCIP_Bool feasible @@ -4902,10 +8002,20 @@ cdef class Model: return feasible def addSol(self, Solution solution, free=True): - """Try to add a solution to the storage. + """ + Try to add a solution to the storage. + + Parameters + ---------- + solution : Solution + solution to store + free : bool, optional + should solution be freed afterwards? (Default value = True) - :param Solution solution: solution to store - :param free: should solution be freed afterwards? (Default value = True) + Returns + ------- + stored : bool + stores whether given solution was good enough to keep """ cdef SCIP_Bool stored @@ -4916,34 +8026,73 @@ cdef class Model: return stored def freeSol(self, Solution solution): - """Free given solution + """ + Free given solution - :param Solution solution: solution to be freed + Parameters + ---------- + solution : Solution + solution to be freed """ PY_SCIP_CALL(SCIPfreeSol(self._scip, &solution.sol)) def getNSols(self): - """gets number of feasible primal solutions stored in the solution storage in case the problem is transformed; - in case the problem stage is SCIP_STAGE_PROBLEM, the number of solution in the original solution candidate - storage is returned - """ + """ + Gets number of feasible primal solutions stored in the solution storage in case the problem is transformed; + in case the problem stage is SCIP_STAGE_PROBLEM, the number of solution in the original solution candidate + storage is returned. + + Returns + ------- + int + + """ return SCIPgetNSols(self._scip) def getNSolsFound(self): - """gets number of feasible primal solutions found so far""" + """ + Gets number of feasible primal solutions found so far. + + Returns + ------- + int + + """ return SCIPgetNSolsFound(self._scip) def getNLimSolsFound(self): - """gets number of feasible primal solutions respecting the objective limit found so far""" + """ + Gets number of feasible primal solutions respecting the objective limit found so far. + + Returns + ------- + int + + """ return SCIPgetNLimSolsFound(self._scip) def getNBestSolsFound(self): - """gets number of feasible primal solutions found so far, that improved the primal bound at the time they were found""" + """ + Gets number of feasible primal solutions found so far, + that improved the primal bound at the time they were found. + + Returns + ------- + int + + """ return SCIPgetNBestSolsFound(self._scip) def getSols(self): - """Retrieve list of all feasible primal solutions stored in the solution storage.""" + """ + Retrieve list of all feasible primal solutions stored in the solution storage. + + Returns + ------- + list of Solution + + """ cdef SCIP_SOL** _sols cdef SCIP_SOL* _sol _sols = SCIPgetSols(self._scip) @@ -4956,15 +8105,30 @@ cdef class Model: return sols def getBestSol(self): - """Retrieve currently best known feasible primal solution.""" + """ + Retrieve currently best known feasible primal solution. + + Returns + ------- + Solution or None + + """ self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) return self._bestSol def getSolObjVal(self, Solution sol, original=True): - """Retrieve the objective value of the solution. + """ + Retrieve the objective value of the solution. + + Parameters + ---------- + sol : Solution + original : bool, optional + objective value in original space (Default value = True) - :param Solution sol: solution - :param original: objective value in original space (Default value = True) + Returns + ------- + float """ if sol == None: @@ -4980,17 +8144,33 @@ cdef class Model: return objval def getSolTime(self, Solution sol): - """Get clock time, when this solution was found. + """ + Get clock time when this solution was found. + + Parameters + ---------- + sol : Solution + + Returns + ------- + float - :param Solution sol: solution - """ return SCIPgetSolTime(self._scip, sol.sol) def getObjVal(self, original=True): - """Retrieve the objective value of value of best solution. + """ + Retrieve the objective value of the best solution. + + Parameters + ---------- + original : bool, optional + objective value in original space (Default value = True) + + Returns + ------- + float - :param original: objective value in original space (Default value = True) """ if SCIPgetNSols(self._scip) == 0: @@ -5010,13 +8190,24 @@ cdef class Model: return self.getSolObjVal(self._bestSol, original) def getSolVal(self, Solution sol, Expr expr): - """Retrieve value of given variable or expression in the given solution or in + """ + Retrieve value of given variable or expression in the given solution or in the LP/pseudo solution if sol == None - :param Solution sol: solution - :param Expr expr: polynomial expression to query the value of + Parameters + ---------- + sol : Solution + expr : Expr + polynomial expression to query the value of + + Returns + ------- + float + + Notes + ----- + A variable is also an expression. - Note: a variable is also an expression """ # no need to create a NULL solution wrapper in case we have a variable if sol == None and isinstance(expr, Variable): @@ -5027,12 +8218,23 @@ cdef class Model: return sol[expr] def getVal(self, Expr expr): - """Retrieve the value of the given variable or expression in the best known solution. + """ + Retrieve the value of the given variable or expression in the best known solution. Can only be called after solving is completed. - :param Expr expr: polynomial expression to query the value of + Parameters + ---------- + expr : Expr + polynomial expression to query the value of + + Returns + ------- + float + + Notes + ----- + A variable is also an expression. - Note: a variable is also an expression """ stage_check = SCIPgetStage(self._scip) not in [SCIP_STAGE_INIT, SCIP_STAGE_FREE] @@ -5043,13 +8245,27 @@ cdef class Model: def hasPrimalRay(self): """ - Returns whether a primal ray is stored that proves unboundedness of the LP relaxation + Returns whether a primal ray is stored that proves unboundedness of the LP relaxation. + + Returns + ------- + bool + """ return SCIPhasPrimalRay(self._scip) def getPrimalRayVal(self, Variable var): """ - Gets value of given variable in primal ray causing unboundedness of the LP relaxation + Gets value of given variable in primal ray causing unboundedness of the LP relaxation. + + Parameters + ---------- + var : Variable + + Returns + ------- + float + """ assert SCIPhasPrimalRay(self._scip), "The problem does not have a primal ray." @@ -5057,7 +8273,12 @@ cdef class Model: def getPrimalRay(self): """ - Gets primal ray causing unboundedness of the LP relaxation + Gets primal ray causing unboundedness of the LP relaxation. + + Returns + ------- + list of float + """ assert SCIPhasPrimalRay(self._scip), "The problem does not have a primal ray." @@ -5071,21 +8292,45 @@ cdef class Model: return ray def getPrimalbound(self): - """Retrieve the best primal bound.""" + """ + Retrieve the best primal bound. + + Returns + ------- + float + + """ return SCIPgetPrimalbound(self._scip) def getDualbound(self): - """Retrieve the best dual bound.""" + """ + Retrieve the best dual bound. + + Returns + ------- + float + + """ return SCIPgetDualbound(self._scip) def getDualboundRoot(self): - """Retrieve the best root dual bound.""" + """ + Retrieve the best root dual bound. + + Returns + ------- + float + + """ return SCIPgetDualboundRoot(self._scip) def writeName(self, Variable var): - """Write the name of the variable to the std out. + """ + Write the name of the variable to the std out. - :param Variable var: variable + Parameters + ---------- + var : Variable """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) @@ -5096,24 +8341,46 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def getStage(self): - """Retrieve current SCIP stage""" + """ + Retrieve current SCIP stage. + + Returns + ------- + int + + """ return SCIPgetStage(self._scip) def getStageName(self): - """Returns name of current stage as string""" + """ + Returns name of current stage as string. + + Returns + ------- + str + + """ if not StageNames: self._getStageNames() return StageNames[self.getStage()] def _getStageNames(self): - """Gets names of stages""" + """Gets names of stages.""" for name in dir(PY_SCIP_STAGE): attr = getattr(PY_SCIP_STAGE, name) if isinstance(attr, int): StageNames[attr] = name def getStatus(self): - """Retrieve solution status.""" + """ + Retrieve solution status. + + Returns + ------- + str + The status of SCIP. + + """ cdef SCIP_STATUS stat = SCIPgetStatus(self._scip) if stat == SCIP_STATUS_OPTIMAL: return "optimal" @@ -5151,7 +8418,14 @@ cdef class Model: return "unknown" def getObjectiveSense(self): - """Retrieve objective sense.""" + """ + Retrieve objective sense. + + Returns + ------- + str + + """ cdef SCIP_OBJSENSE sense = SCIPgetObjsense(self._scip) if sense == SCIP_OBJSENSE_MAXIMIZE: return "maximize" @@ -5161,7 +8435,15 @@ cdef class Model: return "unknown" def catchEvent(self, eventtype, Eventhdlr eventhdlr): - """catches a global (not variable or row dependent) event""" + """ + Catches a global (not variable or row dependent) event. + + Parameters + ---------- + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5173,7 +8455,15 @@ cdef class Model: PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL)) def dropEvent(self, eventtype, Eventhdlr eventhdlr): - """drops a global event (stops to track event)""" + """ + Drops a global event (stops tracking the event). + + Parameters + ---------- + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5185,7 +8475,16 @@ cdef class Model: PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1)) def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): - """catches an objective value or domain change event on the given transformed variable""" + """ + Catches an objective value or domain change event on the given transformed variable. + + Parameters + ---------- + var : Variable + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5195,7 +8494,16 @@ cdef class Model: PY_SCIP_CALL(SCIPcatchVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, NULL)) def dropVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): - """drops an objective value or domain change event (stops to track event) on the given transformed variable""" + """ + Drops an objective value or domain change event (stops tracking the event) on the given transformed variable. + + Parameters + ---------- + var : Variable + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5205,7 +8513,16 @@ cdef class Model: PY_SCIP_CALL(SCIPdropVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, -1)) def catchRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): - """catches a row coefficient, constant, or side change event on the given row""" + """ + Catches a row coefficient, constant, or side change event on the given row. + + Parameters + ---------- + row : Row + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5215,7 +8532,16 @@ cdef class Model: PY_SCIP_CALL(SCIPcatchRowEvent(self._scip, row.scip_row, eventtype, _eventhdlr, NULL, NULL)) def dropRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): - """drops a row coefficient, constant, or side change event (stops to track event) on the given row""" + """ + Drops a row coefficient, constant, or side change event (stops tracking the event) on the given row. + + Parameters + ---------- + row : Row + eventtype : PY_SCIP_EVENTTYPE + eventhdlr : Eventhdlr + + """ cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -5236,10 +8562,14 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def writeStatistics(self, filename="origprob.stats"): - """Write statistics to a file. + """ + Write statistics to a file. + + Parameters + ---------- + filename : str, optional + name of the output file (Default = "origprob.stats") - Keyword arguments: - filename -- name of the output file """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -5253,15 +8583,26 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def getNLPs(self): - """gets total number of LPs solved so far""" + """ + Gets total number of LPs solved so far. + + Returns + ------- + int + + """ return SCIPgetNLPs(self._scip) # Verbosity Methods def hideOutput(self, quiet = True): - """Hide the output. + """ + Hide the output. - :param quiet: hide output? (Default value = True) + Parameters + ---------- + quiet : bool, optional + hide output? (Default value = True) """ SCIPsetMessagehdlrQuiet(self._scip, quiet) @@ -5278,8 +8619,14 @@ cdef class Model: SCIPmessageSetErrorPrinting(relayErrorMessage, NULL) def setLogfile(self, path): - """sets the log file name for the currently installed message handler - :param path: name of log file, or None (no log) + """ + Sets the log file name for the currently installed message handler. + + Parameters + ---------- + path : str or None + name of log file, or None (no log) + """ if path: c_path = str_conversion(path) @@ -5290,60 +8637,90 @@ cdef class Model: # Parameter Methods def setBoolParam(self, name, value): - """Set a boolean-valued parameter. + """ + Set a boolean-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : bool + value of parameter """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetBoolParam(self._scip, n, value)) def setIntParam(self, name, value): - """Set an int-valued parameter. + """ + Set an int-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : int + value of parameter """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetIntParam(self._scip, n, value)) def setLongintParam(self, name, value): - """Set a long-valued parameter. + """ + Set a long-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : int + value of parameter """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetLongintParam(self._scip, n, value)) def setRealParam(self, name, value): - """Set a real-valued parameter. + """ + Set a real-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : float + value of parameter """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetRealParam(self._scip, n, value)) def setCharParam(self, name, value): - """Set a char-valued parameter. + """ + Set a char-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : str + value of parameter """ n = str_conversion(name) PY_SCIP_CALL(SCIPsetCharParam(self._scip, n, ord(value))) def setStringParam(self, name, value): - """Set a string-valued parameter. + """ + Set a string-valued parameter. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : str + value of parameter """ n = str_conversion(name) @@ -5353,8 +8730,13 @@ cdef class Model: def setParam(self, name, value): """Set a parameter with value in int, bool, real, long, char or str. - :param name: name of parameter - :param value: value of parameter + Parameters + ---------- + name : str + name of parameter + value : object + value of parameter + """ cdef SCIP_PARAM* param @@ -5382,10 +8764,19 @@ cdef class Model: def getParam(self, name): - """Get the value of a parameter of type + """ + Get the value of a parameter of type int, bool, real, long, char or str. - :param name: name of parameter + Parameters + ---------- + name : str + name of parameter + + Returns + ------- + object + """ cdef SCIP_PARAM* param @@ -5411,8 +8802,16 @@ cdef class Model: return SCIPparamGetString(param).decode('utf-8') def getParams(self): - """Gets the values of all parameters as a dict mapping parameter names - to their values.""" + """ + Gets the values of all parameters as a dict mapping parameter names + to their values. + + Returns + ------- + dict of str to object + dict mapping parameter names to their values. + + """ cdef SCIP_PARAM** params params = SCIPgetParams(self._scip) @@ -5423,17 +8822,26 @@ cdef class Model: return result def setParams(self, params): - """Sets multiple parameters at once. + """ + Sets multiple parameters at once. + + Parameters + ---------- + params : dict of str to object + dict mapping parameter names to their values. - :param params: dict mapping parameter names to their values. """ for name, value in params.items(): self.setParam(name, value) def readParams(self, file): - """Read an external parameter file. + """ + Read an external parameter file. - :param file: file to be read + Parameters + ---------- + file : str + file to read """ absfile = str_conversion(abspath(file)) @@ -5446,12 +8854,20 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC, user_locale) def writeParams(self, filename='param.set', comments=True, onlychanged=True, verbose=True): - """Write parameter settings to an external file. + """ + Write parameter settings to an external file. + + Parameters + ---------- + filename : str, optional + file to be written (Default value = 'param.set') + comments : bool, optional + write parameter descriptions as comments? (Default value = True) + onlychanged : bool, optional + write only modified parameters (Default value = True) + verbose : bool, optional + indicates whether a success message should be printed - :param filename: file to be written (Default value = 'param.set') - :param comments: write parameter descriptions as comments? (Default value = True) - :param onlychanged: write only modified parameters (Default value = True) - :param verbose: indicates whether a success message should be printed """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") @@ -5466,32 +8882,46 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) def resetParam(self, name): - """Reset parameter setting to its default value + """ + Reset parameter setting to its default value - :param name: parameter to reset + Parameters + ---------- + name : str + parameter to reset """ n = str_conversion(name) PY_SCIP_CALL(SCIPresetParam(self._scip, n)) def resetParams(self): - """Reset parameter settings to their default values""" + """Reset parameter settings to their default values.""" PY_SCIP_CALL(SCIPresetParams(self._scip)) def setEmphasis(self, paraemphasis, quiet = True): - """Set emphasis settings + """ + Set emphasis settings - :param paraemphasis: emphasis to set - :param quiet: hide output? (Default value = True) + Parameters + ---------- + paraemphasis : PY_SCIP_PARAMEMPHASIS + emphasis to set + quiet : bool, optional + hide output? (Default value = True) """ PY_SCIP_CALL(SCIPsetEmphasis(self._scip, paraemphasis, quiet)) def readProblem(self, filename, extension = None): - """Read a problem instance from an external file. + """ + Read a problem instance from an external file. - :param filename: problem file name - :param extension: specify file extension/type (Default value = None) + Parameters + ---------- + filename : str + problem file name + extension : str or None + specify file extension/type (Default value = None) """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) @@ -5513,11 +8943,25 @@ cdef class Model: PY_SCIP_CALL(SCIPcount(self._scip)) def getNReaders(self): - """Get number of currently available readers.""" + """ + Get number of currently available readers. + + Returns + ------- + int + + """ return SCIPgetNReaders(self._scip) def getNCountedSols(self): - """Get number of feasible solution.""" + """ + Get number of feasible solution. + + Returns + ------- + int + + """ cdef SCIP_Bool valid cdef SCIP_Longint nsols @@ -5531,14 +8975,19 @@ cdef class Model: PY_SCIP_CALL(SCIPsetParamsCountsols(self._scip)) def freeReoptSolve(self): - """Frees all solution process data and prepares for reoptimization""" + """Frees all solution process data and prepares for reoptimization.""" PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip)) def chgReoptObjective(self, coeffs, sense = 'minimize'): - """Establish the objective function as a linear expression. + """ + Establish the objective function as a linear expression. - :param coeffs: the coefficients - :param sense: the objective sense (Default value = 'minimize') + Parameters + ---------- + coeffs : list of float + the coefficients + sense : str + the objective sense (Default value = 'minimize') """ @@ -5581,11 +9030,18 @@ cdef class Model: free(_coeffs) def chgVarBranchPriority(self, Variable var, priority): - """Sets the branch priority of the variable. - Variables with higher branch priority are always preferred to variables with lower priority in selection of branching variable. + """ + Sets the branch priority of the variable. + Variables with higher branch priority are always preferred to variables with + lower priority in selection of branching variable. + + Parameters + ---------- + var : Variable + variable to change priority of + priority : int + the new priority of the variable (the default branching priority is 0) - :param Variable var: variable to change priority of - :param priority: the new priority of the variable (the default branching priority is 0) """ assert isinstance(var, Variable), "The given variable is not a pyvar, but %s" % var.__class__.__name__ PY_SCIP_CALL(SCIPchgVarBranchPriority(self._scip, var.scip_var, priority)) @@ -5593,28 +9049,42 @@ cdef class Model: def startStrongbranch(self): """Start strong branching. Needs to be called before any strong branching. Must also later end strong branching. TODO: Propagation option has currently been disabled via Python. - If propagation is enabled then strong branching is not done on the LP, but on additionally created nodes (has some overhead)""" + If propagation is enabled then strong branching is not done on the LP, but on additionally created nodes + (has some overhead). """ PY_SCIP_CALL(SCIPstartStrongbranch(self._scip, False)) def endStrongbranch(self): """End strong branching. Needs to be called if startStrongBranching was called previously. - Between these calls the user can access all strong branching functionality. """ + Between these calls the user can access all strong branching functionality.""" PY_SCIP_CALL(SCIPendStrongbranch(self._scip)) def getVarStrongbranchLast(self, Variable var): - """Get the results of the last strong branching call on this variable (potentially was called + """ + Get the results of the last strong branching call on this variable (potentially was called at another node). - down - The dual bound of the LP after branching down on the variable - up - The dual bound of the LP after branchign up on the variable - downvalid - Whether down stores a valid dual bound or is NULL - upvalid - Whether up stores a valid dual bound or is NULL - solval - The solution value of the variable at the last strong branching call - lpobjval - The LP objective value at the time of the last strong branching call + Parameters + ---------- + var : Variable + variable to get the previous strong branching information from + + Returns + ------- + down : float + The dual bound of the LP after branching down on the variable + up : float + The dual bound of the LP after branchign up on the variable + downvalid : bool + stores whether the returned down value is a valid dual bound, or NULL + upvalid : bool + stores whether the returned up value is a valid dual bound, or NULL + solval : float + The solution value of the variable at the last strong branching call + lpobjval : float + The LP objective value at the time of the last strong branching call - :param Variable var: variable to get the previous strong branching information from """ cdef SCIP_Real down @@ -5629,9 +9099,18 @@ cdef class Model: return down, up, downvalid, upvalid, solval, lpobjval def getVarStrongbranchNode(self, Variable var): - """Get the node number from the last time strong branching was called on the variable + """ + Get the node number from the last time strong branching was called on the variable. + + Parameters + ---------- + var : Variable + variable to get the previous strong branching node from + + Returns + ------- + int - :param Variable var: variable to get the previous strong branching node from """ cdef SCIP_Longint node_num @@ -5640,12 +9119,41 @@ cdef class Model: return node_num def getVarStrongbranch(self, Variable var, itlim, idempotent=False, integral=False): - """ Strong branches and gets information on column variable. + """ + Strong branches and gets information on column variable. + + Parameters + ---------- + var : Variable + Variable to get strong branching information on + itlim : int + LP iteration limit for total strong branching calls + idempotent : bool, optional + Should SCIP's state remain the same after the call? + integral : bool, optional + Boolean on whether the variable is currently integer. + + Returns + ------- + down : float + The dual bound of the LP after branching down on the variable + up : float + The dual bound of the LP after branchign up on the variable + downvalid : bool + stores whether the returned down value is a valid dual bound, or NULL + upvalid : bool + stores whether the returned up value is a valid dual bound, or NULL + downinf : bool + store whether the downwards branch is infeasible + upinf : bool + store whether the upwards branch is infeasible + downconflict : bool + store whether a conflict constraint was created for an infeasible downwards branch + upconflict : bool + store whether a conflict constraint was created for an infeasible upwards branch + lperror : bool + whether an unresolved LP error occurred in the solving process - :param Variable var: Variable to get strong branching information on - :param itlim: LP iteration limit for total strong branching calls - :param idempotent: Should SCIP's state remain the same after the call? - :param integral: Boolean on whether the variable is currently integer. """ cdef SCIP_Real down @@ -5668,25 +9176,43 @@ cdef class Model: return down, up, downvalid, upvalid, downinf, upinf, downconflict, upconflict, lperror def updateVarPseudocost(self, Variable var, valdelta, objdelta, weight): - """Updates the pseudo costs of the given variable and the global pseudo costs after a change of valdelta - in the variable's solution value and resulting change of objdelta in the LP's objective value. - Update is ignored if objdelts is infinite. Weight is in range (0, 1], and affects how it updates - the global weighted sum. + """ + Updates the pseudo costs of the given variable and the global pseudo costs after a change of valdelta + in the variable's solution value and resulting change of objdelta in the LP's objective value. + Update is ignored if objdelts is infinite. Weight is in range (0, 1], and affects how it updates + the global weighted sum. + + Parameters + ---------- + var : Variable + Variable whos pseudo cost will be updated + valdelta : float + The change in variable value (e.g. the fractional amount removed or added by branching) + objdelta : float + The change in objective value of the LP after valdelta change of the variable + weight : float + the weight in range (0,1] of how the update affects the stored weighted sum. - :param Variable var: Variable whos pseudo cost will be updated - :param valdelta: The change in variable value (e.g. the fractional amount removed or added by branching) - :param objdelta: The change in objective value of the LP after valdelta change of the variable - :param weight: the weight in range (0,1] of how the update affects the stored weighted sum. - """ + """ PY_SCIP_CALL(SCIPupdateVarPseudocost(self._scip, var.scip_var, valdelta, objdelta, weight)) def getBranchScoreMultiple(self, Variable var, gains): - """Calculates the branching score out of the gain predictions for a branching with - arbitrary many children. + """ + Calculates the branching score out of the gain predictions for a branching with + arbitrarily many children. + + Parameters + ---------- + var : Variable + variable to calculate the score for + gains : list of float + list of gains for each child. + + Returns + ------- + float - :param Variable var: variable to calculate the score for - :param gains: list of gains for each child. """ assert isinstance(gains, list) @@ -5704,13 +9230,21 @@ cdef class Model: return score def getTreesizeEstimation(self): - """Get an estimation of the final tree size """ + """ + Get an estimate of the final tree size. + + Returns + ------- + float + + """ return SCIPgetTreesizeEstimation(self._scip) def getBipartiteGraphRepresentation(self, prev_col_features=None, prev_edge_features=None, prev_row_features=None, static_only=False, suppress_warnings=False): - """This function generates the bipartite graph representation of an LP, which was first used in + """ + This function generates the bipartite graph representation of an LP, which was first used in the following paper: @inproceedings{conf/nips/GasseCFCL19, title={Exact Combinatorial Optimization with Graph Convolutional Neural Networks}, @@ -5725,11 +9259,27 @@ cdef class Model: An example plugin is a branching rule or an event handler, which is exclusively created to call this function. The user must then make certain to return the appropriate SCIP_RESULT (e.g. DIDNOTRUN) - :param prev_col_features: The list of column features previously returned by this function - :param prev_edge_features: The list of edge features previously returned by this function - :param prev_row_features: The list of row features previously returned by this function - :param static_only: Whether exclusively static features should be generated - :param suppress_warnings: Whether warnings should be suppressed + Parameters + ---------- + prev_col_features : list of list or None, optional + The list of column features previously returned by this function + prev_edge_features : list of list or None, optional + The list of edge features previously returned by this function + prev_row_features : list of list or None, optional + The list of row features previously returned by this function + static_only : bool, optional + Whether exclusively static features should be generated + suppress_warnings : bool, optional + Whether warnings should be suppressed + + Returns + ------- + col_features : list of list + edge_features : list of list + row_features : list of list + dict + The feature mappings for the columns, edges, and rows + """ cdef SCIP* scip = self._scip @@ -6105,8 +9655,15 @@ def readStatistics(filename): Given a .stats file of a solved model, reads it and returns an instance of the Statistics class holding some statistics. - Keyword arguments: - filename -- name of the input file + Parameters + ---------- + filename : str + name of the input file + + Returns + ------- + Statistics + """ result = {} file = open(filename) From 0ecd8eb39ab807eb2c5a0d781ed2616c0c69f965 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:21:42 +0200 Subject: [PATCH 077/135] Add dislcaimer. Remove incorrect warning (#902) --- docs/install.rst | 2 -- src/pyscipopt/scip.pxi | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index b66ee7c26..d9cc7613b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -2,8 +2,6 @@ Installation Guide ################## -**This file is deprecated and will be removed soon. Please see the online documentation.** - This page will detail all methods for installing PySCIPOpt via package managers, which come with their own versions of SCIP. For building PySCIPOpt against your own custom version of SCIP, or for building PySCIPOpt from source, visit :doc:`this page `. diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a57e322e7..e8fd5fd5f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6164,7 +6164,8 @@ cdef class Model: def solveConcurrent(self): """Transforms, presolves, and solves problem using additional solvers which emphasize on - finding solutions.""" + finding solutions. + WARNING: This feature is still experimental and prone to some errors.""" if SCIPtpiGetNumThreads() == 1: warnings.warn("SCIP was compiled without task processing interface. Parallel solve not possible - using optimize() instead of solveConcurrent()") self.optimize() From 39cf83653d9065094f514cf3942ec6bb0ebc3c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:44:18 +0200 Subject: [PATCH 078/135] Remove duplicate entry in CHANGELOG (#903) --- CHANGELOG.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd4af49b..0ef5ff7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,24 +43,6 @@ - Made readStatistics a standalone function ### Removed -## 5.1.1 - 2024-06-22 -### Added -- Added SCIP_STATUS_DUALLIMIT and SCIP_STATUS_PRIMALLIMIT -- Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) -- Added recipe with reformulation for detecting infeasible constraints -- Wrapped SCIPcreateOrigSol and added tests -- Added verbose option for writeProblem and writeParams -- Expanded locale test -- Added methods for creating expression constraints without adding to problem -- Added methods for creating/adding/appending disjunction constraints -- Added check for pt_PT locale in test_model.py -- Added SCIPgetOrigConss and SCIPgetNOrigConss Cython bindings. -- Added transformed=False option to getConss, getNConss, and getNVars -### Fixed -- Fixed locale errors in reading -### Changed -### Removed - ## 5.0.1 - 2024-04-05 ### Added - Added recipe for nonlinear objective functions From ea877d401150bbc2754ce5ed3b4ae3b6b004f12e Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 26 Sep 2024 11:21:18 +0200 Subject: [PATCH 079/135] Followed release.md checklist --- CHANGELOG.md | 6 ++++++ docs/build.rst | 2 +- pyproject.toml | 8 ++++---- setup.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef5ff7e4..463941c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased ### Added +### Fixed +### Changed +### Removed + +## 5.2.0 - 2024.09.26 +### Added - Expanded Statistics class to more problems. - Created Statistics class - Added parser to read .stats file diff --git a/docs/build.rst b/docs/build.rst index b18fe0bda..2a94496b0 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -22,7 +22,7 @@ To download SCIP please either use the pre-built SCIP Optimization Suite availab * - SCIP - PySCIPOpt * - 9.1 - - 5.1+ + - 5.1, 5.2+ * - 9.0 - 5.0.x * - 8.0 diff --git a/pyproject.toml b/pyproject.toml index 43e97359d..7e0068935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ skip="pp*" # currently doesn't work with PyPy skip="pp* cp36* cp37* *musllinux*" before-all = [ "(apt-get update && apt-get install --yes wget) || yum install -y wget zlib libgfortran || brew install wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-linux.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-linux.zip -O scip.zip", "unzip scip.zip", "mv scip_install scip" ] @@ -57,9 +57,9 @@ before-all = ''' #!/bin/bash brew install wget zlib gcc if [[ $CIBW_ARCHS == *"arm"* ]]; then - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-macos-arm.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos-arm.zip -O scip.zip else - wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-macos.zip -O scip.zip + wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos.zip -O scip.zip fi unzip scip.zip mv scip_install src/scip @@ -75,7 +75,7 @@ repair-wheel-command = [ skip="pp* cp36* cp37*" before-all = [ "choco install 7zip wget", - "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.4.0/libscip-windows.zip -O scip.zip", + "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-windows.zip -O scip.zip", "\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"", "mv .\\scip-test\\scip_install .\\test", "mv .\\test .\\scip" diff --git a/setup.py b/setup.py index 9424b5749..ebad98b10 100644 --- a/setup.py +++ b/setup.py @@ -109,7 +109,7 @@ setup( name="PySCIPOpt", - version="5.1.0", + version="5.2.0", description="Python interface and modeling environment for SCIP", long_description=long_description, long_description_content_type="text/markdown", From 908934747a5e3cb4ceaeb2d8dd8ef74cbd3241fd Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 26 Sep 2024 11:25:19 +0200 Subject: [PATCH 080/135] Change cibuildwheel yp new version --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 92b474828..8fda12b09 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v4 - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_TEST_REQUIRES: pytest From cc0797b98a1132ace3b80de3dd2f0f592df7d810 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 26 Sep 2024 11:39:12 +0200 Subject: [PATCH 081/135] override default manylinux2014 --- .github/workflows/build_wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 8fda12b09..ab5d1d61c 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -46,6 +46,7 @@ jobs: CIBW_ARCHS: ${{ matrix.arch }} CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: "pytest {project}/tests" + CIBW_MANYLINUX_*_IMAGE: manylinux_2_28 - uses: actions/upload-artifact@v3 with: From 704495a71f2f8898a7520262dc3726e7bc1e4524 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 26 Sep 2024 11:55:13 +0200 Subject: [PATCH 082/135] Add macosx development flag of 13 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7e0068935..c85a71838 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ if [[ $CIBW_ARCHS == *"arm"* ]]; then wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos-arm.zip -O scip.zip else wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos.zip -O scip.zip + export MACOSX_DEPLOYMENT_TARGET=13.0 fi unzip scip.zip mv scip_install src/scip From 47d86db8b418c013228d4da1fe8dae25251d62a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:34:44 +0100 Subject: [PATCH 083/135] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b612c064d..d85b5373d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ pip install pyscipopt ``` For information on specific versions, installation via Conda, and guides for building from source, -please see the online documentation. +please see the [online documentation](https://pyscipopt.readthedocs.io/en/latest/install.html). Building and solving a model ---------------------------- From a0aa5c3ab122ed8f6af3bfa7d94f39d8d8817dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sat, 5 Oct 2024 13:08:02 +0100 Subject: [PATCH 084/135] Update tutorial to include parameter emphasis and setting (#909) * Add paramter emphasis and setting * Update docs/tutorials/model.rst Co-authored-by: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> * Update docs/tutorials/model.rst Co-authored-by: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> --------- Co-authored-by: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> --- docs/tutorials/constypes.rst | 2 +- docs/tutorials/model.rst | 84 +++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/constypes.rst b/docs/tutorials/constypes.rst index ea714d112..c0ee1b52f 100644 --- a/docs/tutorials/constypes.rst +++ b/docs/tutorials/constypes.rst @@ -80,7 +80,7 @@ coefficient values, and the constraint handler that created the Row. Constraint Information -======================== +====================== The Constraint object can be queried like any other object. Some of the information a Constraint object contains is the name of the constraint handler responsible for the constraint, diff --git a/docs/tutorials/model.rst b/docs/tutorials/model.rst index 57c25bd17..184ee3a70 100644 --- a/docs/tutorials/model.rst +++ b/docs/tutorials/model.rst @@ -109,6 +109,88 @@ all the parameter values that you wish to set, then one can use the command: scip.readParams(path_to_file) +Set Plugin-wide Parameters (Aggressiveness) +=================================== + +We can influence the behavior of some of SCIP's plugins using ``SCIP_PARAMSETTING``. This can be applied +to the heuristics, to the presolvers, and to the separators (respectively with ``setHeuristics``, +``setPresolve``, and ``setSeparating``). + +.. code-block:: python + + from pyscipopt import Model, SCIP_PARAMSETTING + + scip = Model() + scip.setHeuristics(SCIP_PARAMSETTING.AGGRESSIVE) + +There are four parameter settings: + +.. list-table:: A list of the different options and the result + :widths: 25 25 + :align: center + :header-rows: 1 + + * - Option + - Result + * - ``DEFAULT`` + - set to the default values of all the plugin's parameters + * - ``FAST`` + - the time spend for the plugin is decreased + * - ``AGGRESSIVE`` + - such that the plugin is called more aggressively + * - ``OFF`` + - turn off the plugin + +.. note:: This is important to get dual information, as it's necessary to disable presolving and heuristics. + For more information, see the tutorial on getting :doc:`constraint information.` + + +Set Solver Emphasis +=================== + +One can also instruct SCIP to focus on different aspects of the search process. To do this, import +``SCIP_PARAMEMPHASIS`` from ``pyscipopt`` and set the appropriate value. For example, +if the goal is just to find a feasible solution, then we can do the following: + +.. code-block:: python + + from pyscipopt import Model, SCIP_PARAMEMPHASIS + + scip = Model() + scip.setEmphasis(SCIP_PARAMEMPHASIS.FEASIBILITY) + +You can find below a list of the available options, alongside their meaning. + +.. list-table:: Parameter emphasis summary + :widths: 25 25 + :align: center + :header-rows: 1 + + * - Setting + - Meaning + * - ``PARAMEMPHASIS.DEFAULT`` + - to use default values + * - ``PARAMEMPHASIS.COUNTER`` + - to get feasible and "fast" counting process + * - ``PARAMEMPHASIS.CPSOLVER`` + - to get CP like search (e.g. no LP relaxation) + * - ``PARAMEMPHASIS.EASYCIP`` + - to solve easy problems fast + * - ``PARAMEMPHASIS.FEASIBILITY`` + - to detect feasibility fast + * - ``PARAMEMPHASIS.HARDLP`` + - to be capable to handle hard LPs + * - ``PARAMEMPHASIS.OPTIMALITY`` + - to prove optimality fast + * - ``PARAMEMPHASIS.PHASEFEAS`` + - to find feasible solutions during a 3 phase solution process + * - ``PARAMEMPHASIS.PHASEIMPROVE`` + - to find improved solutions during a 3 phase solution process + * - ``PARAMEMPHASIS.PHASEPROOF`` + - to proof optimality during a 3 phase solution process + * - ``PARAMEMPHASIS.NUMERICS`` + - to solve problems which cause numerical issues + Copy a SCIP Model ================== @@ -122,7 +204,7 @@ This model is completely independent from the source model. The data has been du That is, calling ``scip.optimize()`` at this point will have no effect on ``scip_alternate_model``. .. note:: After optimizing users often struggle with reoptimization. To make changes to an - already optimized model, one must first fo the following: + already optimized model, one must first do the following: .. code-block:: python From 54cb49c3e86bde6f1dc5d74511547eafae087667 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 7 Oct 2024 11:50:58 +0200 Subject: [PATCH 085/135] Change theme of documentation --- docs/conf.py | 3 ++- docs/requirements.txt | 3 +-- docs/tutorials/model.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b69be4972..dcc60d874 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,7 +75,8 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_nefertiti" +# html_theme = "sphinx_nefertiti" +html_theme = "sphinx_book_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/requirements.txt b/docs/requirements.txt index 569b1214a..543a862ee 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,4 @@ sphinx -sphinx-rtd-theme -sphinx-nefertiti +sphinx-book-theme sphinxcontrib-bibtex pyscipopt \ No newline at end of file diff --git a/docs/tutorials/model.rst b/docs/tutorials/model.rst index 184ee3a70..5dd4324f8 100644 --- a/docs/tutorials/model.rst +++ b/docs/tutorials/model.rst @@ -110,7 +110,7 @@ all the parameter values that you wish to set, then one can use the command: scip.readParams(path_to_file) Set Plugin-wide Parameters (Aggressiveness) -=================================== +=========================================== We can influence the behavior of some of SCIP's plugins using ``SCIP_PARAMSETTING``. This can be applied to the heuristics, to the presolvers, and to the separators (respectively with ``setHeuristics``, From 0a0e45493f2ce50a70150599b6c4b6f8c8cb25a5 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:00:30 +0200 Subject: [PATCH 086/135] Update docs/conf.py Co-authored-by: Mohammed Ghannam --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index dcc60d874..0326f6336 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,7 +75,6 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = "sphinx_nefertiti" html_theme = "sphinx_book_theme" # Add any paths that contain custom static files (such as style sheets) here, From e5bf75bc9a1768fc6b07e3f38b2d0905d2bae0ac Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:44:14 +0200 Subject: [PATCH 087/135] Add sphinxcontrib.jquery for new theme (#912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 0326f6336..a2ac20544 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,7 @@ "sphinx.ext.mathjax", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", + "sphinxcontrib.jquery", ] # You can define documentation here that will be repeated often From 52f94106e8537d7f9187381ed35bbacaaeb4906d Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:11:43 +0200 Subject: [PATCH 088/135] Update theme (#913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add sphinxcontrib.jquery for new theme * Move jquery to requirements * Remove ext.jquery from conf --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- docs/conf.py | 1 - docs/requirements.txt | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a2ac20544..0326f6336 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,6 @@ "sphinx.ext.mathjax", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", - "sphinxcontrib.jquery", ] # You can define documentation here that will be repeated often diff --git a/docs/requirements.txt b/docs/requirements.txt index 543a862ee..52e8511a2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ sphinx -sphinx-book-theme +sphinxcontrib-jquery sphinxcontrib-bibtex +sphinx-book-theme pyscipopt \ No newline at end of file From 4979c99f71915ca87a47ef31cf4a7334fb2f57e8 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Thu, 10 Oct 2024 08:11:58 +0200 Subject: [PATCH 089/135] Update scip version in integration tests & allow running on PRs --- .github/workflows/integration-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index dd9b378d6..d78162389 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -1,13 +1,16 @@ name: Integration test env: - version: 9.0.0 + version: 9.1.0 # runs on branches and pull requests; doesn't run on tags. on: push: branches: - 'master' + pull_request: + branches: + - 'master' jobs: From 0ca4f652685949d4f540f445bd8a4c2e60c232a7 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 11 Oct 2024 17:12:45 +0200 Subject: [PATCH 090/135] Add comments from dominik --- docs/tutorials/constypes.rst | 90 ++++++++++++++++++------------------ docs/tutorials/index.rst | 6 +-- docs/tutorials/vartypes.rst | 65 +++++++++++++------------- 3 files changed, 81 insertions(+), 80 deletions(-) diff --git a/docs/tutorials/constypes.rst b/docs/tutorials/constypes.rst index c0ee1b52f..c7bc25cd9 100644 --- a/docs/tutorials/constypes.rst +++ b/docs/tutorials/constypes.rst @@ -38,46 +38,29 @@ To create a standard linear or non-linear constraint use the command: # Non-linear constraint nonlinear_cons = scip.addCons(x * y + z == 1, name="nonlinear_cons") -What is a Row? -================ -In a similar fashion to Variables with columns, see :doc:`this page `, -constraints bring up an interesting feature of SCIP when used in the context of an LP. -The context of an LP here means that we are after the LP relaxation of the optimization problem -at some node. Is the constraint even in the LP? -When you solve an optimization problm with SCIP, the problem is first transformed. This process is -called presolve, and is done to accelerate the subsequent solving process. Therefore a constraint -that was originally created may have been transformed entirely, as the original variables that -featured in the constraint have also been changed. Additionally, maybe the constraint was found to be redundant, -i.e., trivially true, and was removed. The constraint is also much more general -than necessary, containing information that is not strictly necessary for solving the LP, -and may not even be representable by linear constraints. -Therefore, when representing a constraint in an LP, we use Row objects. -Be warned however, that this is not necessarily a simple one-to-one matching. Some more complicated -constraints may either have no Row representation in the LP or have multiple such rows -necessary to best represent it in the LP. For a standard linear constraint the Row -that represents the constraint in the LP can be found with the code: +Quicksum +======== -.. code-block:: python +It is very common that when constructing constraints one wants to use the inbuilt ``sum`` function +in Python. For example, consider the common scenario where we have a set of binary variables. - row = scip.getRowLinear(linear_cons) +.. code-block:: python -.. note:: Remember that such a Row representation refers only to the latest LP, and is - best queried when access to the current LP is clear, e.g. when branching. + x = [scip.addVar(vtype='B', name=f"x_{i}") for i in range(1000)] -From a Row object one can easily obtain information about the current LP. Some quick examples are -the lhs, rhs, constant shift, the columns with non-zero coefficient values, the matching -coefficient values, and the constraint handler that created the Row. +A standard constraint in this example may be that exactly one binary variable can be active. +To sum these varaibles we recommend using the custom ``quicksum`` function, as it avoids +intermediate data structure and adds terms inplace. For example: .. code-block:: python - lhs = row.getLhs() - rhs = row.getRhs() - constant = row.getConstant() - cols = row.getCols() - vals = row.getVals() - origin_cons_name = row.getConsOriginConshdlrtype() + scip.addCons(quicksum(x[i] for i in range(1000)) == 1, name="sum_cons") +.. note:: While this is often unnecessary for smaller models, for larger models it can have a substantial + improvement on time spent in model construction. + +.. note:: For ``prod`` there also exists an equivalent ``quickprod`` function. Constraint Information ====================== @@ -156,25 +139,42 @@ An example of such a constraint handler is presented in the lazy constraint tutorial for modelling the subtour elimination constraints :doc:`here ` -Quicksum -======== +What is a Row? +================ -It is very common that when constructing constraints one wants to use the inbuilt ``sum`` function -in Python. For example, consider the common scenario where we have a set of binary variables. +In a similar fashion to Variables with columns, see :doc:`this page `, +constraints bring up an interesting feature of SCIP when used in the context of an LP. +The context of an LP here means that we are after the LP relaxation of the optimization problem +at some node. Is the constraint even in the LP? +When you solve an optimization problm with SCIP, the problem is first transformed. This process is +called presolve, and is done to accelerate the subsequent solving process. Therefore a constraint +that was originally created may have been transformed entirely, as the original variables that +featured in the constraint have also been changed. Additionally, maybe the constraint was found to be redundant, +i.e., trivially true, and was removed. The constraint is also much more general +than necessary, containing information that is not strictly necessary for solving the LP, +and may not even be representable by linear constraints. +Therefore, when representing a constraint in an LP, we use Row objects. +Be warned however, that this is not necessarily a simple one-to-one matching. Some more complicated +constraints may either have no Row representation in the LP or have multiple such rows +necessary to best represent it in the LP. For a standard linear constraint the Row +that represents the constraint in the LP can be found with the code: .. code-block:: python - x = [scip.addVar(vtype='B', name=f"x_{i}") for i in range(1000)] - -A standard constraint in this example may be that exactly one binary variable can be active. -To sum these varaibles we recommend using the custom ``quicksum`` function, as it avoids -intermediate data structure and adds terms inplace. For example: + row = scip.getRowLinear(linear_cons) -.. code-block:: python +.. note:: Remember that such a Row representation refers only to the latest LP, and is + best queried when access to the current LP is clear, e.g. when branching. - scip.addCons(quicksum(x[i] for i in range(1000)) == 1, name="sum_cons") +From a Row object one can easily obtain information about the current LP. Some quick examples are +the lhs, rhs, constant shift, the columns with non-zero coefficient values, the matching +coefficient values, and the constraint handler that created the Row. -.. note:: While this is often unnecessary for smaller models, for larger models it can have a substantial - improvement on time spent in model construction. +.. code-block:: python -.. note:: For ``prod`` there also exists an equivalent ``quickprod`` function. + lhs = row.getLhs() + rhs = row.getRhs() + constant = row.getConstant() + cols = row.getCols() + vals = row.getVals() + origin_cons_name = row.getConsOriginConshdlrtype() \ No newline at end of file diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 1ebe56198..f8125e085 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -11,11 +11,11 @@ more detailed information see `this page Date: Fri, 18 Oct 2024 13:19:13 +0200 Subject: [PATCH 091/135] Minor fixes --- tests/helpers/utils.py | 6 +----- tests/test_model.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 0eaff8c2f..4299c39dd 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -77,19 +77,15 @@ def random_nlp_1(): return model -def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4]): +def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4], knapsackSize = 15): # create solver instance s = Model("Knapsack") - s.hideOutput() # setting the objective sense to maximise s.setMaximize() assert len(weights) == len(costs) - # knapsack size - knapsackSize = 15 - # adding the knapsack variables knapsackVars = [] varNames = [] diff --git a/tests/test_model.py b/tests/test_model.py index 365f3e919..f5dcd062a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -476,4 +476,4 @@ def test_getObjVal(): assert m.getVal(x) == 0 assert m.getObjVal() == 16 - assert m.getVal(x) == 0 + assert m.getVal(x) == 0 \ No newline at end of file From 58ad6d1216fd8aed4dbafcd9204edf5afef49191 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 18 Oct 2024 13:19:39 +0200 Subject: [PATCH 092/135] Add primal_dual_evolution and test and plot --- .../recipes/primal_dual_evolution.py | 50 +++++++++++++++++++ tests/test_recipe_primal_dual_evolution.py | 20 ++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/pyscipopt/recipes/primal_dual_evolution.py create mode 100644 tests/test_recipe_primal_dual_evolution.py diff --git a/src/pyscipopt/recipes/primal_dual_evolution.py b/src/pyscipopt/recipes/primal_dual_evolution.py new file mode 100644 index 000000000..1aad4f45d --- /dev/null +++ b/src/pyscipopt/recipes/primal_dual_evolution.py @@ -0,0 +1,50 @@ +from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE, Eventhdlr + +def get_primal_dual_evolution(model: Model): + + class gapEventhdlr(Eventhdlr): + + def eventinit(self): # we want to collect best primal solutions and best dual solutions + self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + self.model.catchEvent(SCIP_EVENTTYPE.LPSOLVED, self) + self.model.catchEvent(SCIP_EVENTTYPE.NODESOLVED, self) + + + def eventexec(self, event): + # if a new best primal solution was found, we save when it was found and also its objective + if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND: + self.model.data["primal_solutions"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) + + if not self.model.data["dual_solutions"]: + self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + + if self.model.getObjectiveSense() == "minimize": + if self.model.isGT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]): + self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + else: + if self.model.isLT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]): + self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + + if not hasattr(model, "data"): + model.data = {} + + model.data["primal_solutions"] = [] + model.data["dual_solutions"] = [] + hdlr = gapEventhdlr() + model.includeEventhdlr(hdlr, "gapEventHandler", "Event handler which collects primal and dual solution evolution") + + return model + +def plot_primal_dual_evolution(model: Model): + try: + from matplotlib import pyplot as plt + except ImportError: + raise("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.") + + time_primal, val_primal = zip(*model.data["primal_solutions"]) + plt.plot(time_primal, val_primal, label="Primal bound") + time_dual, val_dual = zip(*model.data["dual_solutions"]) + plt.plot(time_dual, val_dual, label="Dual bound") + + plt.legend(loc="best") + plt.show() \ No newline at end of file diff --git a/tests/test_recipe_primal_dual_evolution.py b/tests/test_recipe_primal_dual_evolution.py new file mode 100644 index 000000000..c5a67770f --- /dev/null +++ b/tests/test_recipe_primal_dual_evolution.py @@ -0,0 +1,20 @@ +from pyscipopt.recipes.primal_dual_evolution import get_primal_dual_evolution +from helpers.utils import bin_packing_model + +def test_primal_dual_evolution(): + from random import randint + + model = bin_packing_model(sizes=[randint(1,40) for _ in range(120)], capacity=50) + model.setParam("limits/time",5) + + model.data = {"test": True} + model = get_primal_dual_evolution(model) + + assert "test" in model.data + assert "primal_solutions" in model.data + + model.optimize() + + # these are required because the event handler doesn't capture the final state + model.data["primal_solutions"].append((model.getSolvingTime(), model.getPrimalbound())) + model.data["dual_solutions"].append((model.getSolvingTime(), model.getDualbound())) \ No newline at end of file From 1e5a56f7b770b48bb36f5994014b995b10db7889 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 18 Oct 2024 13:20:15 +0200 Subject: [PATCH 093/135] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef5ff7e4..6b0d5f05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added primal_dual_evolution recipe and a plot recipe - Expanded Statistics class to more problems. - Created Statistics class - Added parser to read .stats file From 53627d21c88b5dcdf20da9c597a41ab234823984 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 18 Oct 2024 13:33:57 +0200 Subject: [PATCH 094/135] More robust testing --- tests/test_recipe_primal_dual_evolution.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_recipe_primal_dual_evolution.py b/tests/test_recipe_primal_dual_evolution.py index c5a67770f..c7e911779 100644 --- a/tests/test_recipe_primal_dual_evolution.py +++ b/tests/test_recipe_primal_dual_evolution.py @@ -17,4 +17,20 @@ def test_primal_dual_evolution(): # these are required because the event handler doesn't capture the final state model.data["primal_solutions"].append((model.getSolvingTime(), model.getPrimalbound())) - model.data["dual_solutions"].append((model.getSolvingTime(), model.getDualbound())) \ No newline at end of file + model.data["dual_solutions"].append((model.getSolvingTime(), model.getDualbound())) + + for i in range(1, len(model.data["primal_solutions"])): + if model.getObjectiveSense() == "minimize": + assert model.data["primal_solutions"][i][1] <= model.data["primal_solutions"][i-1][1] + else: + assert model.data["primal_solutions"][i][1] >= model.data["primal_solutions"][i-1][1] + + for i in range(1, len(model.data["dual_solutions"])): + if model.getObjectiveSense() == "minimize": + assert model.data["dual_solutions"][i][1] >= model.data["dual_solutions"][i-1][1] + else: + assert model.data["dual_solutions"][i][1] <= model.data["dual_solutions"][i-1][1] + + # how to get a simple plot of the data + #from pyscipopt.recipes.primal_dual_evolution import plot_primal_dual_evolution + #plot_primal_dual_evolution(model) \ No newline at end of file From 7c23a54356ef46e96c411aeba84d5de93e01a66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:17:06 +0100 Subject: [PATCH 095/135] Update src/pyscipopt/recipes/primal_dual_evolution.py Co-authored-by: Mohammed Ghannam --- src/pyscipopt/recipes/primal_dual_evolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/recipes/primal_dual_evolution.py b/src/pyscipopt/recipes/primal_dual_evolution.py index 1aad4f45d..670d4456c 100644 --- a/src/pyscipopt/recipes/primal_dual_evolution.py +++ b/src/pyscipopt/recipes/primal_dual_evolution.py @@ -2,7 +2,7 @@ def get_primal_dual_evolution(model: Model): - class gapEventhdlr(Eventhdlr): + class GapEventhdlr(Eventhdlr): def eventinit(self): # we want to collect best primal solutions and best dual solutions self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) From cca56ee1b77ea49ce19c6c6e02ea634864011a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:17:14 +0100 Subject: [PATCH 096/135] Update tests/helpers/utils.py Co-authored-by: Mohammed Ghannam --- tests/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 4299c39dd..969ba4798 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -77,7 +77,7 @@ def random_nlp_1(): return model -def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4], knapsackSize = 15): +def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4], knapsack_size = 15): # create solver instance s = Model("Knapsack") From a3cddcb52489cdb9510d9c60c4a3c2073b5163a2 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Mon, 28 Oct 2024 15:11:48 +0100 Subject: [PATCH 097/135] some comments --- .../finished/plot_primal_dual_evolution.py | 140 ++++++++++++++++++ .../recipes/primal_dual_evolution.py | 57 +++---- tests/test_recipe_primal_dual_evolution.py | 12 +- 3 files changed, 172 insertions(+), 37 deletions(-) create mode 100644 examples/finished/plot_primal_dual_evolution.py diff --git a/examples/finished/plot_primal_dual_evolution.py b/examples/finished/plot_primal_dual_evolution.py new file mode 100644 index 000000000..6db1755f2 --- /dev/null +++ b/examples/finished/plot_primal_dual_evolution.py @@ -0,0 +1,140 @@ +from pyscipopt import Model + +def plot_primal_dual_evolution(model: Model): + try: + from matplotlib import pyplot as plt + except ImportError: + raise("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.") + + time_primal, val_primal = zip(*model.data["primal_log"]) + plt.plot(time_primal, val_primal, label="Primal bound") + time_dual, val_dual = zip(*model.data["dual_log"]) + plt.plot(time_dual, val_dual, label="Dual bound") + + plt.legend(loc="best") + plt.show() + + +def gastrans_model(): + GASTEMP = 281.15 + RUGOSITY = 0.05 + DENSITY = 0.616 + COMPRESSIBILITY = 0.8 + nodes = [ + # name supplylo supplyup pressurelo pressureup cost + ("Anderlues", 0.0, 1.2, 0.0, 66.2, 0.0), # 0 + ("Antwerpen", None, -4.034, 30.0, 80.0, 0.0), # 1 + ("Arlon", None, -0.222, 0.0, 66.2, 0.0), # 2 + ("Berneau", 0.0, 0.0, 0.0, 66.2, 0.0), # 3 + ("Blaregnies", None, -15.616, 50.0, 66.2, 0.0), # 4 + ("Brugge", None, -3.918, 30.0, 80.0, 0.0), # 5 + ("Dudzele", 0.0, 8.4, 0.0, 77.0, 2.28), # 6 + ("Gent", None, -5.256, 30.0, 80.0, 0.0), # 7 + ("Liege", None, -6.385, 30.0, 66.2, 0.0), # 8 + ("Loenhout", 0.0, 4.8, 0.0, 77.0, 2.28), # 9 + ("Mons", None, -6.848, 0.0, 66.2, 0.0), # 10 + ("Namur", None, -2.120, 0.0, 66.2, 0.0), # 11 + ("Petange", None, -1.919, 25.0, 66.2, 0.0), # 12 + ("Peronnes", 0.0, 0.96, 0.0, 66.2, 1.68), # 13 + ("Sinsin", 0.0, 0.0, 0.0, 63.0, 0.0), # 14 + ("Voeren", 20.344, 22.012, 50.0, 66.2, 1.68), # 15 + ("Wanze", 0.0, 0.0, 0.0, 66.2, 0.0), # 16 + ("Warnand", 0.0, 0.0, 0.0, 66.2, 0.0), # 17 + ("Zeebrugge", 8.87, 11.594, 0.0, 77.0, 2.28), # 18 + ("Zomergem", 0.0, 0.0, 0.0, 80.0, 0.0) # 19 + ] + arcs = [ + # node1 node2 diameter length active */ + (18, 6, 890.0, 4.0, False), + (18, 6, 890.0, 4.0, False), + (6, 5, 890.0, 6.0, False), + (6, 5, 890.0, 6.0, False), + (5, 19, 890.0, 26.0, False), + (9, 1, 590.1, 43.0, False), + (1, 7, 590.1, 29.0, False), + (7, 19, 590.1, 19.0, False), + (19, 13, 890.0, 55.0, False), + (15, 3, 890.0, 5.0, True), + (15, 3, 395.0, 5.0, True), + (3, 8, 890.0, 20.0, False), + (3, 8, 395.0, 20.0, False), + (8, 17, 890.0, 25.0, False), + (8, 17, 395.0, 25.0, False), + (17, 11, 890.0, 42.0, False), + (11, 0, 890.0, 40.0, False), + (0, 13, 890.0, 5.0, False), + (13, 10, 890.0, 10.0, False), + (10, 4, 890.0, 25.0, False), + (17, 16, 395.5, 10.5, False), + (16, 14, 315.5, 26.0, True), + (14, 2, 315.5, 98.0, False), + (2, 12, 315.5, 6.0, False) + ] + + model = Model() + + # create flow variables + flow = {} + for arc in arcs: + flow[arc] = model.addVar("flow_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), # names of nodes in arc + lb=0.0 if arc[4] else None) # no lower bound if not active + + # pressure difference variables + pressurediff = {} + for arc in arcs: + pressurediff[arc] = model.addVar("pressurediff_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), + # names of nodes in arc + lb=None) + + # supply variables + supply = {} + for node in nodes: + supply[node] = model.addVar("supply_%s" % (node[0]), lb=node[1], ub=node[2], obj=node[5]) + + # square pressure variables + pressure = {} + for node in nodes: + pressure[node] = model.addVar("pressure_%s" % (node[0]), lb=node[3] ** 2, ub=node[4] ** 2) + + # node balance constrains, for each node i: outflows - inflows = supply + for nid, node in enumerate(nodes): + # find arcs that go or end at this node + flowbalance = 0 + for arc in arcs: + if arc[0] == nid: # arc is outgoing + flowbalance += flow[arc] + elif arc[1] == nid: # arc is incoming + flowbalance -= flow[arc] + else: + continue + + model.addCons(flowbalance == supply[node], name="flowbalance%s" % node[0]) + + # pressure difference constraints: pressurediff[node1 to node2] = pressure[node1] - pressure[node2] + for arc in arcs: + model.addCons(pressurediff[arc] == pressure[nodes[arc[0]]] - pressure[nodes[arc[1]]], + "pressurediffcons_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) + + # pressure loss constraints: + from math import log10 + for arc in arcs: + coef = 96.074830e-15 * arc[2] ** 5 * (2.0 * log10(3.7 * arc[2] / RUGOSITY)) ** 2 / COMPRESSIBILITY / GASTEMP / \ + arc[3] / DENSITY + if arc[4]: # active + model.addCons(flow[arc] ** 2 + coef * pressurediff[arc] <= 0.0, + "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) + else: + model.addCons(flow[arc] * abs(flow[arc]) - coef * pressurediff[arc] == 0.0, + "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) + + return model + + +if __name__=="__main__": + from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr + + model = gastrans_model() + model.attach_primal_dual_evolution_eventhdlr() + + model.optimize() + plot_primal_dual_evolution(model) diff --git a/src/pyscipopt/recipes/primal_dual_evolution.py b/src/pyscipopt/recipes/primal_dual_evolution.py index 670d4456c..dd13d162e 100644 --- a/src/pyscipopt/recipes/primal_dual_evolution.py +++ b/src/pyscipopt/recipes/primal_dual_evolution.py @@ -1,7 +1,9 @@ from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE, Eventhdlr -def get_primal_dual_evolution(model: Model): - +def attach_primal_dual_evolution_eventhdlr(model: Model): + """ + + """ class GapEventhdlr(Eventhdlr): def eventinit(self): # we want to collect best primal solutions and best dual solutions @@ -13,38 +15,39 @@ def eventinit(self): # we want to collect best primal solutions and best dual so def eventexec(self, event): # if a new best primal solution was found, we save when it was found and also its objective if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND: - self.model.data["primal_solutions"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) + self.model.data["primal_log"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) + + if not self.model.data["dual_log"]: + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + + if self.model.getObjectiveSense() == "minimize": + if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + else: + if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + + def eventexitsol(self): + if self.model.data["primal_log"][-1] and self.model.getPrimalbound() != self.model.data["primal_log"][-1][1]: + self.model.data["primal_log"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) - if not self.model.data["dual_solutions"]: - self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + if not self.model.data["dual_log"]: + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) if self.model.getObjectiveSense() == "minimize": - if self.model.isGT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]): - self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) else: - if self.model.isLT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]): - self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound())) + if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): + self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + if not hasattr(model, "data"): model.data = {} - model.data["primal_solutions"] = [] - model.data["dual_solutions"] = [] - hdlr = gapEventhdlr() + model.data["primal_log"] = [] + model.data["dual_log"] = [] + hdlr = GapEventhdlr() model.includeEventhdlr(hdlr, "gapEventHandler", "Event handler which collects primal and dual solution evolution") - return model - -def plot_primal_dual_evolution(model: Model): - try: - from matplotlib import pyplot as plt - except ImportError: - raise("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.") - - time_primal, val_primal = zip(*model.data["primal_solutions"]) - plt.plot(time_primal, val_primal, label="Primal bound") - time_dual, val_dual = zip(*model.data["dual_solutions"]) - plt.plot(time_dual, val_dual, label="Dual bound") - - plt.legend(loc="best") - plt.show() \ No newline at end of file + return model \ No newline at end of file diff --git a/tests/test_recipe_primal_dual_evolution.py b/tests/test_recipe_primal_dual_evolution.py index c7e911779..f1b2632fc 100644 --- a/tests/test_recipe_primal_dual_evolution.py +++ b/tests/test_recipe_primal_dual_evolution.py @@ -1,4 +1,4 @@ -from pyscipopt.recipes.primal_dual_evolution import get_primal_dual_evolution +from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr from helpers.utils import bin_packing_model def test_primal_dual_evolution(): @@ -8,17 +8,13 @@ def test_primal_dual_evolution(): model.setParam("limits/time",5) model.data = {"test": True} - model = get_primal_dual_evolution(model) + model = attach_primal_dual_evolution_eventhdlr(model) assert "test" in model.data assert "primal_solutions" in model.data model.optimize() - # these are required because the event handler doesn't capture the final state - model.data["primal_solutions"].append((model.getSolvingTime(), model.getPrimalbound())) - model.data["dual_solutions"].append((model.getSolvingTime(), model.getDualbound())) - for i in range(1, len(model.data["primal_solutions"])): if model.getObjectiveSense() == "minimize": assert model.data["primal_solutions"][i][1] <= model.data["primal_solutions"][i-1][1] @@ -30,7 +26,3 @@ def test_primal_dual_evolution(): assert model.data["dual_solutions"][i][1] >= model.data["dual_solutions"][i-1][1] else: assert model.data["dual_solutions"][i][1] <= model.data["dual_solutions"][i-1][1] - - # how to get a simple plot of the data - #from pyscipopt.recipes.primal_dual_evolution import plot_primal_dual_evolution - #plot_primal_dual_evolution(model) \ No newline at end of file From 7c87182fd60a99928f11bf36eb09a72524fc9239 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Mon, 28 Oct 2024 15:16:10 +0100 Subject: [PATCH 098/135] remove is_optimized_mode --- tests/test_memory.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_memory.py b/tests/test_memory.py index 2433c2c6a..a0c6d9363 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -1,15 +1,14 @@ import pytest from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use -from helpers.utils import is_optimized_mode def test_not_freed(): - if is_optimized_mode(): + if is_memory_freed(): pytest.skip() m = Model() assert not is_memory_freed() def test_freed(): - if is_optimized_mode(): + if is_memory_freed(): pytest.skip() m = Model() del m From 06b4e724f282939d0e1e21e6f73508a479f30f10 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Mon, 28 Oct 2024 15:29:52 +0100 Subject: [PATCH 099/135] add docstring. remove useless util --- src/pyscipopt/recipes/primal_dual_evolution.py | 9 ++++++++- tests/helpers/utils.py | 8 -------- tests/test_heur.py | 4 ++-- tests/test_recipe_primal_dual_evolution.py | 14 +++++++------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/pyscipopt/recipes/primal_dual_evolution.py b/src/pyscipopt/recipes/primal_dual_evolution.py index dd13d162e..51878bd89 100644 --- a/src/pyscipopt/recipes/primal_dual_evolution.py +++ b/src/pyscipopt/recipes/primal_dual_evolution.py @@ -2,7 +2,14 @@ def attach_primal_dual_evolution_eventhdlr(model: Model): """ - + Attaches an event handler to a given SCIP model that collects primal and dual solutions, + along with the solving time when they were found. + The data is saved in model.data["primal_log"] and model.data["dual_log"]. They consist of + a list of tuples, each tuple containing the solving time and the corresponding solution. + + A usage example can be found in examples/finished/plot_primal_dual_evolution.py. The + example takes the information provided by this recipe and uses it to plot the evolution + of the dual and primal bounds over time. """ class GapEventhdlr(Eventhdlr): diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 969ba4798..4166c3027 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -1,14 +1,6 @@ from pyscipopt import Model, quicksum, SCIP_PARAMSETTING, exp, log, sqrt, sin from typing import List -from pyscipopt.scip import is_memory_freed - - -def is_optimized_mode(): - model = Model() - return is_memory_freed() - - def random_mip_1(disable_sepa=True, disable_huer=True, disable_presolve=True, node_lim=2000, small=False): model = Model() diff --git a/tests/test_heur.py b/tests/test_heur.py index 4af260e5b..9e9acd87f 100644 --- a/tests/test_heur.py +++ b/tests/test_heur.py @@ -6,7 +6,7 @@ from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_PARAMSETTING, SCIP_HEURTIMING, SCIP_LPSOLSTAT from pyscipopt.scip import is_memory_freed -from helpers.utils import random_mip_1, is_optimized_mode +from helpers.utils import random_mip_1 class MyHeur(Heur): @@ -106,7 +106,7 @@ def test_heur(): assert round(sol[y]) == 0.0 def test_heur_memory(): - if is_optimized_mode(): + if is_memory_freed(): pytest.skip() def inner(): diff --git a/tests/test_recipe_primal_dual_evolution.py b/tests/test_recipe_primal_dual_evolution.py index f1b2632fc..d6d12d644 100644 --- a/tests/test_recipe_primal_dual_evolution.py +++ b/tests/test_recipe_primal_dual_evolution.py @@ -11,18 +11,18 @@ def test_primal_dual_evolution(): model = attach_primal_dual_evolution_eventhdlr(model) assert "test" in model.data - assert "primal_solutions" in model.data + assert "primal_log" in model.data model.optimize() - for i in range(1, len(model.data["primal_solutions"])): + for i in range(1, len(model.data["primal_log"])): if model.getObjectiveSense() == "minimize": - assert model.data["primal_solutions"][i][1] <= model.data["primal_solutions"][i-1][1] + assert model.data["primal_log"][i][1] <= model.data["primal_log"][i-1][1] else: - assert model.data["primal_solutions"][i][1] >= model.data["primal_solutions"][i-1][1] + assert model.data["primal_log"][i][1] >= model.data["primal_log"][i-1][1] - for i in range(1, len(model.data["dual_solutions"])): + for i in range(1, len(model.data["dual_log"])): if model.getObjectiveSense() == "minimize": - assert model.data["dual_solutions"][i][1] >= model.data["dual_solutions"][i-1][1] + assert model.data["dual_log"][i][1] >= model.data["dual_log"][i-1][1] else: - assert model.data["dual_solutions"][i][1] <= model.data["dual_solutions"][i-1][1] + assert model.data["dual_log"][i][1] <= model.data["dual_log"][i-1][1] From 53292cce771f63c704814d65dc7a3d01d49e1360 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 11:28:07 +0100 Subject: [PATCH 100/135] Remove macosx development flag --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c85a71838..7e0068935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,6 @@ if [[ $CIBW_ARCHS == *"arm"* ]]; then wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos-arm.zip -O scip.zip else wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos.zip -O scip.zip - export MACOSX_DEPLOYMENT_TARGET=13.0 fi unzip scip.zip mv scip_install src/scip From 8dbd6e56281e1b8eab1642bee51b5843b891badb Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 11:37:21 +0100 Subject: [PATCH 101/135] Readd macosx development flag --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7e0068935..c85a71838 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ if [[ $CIBW_ARCHS == *"arm"* ]]; then wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos-arm.zip -O scip.zip else wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos.zip -O scip.zip + export MACOSX_DEPLOYMENT_TARGET=13.0 fi unzip scip.zip mv scip_install src/scip From 264d2796f11a8af62bce6f016c48122ecb7c5be9 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 11:56:45 +0100 Subject: [PATCH 102/135] Add macosx deployment flag to repair wheel command --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c85a71838..202cf24b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ mv scip_install src/scip ''' environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} repair-wheel-command = [ + "export MACOSX_DEPLOYMENT_TARGET=13.0", "delocate-listdeps {wheel}", "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}", ] From 5497b7c29e92daea88cab692c1f6518327f37806 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 12:27:03 +0100 Subject: [PATCH 103/135] Expand repair-wheel-command --- pyproject.toml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 202cf24b8..861e39c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ before-all = ''' brew install wget zlib gcc if [[ $CIBW_ARCHS == *"arm"* ]]; then wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos-arm.zip -O scip.zip + export MACOSX_DEPLOYMENT_TARGET=14.0 else wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.5.0/libscip-macos.zip -O scip.zip export MACOSX_DEPLOYMENT_TARGET=13.0 @@ -66,11 +67,17 @@ unzip scip.zip mv scip_install src/scip ''' environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} -repair-wheel-command = [ - "export MACOSX_DEPLOYMENT_TARGET=13.0", - "delocate-listdeps {wheel}", - "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}", -] +repair-wheel-command = ''' +#!/bin/bash +if [[ $CIBW_ARCHS == *"arm"* ]]; then + export MACOSX_DEPLOYMENT_TARGET=14.0 + delocate-listdeps {wheel} + delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} +else + export MACOSX_DEPLOYMENT_TARGET=13.0 + delocate-listdeps {wheel} + delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} +''' [tool.cibuildwheel.windows] From 631118cf9bfe446d28757b38d451b8b3306e6f52 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 12:41:51 +0100 Subject: [PATCH 104/135] Comment out statistics test for faling windows --- tests/test_reader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_reader.py b/tests/test_reader.py index ef2534ded..eea32a493 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,3 +1,5 @@ +import pdb + import pytest import os @@ -80,6 +82,7 @@ def test_sudoku_reader(): deleteFile("model.sod") +@pytest.mark.skip(reason="Test fails on Windows when using cibuildwheel. Cannot find tests/data") def test_readStatistics(): m = Model(problemName="readStats") x = m.addVar(vtype="I") From 765ef8842fe93e023a52173e93df4ac6dfe80d4c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 12:51:46 +0100 Subject: [PATCH 105/135] Add different attempt at conditional logic --- pyproject.toml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 861e39c0f..498154a65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,15 +68,17 @@ mv scip_install src/scip ''' environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} repair-wheel-command = ''' -#!/bin/bash -if [[ $CIBW_ARCHS == *"arm"* ]]; then - export MACOSX_DEPLOYMENT_TARGET=14.0 - delocate-listdeps {wheel} - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} -else - export MACOSX_DEPLOYMENT_TARGET=13.0 - delocate-listdeps {wheel} - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} + bash -c ' + if [[ $CIBW_ARCHS == *"arm"* ]]; then + export MACOSX_DEPLOYMENT_TARGET=14.0 + delocate-listdeps {wheel} + delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} + else + export MACOSX_DEPLOYMENT_TARGET=13.0 + delocate-listdeps {wheel} + delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} + fi + ' ''' From e2dfcae757b15a16f7188e32880e9c6ea7aaa702 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 13:00:29 +0100 Subject: [PATCH 106/135] Add second explicit manylinux option --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 498154a65..b635ce652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ version = {attr = "pyscipopt._version.__version__"} [tool.cibuildwheel] skip="pp*" # currently doesn't work with PyPy +manylinux-x86_64-image = "manylinux_2_28" [tool.cibuildwheel.linux] From 6abf3ad769b538b661a0e1bc129f9c8c65ae6164 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 14:00:40 +0100 Subject: [PATCH 107/135] Update all actions to v4" --- .github/workflows/build_wheels.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index ab5d1d61c..a73127749 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -47,6 +47,7 @@ jobs: CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: "pytest {project}/tests" CIBW_MANYLINUX_*_IMAGE: manylinux_2_28 + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.architecture == 'x86_64' && '13.0' || '14.0' }} - uses: actions/upload-artifact@v3 with: @@ -56,13 +57,13 @@ jobs: name: Build source distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build sdist shell: bash -l {0} run: pipx run build --sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz From b9aeb5a54ec2e0a6263cd7c891067144b5d0d7b2 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 14:17:54 +0100 Subject: [PATCH 108/135] Update version.py --- src/pyscipopt/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/_version.py b/src/pyscipopt/_version.py index 40f5033b9..f279ec922 100644 --- a/src/pyscipopt/_version.py +++ b/src/pyscipopt/_version.py @@ -1 +1 @@ -__version__ = '5.1.1' +__version__ = '5.2.0' From 1ab162e74790e8e2b2831861d33cfdf1e603791a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 14:21:48 +0100 Subject: [PATCH 109/135] Change date. Remove redundant import --- CHANGELOG.md | 2 +- tests/test_reader.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 463941c07..51cbce72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### Changed ### Removed -## 5.2.0 - 2024.09.26 +## 5.2.0 - 2024.10.29 ### Added - Expanded Statistics class to more problems. - Created Statistics class diff --git a/tests/test_reader.py b/tests/test_reader.py index eea32a493..93d10c84b 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,5 +1,3 @@ -import pdb - import pytest import os From 9b46965ebc916c739b5eee8b98cb477238ca5e1a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 14:25:44 +0100 Subject: [PATCH 110/135] Remove incorrect macosx devlopment flag --- .github/workflows/build_wheels.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index a73127749..4ef051a2b 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -47,7 +47,6 @@ jobs: CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: "pytest {project}/tests" CIBW_MANYLINUX_*_IMAGE: manylinux_2_28 - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.architecture == 'x86_64' && '13.0' || '14.0' }} - uses: actions/upload-artifact@v3 with: From e56d0b699d1fb754b5972730807bbaa8b40ed2f9 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Oct 2024 16:23:11 +0100 Subject: [PATCH 111/135] Update missing v3 --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 4ef051a2b..208d2173c 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -48,7 +48,7 @@ jobs: CIBW_TEST_COMMAND: "pytest {project}/tests" CIBW_MANYLINUX_*_IMAGE: manylinux_2_28 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl From 573de28c02f5a13c24d1006b3aaf422ecb83b97a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 30 Oct 2024 09:02:39 +0100 Subject: [PATCH 112/135] Change upload logic to merge artifacts --- .github/workflows/build_wheels.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 208d2173c..85f39af19 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -50,6 +50,7 @@ jobs: - uses: actions/upload-artifact@v4 with: + name: wheels-${{ matrix.os}}-${{ matrix.arch }} path: ./wheelhouse/*.whl build_sdist: @@ -64,16 +65,24 @@ jobs: - uses: actions/upload-artifact@v4 with: + name: source-distribution path: dist/*.tar.gz - upload_pypi: + merge: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + + upload_pypi: + needs: [build_wheels, build_sdist, merge] + runs-on: ubuntu-latest if: github.event.inputs.upload_to_pypi == 'true' steps: - uses: actions/download-artifact@v4 with: - name: artifact + name: merged-artifacts path: dist - uses: pypa/gh-action-pypi-publish@release/v1 From 5c9843c6b72796e4443c7ddca46f3ae89db245e2 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 30 Oct 2024 12:22:17 +0100 Subject: [PATCH 113/135] Update to v5.2.1 --- .github/workflows/build_wheels.yml | 5 +++-- CHANGELOG.md | 2 +- setup.py | 2 +- src/pyscipopt/_version.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 85f39af19..067d01993 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -68,7 +68,8 @@ jobs: name: source-distribution path: dist/*.tar.gz - merge: + merge_artifacts: + name: Merge Artifacts needs: [build_wheels, build_sdist] runs-on: ubuntu-latest steps: @@ -76,7 +77,7 @@ jobs: uses: actions/upload-artifact/merge@v4 upload_pypi: - needs: [build_wheels, build_sdist, merge] + needs: [build_wheels, build_sdist, merge_artifacts] runs-on: ubuntu-latest if: github.event.inputs.upload_to_pypi == 'true' steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cbce72b..2b2930d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### Changed ### Removed -## 5.2.0 - 2024.10.29 +## 5.2.1 - 2024.10.29 ### Added - Expanded Statistics class to more problems. - Created Statistics class diff --git a/setup.py b/setup.py index ebad98b10..0e83c38d9 100644 --- a/setup.py +++ b/setup.py @@ -109,7 +109,7 @@ setup( name="PySCIPOpt", - version="5.2.0", + version="5.2.1", description="Python interface and modeling environment for SCIP", long_description=long_description, long_description_content_type="text/markdown", diff --git a/src/pyscipopt/_version.py b/src/pyscipopt/_version.py index f279ec922..4dc2ef264 100644 --- a/src/pyscipopt/_version.py +++ b/src/pyscipopt/_version.py @@ -1 +1 @@ -__version__ = '5.2.0' +__version__ = '5.2.1' From 0fe86893d381ffe1f53ddc2b18f98e8e584dea98 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 31 Oct 2024 11:11:04 +0100 Subject: [PATCH 114/135] Add flag warning mac users of new osx requirements --- docs/install.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/install.rst b/docs/install.rst index d9cc7613b..56fd18ff9 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -28,6 +28,9 @@ To install PySCIPOpt simply run the command: TLDR: Older linux distributions may not work for newer versions of PySCIPOpt installed via pip. +.. note:: For Mac users: PySCIPOpt versions newer than 5.1.1 installed via PyPI now only support + MACOSX 13+ for users running x86_64 architecture, and MACOSX 14+ for users running newer Apple silicon. + .. note:: For versions older than 4.4.0 installed via PyPI SCIP is not automatically installed. This means that SCIP must be installed yourself. If it is not installed globally, then the ``SCIPOPTDIR`` environment flag must be set, see :doc:`this page ` for more details. From 3886f4af48ee386058c95839bf1f265b880cb50f Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 7 Nov 2024 15:07:22 +0100 Subject: [PATCH 115/135] Clean up code and example --- .../finished/plot_primal_dual_evolution.py | 158 ++++-------------- .../recipes/primal_dual_evolution.py | 26 +-- 2 files changed, 42 insertions(+), 142 deletions(-) diff --git a/examples/finished/plot_primal_dual_evolution.py b/examples/finished/plot_primal_dual_evolution.py index 6db1755f2..8d4c966bb 100644 --- a/examples/finished/plot_primal_dual_evolution.py +++ b/examples/finished/plot_primal_dual_evolution.py @@ -1,140 +1,54 @@ +""" +This example show how to retrieve the primal and dual solutions during the optimization process +and plot them as a function of time. The model is about gas transportation and can be found in +PySCIPOpt/tests/helpers/utils.py + +It makes use of the attach_primal_dual_evolution_eventhdlr recipe. + +Requires matplotlib, and may require PyQt6 to show the plot. +""" + from pyscipopt import Model def plot_primal_dual_evolution(model: Model): try: from matplotlib import pyplot as plt except ImportError: - raise("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.") - - time_primal, val_primal = zip(*model.data["primal_log"]) + raise ImportError("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.\ + You may also need to install PyQt6 to show the plot.") + + assert model.data["primal_log"], "Could not find any feasible solutions" + time_primal, val_primal = map(list,zip(*model.data["primal_log"])) + time_dual, val_dual = map(list,zip(*model.data["dual_log"])) + + + if time_primal[-1] < time_dual[-1]: + time_primal.append(time_dual[-1]) + val_primal.append(val_primal[-1]) + + if time_primal[-1] > time_dual[-1]: + time_dual.append(time_primal[-1]) + val_dual.append(val_dual[-1]) + plt.plot(time_primal, val_primal, label="Primal bound") - time_dual, val_dual = zip(*model.data["dual_log"]) plt.plot(time_dual, val_dual, label="Dual bound") plt.legend(loc="best") plt.show() - -def gastrans_model(): - GASTEMP = 281.15 - RUGOSITY = 0.05 - DENSITY = 0.616 - COMPRESSIBILITY = 0.8 - nodes = [ - # name supplylo supplyup pressurelo pressureup cost - ("Anderlues", 0.0, 1.2, 0.0, 66.2, 0.0), # 0 - ("Antwerpen", None, -4.034, 30.0, 80.0, 0.0), # 1 - ("Arlon", None, -0.222, 0.0, 66.2, 0.0), # 2 - ("Berneau", 0.0, 0.0, 0.0, 66.2, 0.0), # 3 - ("Blaregnies", None, -15.616, 50.0, 66.2, 0.0), # 4 - ("Brugge", None, -3.918, 30.0, 80.0, 0.0), # 5 - ("Dudzele", 0.0, 8.4, 0.0, 77.0, 2.28), # 6 - ("Gent", None, -5.256, 30.0, 80.0, 0.0), # 7 - ("Liege", None, -6.385, 30.0, 66.2, 0.0), # 8 - ("Loenhout", 0.0, 4.8, 0.0, 77.0, 2.28), # 9 - ("Mons", None, -6.848, 0.0, 66.2, 0.0), # 10 - ("Namur", None, -2.120, 0.0, 66.2, 0.0), # 11 - ("Petange", None, -1.919, 25.0, 66.2, 0.0), # 12 - ("Peronnes", 0.0, 0.96, 0.0, 66.2, 1.68), # 13 - ("Sinsin", 0.0, 0.0, 0.0, 63.0, 0.0), # 14 - ("Voeren", 20.344, 22.012, 50.0, 66.2, 1.68), # 15 - ("Wanze", 0.0, 0.0, 0.0, 66.2, 0.0), # 16 - ("Warnand", 0.0, 0.0, 0.0, 66.2, 0.0), # 17 - ("Zeebrugge", 8.87, 11.594, 0.0, 77.0, 2.28), # 18 - ("Zomergem", 0.0, 0.0, 0.0, 80.0, 0.0) # 19 - ] - arcs = [ - # node1 node2 diameter length active */ - (18, 6, 890.0, 4.0, False), - (18, 6, 890.0, 4.0, False), - (6, 5, 890.0, 6.0, False), - (6, 5, 890.0, 6.0, False), - (5, 19, 890.0, 26.0, False), - (9, 1, 590.1, 43.0, False), - (1, 7, 590.1, 29.0, False), - (7, 19, 590.1, 19.0, False), - (19, 13, 890.0, 55.0, False), - (15, 3, 890.0, 5.0, True), - (15, 3, 395.0, 5.0, True), - (3, 8, 890.0, 20.0, False), - (3, 8, 395.0, 20.0, False), - (8, 17, 890.0, 25.0, False), - (8, 17, 395.0, 25.0, False), - (17, 11, 890.0, 42.0, False), - (11, 0, 890.0, 40.0, False), - (0, 13, 890.0, 5.0, False), - (13, 10, 890.0, 10.0, False), - (10, 4, 890.0, 25.0, False), - (17, 16, 395.5, 10.5, False), - (16, 14, 315.5, 26.0, True), - (14, 2, 315.5, 98.0, False), - (2, 12, 315.5, 6.0, False) - ] - - model = Model() - - # create flow variables - flow = {} - for arc in arcs: - flow[arc] = model.addVar("flow_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), # names of nodes in arc - lb=0.0 if arc[4] else None) # no lower bound if not active - - # pressure difference variables - pressurediff = {} - for arc in arcs: - pressurediff[arc] = model.addVar("pressurediff_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0]), - # names of nodes in arc - lb=None) - - # supply variables - supply = {} - for node in nodes: - supply[node] = model.addVar("supply_%s" % (node[0]), lb=node[1], ub=node[2], obj=node[5]) - - # square pressure variables - pressure = {} - for node in nodes: - pressure[node] = model.addVar("pressure_%s" % (node[0]), lb=node[3] ** 2, ub=node[4] ** 2) - - # node balance constrains, for each node i: outflows - inflows = supply - for nid, node in enumerate(nodes): - # find arcs that go or end at this node - flowbalance = 0 - for arc in arcs: - if arc[0] == nid: # arc is outgoing - flowbalance += flow[arc] - elif arc[1] == nid: # arc is incoming - flowbalance -= flow[arc] - else: - continue - - model.addCons(flowbalance == supply[node], name="flowbalance%s" % node[0]) - - # pressure difference constraints: pressurediff[node1 to node2] = pressure[node1] - pressure[node2] - for arc in arcs: - model.addCons(pressurediff[arc] == pressure[nodes[arc[0]]] - pressure[nodes[arc[1]]], - "pressurediffcons_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) - - # pressure loss constraints: - from math import log10 - for arc in arcs: - coef = 96.074830e-15 * arc[2] ** 5 * (2.0 * log10(3.7 * arc[2] / RUGOSITY)) ** 2 / COMPRESSIBILITY / GASTEMP / \ - arc[3] / DENSITY - if arc[4]: # active - model.addCons(flow[arc] ** 2 + coef * pressurediff[arc] <= 0.0, - "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) - else: - model.addCons(flow[arc] * abs(flow[arc]) - coef * pressurediff[arc] == 0.0, - "pressureloss_%s_%s" % (nodes[arc[0]][0], nodes[arc[1]][0])) - - return model - - if __name__=="__main__": from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr - + import os + import sys + + # just a way to import files from different folders, not important + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tests/helpers'))) + + from utils import gastrans_model + model = gastrans_model() - model.attach_primal_dual_evolution_eventhdlr() + model.data = {} + attach_primal_dual_evolution_eventhdlr(model) model.optimize() plot_primal_dual_evolution(model) diff --git a/src/pyscipopt/recipes/primal_dual_evolution.py b/src/pyscipopt/recipes/primal_dual_evolution.py index 51878bd89..3e86a03d9 100644 --- a/src/pyscipopt/recipes/primal_dual_evolution.py +++ b/src/pyscipopt/recipes/primal_dual_evolution.py @@ -22,34 +22,20 @@ def eventinit(self): # we want to collect best primal solutions and best dual so def eventexec(self, event): # if a new best primal solution was found, we save when it was found and also its objective if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND: - self.model.data["primal_log"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) + self.model.data["primal_log"].append([self.model.getSolvingTime(), self.model.getPrimalbound()]) if not self.model.data["dual_log"]: - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) if self.model.getObjectiveSense() == "minimize": if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) else: if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) + self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) + - def eventexitsol(self): - if self.model.data["primal_log"][-1] and self.model.getPrimalbound() != self.model.data["primal_log"][-1][1]: - self.model.data["primal_log"].append((self.model.getSolvingTime(), self.model.getPrimalbound())) - - if not self.model.data["dual_log"]: - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) - - if self.model.getObjectiveSense() == "minimize": - if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) - else: - if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): - self.model.data["dual_log"].append((self.model.getSolvingTime(), self.model.getDualbound())) - - - if not hasattr(model, "data"): + if not hasattr(model, "data") or model.data==None: model.data = {} model.data["primal_log"] = [] From e011945b7f21d8b729787114bd0059438202bc74 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 7 Nov 2024 15:07:31 +0100 Subject: [PATCH 116/135] Add inits for ease of import later --- examples/finished/__init__.py | 0 tests/__init__.py | 0 tests/helpers/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/finished/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/helpers/__init__.py diff --git a/examples/finished/__init__.py b/examples/finished/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb From 9db8870dbcb30c4e46824d68ce2ea509084b8fd9 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 12 Nov 2024 17:17:53 +0100 Subject: [PATCH 117/135] Restore optimized test --- tests/test_heur.py | 4 ++-- tests/test_memory.py | 6 +++++- tests/test_model.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_heur.py b/tests/test_heur.py index 9e9acd87f..454f11c74 100644 --- a/tests/test_heur.py +++ b/tests/test_heur.py @@ -4,7 +4,7 @@ import pytest from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_PARAMSETTING, SCIP_HEURTIMING, SCIP_LPSOLSTAT -from pyscipopt.scip import is_memory_freed +from test_memory import is_optimized_mode from helpers.utils import random_mip_1 @@ -106,7 +106,7 @@ def test_heur(): assert round(sol[y]) == 0.0 def test_heur_memory(): - if is_memory_freed(): + if is_optimized_mode(): pytest.skip() def inner(): diff --git a/tests/test_memory.py b/tests/test_memory.py index a0c6d9363..f67a77f6a 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -15,4 +15,8 @@ def test_freed(): assert is_memory_freed() def test_print_memory_in_use(): - print_memory_in_use() \ No newline at end of file + print_memory_in_use() + +def is_optimized_mode(): + model = Model() + return is_memory_freed() \ No newline at end of file diff --git a/tests/test_model.py b/tests/test_model.py index f5dcd062a..800ae7ab8 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -3,6 +3,7 @@ import itertools from pyscipopt import Model, SCIP_STAGE, SCIP_PARAMSETTING, quicksum +from pyscipopt.scip import is_memory_freed def test_model(): # create solver instance From 4e6b9d3c81afbadf086ef81d151292f3f221e827 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Fri, 15 Nov 2024 12:17:13 +0100 Subject: [PATCH 118/135] Update RELEASE.md --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 9f600ec82..baa2fe9de 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -16,4 +16,4 @@ git tag vX.X.X git push origin vX.X.X ``` - [ ] Then make a github [release](https://github.com/scipopt/PySCIPOpt/releases/new) from this new tag. -- [ ] Update documentation by running the `Generate Docs` workflow in Actions->Generate Docs. \ No newline at end of file +- [ ] Update the documentation: from readthedocs.io -> Builds -> Build version (latest and stable) From 15f7e1bcfd8802060b08442c5aaa51a58b2eb4c0 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 15 Nov 2024 15:28:52 +0100 Subject: [PATCH 119/135] finally working --- tests/__init__.py | 0 tests/data/readStatistics.stats | 172 ++++++++++++++++++++++++++++++++ tests/helpers/__init__.py | 0 tests/test_memory.py | 4 +- tests/test_model.py | 1 - 5 files changed, 174 insertions(+), 3 deletions(-) delete mode 100644 tests/__init__.py create mode 100644 tests/data/readStatistics.stats delete mode 100644 tests/helpers/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/data/readStatistics.stats b/tests/data/readStatistics.stats new file mode 100644 index 000000000..c98992f6f --- /dev/null +++ b/tests/data/readStatistics.stats @@ -0,0 +1,172 @@ +SCIP Status : solving was interrupted [solution limit reached] +Total Time : 0.00 + solving : 0.00 + presolving : 0.00 (included in solving) + reading : 0.00 + copying : 0.00 (0 times copied the problem) +Original Problem : + Problem name : model + Variables : 1 (0 binary, 0 integer, 0 implicit integer, 1 continuous) + Constraints : 0 initial, 0 maximal + Objective : minimize, 0 non-zeros (abs.min = 1e+20, abs.max = -1e+20) +Presolved Problem : + Problem name : t_model + Variables : 1 (0 binary, 0 integer, 0 implicit integer, 1 continuous) + Constraints : 0 initial, 0 maximal + Objective : minimize, 0 non-zeros (abs.min = 1e+20, abs.max = -1e+20) + Nonzeros : 0 constraint, 0 clique table +Presolvers : ExecTime SetupTime Calls FixedVars AggrVars ChgTypes ChgBounds AddHoles DelCons AddCons ChgSides ChgCoefs + boundshift : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + convertinttobin : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + domcol : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + dualagg : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + dualcomp : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + dualinfer : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + dualsparsify : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + gateextraction : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + implics : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + inttobinary : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + qpkktref : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + redvub : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + sparsify : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + stuffing : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + trivial : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + tworowbnd : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + dualfix : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + genvbounds : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + probing : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + pseudoobj : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + symmetry : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + vbounds : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + benders : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + components : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + root node : - - - 0 - - 0 - - - - - +Constraints : Number MaxNumber #Separate #Propagate #EnfoLP #EnfoRelax #EnfoPS #Check #ResProp Cutoffs DomReds Cuts Applied Conss Children + benderslp : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + integral : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + benders : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + fixedvar : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + countsols : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + components : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Constraint Timings : TotalTime SetupTime Separate Propagate EnfoLP EnfoPS EnfoRelax Check ResProp SB-Prop + benderslp : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + integral : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + benders : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + fixedvar : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + countsols : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + components : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +Propagators : #Propagate #ResProp Cutoffs DomReds + dualfix : 0 0 0 0 + genvbounds : 0 0 0 0 + nlobbt : 0 0 0 0 + obbt : 0 0 0 0 + probing : 0 0 0 0 + pseudoobj : 0 0 0 0 + redcost : 0 0 0 0 + rootredcost : 0 0 0 0 + symmetry : 0 0 0 0 + vbounds : 0 0 0 0 +Propagator Timings : TotalTime SetupTime Presolve Propagate ResProp SB-Prop + dualfix : 0.00 0.00 0.00 0.00 0.00 0.00 + genvbounds : 0.00 0.00 0.00 0.00 0.00 0.00 + nlobbt : 0.00 0.00 0.00 0.00 0.00 0.00 + obbt : 0.00 0.00 0.00 0.00 0.00 0.00 + probing : 0.00 0.00 0.00 0.00 0.00 0.00 + pseudoobj : 0.00 0.00 0.00 0.00 0.00 0.00 + redcost : 0.00 0.00 0.00 0.00 0.00 0.00 + rootredcost : 0.00 0.00 0.00 0.00 0.00 0.00 + symmetry : 0.00 0.00 0.00 0.00 0.00 0.00 + vbounds : 0.00 0.00 0.00 0.00 0.00 0.00 +Conflict Analysis : Time Calls Success DomReds Conflicts Literals Reconvs ReconvLits Dualrays Nonzeros LP Iters (pool size: [--,--]) + propagation : 0.00 0 0 - 0 0.0 0 0.0 - - - + infeasible LP : 0.00 0 0 - 0 0.0 0 0.0 0 0.0 0 + bound exceed. LP : 0.00 0 0 - 0 0.0 0 0.0 0 0.0 0 + strong branching : 0.00 0 0 - 0 0.0 0 0.0 - - 0 + pseudo solution : 0.00 0 0 - 0 0.0 0 0.0 - - - + applied globally : 0.00 - - 0 0 0.0 - - 0 - - + applied locally : - - - 0 0 0.0 - - 0 - - +Primal Heuristics : ExecTime SetupTime Calls Found Best + LP solutions : 0.00 - - 0 0 + relax solutions : 0.00 - - 0 0 + pseudo solutions : 0.00 - - 0 0 + strong branching : 0.00 - - 0 0 + actconsdiving : 0.00 0.00 0 0 0 + adaptivediving : 0.00 0.00 0 0 0 + alns : 0.00 0.00 0 0 0 + bound : 0.00 0.00 0 0 0 + clique : 0.00 0.00 0 0 0 + coefdiving : 0.00 0.00 0 0 0 + completesol : 0.00 0.00 0 0 0 + conflictdiving : 0.00 0.00 0 0 0 + crossover : 0.00 0.00 0 0 0 + dins : 0.00 0.00 0 0 0 + distributiondivin: 0.00 0.00 0 0 0 + dps : 0.00 0.00 0 0 0 + dualval : 0.00 0.00 0 0 0 + farkasdiving : 0.00 0.00 0 0 0 + feaspump : 0.00 0.00 0 0 0 + fixandinfer : 0.00 0.00 0 0 0 + fracdiving : 0.00 0.00 0 0 0 + gins : 0.00 0.00 0 0 0 + guideddiving : 0.00 0.00 0 0 0 + indicator : 0.00 0.00 0 0 0 + indicatordiving : 0.00 0.00 0 0 0 + intdiving : 0.00 0.00 0 0 0 + intshifting : 0.00 0.00 0 0 0 + linesearchdiving : 0.00 0.00 0 0 0 + localbranching : 0.00 0.00 0 0 0 + locks : 0.00 0.00 0 0 0 + lpface : 0.00 0.00 0 0 0 + mpec : 0.00 0.00 0 0 0 + multistart : 0.00 0.00 0 0 0 + mutation : 0.00 0.00 0 0 0 + nlpdiving : 0.00 0.00 0 0 0 + objpscostdiving : 0.00 0.00 0 0 0 + octane : 0.00 0.00 0 0 0 + ofins : 0.00 0.00 0 0 0 + oneopt : 0.00 0.00 0 0 0 + padm : 0.00 0.00 0 0 0 + proximity : 0.00 0.00 0 0 0 + pscostdiving : 0.00 0.00 0 0 0 + randrounding : 0.00 0.00 0 0 0 + rens : 0.00 0.00 0 0 0 + reoptsols : 0.00 0.00 0 0 0 + repair : 0.00 0.00 0 0 0 + rins : 0.00 0.00 0 0 0 + rootsoldiving : 0.00 0.00 0 0 0 + rounding : 0.00 0.00 0 0 0 + scheduler : 0.00 0.00 0 0 0 + shiftandpropagate: 0.00 0.00 0 0 0 + shifting : 0.00 0.00 0 0 0 + simplerounding : 0.00 0.00 0 0 0 + subnlp : 0.00 0.00 0 0 0 + trivial : 0.00 0.00 0 0 0 + trivialnegation : 0.00 0.00 0 0 0 + trustregion : 0.00 0.00 0 0 0 + trysol : 0.00 0.00 0 0 0 + twoopt : 0.00 0.00 0 0 0 + undercover : 0.00 0.00 0 0 0 + vbounds : 0.00 0.00 0 0 0 + veclendiving : 0.00 0.00 0 0 0 + zeroobj : 0.00 0.00 0 0 0 + zirounding : 0.00 0.00 0 0 0 + other solutions : - - - 0 - +LNS (Scheduler) : Calls SetupTime SolveTime SolveNodes Sols Best Exp3 Exp3-IX EpsGreedy UCB TgtFixRate Opt Inf Node Stal Sol Usr Othr Actv + rens : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + rins : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + mutation : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + localbranching : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + crossover : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + proximity : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + zeroobjective : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + dins : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 + trustregion : 0 0.00 0.00 0 0 0 0.00000 0.00000 -1.00000 1.00000 0.900 0 0 0 0 0 0 0 1 +Solution : + Solutions found : 0 (0 improvements) + Primal Bound : - + Dual Bound : - + Gap : infinite +Integrals : Total Avg% + primal-dual : 0.02 100.00 + primal-ref : - - (not evaluated) + dual-ref : - - (not evaluated) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_memory.py b/tests/test_memory.py index f67a77f6a..f73070d25 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -2,13 +2,13 @@ from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use def test_not_freed(): - if is_memory_freed(): + if is_optimized_mode(): pytest.skip() m = Model() assert not is_memory_freed() def test_freed(): - if is_memory_freed(): + if is_optimized_mode(): pytest.skip() m = Model() del m diff --git a/tests/test_model.py b/tests/test_model.py index 800ae7ab8..f5dcd062a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -3,7 +3,6 @@ import itertools from pyscipopt import Model, SCIP_STAGE, SCIP_PARAMSETTING, quicksum -from pyscipopt.scip import is_memory_freed def test_model(): # create solver instance From a006ea6b655dc7eab2c72d838d908f1e902e3cca Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 15 Nov 2024 16:22:46 +0100 Subject: [PATCH 120/135] remove bad contributions --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxi | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be1a273b4..4c444dd67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed ### Changed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e8fd5fd5f..e24e65efa 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2057,6 +2057,15 @@ cdef class Model: def freeTransform(self): """Frees all solution process data including presolving and transformed problem, only original problem is kept.""" + if self.getStage() not in [SCIP_STAGE_INIT, + SCIP_STAGE_PROBLEM, + SCIP_STAGE_TRANSFORMED, + SCIP_STAGE_PRESOLVING, + SCIP_STAGE_PRESOLVED, + SCIP_STAGE_SOLVING, + SCIP_STAGE_SOLVED]: + raise Warning("method cannot be called in stage %i." % self.getStage()) + self._modelvars = { var: value for var, value in self._modelvars.items() @@ -6175,6 +6184,11 @@ cdef class Model: def presolve(self): """Presolve the problem.""" + if self.getStage() not in [SCIP_STAGE_PROBLEM, SCIP_STAGE_TRANSFORMED,\ + SCIP_STAGE_PRESOLVING, SCIP_STAGE_PRESOLVED, \ + SCIP_STAGE_SOLVED]: + raise Warning("method cannot be called in stage %i." % self.getStage()) + PY_SCIP_CALL(SCIPpresolve(self._scip)) self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) @@ -8977,6 +8991,15 @@ cdef class Model: def freeReoptSolve(self): """Frees all solution process data and prepares for reoptimization.""" + + if self.getStage() not in [SCIP_STAGE_INIT, + SCIP_STAGE_PROBLEM, + SCIP_STAGE_TRANSFORMED, + SCIP_STAGE_PRESOLVING, + SCIP_STAGE_PRESOLVED, + SCIP_STAGE_SOLVING, + SCIP_STAGE_SOLVED]: + raise Warning("method cannot be called in stage %i." % self.getStage()) PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip)) def chgReoptObjective(self, coeffs, sense = 'minimize'): From 6876af9db7fc3eb3d224c030ba8b53975fee2b20 Mon Sep 17 00:00:00 2001 From: Light Lee Date: Mon, 18 Nov 2024 16:09:38 +0800 Subject: [PATCH 121/135] solve problem with multi-threads (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 1. model optimize without gil 2. solveFirstInterruptOthers 3. add copy model method 4. add copy model solution 5. add some Solution methods * 1. model optimize without gil 2. model addOrigVarsConssObjectiveFrom 3. add some Solution methods * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * remove addOrigVarsConssObjectiveFrom * rename getSolOrigin to getOrigin --------- Co-authored-by: Light1_Lee Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 4 +++ src/pyscipopt/__init__.py | 1 + src/pyscipopt/scip.pxd | 23 ++++++++++++++++- src/pyscipopt/scip.pxi | 52 +++++++++++++++++++++++++++++++++++++++ tests/test_copy.py | 2 ++ tests/test_nogil.py | 23 +++++++++++++++++ tests/test_solution.py | 28 ++++++++++++++++++++- 7 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 tests/test_nogil.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c444dd67..203eeb454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ - Added additional tests to test_nodesel, test_heur, and test_strong_branching - Migrated documentation to Readthedocs - `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers +- Added Model method: optimizeNogil +- Added Solution method: getOrigin, retransform, translate +- Added SCIP.pxd: SCIP_SOLORIGIN, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPhashmapCreate, SCIPhashmapFree +- Added additional tests to test_multi_threads, test_solution, and test_copy ### Fixed - Fixed too strict getObjVal, getVal check ### Changed diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index cd6528f74..eece6c939 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -46,3 +46,4 @@ from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE +from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b0ca1ecf2..2b629053c 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -320,6 +320,17 @@ cdef extern from "scip/scip.h": SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_SEPA SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_REOPT + ctypedef int SCIP_SOLORIGIN + cdef extern from "scip/type_sol.h": + SCIP_SOLORIGIN SCIP_SOLORIGIN_ORIGINAL + SCIP_SOLORIGIN SCIP_SOLORIGIN_ZERO + SCIP_SOLORIGIN SCIP_SOLORIGIN_LPSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_NLPSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_RELAXSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_PSEUDOSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_PARTIAL + SCIP_SOLORIGIN SCIP_SOLORIGIN_UNKNOWN + ctypedef bint SCIP_Bool ctypedef long long SCIP_Longint @@ -532,6 +543,8 @@ cdef extern from "scip/scip.h": SCIP_Bool threadsafe, SCIP_Bool passmessagehdlr, SCIP_Bool* valid) + SCIP_RETCODE SCIPcopyOrigVars(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_VAR** fixedvars, SCIP_Real* fixedvals, int nfixedvars ) + SCIP_RETCODE SCIPcopyOrigConss(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_Bool enablepricing, SCIP_Bool* valid) SCIP_RETCODE SCIPmessagehdlrCreate(SCIP_MESSAGEHDLR **messagehdlr, SCIP_Bool bufferedoutput, const char *filename, @@ -669,6 +682,7 @@ cdef extern from "scip/scip.h": # Solve Methods SCIP_RETCODE SCIPsolve(SCIP* scip) + SCIP_RETCODE SCIPsolve(SCIP* scip) noexcept nogil SCIP_RETCODE SCIPsolveConcurrent(SCIP* scip) SCIP_RETCODE SCIPfreeTransform(SCIP* scip) SCIP_RETCODE SCIPpresolve(SCIP* scip) @@ -871,7 +885,9 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPreadSolFile(SCIP* scip, const char* filename, SCIP_SOL* sol, SCIP_Bool xml, SCIP_Bool* partial, SCIP_Bool* error) SCIP_RETCODE SCIPcheckSol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* feasible) SCIP_RETCODE SCIPcheckSolOrig(SCIP* scip, SCIP_SOL* sol, SCIP_Bool* feasible, SCIP_Bool printreason, SCIP_Bool completely) - + SCIP_RETCODE SCIPretransformSol(SCIP* scip, SCIP_SOL* sol) + SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol) + SCIP_SOLORIGIN SCIPsolGetOrigin(SCIP_SOL* sol) SCIP_Real SCIPgetSolTime(SCIP* scip, SCIP_SOL* sol) SCIP_RETCODE SCIPsetRelaxSolVal(SCIP* scip, SCIP_RELAX* relax, SCIP_VAR* var, SCIP_Real val) @@ -1367,6 +1383,11 @@ cdef extern from "scip/scip.h": BMS_BLKMEM* SCIPblkmem(SCIP* scip) + # pub_misc.h + SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize) + void SCIPhashmapFree(SCIP_HASHMAP** hashmap) + + cdef extern from "scip/tree.h": int SCIPnodeGetNAddedConss(SCIP_NODE* node) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e24e65efa..d2063f5fd 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -258,6 +258,16 @@ cdef class PY_SCIP_ROWORIGINTYPE: SEPA = SCIP_ROWORIGINTYPE_SEPA REOPT = SCIP_ROWORIGINTYPE_REOPT +cdef class PY_SCIP_SOLORIGIN: + ORIGINAL = SCIP_SOLORIGIN_ORIGINAL + ZERO = SCIP_SOLORIGIN_ZERO + LPSOL = SCIP_SOLORIGIN_LPSOL + NLPSOL = SCIP_SOLORIGIN_NLPSOL + RELAXSOL = SCIP_SOLORIGIN_RELAXSOL + PSEUDOSOL = SCIP_SOLORIGIN_PSEUDOSOL + PARTIAL = SCIP_SOLORIGIN_PARTIAL + UNKNOWN = SCIP_SOLORIGIN_UNKNOWN + def PY_SCIP_CALL(SCIP_RETCODE rc): if rc == SCIP_OKAY: pass @@ -1009,6 +1019,40 @@ cdef class Solution: if not stage_check or self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING: raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})") + def getOrigin(self): + """ + Returns origin of solution: where to retrieve uncached elements. + + Returns + ------- + PY_SCIP_SOLORIGIN + """ + return SCIPsolGetOrigin(self.sol) + + def retransform(self): + """ retransforms solution to original problem space """ + PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol)) + + def translate(self, Model target): + """ + translate solution to a target model solution + + Parameters + ---------- + target : Model + + Returns + ------- + targetSol: Solution + """ + if self.getOrigin() != SCIP_SOLORIGIN_ORIGINAL: + PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol)) + cdef Solution targetSol = Solution.create(target._scip, NULL) + cdef SCIP_VAR** source_vars = SCIPgetOrigVars(self.scip) + + PY_SCIP_CALL(SCIPtranslateSubSol(target._scip, self.scip, self.sol, NULL, source_vars, &(targetSol.sol))) + return targetSol + cdef class BoundChange: """Bound change.""" @@ -6170,6 +6214,14 @@ cdef class Model: """Optimize the problem.""" PY_SCIP_CALL(SCIPsolve(self._scip)) self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) + + def optimizeNogil(self): + """Optimize the problem without GIL.""" + cdef SCIP_RETCODE rc; + with nogil: + rc = SCIPsolve(self._scip) + PY_SCIP_CALL(rc) + self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) def solveConcurrent(self): """Transforms, presolves, and solves problem using additional solvers which emphasize on diff --git a/tests/test_copy.py b/tests/test_copy.py index b518d0a13..dd5aed8ae 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,4 +1,5 @@ from pyscipopt import Model +from helpers.utils import random_mip_1 def test_copy(): # create solver instance @@ -18,3 +19,4 @@ def test_copy(): s2.optimize() assert s.getObjVal() == s2.getObjVal() + diff --git a/tests/test_nogil.py b/tests/test_nogil.py new file mode 100644 index 000000000..90b6bdfb3 --- /dev/null +++ b/tests/test_nogil.py @@ -0,0 +1,23 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from pyscipopt import Model +from helpers.utils import random_mip_1 + +N_Threads = 4 + + +def test_optimalNogil(): + ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True) + models = [Model(sourceModel=ori_model) for _ in range(N_Threads)] + for i in range(N_Threads): + models[i].setParam("randomization/permutationseed", i) + + ori_model.optimize() + + with ThreadPoolExecutor(max_workers=N_Threads) as executor: + futures = [executor.submit(Model.optimizeNogil, model) for model in models] + for future in as_completed(futures): + pass + for model in models: + assert model.getStatus() == "optimal" + assert abs(ori_model.getObjVal() - model.getObjVal()) < 1e-6 + diff --git a/tests/test_solution.py b/tests/test_solution.py index 61846b167..4ab9fd5b6 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -1,6 +1,7 @@ import re import pytest -from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod +from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod, SCIP_SOLORIGIN +from helpers.utils import random_mip_1 def test_solution_getbest(): @@ -193,3 +194,28 @@ def test_getSols(): assert len(m.getSols()) >= 1 assert any(m.isEQ(sol[x], 0.0) for sol in m.getSols()) + + +def test_getOrigin_retrasform(): + m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + m.optimize() + + sol = m.getBestSol() + assert sol.getOrigin() == SCIP_SOLORIGIN.ZERO + + sol.retransform() + assert sol.getOrigin() == SCIP_SOLORIGIN.ORIGINAL + + +def test_translate(): + m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + m.optimize() + sol = m.getBestSol() + + m1 = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + sol1 = sol.translate(m1) + assert m1.addSol(sol1) == True + assert m1.getNSols() == 1 + m1.optimize() + assert m.getObjVal() == m1.getObjVal() + From dc37d78b8579401f0c9bacfb70704a9c83138ebd Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 27 Nov 2024 14:32:46 +0100 Subject: [PATCH 122/135] Allow writeProblem to write to stdout --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++++++++++++++ src/pyscipopt/scip.pxi | 35 +++++++++++++++---------- tests/test_model.py | 1 + 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e56372c1..2ce79d04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,68 @@ ## Unreleased ### Added +- Added stage checks to presolve, freereoptsolve, freetransform +- Added primal_dual_evolution recipe and a plot recipe +### Fixed +### Changed +- Allowed writeModel to print to standard output +### Removed + +## 5.2.1 - 2024.10.29 +### Added +- Expanded Statistics class to more problems. +- Created Statistics class +- Added parser to read .stats file +- Release checklist in `RELEASE.md` +- Added Python definitions and wrappers for SCIPstartStrongbranch, SCIPendStrongbranch SCIPgetBranchScoreMultiple, + SCIPgetVarStrongbranchInt, SCIPupdateVarPseudocost, SCIPgetVarStrongbranchFrac, SCIPcolGetAge, + SCIPgetVarStrongbranchLast, SCIPgetVarStrongbranchNode, SCIPallColsInLP, SCIPcolGetAge +- Added getBipartiteGraphRepresentation +- Added helper functions that facilitate testing +- Added Python definitions and wrappers for SCIPgetNImplVars, SCIPgetNContVars, SCIPvarMayRoundUp, + SCIPvarMayRoundDown, SCIPcreateLPSol, SCIPfeasFloor, SCIPfeasCeil, SCIPfeasRound, SCIPgetPrioChild, + SCIPgetPrioSibling +- Added additional tests to test_nodesel, test_heur, and test_strong_branching +- Migrated documentation to Readthedocs +- `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers +- Added Model method: optimizeNogil +- Added Solution method: getOrigin, retransform, translate +- Added SCIP.pxd: SCIP_SOLORIGIN, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPhashmapCreate, SCIPhashmapFree +- Added additional tests to test_multi_threads, test_solution, and test_copy +### Fixed +- Fixed too strict getObjVal, getVal check +### Changed +- Changed createSol to now have an option of initialising at the current LP solution +- Unified documentation style of scip.pxi to numpydocs +### Removed + +## 5.1.1 - 2024-06-22 +### Added +- Added SCIP_STATUS_DUALLIMIT and SCIP_STATUS_PRIMALLIMIT +- Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) +- Added recipe with reformulation for detecting infeasible constraints +- Wrapped SCIPcreateOrigSol and added tests +- Added verbose option for writeProblem and writeParams +- Expanded locale test +- Added methods for creating expression constraints without adding to problem +- Added methods for creating/adding/appending disjunction constraints +- Added check for pt_PT locale in test_model.py +- Added SCIPgetOrigConss and SCIPgetNOrigConss Cython bindings. +- Added transformed=False option to getConss, getNConss, and getNVars +### Fixed +- Fixed locale errors in reading +### Changed +- Made readStatistics a standalone function +### Removed + +## 5.0.1 - 2024-04-05 +### Added +- Added recipe for nonlinear objective functions - Added method for adding piecewise linear constraints - Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation - New test for model setLogFile ### Fixed +- Fixed locale fix - Fixed model.setLogFile(None) error - Add recipes sub-package - Fixed "weakly-referenced object no longer exists" when calling dropEvent in test_customizedbenders diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index b3e16e3b1..711f64fdc 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1453,29 +1453,38 @@ cdef class Model: if not onlyroot: self.setIntParam("propagating/maxrounds", 0) - def writeProblem(self, filename='model.cip', trans=False, genericnames=False): + def writeProblem(self, filename='model.cip', trans=False, genericnames=False, verbose=True): """Write current model/problem to a file. :param filename: the name of the file to be used (Default value = 'model.cip'). Should have an extension corresponding to one of the readable file formats, described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. :param trans: indicates whether the transformed problem is written to file (Default value = False) :param genericnames: indicates whether the problem should be written with generic variable and constraint names (Default value = False) - + :param verbose: whether to print a success message """ user_locale = locale.getlocale() locale.setlocale(locale.LC_ALL, "C") - str_absfile = abspath(filename) - absfile = str_conversion(str_absfile) - fn, ext = splitext(absfile) - if len(ext) == 0: - ext = str_conversion('.cip') - fn = fn + ext - ext = ext[1:] - if trans: - PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, fn, ext, genericnames)) + if filename: + str_absfile = abspath(filename) + absfile = str_conversion(str_absfile) + fn, ext = splitext(absfile) + if len(ext) == 0: + ext = str_conversion('.cip') + fn = fn + ext + ext = ext[1:] + + if trans: + PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, fn, ext, genericnames)) + else: + PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, fn, ext, genericnames)) + + if verbose: + print('wrote problem to file ' + str_absfile) else: - PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, fn, ext, genericnames)) - print('wrote problem to file ' + str_absfile) + if trans: + PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, NULL, str_conversion('.cip')[1:], genericnames)) + else: + PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, NULL, str_conversion('.cip')[1:], genericnames)) locale.setlocale(locale.LC_ALL, user_locale) diff --git a/tests/test_model.py b/tests/test_model.py index df5e32cc2..ff06a6068 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -66,6 +66,7 @@ def test_model(): s.writeProblem('model') s.writeProblem('model.lp') + s.writeProblem(filename=False) s.freeProb() s = Model() From ef2f7f8b4a3486b8b06df9f6d4c1acd24c9fa2db Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 27 Nov 2024 14:35:40 +0100 Subject: [PATCH 123/135] correct method name --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce79d04c..1c6cdf100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Added primal_dual_evolution recipe and a plot recipe ### Fixed ### Changed -- Allowed writeModel to print to standard output +- Allowed writeProblem to print to standard output ### Removed ## 5.2.1 - 2024.10.29 From b8a9cd6eeef753592fbb70e73439effab7646595 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 27 Nov 2024 14:43:23 +0100 Subject: [PATCH 124/135] corrected docstring --- src/pyscipopt/scip.pxi | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4f15cd480..2bbcb8105 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2890,12 +2890,23 @@ cdef class Model: self.setIntParam("propagating/maxrounds", 0) def writeProblem(self, filename='model.cip', trans=False, genericnames=False, verbose=True): - """Write current model/problem to a file. + """ + Write current model/problem to a file. + + Parameters + ---------- + filename : str, optional + the name of the file to be used (Default value = 'model.cip'). + Should have an extension corresponding to one of the readable file formats, + described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. + trans : bool, optional + indicates whether the transformed problem is written to file (Default value = False) + genericnames : bool, optional + indicates whether the problem should be written with generic variable + and constraint names (Default value = False) + verbose : bool, optional + indicates whether a success message should be printed - :param filename: the name of the file to be used (Default value = 'model.cip'). Should have an extension corresponding to one of the readable file formats, described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. - :param trans: indicates whether the transformed problem is written to file (Default value = False) - :param genericnames: indicates whether the problem should be written with generic variable and constraint names (Default value = False) - :param verbose: whether to print a success message """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") From 90514983d17f8ad60b34b837301280b822b6a795 Mon Sep 17 00:00:00 2001 From: DominikKamp <130753997+DominikKamp@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:19:44 +0100 Subject: [PATCH 125/135] Correct output redirection --- src/pyscipopt/scip.pxi | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d2063f5fd..2f44eebf5 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -12,7 +12,7 @@ cimport cython from cpython cimport Py_INCREF, Py_DECREF from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPointer from libc.stdlib cimport malloc, free -from libc.stdio cimport fdopen, fclose +from libc.stdio cimport stdout, stderr, fdopen, fputs, fflush, fclose from posix.stdio cimport fileno from collections.abc import Iterable @@ -1851,10 +1851,22 @@ cdef class Constraint: cdef void relayMessage(SCIP_MESSAGEHDLR *messagehdlr, FILE *file, const char *msg) noexcept: - sys.stdout.write(msg.decode('UTF-8')) + if file is stdout: + sys.stdout.write(msg.decode('UTF-8')) + elif file is stderr: + sys.stderr.write(msg.decode('UTF-8')) + else: + if msg is not NULL: + fputs(msg, file) + fflush(file) cdef void relayErrorMessage(void *messagehdlr, FILE *file, const char *msg) noexcept: - sys.stderr.write(msg.decode('UTF-8')) + if file is NULL: + sys.stderr.write(msg.decode('UTF-8')) + else: + if msg is not NULL: + fputs(msg, file) + fflush(file) # - remove create(), includeDefaultPlugins(), createProbBasic() methods # - replace free() by "destructor" From 41870c4101e589b0a843d54aa054df34ddab0f61 Mon Sep 17 00:00:00 2001 From: DominikKamp <130753997+DominikKamp@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:40:35 +0100 Subject: [PATCH 126/135] Add redirection test --- tests/test_model.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index f5dcd062a..a63b4f201 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -3,6 +3,7 @@ import itertools from pyscipopt import Model, SCIP_STAGE, SCIP_PARAMSETTING, quicksum +from helpers.utils import random_mip_1 def test_model(): # create solver instance @@ -476,4 +477,36 @@ def test_getObjVal(): assert m.getVal(x) == 0 assert m.getObjVal() == 16 - assert m.getVal(x) == 0 \ No newline at end of file + assert m.getVal(x) == 0 + +# tests writeProblem() after redirectOutput() +def test_redirection(): + + # create problem instances + original = random_mip_1(False, False, False, -1, True) + redirect = Model() + + # redirect console output + original.redirectOutput() + + # write problem instance + original.writeProblem("redirection.lp") + + # solve original instance + original.optimize() + + # read problem instance + redirect.readProblem("redirection.lp") + + # remove problem file + os.remove("redirection.lp") + + # compare problem dimensions + assert redirect.getNVars(False) == original.getNVars(False) + assert redirect.getNConss(False) == original.getNConss(False) + + # solve redirect instance + redirect.optimize() + + # compare objective values + assert original.isEQ(redirect.getObjVal(), original.getObjVal()) From ec5a3465319cc0bc405b395e57b94dba02cc929a Mon Sep 17 00:00:00 2001 From: DominikKamp <130753997+DominikKamp@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:48:28 +0100 Subject: [PATCH 127/135] Add bugfix to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 203eeb454..f9e8472ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed +- Only redirect stdout and stderr in redirectOutput() so that file output still works afterwards ### Changed ### Removed From c40df4b3a40921f6c79f43a6db386b62aa2debc7 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 30 Nov 2024 08:57:13 +0100 Subject: [PATCH 128/135] Replaced with displayProblem --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxi | 29 +++++++++++++++++++++++++++++ tests/test_model.py | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6cdf100..21c638d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ ## Unreleased ### Added +- Added displayProblem to print problem to stdout - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed ### Changed -- Allowed writeProblem to print to standard output ### Removed ## 5.2.1 - 2024.10.29 diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 2bbcb8105..e5bea801f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2889,6 +2889,35 @@ cdef class Model: if not onlyroot: self.setIntParam("propagating/maxrounds", 0) + def displayProblem(self, ext='.cip', trans=False, genericnames=False): + """ + Write current model/problem to a file. + + Parameters + ---------- + ext : str, optional + the extension to be used (Default value = '.cip'). + Should have an extension corresponding to one of the readable file formats, + described in https://www.scipopt.org/doc/html/group__FILEREADERS.php. + trans : bool, optional + indicates whether the transformed problem is written to file (Default value = False) + genericnames : bool, optional + indicates whether the problem should be written with generic variable + and constraint names (Default value = False) + verbose : bool, optional + indicates whether a success message should be printed + + """ + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + + if trans: + PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, NULL, str_conversion(ext)[1:], genericnames)) + else: + PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, NULL, str_conversion(ext)[1:], genericnames)) + + locale.setlocale(locale.LC_NUMERIC,user_locale) + def writeProblem(self, filename='model.cip', trans=False, genericnames=False, verbose=True): """ Write current model/problem to a file. diff --git a/tests/test_model.py b/tests/test_model.py index 41c017563..fa779f6a8 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -67,7 +67,7 @@ def test_model(): s.writeProblem('model') s.writeProblem('model.lp') - s.writeProblem(filename=False) + s.displayProblem() s.freeProb() s = Model() From c308eb2f0f7f4fbe5c79cec366819662b0f9c99f Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 30 Nov 2024 09:20:36 +0100 Subject: [PATCH 129/135] Better default name for indicator cons --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxi | 7 +++++-- tests/test_cons.py | 21 +++++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c638d26..1e0ac8c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed +- Fixed default name for indicator constraints ### Changed ### Removed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e5bea801f..3f8a0d63e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5330,7 +5330,7 @@ cdef class Model: return pyCons - def addConsIndicator(self, cons, binvar=None, activeone=True, name="IndicatorCons", + def addConsIndicator(self, cons, binvar=None, activeone=True, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): @@ -5348,7 +5348,7 @@ cdef class Model: activeone : bool, optional constraint should active if binvar is 1 (0 if activeone = False) name : str, optional - name of the constraint (Default value = "IndicatorCons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -5381,6 +5381,9 @@ cdef class Model: if cons._lhs is not None and cons._rhs is not None: raise ValueError("expected inequality that has either only a left or right hand side") + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + if cons.expr.degree() > 1: raise ValueError("expected linear inequality, expression has degree %d" % cons.expr.degree()) diff --git a/tests/test_cons.py b/tests/test_cons.py index 8b11250ae..cccd28adc 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -101,20 +101,33 @@ def test_SOScons(): def test_cons_indicator(): m = Model() - x = m.addVar(lb=0) + x = m.addVar(lb=0, obj=1) binvar = m.addVar(vtype="B", lb=1) - c = m.addConsIndicator(x >= 1, binvar) + c1 = m.addConsIndicator(x >= 1, binvar) - slack = m.getSlackVarIndicator(c) + assert c1.name == "c1" + + c2 = m.addCons(x <= 3) + + c3 = m.addConsIndicator(x >= 0, binvar) + assert c3.name == "c4" + + # because addConsIndicator actually adds two constraints + assert m.getNConss() == 5 + + slack = m.getSlackVarIndicator(c1) m.optimize() + assert m.getNConss() == 5 assert m.isEQ(m.getVal(slack), 0) assert m.isEQ(m.getVal(binvar), 1) assert m.isEQ(m.getVal(x), 1) - assert c.getConshdlrName() == "indicator" + assert c1.getConshdlrName() == "indicator" + +test_cons_indicator() @pytest.mark.xfail( reason="addConsIndicator doesn't behave as expected when binary variable is False. See Issue #717." From 674fcb06b86758c05ece97c6915b7bd5d4fa110d Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 2 Dec 2024 10:34:47 +0100 Subject: [PATCH 130/135] Remove forgotten function call --- tests/test_cons.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_cons.py b/tests/test_cons.py index cccd28adc..51077d86b 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -126,9 +126,6 @@ def test_cons_indicator(): assert m.isEQ(m.getVal(x), 1) assert c1.getConshdlrName() == "indicator" - -test_cons_indicator() - @pytest.mark.xfail( reason="addConsIndicator doesn't behave as expected when binary variable is False. See Issue #717." ) From aed4cd5e0b04ec3d61ceaad59b6ad6ceff99de1b Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 2 Dec 2024 10:36:49 +0100 Subject: [PATCH 131/135] Rename to printProblem, fix docstring --- CHANGELOG.md | 2 +- src/pyscipopt/scip.pxi | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c638d26..93e9363e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased ### Added -- Added displayProblem to print problem to stdout +- Added printProblem to print problem to stdout - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e5bea801f..04dba32f2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2889,9 +2889,9 @@ cdef class Model: if not onlyroot: self.setIntParam("propagating/maxrounds", 0) - def displayProblem(self, ext='.cip', trans=False, genericnames=False): + def printProblem(self, ext='.cip', trans=False, genericnames=False): """ - Write current model/problem to a file. + Write current model/problem to standard output. Parameters ---------- @@ -2904,9 +2904,6 @@ cdef class Model: genericnames : bool, optional indicates whether the problem should be written with generic variable and constraint names (Default value = False) - verbose : bool, optional - indicates whether a success message should be printed - """ user_locale = locale.getlocale(category=locale.LC_NUMERIC) locale.setlocale(locale.LC_NUMERIC, "C") From 3b4eb05d40a88a46c178430d37d79ddca1c86d5e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 2 Dec 2024 10:39:08 +0100 Subject: [PATCH 132/135] Fix test name --- tests/test_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index fa779f6a8..2d07331e5 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -67,7 +67,7 @@ def test_model(): s.writeProblem('model') s.writeProblem('model.lp') - s.displayProblem() + s.printProblem() s.freeProb() s = Model() From 083efc6ba817dd09ab8878ce28fd592eb9d2f60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dion=C3=ADsio?= <57299939+Joao-Dionisio@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:28:19 +0100 Subject: [PATCH 133/135] Added categorical data example (#932) * Added categorical data example * Added description and changed code slightly --- CHANGELOG.md | 1 + examples/finished/categorical_data.py | 73 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 examples/finished/categorical_data.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e9363e7..05df22882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added categorical data example - Added printProblem to print problem to stdout - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe diff --git a/examples/finished/categorical_data.py b/examples/finished/categorical_data.py new file mode 100644 index 000000000..07f122aa8 --- /dev/null +++ b/examples/finished/categorical_data.py @@ -0,0 +1,73 @@ +""" +This example shows how one can optimize a model with categorical data by converting it into integers. + +There are three employees (Alice, Bob, Charlie) and three shifts. Each shift is assigned an integer: + +Morning - 0 +Afternoon - 1 +Night - 2 + +The employees have availabilities (e.g. Alice can only work in the Morning and Afternoon), and different +salary demands. These constraints, and an additional one stipulating that every shift must be covered, +allows us to model a MIP with the objective of minimizing the money spent on salary. +""" + +from pyscipopt import Model + +# Define categorical data +shift_to_int = {"Morning": 0, "Afternoon": 1, "Night": 2} +employees = ["Alice", "Bob", "Charlie"] + +# Employee availability +availability = { + "Alice": ["Morning", "Afternoon"], + "Bob": ["Afternoon", "Night"], + "Charlie": ["Morning", "Night"] +} + +# Transform availability into integer values +availability_int = {} +for emp, available_shifts in availability.items(): + availability_int[emp] = [shift_to_int[shift] for shift in available_shifts] + + +# Employees have different salary demands +cost = { + "Alice": [2,4,1], + "Bob": [3,2,7], + "Charlie": [3,3,3] +} + +# Create the model +model = Model("Shift Assignment") + +# x[e, s] = 1 if employee e is assigned to shift s +x = {} +for e in employees: + for s in shift_to_int.values(): + x[e, s] = model.addVar(vtype="B", name=f"x({e},{s})") + +# Each shift must be assigned to exactly one employee +for s in shift_to_int.values(): + model.addCons(sum(x[e, s] for e in employees) == 1) + +# Employees can only work shifts they are available for +for e in employees: + for s in shift_to_int.values(): + if s not in availability_int[e]: + model.addCons(x[e, s] == 0) + +# Minimize shift assignment cost +model.setObjective( + sum(cost[e][s]*x[e, s] for e in employees for s in shift_to_int.values()), "minimize" +) + +# Solve the problem +model.optimize() + +# Display the results +print("\nOptimal Shift Assignment:") +for e in employees: + for s, s_id in shift_to_int.items(): + if model.getVal(x[e, s_id]) > 0.5: + print("%s is assigned to %s" % (e, s)) From cfbd832f694349c5bc423d9cbc75be2166955d59 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:41:45 +0100 Subject: [PATCH 134/135] Updates mac script to use precompiled version (#933) * Updates mac script to use precompiled version * Adds license skip * Update version to 9.2.0. Add CHANGELOG entry * Change nae of windows exe installer --- .github/workflows/integration-test.yml | 32 +++++--------------------- CHANGELOG.md | 1 + 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d78162389..0ba7e9270 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -1,7 +1,7 @@ name: Integration test env: - version: 9.1.0 + version: 9.2.0 # runs on branches and pull requests; doesn't run on tags. on: @@ -57,7 +57,7 @@ jobs: - name: Download dependencies (SCIPOptSuite) shell: powershell - run: wget https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/SCIPOptSuite-${{ env.version }}-win64-VS15.exe -outfile scipopt-installer.exe + run: wget https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/SCIPOptSuite-${{ env.version }}-win64.exe -outfile scipopt-installer.exe - name: Install dependencies (SCIPOptSuite) shell: cmd @@ -93,33 +93,13 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Cache dependencies (SCIPOptSuite) - id: cache-scip - uses: actions/cache@v2 - with: - path: | - ${{ runner.workspace }}/scipoptsuite - ~/Library/Caches/Homebrew/tbb--* - /usr/local/opt/tbb* - ~/Library/Caches/Homebrew/downloads/*--tbb-* - ~/Library/Caches/Homebrew/boost--* - /usr/local/opt/boost* - ~/Library/Caches/Homebrew/downloads/*--boost-* - key: ${{ runner.os }}-scipopt-${{ env.version }}-${{ hashFiles('**/lockfiles') }} - restore-keys: | - ${{ runner.os }}-scipopt-${{ env.version }}- - - name: Install dependencies (SCIPOptSuite) - if: steps.cache-scip.outputs.cache-hit != 'true' run: | brew install tbb boost bison - wget --quiet --no-check-certificate https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/scipoptsuite-${{ env.version }}.tgz - tar xfz scipoptsuite-${{ env.version }}.tgz - cd scipoptsuite-${{ env.version }} - mkdir build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=${{ runner.workspace }}/scipoptsuite -DIPOPT=off -DSYM=none -DTPI=tny -DREADLINE=off - make install -j + wget --quiet --no-check-certificate https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/SCIPOptSuite-${{ env.version }}-Darwin.sh + chmod +x SCIPOptSuite-${{ env.version }}-Darwin.sh + ./SCIPOptSuite-${{ env.version }}-Darwin.sh --skip-license --include-subdir + mv SCIPOptSuite-${{ env.version }}-Darwin ${{ runner.workspace }}/scipoptsuite - name: Setup python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 05df22882..8a4866b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added primal_dual_evolution recipe and a plot recipe ### Fixed ### Changed +- GitHub actions using Mac now use precompiled SCIP from latest release ### Removed ## 5.2.1 - 2024.10.29 From 6cc9cccb6680ca1c1b2dbbe5a310ea7ca7679b87 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 3 Dec 2024 12:09:00 +0100 Subject: [PATCH 135/135] Changed test --- CHANGELOG.md | 1 + tests/test_cons.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4866b00..36feae611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added stage checks to presolve, freereoptsolve, freetransform - Added primal_dual_evolution recipe and a plot recipe ### Fixed +- Added default names to indicator constraints ### Changed - GitHub actions using Mac now use precompiled SCIP from latest release ### Removed diff --git a/tests/test_cons.py b/tests/test_cons.py index 51077d86b..42801bd93 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -120,7 +120,7 @@ def test_cons_indicator(): m.optimize() - assert m.getNConss() == 5 + assert m.getNConss(transformed=False) == 5 assert m.isEQ(m.getVal(slack), 0) assert m.isEQ(m.getVal(binvar), 1) assert m.isEQ(m.getVal(x), 1)