diff --git a/src/pyoframe/io.py b/src/pyoframe/io.py index 3a25a9e..23aaf56 100644 --- a/src/pyoframe/io.py +++ b/src/pyoframe/io.py @@ -139,10 +139,20 @@ def get_var_map(m: "Model", use_var_names): def to_file( - m: "Model", file_path: Optional[Union[str, Path]], use_var_names=False + m: "Model", file_path: Optional[Union[str, Path]] = None, use_var_names=False ) -> Path: """ Write out a model to a lp file. + + Args: + m: The model to write out. + file_path: The path to write the model to. If None, a temporary file is created. The caller is responsible for + deleting the file after use. + use_var_names: If True, variable names are used in the lp file. Otherwise, variable + indices are used. + + Returns: + The path to the lp file. """ if file_path is None: with NamedTemporaryFile( diff --git a/src/pyoframe/solvers.py b/src/pyoframe/solvers.py index 5cc504b..2fc6ff8 100644 --- a/src/pyoframe/solvers.py +++ b/src/pyoframe/solvers.py @@ -5,7 +5,7 @@ from abc import abstractmethod, ABC from functools import lru_cache from pathlib import Path -from typing import Any, Dict, List, Optional, Type, Union, TYPE_CHECKING +from typing import Any, Dict, Optional, Type, Union, TYPE_CHECKING import polars as pl @@ -50,7 +50,6 @@ def solve( solver=None, directory: Optional[Union[Path, str]] = None, use_var_names=False, - env=None, log_fn=None, warmstart_fn=None, basis_fn=None, @@ -68,8 +67,13 @@ def solve( raise ValueError(f"Solver {solver} not recognized or supported.") solver_cls = solver_registry[solver] - m.solver = solver_cls(m, log_to_console) - m.solver_model = m.solver.create_solver_model(directory, use_var_names, env) + m.solver = solver_cls( + m, + log_to_console, + params={param: value for param, value in m.params}, + directory=directory, + ) + m.solver_model = m.solver.create_solver_model(use_var_names) m.solver.solver_model = m.solver_model for attr_container in [m.variables, m.constraints, [m]]: @@ -77,9 +81,6 @@ def solve( for param_name, param_value in container.attr: m.solver.set_attr(container, param_name, param_value) - for param, value in m.params: - m.solver.set_param(param, value) - result = m.solver.solve(log_fn, warmstart_fn, basis_fn, solution_file) result = m.solver.process_result(result) m.result = result @@ -99,20 +100,19 @@ def solve( class Solver(ABC): - def __init__(self, model, log_to_console): + def __init__(self, model: "Model", log_to_console, params, directory): self._model = model self.solver_model: Optional[Any] = None - self.log_to_console = log_to_console + self.log_to_console: bool = log_to_console + self.params = params + self.directory = directory @abstractmethod - def create_solver_model(self, directory, use_var_names, env) -> Any: ... + def create_solver_model(self, use_var_names) -> Any: ... @abstractmethod def set_attr(self, element, param_name, param_value): ... - @abstractmethod - def set_param(self, param_name, param_value): ... - @abstractmethod def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result: ... @@ -135,12 +135,25 @@ def _get_all_rc(self): ... @abstractmethod def _get_all_slack(self): ... + def dispose(self): + """ + Clean up any resources that wouldn't be cleaned up by the garbage collector. + + For now, this is only used by the Gurobi solver to call .dispose() on the solver model and Gurobi environment + which helps close a connection to the Gurobi Computer Server. Note that this effectively disables commands that + need access to the solver model (like .slack and .RC) + """ + class FileBasedSolver(Solver): - def create_solver_model( - self, directory: Optional[Union[Path, str]], use_var_names, env - ) -> Any: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.problem_file: Optional[Path] = None + self.keep_files = self.directory is not None + + def create_solver_model(self, use_var_names) -> Any: problem_file = None + directory = self.directory if directory is not None: if isinstance(directory, str): directory = Path(directory) @@ -150,12 +163,14 @@ def create_solver_model( self._model.name if self._model.name is not None else "pyoframe-problem" ) problem_file = directory / f"{filename}.lp" - problem_file = self._model.to_file(problem_file, use_var_names=use_var_names) + self.problem_file = self._model.to_file( + problem_file, use_var_names=use_var_names + ) assert self._model.io_mappers is not None - return self.create_solver_model_from_lp(problem_file, env) + return self.create_solver_model_from_lp() @abstractmethod - def create_solver_model_from_lp(self, problem_file: Path, env) -> Any: ... + def create_solver_model_from_lp(self) -> Any: ... def set_attr(self, element, param_name, param_value): if isinstance(param_value, pl.DataFrame): @@ -216,29 +231,27 @@ class GurobiSolver(FileBasedSolver): 17: "internal_solver_error", } - def create_solver_model_from_lp(self, problem_fn, env) -> Any: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.log_to_console: + self.params["LogToConsole"] = 0 + self.env = None + + def create_solver_model_from_lp(self) -> Any: """ Solve a linear problem using the gurobi solver. This function communicates with gurobi using the gurubipy package. """ + assert self.problem_file is not None + self.env = gurobipy.Env(params=self.params) - if env is None: - if self.log_to_console: - env = gurobipy.Env() - else: - # See https://support.gurobi.com/hc/en-us/articles/360044784552-How-do-I-suppress-all-console-output-from-Gurobi - env = gurobipy.Env(empty=True) - env.setParam("LogToConsole", 0) - env.start() + m = gurobipy.read(_path_to_str(self.problem_file), env=self.env) + if not self.keep_files: + self.problem_file.unlink() - m = gurobipy.read(_path_to_str(problem_fn), env=env) return m - def set_param(self, param_name, param_value): - assert self.solver_model is not None - self.solver_model.setParam(param_name, param_value) - @lru_cache def _get_var_mapping(self): assert self.solver_model is not None @@ -350,6 +363,12 @@ def _get_all_slack_unmapped(self): } ) + def dispose(self): + if self.solver_model is not None: + self.solver_model.dispose() + if self.env is not None: + self.env.dispose() + def _path_to_str(path: Union[Path, str]) -> str: """