diff --git a/CHANGELOG.md b/CHANGELOG.md index ed75915c6..21883104f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,29 @@ ## Unreleased ### Added +- Interface to include custom cut selector plugins +- New test for cut selector plugin +- Add SCIP function SCIPgetCutLPSolCutoffDistance and wrapper getCutLPSolCutoffDistance +- Add SCIP function SCIPprintBestTransSol and wrapper writeBestTransSol +- Add SCIP function SCIPprintTransSol and wrapper writeTransSol +- Add SCIP function SCIPgetRowNumIntCols and wrapper getRowNumIntCols +- Add SCIP function SCIProwGetNNonz and wrapper rowGetNNonz +- Add SCIP function SCIPgetRowObjParallelism and wrapper getRowObjParallelism +- Add SCIP function SCIPgetNSepaRounds and wrapper getNSepaRounds +- Add SCIP function SCIPgetRowLinear and wrapper getRowLinear +- Add SCIP function SCIProwIsInGlobalCutpool and wrapper isInGlobalCutpool +- Add SCIP function SCIProwGetParallelism and wrapper getRowParallelism +- Add getObjCoeff call to Column +- Add isLocal call to Row +- Add getNorm call to Row +- Add getRowDualSol to Row +- Add getDualSolVal to Model - added activeone parameter of addConsIndicator() allows to activate the constraint if the binary (indicator) variable is 1 or 0. - added function getSlackVarIndicator(), returns the slack variable of the indicator constraint. ### Fixed - cmake / make install works from build directory ### Changed + ### Removed ## 4.0.0 - 2021-12-15 diff --git a/src/pyscipopt/cutsel.pxi b/src/pyscipopt/cutsel.pxi new file mode 100644 index 000000000..e953cb1e9 --- /dev/null +++ b/src/pyscipopt/cutsel.pxi @@ -0,0 +1,99 @@ +##@file cutsel.pxi +#@brief Base class of the Cutsel Plugin +cdef class Cutsel: + cdef public Model model + + def cutselfree(self): + '''frees memory of cut selector''' + pass + + def cutselinit(self): + ''' executed after the problem is transformed. use this call to initialize cut selector data.''' + pass + + def cutselexit(self): + '''executed before the transformed problem is freed''' + pass + + def cutselinitsol(self): + '''executed when the presolving is finished and the branch-and-bound process is about to begin''' + pass + + def cutselexitsol(self): + '''executed before the branch-and-bound process is freed''' + pass + + def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): + '''first method called in each iteration in the main solving loop. ''' + # this method needs to be implemented by the user + return {} + + +cdef SCIP_RETCODE PyCutselCopy (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + return SCIP_OKAY + +cdef SCIP_RETCODE PyCutselFree (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + PyCutsel.cutselfree() + Py_DECREF(PyCutsel) + return SCIP_OKAY + +cdef SCIP_RETCODE PyCutselInit (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + PyCutsel.cutselinit() + return SCIP_OKAY + + +cdef SCIP_RETCODE PyCutselExit (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + PyCutsel.cutselexit() + return SCIP_OKAY + +cdef SCIP_RETCODE PyCutselInitsol (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + PyCutsel.cutselinitsol() + return SCIP_OKAY + +cdef SCIP_RETCODE PyCutselExitsol (SCIP* scip, SCIP_CUTSEL* cutsel) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + PyCutsel.cutselexitsol() + return SCIP_OKAY + +cdef SCIP_RETCODE PyCutselSelect (SCIP* scip, SCIP_CUTSEL* cutsel, SCIP_ROW** cuts, int ncuts, + SCIP_ROW** forcedcuts, int nforcedcuts, SCIP_Bool root, int maxnselectedcuts, + int* nselectedcuts, SCIP_RESULT* result) with gil: + cdef SCIP_CUTSELDATA* cutseldata + cdef SCIP_ROW* scip_row + cutseldata = SCIPcutselGetData(cutsel) + PyCutsel = cutseldata + + # translate cuts to python + pycuts = [Row.create(cuts[i]) for i in range(ncuts)] + pyforcedcuts = [Row.create(forcedcuts[i]) for i in range(nforcedcuts)] + result_dict = PyCutsel.cutselselect(pycuts, pyforcedcuts, root, maxnselectedcuts) + + # Retrieve the sorted cuts. Note that these do not need to be returned explicitly in result_dict. + # Pycuts could have been sorted in place in cutselselect() + pycuts = result_dict.get('cuts', pycuts) + + assert len(pycuts) == ncuts + assert len(pyforcedcuts) == nforcedcuts + + #sort cuts + for i,cut in enumerate(pycuts): + cuts[i] = ((cut).scip_row) + + nselectedcuts[0] = result_dict.get('nselectedcuts', 0) + result[0] = result_dict.get('result', result[0]) + + return SCIP_OKAY diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b856c5cb4..523210fd6 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -364,6 +364,12 @@ cdef extern from "scip/scip.h": ctypedef struct SCIP_BRANCHRULEDATA: pass + ctypedef struct SCIP_CUTSEL: + pass + + ctypedef struct SCIP_CUTSELDATA: + pass + ctypedef struct SCIP_PRESOL: pass @@ -736,6 +742,7 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPaddPoolCut(SCIP* scip, SCIP_ROW* row) SCIP_Real SCIPgetCutEfficacy(SCIP* scip, SCIP_SOL* sol, SCIP_ROW* cut) SCIP_Bool SCIPisCutEfficacious(SCIP* scip, SCIP_SOL* sol, SCIP_ROW* cut) + SCIP_Real SCIPgetCutLPSolCutoffDistance(SCIP* scip, SCIP_SOL* sol, SCIP_ROW* cut) int SCIPgetNCuts(SCIP* scip) int SCIPgetNCutsApplied(SCIP* scip) SCIP_RETCODE SCIPseparateSol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool pretendroot, SCIP_Bool allowlocal, SCIP_Bool onlydelayed, SCIP_Bool* delayed, SCIP_Bool* cutoff) @@ -789,7 +796,9 @@ cdef extern from "scip/scip.h": 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) SCIP_RETCODE SCIPfreeSol(SCIP* scip, SCIP_SOL** sol) SCIP_RETCODE SCIPprintBestSol(SCIP* scip, FILE* outfile, SCIP_Bool printzeros) + SCIP_RETCODE SCIPprintBestTransSol(SCIP* scip, FILE* outfile, SCIP_Bool printzeros) SCIP_RETCODE SCIPprintSol(SCIP* scip, SCIP_SOL* sol, FILE* outfile, SCIP_Bool printzeros) + SCIP_RETCODE SCIPprintTransSol(SCIP* scip, SCIP_SOL* sol, FILE* outfile, SCIP_Bool printzeros) SCIP_Real SCIPgetPrimalbound(SCIP* scip) SCIP_Real SCIPgetGap(SCIP* scip) int SCIPgetDepth(SCIP* scip) @@ -815,6 +824,9 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPflushRowExtensions(SCIP* scip, SCIP_ROW* row) SCIP_RETCODE SCIPaddVarToRow(SCIP* scip, SCIP_ROW* row, SCIP_VAR* var, SCIP_Real val) SCIP_RETCODE SCIPprintRow(SCIP* scip, SCIP_ROW* row, FILE* file) + int SCIPgetRowNumIntCols(SCIP* scip, SCIP_ROW* row) + int SCIProwGetNNonz(SCIP_ROW* row) + SCIP_Real SCIPgetRowObjParallelism(SCIP* scip, SCIP_ROW* row) # Column Methods SCIP_Real SCIPgetColRedcost(SCIP* scip, SCIP_COL* col) @@ -1081,6 +1093,24 @@ cdef extern from "scip/scip.h": const char* SCIPbranchruleGetName(SCIP_BRANCHRULE* branchrule) SCIP_BRANCHRULE* SCIPfindBranchrule(SCIP* scip, const char* name) + # cut selector plugin + SCIP_RETCODE SCIPincludeCutsel(SCIP* scip, + const char* name, + const char* desc, + int priority, + SCIP_RETCODE (*cutselcopy) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselfree) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselinit) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselexit) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselinitsol) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselexitsol) (SCIP* scip, SCIP_CUTSEL* cutsel), + SCIP_RETCODE (*cutselselect) (SCIP* scip, SCIP_CUTSEL* cutsel, SCIP_ROW** cuts, + int ncuts, SCIP_ROW** forcedcuts, int nforcedcuts, + SCIP_Bool root, int maxnselectedcuts, + int* nselectedcuts, SCIP_RESULT* result), + SCIP_CUTSELDATA* cutseldata) + SCIP_CUTSELDATA* SCIPcutselGetData(SCIP_CUTSEL* cutsel) + # Benders' decomposition plugin SCIP_RETCODE SCIPincludeBenders(SCIP* scip, const char* name, @@ -1198,6 +1228,7 @@ cdef extern from "scip/scip.h": SCIP_Longint SCIPgetNInfeasibleLeaves(SCIP* scip) SCIP_Longint SCIPgetNLPs(SCIP* scip) SCIP_Longint SCIPgetNLPIterations(SCIP* scip) + int SCIPgetNSepaRounds(SCIP* scip) # Parameter Functions SCIP_RETCODE SCIPsetBoolParam(SCIP* scip, char* name, SCIP_Bool value) @@ -1301,6 +1332,7 @@ cdef extern from "scip/cons_linear.h": SCIP_VAR** SCIPgetVarsLinear(SCIP* scip, SCIP_CONS* cons) int SCIPgetNVarsLinear(SCIP* scip, SCIP_CONS* cons) SCIP_Real* SCIPgetValsLinear(SCIP* scip, SCIP_CONS* cons) + SCIP_ROW* SCIPgetRowLinear(SCIP* scip, SCIP_CONS* cons) cdef extern from "scip/cons_nonlinear.h": SCIP_EXPR* SCIPgetExprNonlinear(SCIP_CONS* cons) @@ -1690,12 +1722,14 @@ cdef extern from "scip/pub_lp.h": SCIP_Bool SCIProwIsLocal(SCIP_ROW* row) SCIP_Bool SCIProwIsModifiable(SCIP_ROW* row) SCIP_Bool SCIProwIsRemovable(SCIP_ROW* row) + SCIP_Bool SCIProwIsInGlobalCutpool(SCIP_ROW* row) int SCIProwGetNNonz(SCIP_ROW* row) int SCIProwGetNLPNonz(SCIP_ROW* row) SCIP_COL** SCIProwGetCols(SCIP_ROW* row) SCIP_Real* SCIProwGetVals(SCIP_ROW* row) SCIP_Real SCIProwGetNorm(SCIP_ROW* row) SCIP_Real SCIProwGetDualsol(SCIP_ROW* row) + SCIP_Real SCIProwGetParallelism(SCIP_ROW* row1, SCIP_ROW* row2, const char orthofunc) int SCIProwGetAge(SCIP_ROW* row) SCIP_Bool SCIProwIsRemovable(SCIP_ROW* row) SCIP_ROWORIGINTYPE SCIProwGetOrigintype(SCIP_ROW* row) diff --git a/src/pyscipopt/scip.pyx b/src/pyscipopt/scip.pyx index 6bdb8b8a7..854c80a64 100644 --- a/src/pyscipopt/scip.pyx +++ b/src/pyscipopt/scip.pyx @@ -21,6 +21,7 @@ include "benders.pxi" include "benderscut.pxi" include "branchrule.pxi" include "conshdlr.pxi" +include "cutsel.pxi" include "event.pxi" include "heuristic.pxi" include "presol.pxi" @@ -364,6 +365,10 @@ cdef class Column: """gets upper bound of column""" return SCIPcolGetUb(self.scip_col) + def getObjCoeff(self): + """gets objective value coefficient of a column""" + return SCIPcolGetObj(self.scip_col) + def __hash__(self): return hash(self.scip_col) @@ -422,6 +427,10 @@ cdef class Row: """returns TRUE iff the activity of the row (without the row's constant) is always integral in a feasible solution """ return SCIProwIsIntegral(self.scip_row) + def isLocal(self): + """returns TRUE iff the row is only valid locally """ + return SCIProwIsLocal(self.scip_row) + def isModifiable(self): """returns TRUE iff row is modifiable during node processing (subject to column generation) """ return SCIProwIsModifiable(self.scip_row) @@ -430,6 +439,10 @@ cdef class Row: """returns TRUE iff row is removable from the LP (due to aging or cleanup)""" return SCIProwIsRemovable(self.scip_row) + def isInGlobalCutpool(self): + """return TRUE iff row is a member of the global cut pool""" + return SCIProwIsInGlobalCutpool(self.scip_row) + def getOrigintype(self): """returns type of origin that created the row""" return SCIProwGetOrigintype(self.scip_row) @@ -452,6 +465,10 @@ cdef class Row: cdef SCIP_Real* vals = SCIProwGetVals(self.scip_row) return [vals[i] for i in range(self.getNNonz())] + def getNorm(self): + """gets Euclidean norm of row vector """ + return SCIProwGetNorm(self.scip_row) + def __hash__(self): return hash(self.scip_row) @@ -1903,6 +1920,27 @@ cdef class Model: """Prints row.""" PY_SCIP_CALL(SCIPprintRow(self._scip, row.scip_row, NULL)) + def getRowNumIntCols(self, Row row): + """Returns number of intergal columns in the row""" + return SCIPgetRowNumIntCols(self._scip, row.scip_row) + + def rowGetNNonz(self, Row row): + """Gets number of non-zero etnries in the row""" + return PY_SCIP_CALL(SCIProwGetNNonz(row.scip_row)) + + def getRowObjParallelism(self, Row row): + """Returns 1 if the row is parallel, and 0 if orthogonal""" + 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).""" + return SCIProwGetParallelism(row1.scip_row, row2.scip_row, orthofunc) + + def getRowDualSol(self, Row row): + """Gets the dual LP solution of a row""" + 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""" @@ -1916,6 +1954,10 @@ cdef class Model: """ 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""" 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""" + 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""" cdef SCIP_Bool infeasible @@ -1930,6 +1972,11 @@ cdef class Model: """Retrieve number of currently applied cuts""" return SCIPgetNCutsApplied(self._scip) + def getNSepaRounds(self): + """Retrieve the number of separation rounds that have been performed + at the current node""" + 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; @@ -3076,6 +3123,20 @@ cdef class Model: valsdict[bytes(SCIPvarGetName(_vars[i])).decode('utf-8')] = _vals[i] 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! + + :param Constraint cons: linear constraint to get the coefficients of + + """ + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == 'linear': + raise Warning("coefficients not available for constraints of type ", constype) + + cdef SCIP_ROW* row = SCIPgetRowLinear(self._scip, cons.scip_cons) + return Row.create(row) + def getDualsolLinear(self, Constraint cons): """Retrieve the dual solution to a linear constraint. @@ -3128,6 +3189,24 @@ cdef class Model: raise Warning("no reduced cost available for variable " + var.name) return redcost + def getDualSolVal(self, Constraint cons, boundconstraint=False): + """Retrieve returns dual solution value of a constraint. + + :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 + + """ + cdef SCIP_Real _dualsol + cdef SCIP_Bool _bounded + + if boundconstraint: + SCIPgetDualSolVal(self._scip, cons.scip_cons, &_dualsol, &_bounded) + else: + SCIPgetDualSolVal(self._scip, cons.scip_cons, &_dualsol, NULL) + + return _dualsol + + def optimize(self): """Optimize the problem.""" PY_SCIP_CALL(SCIPsolve(self._scip)) @@ -3666,6 +3745,24 @@ cdef class Model: Py_INCREF(relax) + def includeCutsel(self, Cutsel cutsel, name, desc, priority): + """include a 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) + des = str_conversion(desc) + PY_SCIP_CALL(SCIPincludeCutsel(self._scip, nam, des, + priority, PyCutselCopy, PyCutselFree, PyCutselInit, PyCutselExit, + PyCutselInitsol, PyCutselExitsol, PyCutselSelect, + cutsel)) + cutsel.model = weakref.proxy(self) + Py_INCREF(cutsel) + def includeBranchrule(self, Branchrule branchrule, name, desc, priority, maxdepth, maxbounddist): """Include a branching rule. @@ -4143,6 +4240,19 @@ cdef class Model: PY_SCIP_CALL(SCIPprintBestSol(self._scip, cfile, write_zeros)) fclose(cfile) + def writeBestTransSol(self, filename="transprob.sol", write_zeros=False): + """Write the best feasible primal solution for the transformed problem to a file. + + Keyword arguments: + filename -- name of the output file + write_zeros -- include variables that are set to zero + """ + # use this double opening pattern to ensure that IOErrors are + # triggered early and in python not in C, Cython or SCIP. + with open(filename, "w") as f: + cfile = fdopen(f.fileno(), "w") + PY_SCIP_CALL(SCIPprintBestTransSol(self._scip, cfile, write_zeros)) + def writeSol(self, Solution solution, filename="origprob.sol", write_zeros=False): """Write the given primal solution to a file. @@ -4158,6 +4268,20 @@ cdef class Model: PY_SCIP_CALL(SCIPprintSol(self._scip, solution.sol, cfile, write_zeros)) fclose(cfile) + def writeTransSol(self, Solution solution, filename="transprob.sol", write_zeros=False): + """Write the given transformed primal solution to a file. + + Keyword arguments: + solution -- transformed solution to write + filename -- name of the output file + write_zeros -- include variables that are set to zero + """ + # use this doubled opening pattern to ensure that IOErrors are + # triggered early and in Python not in C,Cython or SCIP. + with open(filename, "w") as f: + cfile = fdopen(f.fileno(), "w") + PY_SCIP_CALL(SCIPprintTransSol(self._scip, solution.sol, cfile, write_zeros)) + # perhaps this should not be included as it implements duplicated functionality # (as does it's namesake in SCIP) def readSol(self, filename): diff --git a/tests/test_cutsel.py b/tests/test_cutsel.py new file mode 100644 index 000000000..a0c193031 --- /dev/null +++ b/tests/test_cutsel.py @@ -0,0 +1,85 @@ +from pyscipopt import Model, quicksum, SCIP_RESULT, SCIP_PARAMSETTING +from pyscipopt.scip import Cutsel +import itertools + + +class MaxEfficacyCutsel(Cutsel): + + def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): + """ + Selects the 10 cuts with largest efficacy. Ensures that all forced cuts are passed along. + Overwrites the base cutselselect of Cutsel. + + :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 + :return: sorted cuts and forcedcuts + """ + + 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} + + +def test_cut_selector(): + scip = Model() + scip.setIntParam("presolving/maxrounds", 3) + # scip.setHeuristics(SCIP_PARAMSETTING.OFF) + + cutsel = MaxEfficacyCutsel() + scip.includeCutsel(cutsel, 'max_efficacy', 'maximises efficacy', 5000000) + + # 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') + + scip.optimize() + + +if __name__ == "__main__": + test_cut_selector()