diff --git a/linopy/constants.py b/linopy/constants.py index 9b4c69ea..5941d014 100644 --- a/linopy/constants.py +++ b/linopy/constants.py @@ -208,7 +208,7 @@ class Result: status: Status solution: Union[Solution, None] = None - solver_model: Union[Any, None] = None + solver_model: Any = None def __repr__(self) -> str: solver_model_string = ( diff --git a/linopy/model.py b/linopy/model.py index 60e47972..17b8ae8e 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -49,7 +49,7 @@ ) from linopy.matrices import MatrixAccessor from linopy.objective import Objective -from linopy.solvers import available_solvers, quadratic_solvers +from linopy.solvers import available_solvers, quadratic_solvers, Solver from linopy.types import ( ConstantLike, ConstraintLike, @@ -79,6 +79,9 @@ class Model: the optimization process. """ + solver_model: Any + solver_name: str + __slots__ = ( # containers "_variables", diff --git a/linopy/solvers.py b/linopy/solvers.py index a2c42764..bcc67550 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -149,6 +149,25 @@ class SolverName(enum.Enum): PIPS = "pips" +def path_to_string(path: Path) -> str: + """ + Convert a pathlib.Path to a string. + """ + return str(path.resolve()) + + +def read_sense_from_problem_file(problem_fn: Path | str): + f = open(problem_fn, "r").read() + return "min" if "min" in f else "max" + + +def read_io_api_from_problem_file(problem_fn: Path | str): + if isinstance(problem_fn, Path): + return path_to_string(problem_fn).split(".")[-1] + else: + return problem_fn.split(".")[-1] + + class Solver: """ A solver class for the solving of a given linear problem from an input file. @@ -171,12 +190,6 @@ def __init__( f"Keyword argument `io_api` has to be one of {IO_APIS} or None" ) - def path_to_string(self, path: Path) -> str: - """ - Convert a pathlib.Path to a string. - """ - return str(path.resolve()) - def safe_get_solution(self, status: Status, func: Callable) -> Solution: """ Get solution from function call, if status is unknown still try to run it. @@ -202,13 +215,6 @@ def maybe_adjust_objective_sign(self, solution: Solution) -> None: ) solution.objective *= -1 - def read_sense_from_problem_file(self, problem_fn): - f = open(problem_fn).read() - return "min" if "min" in f else "max" - - def read_io_api_from_problem_file(self, problem_fn): - return self.path_to_string(problem_fn).split(".")[-1] - def solve_problem_file(self): """ Function to solve a given linear problem using a specific solver from an input problem file. @@ -283,8 +289,8 @@ def solve_problem_file( "No solution file specified. For solving with CBC this is necessary." ) - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) # printingOptions is about what goes in solution file command = f"cbc -printingOptions all -import {problem_fn} " @@ -435,8 +441,8 @@ def solve_problem_file( "No solution file specified. For solving with GLPK this is necessary." ) - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) Path(solution_fn).parent.mkdir(exist_ok=True) suffix = problem_fn.suffix[1:] @@ -594,14 +600,18 @@ def solve_problem_file( # check if problem file name is specified if problem_fn is None and self.io_api != "direct": raise ValueError("No problem file specified.") + elif problem_fn is not None: + # for highs solver, the path needs to be a string + problem_fn_ = path_to_string(problem_fn) if self.io_api != "direct": - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + # for non-direct execution retrieve sense and io_api information + self.sense = read_sense_from_problem_file(problem_fn_) + self.io_api = read_io_api_from_problem_file(problem_fn_) if self.io_api is None or self.io_api in FILE_IO_APIS: h = highspy.Highs() - h.readModel(self.path_to_string(problem_fn)) + h.readModel(problem_fn_) elif self.io_api == "direct" and model is not None: h = model.to_highspy() else: @@ -626,16 +636,16 @@ def solve_problem_file( if log_fn is None and model is not None: log_fn = model.solver_dir / "highs.log" if log_fn is not None: - self.solver_options["log_file"] = self.path_to_string(log_fn) + self.solver_options["log_file"] = path_to_string(log_fn) logger.info(f"Log file at {self.solver_options['log_file']}") for k, v in self.solver_options.items(): h.setOptionValue(k, v) if warmstart_fn is not None and warmstart_fn.suffix == ".sol": - h.readSolution(self.path_to_string(warmstart_fn), 0) + h.readSolution(path_to_string(warmstart_fn), 0) elif warmstart_fn: - h.readBasis(self.path_to_string(warmstart_fn)) + h.readBasis(path_to_string(warmstart_fn)) h.run() @@ -645,10 +655,10 @@ def solve_problem_file( status.legacy_status = condition if basis_fn: - h.writeBasis(self.path_to_string(basis_fn)) + h.writeBasis(path_to_string(basis_fn)) if solution_fn: - h.writeSolution(self.path_to_string(solution_fn), 0) + h.writeSolution(path_to_string(solution_fn), 0) def get_solver_solution() -> Solution: objective = h.getObjectiveValue() @@ -742,17 +752,19 @@ def solve_problem_file( # check if problem file name is specified if problem_fn is None and self.io_api != "direct": raise ValueError("No problem file specified.") + elif problem_fn is not None: + problem_fn_ = path_to_string(problem_fn) if self.io_api != "direct": - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn_) + self.io_api = read_io_api_from_problem_file(problem_fn_) with contextlib.ExitStack() as stack: if self.env is None: self.env = stack.enter_context(gurobipy.Env()) if self.io_api is None or self.io_api in FILE_IO_APIS: - m = gurobipy.read(self.path_to_string(problem_fn), env=self.env) + m = gurobipy.read(problem_fn_, env=self.env) elif self.io_api == "direct" and model is not None: m = model.to_gurobipy(env=self.env) else: @@ -764,21 +776,21 @@ def solve_problem_file( for key, value in self.solver_options.items(): m.setParam(key, value) if log_fn is not None: - m.setParam("logfile", self.path_to_string(log_fn)) + m.setParam("logfile", path_to_string(log_fn)) if warmstart_fn is not None: - m.read(self.path_to_string(warmstart_fn)) + m.read(path_to_string(warmstart_fn)) m.optimize() if basis_fn is not None: try: - m.write(self.path_to_string(basis_fn)) + m.write(path_to_string(basis_fn)) except gurobipy.GurobiError as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn is not None and solution_fn.suffix == ".sol": try: - m.write(self.path_to_string(solution_fn)) + m.write(path_to_string(solution_fn)) except gurobipy.GurobiError as err: logger.info("Unable to save solution file. Raised error: %s", err) @@ -870,13 +882,13 @@ def solve_problem_file( if problem_fn is None: raise ValueError("No problem file specified.") - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) m = cplex.Cplex() if log_fn is not None: - log_f = open(self.path_to_string(log_fn), "w") + log_f = open(path_to_string(log_fn), "w") m.set_results_stream(log_f) m.set_warning_stream(log_f) m.set_error_stream(log_f) @@ -889,10 +901,10 @@ def solve_problem_file( param = getattr(param, key_layer) param.set(value) - m.read(self.path_to_string(problem_fn)) + m.read(path_to_string(problem_fn)) if warmstart_fn is not None: - m.start.read_basis(self.path_to_string(warmstart_fn)) + m.start.read_basis(path_to_string(warmstart_fn)) is_lp = m.problem_type[m.get_problem_type()] == "LP" @@ -901,7 +913,7 @@ def solve_problem_file( if solution_fn is not None: try: - m.solution.write(self.path_to_string(solution_fn)) + m.solution.write(path_to_string(solution_fn)) except cplex.exceptions.errors.CplexSolverError as err: logger.info("Unable to save solution file. Raised error: %s", err) @@ -916,7 +928,7 @@ def solve_problem_file( def get_solver_solution() -> Solution: if basis_fn and is_lp: try: - m.solution.basis.write(self.path_to_string(basis_fn)) + m.solution.basis.write(path_to_string(basis_fn)) except cplex.exceptions.errors.CplexSolverError: logger.info("No model basis stored") @@ -996,11 +1008,11 @@ def solve_problem_file( if problem_fn is None: raise ValueError("No problem file specified.") - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) m = scip.Model() - m.readProblem(self.path_to_string(problem_fn)) + m.readProblem(path_to_string(problem_fn)) if self.solver_options is not None: emphasis = self.solver_options.pop("setEmphasis", None) @@ -1018,7 +1030,7 @@ def solve_problem_file( m.setParams(self.solver_options) if log_fn is not None: - m.setLogfile(self.path_to_string(log_fn)) + m.setLogfile(path_to_string(log_fn)) if warmstart_fn: logger.warning("Warmstart not implemented for SCIP") @@ -1033,7 +1045,7 @@ def solve_problem_file( if solution_fn: try: - m.writeSol(m.getBestSol(), filename=self.path_to_string(solution_fn)) + m.writeSol(m.getBestSol(), filename=path_to_string(solution_fn)) except FileNotFoundError as err: logger.warning("Unable to save solution file. Raised error: %s", err) @@ -1136,32 +1148,32 @@ def solve_problem_file( if problem_fn is None: raise ValueError("No problem file specified.") - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) m = xpress.problem() - m.read(self.path_to_string(problem_fn)) + m.read(path_to_string(problem_fn)) m.setControl(self.solver_options) if log_fn is not None: - m.setlogfile(self.path_to_string(log_fn)) + m.setlogfile(path_to_string(log_fn)) if warmstart_fn is not None: - m.readbasis(self.path_to_string(warmstart_fn)) + m.readbasis(path_to_string(warmstart_fn)) m.solve() if basis_fn is not None: try: - m.writebasis(self.path_to_string(basis_fn)) + m.writebasis(path_to_string(basis_fn)) except Exception as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn is not None: try: # TODO: possibly update saving of solution file - m.tofile(self.path_to_string(solution_fn), filetype="sol") + m.tofile(path_to_string(solution_fn), filetype="sol") except Exception as err: logger.info("Unable to save solution file. Raised error: %s", err) @@ -1264,8 +1276,8 @@ def solve_problem_file( raise ValueError("No problem file specified.") if self.io_api != "direct": - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) with contextlib.ExitStack() as stack: if self.env is None: @@ -1273,7 +1285,7 @@ def solve_problem_file( with self.env.Task() as m: if self.io_api is None or self.io_api in FILE_IO_APIS: - m.readdata(self.path_to_string(problem_fn)) + m.readdata(path_to_string(problem_fn)) elif self.io_api == "direct" and model is not None: model.to_mosek(m) else: @@ -1286,7 +1298,7 @@ def solve_problem_file( if log_fn is not None: m.linkfiletostream( - mosek.streamtype.log, self.path_to_string(log_fn), 0 + mosek.streamtype.log, path_to_string(log_fn), 0 ) else: m.set_Stream(mosek.streamtype.log, sys.stdout.write) @@ -1298,7 +1310,7 @@ def solve_problem_file( skx = [mosek.stakey.low] * m.getnumvar() skc = [mosek.stakey.bas] * m.getnumcon() - with open(self.path_to_string(warmstart_fn)) as f: + with open(path_to_string(warmstart_fn)) as f: for line in f: if line.startswith("NAME "): break @@ -1351,7 +1363,7 @@ def solve_problem_file( if basis_fn is not None: if m.solutiondef(mosek.soltype.bas): - with open(self.path_to_string(basis_fn), "w") as f: + with open(path_to_string(basis_fn), "w") as f: f.write(f"NAME {basis_fn}\n") skc = [ @@ -1411,7 +1423,7 @@ def solve_problem_file( if solution_fn is not None: try: m.writesolution( - mosek.soltype.bas, self.path_to_string(solution_fn) + mosek.soltype.bas, path_to_string(solution_fn) ) except mosek.Error as err: logger.info( @@ -1520,36 +1532,36 @@ def solve_problem_file( if problem_fn is None: raise ValueError("No problem file specified.") - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) if self.env is None: self.env = coptpy.Envr() m = self.env.createModel() - m.read(self.path_to_string(problem_fn)) + m.read(path_to_string(problem_fn)) if log_fn is not None: - m.setLogFile(self.path_to_string(log_fn)) + m.setLogFile(path_to_string(log_fn)) for k, v in self.solver_options.items(): m.setParam(k, v) if warmstart_fn is not None: - m.readBasis(self.path_to_string(warmstart_fn)) + m.readBasis(path_to_string(warmstart_fn)) m.solve() if basis_fn and m.HasBasis: try: - m.write(self.path_to_string(basis_fn)) + m.write(path_to_string(basis_fn)) except coptpy.CoptError as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn: try: - m.write(self.path_to_string(solution_fn)) + m.write(path_to_string(solution_fn)) except coptpy.CoptError as err: logger.info("No model solution stored. Raised error: %s", err) @@ -1654,8 +1666,8 @@ def solve_problem_file( if problem_fn is None: raise ValueError("No problem file specified.") - self.sense = self.read_sense_from_problem_file(problem_fn) - self.io_api = self.read_io_api_from_problem_file(problem_fn) + self.sense = read_sense_from_problem_file(problem_fn) + self.io_api = read_io_api_from_problem_file(problem_fn) if model is not None: if ( @@ -1666,17 +1678,17 @@ def solve_problem_file( ) if self.env is None: - self.env = mindoptpy.Env(self.path_to_string(log_fn) if log_fn else "") + self.env = mindoptpy.Env(path_to_string(log_fn) if log_fn else "") self.env.start() - m = mindoptpy.read(self.path_to_string(problem_fn), self.env) + m = mindoptpy.read(path_to_string(problem_fn), self.env) for k, v in self.solver_options.items(): m.setParam(k, v) if warmstart_fn: try: - m.read(self.path_to_string(warmstart_fn)) + m.read(path_to_string(warmstart_fn)) except mindoptpy.MindoptError as err: logger.info("Model basis could not be read. Raised error: %s", err) @@ -1684,13 +1696,13 @@ def solve_problem_file( if basis_fn: try: - m.write(self.path_to_string(basis_fn)) + m.write(path_to_string(basis_fn)) except mindoptpy.MindoptError as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn: try: - m.write(self.path_to_string(solution_fn)) + m.write(path_to_string(solution_fn)) except mindoptpy.MindoptError as err: logger.info("No model solution stored. Raised error: %s", err)