diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 38061419..3212cb72 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -2,7 +2,7 @@ Release Notes ============= .. Upcoming Version - +* Add ``mock_solve`` option to model.solve for quick testing without actual solving * Bugfix for missing dependency for jupyter notebook example in documentation * Add support for SOS1 and SOS2 (Special Ordered Sets) constraints via ``Model.add_sos_constraints()`` and ``Model.remove_sos_constraints()`` * Add simplify method to LinearExpression to combine duplicate terms diff --git a/linopy/model.py b/linopy/model.py index 7e028441..f41d91ba 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -1124,6 +1124,7 @@ def solve( slice_size: int = 2_000_000, remote: RemoteHandler | OetcHandler = None, # type: ignore progress: bool | None = None, + mock_solve: bool = False, **solver_options: Any, ) -> tuple[str, str]: """ @@ -1191,6 +1192,8 @@ def solve( Whether to show a progress bar of writing the lp file. The default is None, which means that the progress bar is shown if the model has more than 10000 variables and constraints. + mock_solve : bool, optional + Whether to run a mock solve. This will skip the actual solving. Variables will be set to have dummy values **solver_options : kwargs Options passed to the solver. @@ -1200,6 +1203,11 @@ def solve( Tuple containing the status and termination condition of the optimization process. """ + if mock_solve: + return self._mock_solve( + sanitize_zeros=sanitize_zeros, sanitize_infinities=sanitize_infinities + ) + # clear cached matrix properties potentially present from previous solve commands self.matrices.clean_cached_properties() @@ -1375,6 +1383,40 @@ def solve( return result.status.status.value, result.status.termination_condition.value + def _mock_solve( + self, + sanitize_zeros: bool = True, + sanitize_infinities: bool = True, + ) -> tuple[str, str]: + solver_name = "mock" + + # clear cached matrix properties potentially present from previous solve commands + self.matrices.clean_cached_properties() + + logger.info(f" Solve problem using {solver_name.title()} solver") + # reset result + self.reset_solution() + + if sanitize_zeros: + self.constraints.sanitize_zeros() + + if sanitize_infinities: + self.constraints.sanitize_infinities() + + self.objective._value = 0.0 + self.status = "ok" + self.termination_condition = TerminationCondition.optimal.value + self.solver_model = None + self.solver_name = solver_name + + for name, var in self.variables.items(): + var.solution = xr.DataArray(0.0, var.coords) + + for name, con in self.constraints.items(): + con.dual = xr.DataArray(0.0, con.labels.coords) + + return "ok", "none" + def compute_infeasibilities(self) -> list[int]: """ Compute a set of infeasible constraints. diff --git a/test/test_optimization.py b/test/test_optimization.py index 12399a4e..cac5be9d 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -464,6 +464,19 @@ def test_model_maximization( assert np.isclose(m.objective.value or 0, 3.3) +def test_mock_solve(model_maximization: Model) -> None: + m = model_maximization + assert m.objective.sense == "max" + assert m.objective.value is None + + status, condition = m.solve(solver="some_non_existant_solver", mock_solve=True) + assert status == "ok" + assert m.objective.value == 0 + x_solution = m.variables["x"].solution + assert x_solution.coords == m.variables["x"].coords + assert (x_solution == 0).all() + + @pytest.mark.parametrize("solver,io_api,explicit_coordinate_names", params) def test_default_settings_chunked( model_chunked: Model, solver: str, io_api: str, explicit_coordinate_names: bool