diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index c5053ebba..277789ce9 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2330,6 +2330,12 @@ cdef class Model: cdef _getOrCreateCons(self, SCIP_CONS* scip_cons) cdef _getOrCreateVar(self, SCIP_VAR* scip_var) cdef _free_scip_instance(self) + + cdef _ensureFreshBestSol(self) + cdef SCIP_CONS* _getTransformedCons(self, Constraint cons, str expected_cons_type) + cdef SCIP_EVENTHDLR* _getEventhdlr(self, object eventhdlr) + cdef SCIP_BENDERS* _getBenders(self, object benders) + cdef SCIP_HEUR* _getHeur(self, object heur) @staticmethod cdef create(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 214b8bad5..a849a30b3 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -7083,6 +7083,40 @@ cdef class Model: PY_SCIP_CALL(SCIPaddCoefLogicor(self._scip, logicor_cons.scip_cons, var.scip_var)) + cdef SCIP_CONS* _getTransformedCons(self, Constraint cons, str expected_cons_type): + """ + Validates constraint type and retrieves its transformed representation. + + Parameters + ---------- + cons : Constraint + Constraint to get the transformed representation for. + expected_cons_type : str + Expected constraint type name (e.g. 'linear', 'logicor', 'knapsack'). + + Returns + ------- + SCIP_CONS* + Pointer to the transformed SCIP constraint, or NULL if transformed constraint does not exist. + + Raises + ------ + Warning + If the constraint handler type does not match expected_cons_type. + + """ + cdef SCIP_CONS* transcons + + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == expected_cons_type: + raise Warning(f"dual solution values not available for constraints of type {constype}") + + if cons.isOriginal(): + PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) + return transcons + + return cons.scip_cons + def getDualsolLogicor(self, Constraint logicor_cons): """ Gets the dual solution of a logicor constraint in the current LP. @@ -7098,18 +7132,9 @@ cdef class Model: Zero if presolve removed the transformed constraint. """ - cdef SCIP_CONS* transcons - - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(logicor_cons.scip_cons))).decode('UTF-8') - if not constype == 'logicor': - raise Warning("dual solution values not available for constraints of type ", constype) - - if logicor_cons.isOriginal(): - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, logicor_cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - else: - transcons = logicor_cons.scip_cons + cdef SCIP_CONS* transcons = self._getTransformedCons(logicor_cons, "logicor") + if transcons == NULL: + return 0.0 return SCIPgetDualsolLogicor(self._scip, transcons) @@ -7128,18 +7153,9 @@ cdef class Model: Zero if presolve removed the transformed constraint. """ - cdef SCIP_CONS* transcons - - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(logicor_cons.scip_cons))).decode('UTF-8') - if not constype == 'logicor': - raise Warning("dual solution values not available for constraints of type ", constype) - - if logicor_cons.isOriginal(): - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, logicor_cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - else: - transcons = logicor_cons.scip_cons + cdef SCIP_CONS* transcons = self._getTransformedCons(logicor_cons, "logicor") + if transcons == NULL: + return 0.0 return SCIPgetDualfarkasLogicor(self._scip, transcons) @@ -9211,17 +9227,10 @@ cdef class Model: Zero if presolve removed the transformed constraint. """ - cdef SCIP_CONS* transcons + cdef SCIP_CONS* transcons = self._getTransformedCons(cons, "linear") + if transcons == NULL: + return 0.0 - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') - if not constype == 'linear': - raise Warning("dual solution values not available for constraints of type ", constype) - if cons.isOriginal(): - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - else: - transcons = cons.scip_cons return SCIPgetDualsolLinear(self._scip, transcons) def getDualsolKnapsack(self, Constraint cons): @@ -9238,18 +9247,10 @@ cdef class Model: float """ - cdef SCIP_CONS* transcons + cdef SCIP_CONS* transcons = self._getTransformedCons(cons, "knapsack") + if transcons == NULL: + return 0.0 - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') - if not constype == 'knapsack': - raise Warning("dual solution values not available for constraints of type ", constype) - if cons.isOriginal(): - # presolve may have removed it, and then there's no dual to read - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - else: - transcons = cons.scip_cons return SCIPgetDualsolKnapsack(self._scip, transcons) def getDualMultiplier(self, Constraint cons): @@ -9283,16 +9284,12 @@ cdef class Model: Zero if presolve removed the transformed constraint. """ - cdef SCIP_CONS* transcons - # TODO this should ideally be handled on the SCIP side - if cons.isOriginal(): - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - return SCIPgetDualfarkasLinear(self._scip, transcons) - else: - return SCIPgetDualfarkasLinear(self._scip, cons.scip_cons) + cdef SCIP_CONS* transcons = self._getTransformedCons(cons, "linear") + if transcons == NULL: + return 0.0 + + return SCIPgetDualfarkasLinear(self._scip, transcons) def getDualfarkasKnapsack(self, Constraint cons): """ @@ -9308,17 +9305,12 @@ cdef class Model: float """ - cdef SCIP_CONS* transcons - # TODO this should ideally be handled on the SCIP side - if cons.isOriginal(): - # presolve may have removed it, and then there's no Farkas value to read - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) - if transcons == NULL: - return 0.0 - return SCIPgetDualfarkasKnapsack(self._scip, transcons) - else: - return SCIPgetDualfarkasKnapsack(self._scip, cons.scip_cons) + cdef SCIP_CONS* transcons = self._getTransformedCons(cons, "knapsack") + if transcons == NULL: + return 0.0 + + return SCIPgetDualfarkasKnapsack(self._scip, transcons) def getVarRedcost(self, Variable var): """ @@ -9504,6 +9496,25 @@ cdef class Model: stacklevel=2 ) + cdef SCIP_BENDERS* _getBenders(self, object benders): + """ + Retrieves the SCIP_BENDERS pointer for the given Benders object or the default decomposition. + + Parameters + ---------- + benders : Benders or None + The Benders' decomposition object, or None to use the default Benders' decomposition. + + Returns + ------- + SCIP_BENDERS* + Pointer to the underlying SCIP_BENDERS structure, or NULL if not found. + + """ + if benders is None: + return SCIPfindBenders(self._scip, "default") + return (benders)._benders + def updateBendersLowerbounds(self, lowerbounds, Benders benders=None): """ Updates the subproblem lower bounds for benders using @@ -9516,15 +9527,10 @@ cdef class Model: benders : Benders or None, optional """ - cdef SCIP_BENDERS* _benders + cdef SCIP_BENDERS* _benders = self._getBenders(benders) assert type(lowerbounds) is dict - if benders is None: - _benders = SCIPfindBenders(self._scip, "default") - else: - _benders = benders._benders - for d in lowerbounds.keys(): SCIPbendersUpdateSubproblemLowerbound(_benders, d, lowerbounds[d]) @@ -9590,7 +9596,7 @@ cdef class Model: PY_SCIP_BENDERSENFOTYPE: LP, RELAX, PSEUDO or CHECK. Default is LP. """ - cdef SCIP_BENDERS* scip_benders + cdef SCIP_BENDERS* scip_benders = self._getBenders(benders) cdef SCIP_SOL* scip_sol if isinstance(solution, Solution): @@ -9598,11 +9604,6 @@ cdef class Model: else: scip_sol = NULL - if benders is None: - scip_benders = SCIPfindBenders(self._scip, "default") - else: - scip_benders = benders._benders - retcode = SCIPsetupBendersSubproblem(self._scip, scip_benders, scip_sol, probnumber, checktype) PY_SCIP_CALL(retcode) @@ -9632,7 +9633,7 @@ cdef class Model: """ - cdef SCIP_BENDERS* scip_benders + cdef SCIP_BENDERS* scip_benders = self._getBenders(benders) cdef SCIP_SOL* scip_sol cdef SCIP_Real objective cdef SCIP_Bool infeasible @@ -9642,11 +9643,6 @@ cdef class Model: else: scip_sol = NULL - if benders is None: - scip_benders = SCIPfindBenders(self._scip, "default") - else: - scip_benders = benders._benders - PY_SCIP_CALL(SCIPsolveBendersSubproblem(self._scip, scip_benders, scip_sol, probnumber, &infeasible, solvecip, &objective)) @@ -9670,14 +9666,9 @@ cdef class Model: Model """ - cdef SCIP_BENDERS* scip_benders + cdef SCIP_BENDERS* scip_benders = self._getBenders(benders) cdef SCIP* scip_subprob - if benders is None: - scip_benders = SCIPfindBenders(self._scip, "default") - else: - scip_benders = benders._benders - scip_subprob = SCIPbendersSubproblem(scip_benders, probnumber) return Model.create(scip_subprob) @@ -9701,14 +9692,9 @@ cdef class Model: Variable or None """ - cdef SCIP_BENDERS* _benders + cdef SCIP_BENDERS* _benders = self._getBenders(benders) cdef SCIP_VAR* _mappedvar - if benders is None: - _benders = SCIPfindBenders(self._scip, "default") - else: - _benders = benders._benders - if probnumber == -1: PY_SCIP_CALL(SCIPgetBendersMasterVar(self._scip, _benders, var.scip_var, &_mappedvar)) else: @@ -9734,14 +9720,9 @@ cdef class Model: Variable """ - cdef SCIP_BENDERS* _benders + cdef SCIP_BENDERS* _benders = self._getBenders(benders) cdef SCIP_VAR* _auxvar - if benders is None: - _benders = SCIPfindBenders(self._scip, "default") - else: - _benders = benders._benders - _auxvar = SCIPbendersGetAuxiliaryVar(_benders, probnumber) return self._getOrCreateVar(_auxvar) @@ -9764,15 +9745,10 @@ cdef class Model: flag to indicate whether the current subproblem is optimal for the master """ - cdef SCIP_BENDERS* _benders + cdef SCIP_BENDERS* _benders = self._getBenders(benders) cdef SCIP_SOL* scip_sol cdef SCIP_Bool optimal - if benders is None: - _benders = SCIPfindBenders(self._scip, "default") - else: - _benders = benders._benders - if isinstance(solution, Solution): scip_sol = solution.sol else: @@ -11097,6 +11073,26 @@ cdef class Model: locale.setlocale(locale.LC_NUMERIC,user_locale) + cdef SCIP_HEUR* _getHeur(self, object heur): + """ + Retrieves the SCIP_HEUR pointer corresponding to a heuristic object. + + Parameters + ---------- + heur : Heur or None + The heuristic object to resolve. + + Returns + ------- + SCIP_HEUR* + Pointer to the SCIP heuristic, or NULL if heur is not a Heur instance or not found. + + """ + if isinstance(heur, Heur): + n = str_conversion(heur.name) + return SCIPfindHeur(self._scip, n) + return NULL + def createSol(self, Heur heur = None, initlp=False): """ Create a new primal solution in the transformed space. @@ -11113,14 +11109,9 @@ cdef class Model: Solution """ - cdef SCIP_HEUR* _heur cdef SCIP_SOL* _sol + cdef SCIP_HEUR* _heur = self._getHeur(heur) - if isinstance(heur, Heur): - n = str_conversion(heur.name) - _heur = SCIPfindHeur(self._scip, n) - else: - _heur = NULL if not initlp: PY_SCIP_CALL(SCIPcreateSol(self._scip, &_sol, _heur)) else: @@ -11142,14 +11133,9 @@ cdef class Model: Solution """ - cdef SCIP_HEUR* _heur cdef SCIP_SOL* _sol + cdef SCIP_HEUR* _heur = self._getHeur(heur) - if isinstance(heur, Heur): - n = str_conversion(heur.name) - _heur = SCIPfindHeur(self._scip, n) - else: - _heur = NULL PY_SCIP_CALL(SCIPcreatePartialSol(self._scip, &_sol, _heur)) partialsolution = Solution.create(self._scip, _sol) return partialsolution @@ -11171,11 +11157,7 @@ cdef class Model: 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 + _heur = self._getHeur(heur) PY_SCIP_CALL(SCIPcreateOrigSol(self._scip, &_sol, _heur)) solution = Solution.create(self._scip, _sol) @@ -11626,6 +11608,14 @@ cdef class Model: raise ValueError("Cannot get solution time: solution is None or NULL") return SCIPgetSolTime(self._scip, sol.sol) + cdef _ensureFreshBestSol(self): + """ + Refreshes the internal _bestSol attribute if the underlying SCIP pointer has changed. + """ + cdef SCIP_SOL* current_best_sol = SCIPgetBestSol(self._scip) + if self._bestSol is None or self._bestSol.sol != current_best_sol: + self._bestSol = Solution.create(self._scip, current_best_sol) + def getObjVal(self, original=True): """ Retrieve the objective value of the best solution. @@ -11641,10 +11631,7 @@ cdef class Model: """ - # Ensure _bestSol is up-to-date (cheap pointer comparison) - cdef SCIP_SOL* current_best_sol = SCIPgetBestSol(self._scip) - if self._bestSol is None or self._bestSol.sol != current_best_sol: - self._bestSol = Solution.create(self._scip, current_best_sol) + self._ensureFreshBestSol() if SCIPgetNSols(self._scip) == 0: if self.getStage() != SCIP_STAGE_SOLVING: @@ -11713,10 +11700,7 @@ cdef class Model: if SCIPgetStage(self._scip) in {SCIP_STAGE_INIT, SCIP_STAGE_FREE}: raise Warning("Method cannot be called in stage ", self.getStage()) - # Ensure _bestSol is up-to-date (cheap pointer comparison) - cdef SCIP_SOL* current_best_sol = SCIPgetBestSol(self._scip) - if self._bestSol is None or self._bestSol.sol != current_best_sol: - self._bestSol = Solution.create(self._scip, current_best_sol) + self._ensureFreshBestSol() if self._bestSol.sol == NULL and SCIPgetStage(self._scip) != SCIP_STAGE_SOLVING: raise Warning("No solution available") @@ -11925,6 +11909,31 @@ cdef class Model: else: return "unknown" + cdef SCIP_EVENTHDLR* _getEventhdlr(self, object eventhdlr): + """ + Resolves an event handler object to its underlying SCIP_EVENTHDLR pointer. + + Parameters + ---------- + eventhdlr : Eventhdlr + The event handler object to resolve. + + Returns + ------- + SCIP_EVENTHDLR* + Pointer to the SCIP event handler, or NULL if not found. + + Raises + ------ + Warning + If eventhdlr is not an instance of Eventhdlr. + + """ + if isinstance(eventhdlr, Eventhdlr): + n = str_conversion(eventhdlr.name) + return SCIPfindEventhdlr(self._scip, n) + raise Warning(f"Expected an Eventhdlr instance, got {type(eventhdlr).__name__}") + def catchEvent(self, eventtype, Eventhdlr eventhdlr): """ Catches a global (not variable or row dependent) event. @@ -11935,12 +11944,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL)) eventhdlr._caught_events.append(eventtype) @@ -11955,12 +11959,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1)) if eventtype in eventhdlr._caught_events: @@ -11977,12 +11976,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPcatchVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, NULL)) def dropVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): @@ -11996,12 +11990,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPdropVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, -1)) def catchRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): @@ -12015,12 +12004,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPcatchRowEvent(self._scip, row.scip_row, eventtype, _eventhdlr, NULL, NULL)) def dropRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): @@ -12034,12 +12018,7 @@ cdef class Model: eventhdlr : Eventhdlr """ - cdef SCIP_EVENTHDLR* _eventhdlr - if isinstance(eventhdlr, Eventhdlr): - n = str_conversion(eventhdlr.name) - _eventhdlr = SCIPfindEventhdlr(self._scip, n) - else: - raise Warning("event handler not found") + cdef SCIP_EVENTHDLR* _eventhdlr = self._getEventhdlr(eventhdlr) PY_SCIP_CALL(SCIPdropRowEvent(self._scip, row.scip_row, eventtype, _eventhdlr, NULL, -1)) # Statistic Methods