diff --git a/CHANGELOG.md b/CHANGELOG.md index aabcecf95..fbec1d025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,16 @@ ## Unreleased ### Added - Added more type annotations and deprecation markers to the stubs. +- Added tests for `getObjVal()` and `getVal()` ### Fixed +- Fixed incorrect `getObjVal()` result when called before `getBestSol()` (e.g., in event handlers). ### Changed +- Replaced `format()` with f-strings in examples ### Removed ## 6.3.0 - 2026.10.11 ### Added -- Added the following methods with tests: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()`, `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`,\ -`getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()`, `getUpperbound()`, `getNObjlimLeaves()` +- Added the following methods with tests: `getNNodesLeft()`, `getNRuns()`, `getNReoptRuns()`, `addNNodes()`, `getDeterministicTime()`, `getAvgDualbound()`, `getMaxTotalDepth()`, `getNBacktracks()`, `getFocusNode()`, `getAvgLowerbound()`, `getFirstPrimalBound()`, `getLowerboundRoot()`, `getUpperbound()`, `getNObjlimLeaves()` - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - Added `addConsLogicor()`, `addCoefLogicor()`, `getNVarsLogicor()`, `getVarsLogicor()`, `getDualsolLogicor()` and `getDualfarkasLogicor()` for SCIP logicor constraints (#977) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` diff --git a/README.md b/README.md index 75494b47f..fe1d46b60 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ model.setObjective(x + y) model.addCons(2*x - y*y >= 0) model.optimize() sol = model.getBestSol() -print("x: {}".format(sol[x])) -print("y: {}".format(sol[y])) +print(f"x: {sol[x]}") +print(f"y: {sol[y]}") ``` Writing new plugins diff --git a/docs/tutorials/eventhandler.rst b/docs/tutorials/eventhandler.rst index 6ce5322fc..8c28216e5 100644 --- a/docs/tutorials/eventhandler.rst +++ b/docs/tutorials/eventhandler.rst @@ -37,7 +37,7 @@ The following is an example the prints the value of the objective function whene from pyscipopt import Model, SCIP_EVENTTYPE def print_obj_value(model, event): - print("New best solution found with objective value: {}".format(model.getObjVal())) + print(f"New best solution found with objective value: {model.getObjVal()}") m = Model() m.attachEventHandlerCallback(print_obj_value, [SCIP_EVENTTYPE.BESTSOLFOUND]) @@ -72,7 +72,7 @@ and then include it in the model using the :code:`Model.includeEventHandler` met def eventexec(self, event): self.count += 1 - print("!!!![@BestSolCounter] New best solution found. Total best solutions found: {}".format(self.count)) + print(f"!!!![@BestSolCounter] New best solution found. Total best solutions found: {self.count}") m = Model() diff --git a/examples/finished/diet.py b/examples/finished/diet.py index 53fbf83f4..244296460 100644 --- a/examples/finished/diet.py +++ b/examples/finished/diet.py @@ -88,7 +88,7 @@ def make_inst(): for b["Cal"] in [None, 3500, 3000, 2500]: - print("\nDiet for a maximum of {0} calories".format(b["Cal"] if b["Cal"] != None else "unlimited")) + print(f"\nDiet for a maximum of {b['Cal'] if b['Cal'] != None else 'unlimited'} calories") model = diet(F, N, a, b, c, d) model.hideOutput() # silent mode model.optimize() @@ -97,10 +97,9 @@ def make_inst(): x, y, z, v = model.data for j in x: if model.getVal(x[j]) > 0: - print("{0:30s}: {1:3.1f} dishes --> {2:4.2f} added to objective".format(j, model.getVal(x[j]), - model.getVal(y[j]))) + print(f"{j:30s}: {model.getVal(x[j]):3.1f} dishes --> {model.getVal(y[j]):4.2f} added to objective") print("amount spent:", model.getObjVal()) print("amount of nutrients:") for i in z: - print("{0:30s}: {1:4.2f}".format(i, model.getVal(z[i]))) + print(f"{i:30s}: {model.getVal(z[i]):4.2f}") diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 52123c2aa..4e7022974 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1255,9 +1255,7 @@ cdef class BoundChange: return SCIPboundchgIsRedundant(self.scip_boundchg) def __repr__(self): - return "{} {} {}".format(self.getVar(), - _SCIP_BOUNDTYPE_TO_STRING[self.getBoundtype()], - self.getNewBound()) + return f"{self.getVar()} {_SCIP_BOUNDTYPE_TO_STRING[self.getBoundtype()]} {self.getNewBound()}" cdef class DomainChanges: """Set of domain changes.""" @@ -11626,6 +11624,11 @@ 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) + if SCIPgetNSols(self._scip) == 0: if self.getStage() != SCIP_STAGE_SOLVING: raise Warning("Without a solution, method can only be called in stage SOLVING.") diff --git a/tests/test_model.py b/tests/test_model.py index 6736fbb5f..0b90891f2 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -486,6 +486,82 @@ def test_getObjVal(): assert m.getObjVal() == 16 assert m.getVal(x) == 0 +def test_getObjVal_missing_bestSol(): + m = Model() + m.hideOutput() + + x = m.addVar("x", obj=2.0) + m.addCons(x >= 1.0) + + # Add a solution + sol = m.createSol() + m.setSolVal(sol, x, 10.0) + m.addSol(sol) + + # Without the fix from #1272, m._bestSol remained NULL, + # causing an assertion failure: assert self._bestSol.sol != NULL + assert m.getObjVal() == 20.0 + +def test_getObjVal_outdated_bestSol(): + m = Model() + m.hideOutput() + + x = m.addVar("x", obj=2.0) + m.addCons(x >= 1.0) + + # Add initial solution and populate _bestSol + sol1 = m.createSol() + m.setSolVal(sol1, x, 10.0) + m.addSol(sol1) + m.getBestSol() # populates m._bestSol with sol1 + + # Add a better solution + sol2 = m.createSol() + m.setSolVal(sol2, x, 3.0) + m.addSol(sol2) + + # Without the fix from #1272, _bestSol remained stuck + # on sol1 and incorrectly returned 20.0 instead of 6.0 + assert m.getObjVal() == 6.0 + +def test_getVal_missing_bestSol(): + m = Model() + m.hideOutput() + + x = m.addVar("x", obj=2.0) + m.addCons(x >= 1.0) + + # Add a solution + sol = m.createSol() + m.setSolVal(sol, x, 10.0) + m.addSol(sol) + + # Without the fix from #993, m._bestSol was None here + # causing "No solution available" warning + assert m.getVal(x) == 10.0 + +def test_getVal_outdated_bestSol(): + m = Model() + m.hideOutput() + + x = m.addVar("x", obj=2.0) + m.addCons(x >= 1.0) + + # Add initial solution and populate _bestSol + sol1 = m.createSol() + m.setSolVal(sol1, x, 10.0) + m.addSol(sol1) + m.getBestSol() # populates m._bestSol with sol1 + + # Add a better solution + sol2 = m.createSol() + m.setSolVal(sol2, x, 3.0) + m.addSol(sol2) + + # Without the fix from #993, _bestSol remained stuck + # on sol1 and incorrectly returned 10.0 instead of 3.0 + assert m.getVal(x) == 3.0 + # tests writeProblem() after redirectOutput() def test_redirection():