From 0feaf9dc02d7fadb4c11cbd897f6338a54a0acfb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 28 Oct 2023 16:26:23 +0200 Subject: [PATCH 1/4] add MindOpt solver --- .github/workflows/CI.yaml | 2 +- doc/api.rst | 1 + doc/index.rst | 1 + doc/prerequisites.rst | 1 + doc/release_notes.rst | 8 +++- linopy/solvers.py | 85 ++++++++++++++++++++++++++++++++++++++- setup.py | 1 + 7 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index eb909cd2..5313a18a 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -36,7 +36,7 @@ jobs: run: | sudo apt-get install glpk-utils sudo apt-get install coinor-cbc - pip install highspy + pip install highspy mindoptpy - name: Install macos dependencies if: matrix.os == 'macos-latest' diff --git a/doc/api.rst b/doc/api.rst index a09db12d..e3cec401 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -133,6 +133,7 @@ Solvers solvers.run_cplex solvers.run_gurobi solvers.run_xpress + solvers.run_mindopt Solving ======= diff --git a/doc/index.rst b/doc/index.rst index 56b3b379..79b95687 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -42,6 +42,7 @@ flexible data-handling features: - `Cbc `__ - `GLPK `__ - `HiGHS `__ + - `MindOpt `__ - `Gurobi `__ - `Xpress `__ - `Cplex `__ diff --git a/doc/prerequisites.rst b/doc/prerequisites.rst index bd54dc14..a6a7fa0f 100644 --- a/doc/prerequisites.rst +++ b/doc/prerequisites.rst @@ -36,6 +36,7 @@ Linopy won't work without a solver. Currently, the following solvers are support - `Gurobi `__ - closed source, commercial, very fast - `Xpress `__ - closed source, commercial, very fast - `Cplex `__ - closed source, commercial, very fast +- `MindOpt `__ - For a subset of the solvers, Linopy provides a wrapper. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 346d7191..f9042ff3 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -1,8 +1,12 @@ Release Notes ============= -.. Upcoming Release -.. ---------------- +Upcoming Release +---------------- + +**New Features** + +* Support for MindOpt solver was added. Version 0.3.0 ------------- diff --git a/linopy/solvers.py b/linopy/solvers.py index 9d5b959b..69fd4b06 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -24,7 +24,7 @@ TerminationCondition, ) -quadratic_solvers = ["gurobi", "xpress", "cplex", "highs"] +quadratic_solvers = ["gurobi", "xpress", "cplex", "highs", "mindopt"] available_solvers = [] @@ -54,11 +54,15 @@ import xpress available_solvers.append("xpress") +with contextlib.suppress(ImportError): + import mindoptpy + + available_solvers.append("mindopt") logger = logging.getLogger(__name__) io_structure = dict( - lp_file={"gurobi", "xpress", "cbc", "glpk", "cplex"}, blocks={"pips"} + lp_file={"gurobi", "xpress", "cbc", "glpk", "cplex", "mindopt"}, blocks={"pips"} ) @@ -705,6 +709,83 @@ def get_solver_solution() -> Solution: return Result(status, solution, m) +def run_mindopt( + model, + io_api=None, + problem_fn=None, + solution_fn=None, + log_fn=None, + warmstart_fn=None, + basis_fn=None, + keep_files=False, + env=None, + **solver_options, +): + """ + Solve a linear problem using the MindOpt solver. + + https://solver.damo.alibaba.com/doc/en/html/index.html + + For more information on solver options, see + https://solver.damo.alibaba.com/doc/en/html/API2/param/index.html + """ + CONDITION_MAP = { + -1: "error", + 0: "unknown", + 1: "optimal", + 2: "infeasible", + 3: "unbounded", + 4: "infeasible_or_unbounded", + 5: "suboptimal", + } + + if io_api is not None and io_api not in ["lp", "mps"]: + logger.warning( + f"IO setting '{io_api}' not available for mindopt solver. " + "Falling back to `lp`." + ) + + problem_fn = model.to_file(problem_fn) + + problem_fn = maybe_convert_path(problem_fn) + log_fn = "" if not log_fn else maybe_convert_path(log_fn) + + env = mindoptpy.Env(log_fn) + env.start() + + m = mindoptpy.read(problem_fn, env) + + for k, v in solver_options.items(): + m.setParam(k, v) + + m.optimize() + + condition = m.status + termination_condition = CONDITION_MAP.get(condition, condition) + status = Status.from_termination_condition(termination_condition) + status.legacy_status = condition + + def get_solver_solution() -> Solution: + objective = m.objval + + sol = pd.Series({v.varname: v.X for v in m.getVars()}, dtype=float) + sol = set_int_index(sol) + + try: + dual = pd.Series({c.constrname: c.DualSoln for c in m.getConstrs()}) + dual = set_int_index(dual) + except mindoptpy.MindoptError: + logger.warning("Dual values of MILP couldn't be parsed") + dual = pd.Series(dtype=float) + + return Solution(sol, dual, objective) + + solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "mindopt") + + return Result(status, solution, m) + + def run_pips( model, io_api=None, diff --git a/setup.py b/setup.py index 7217211d..fdfa1ab6 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ "highspy", "cplex", "xpress", + "mindoptpy", ], }, classifiers=[ From 70fa442c16edfb8ceaa25bc0f62196b541ddebef Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 28 Oct 2023 16:37:23 +0200 Subject: [PATCH 2/4] test: add dictionary entry for time limit --- linopy/solvers.py | 2 +- test/test_optimization.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 69fd4b06..1a62d525 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -24,7 +24,7 @@ TerminationCondition, ) -quadratic_solvers = ["gurobi", "xpress", "cplex", "highs", "mindopt"] +quadratic_solvers = ["gurobi", "xpress", "cplex", "highs"] available_solvers = [] diff --git a/test/test_optimization.py b/test/test_optimization.py index 4322c5d7..47645a48 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -345,6 +345,7 @@ def test_solver_options(model, solver, io_api): "scip": {"time_limit": 1}, "xpress": {"maxtime": 1}, "highs": {"time_limit": 1}, + "mindopt": {"MaxTime": 1}, } status, condition = model.solve(solver, io_api=io_api, **time_limit_option[solver]) assert status == "ok" From fb882d70df10f5592d9738de7bec5483009f73aa Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 29 Oct 2023 18:08:03 +0100 Subject: [PATCH 3/4] add code for read/write model basis --- linopy/solvers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/linopy/solvers.py b/linopy/solvers.py index 1a62d525..a8d146b0 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -749,6 +749,8 @@ def run_mindopt( problem_fn = maybe_convert_path(problem_fn) log_fn = "" if not log_fn else maybe_convert_path(log_fn) + warmstart_fn = maybe_convert_path(warmstart_fn) + basis_fn = maybe_convert_path(basis_fn) env = mindoptpy.Env(log_fn) env.start() @@ -758,8 +760,20 @@ def run_mindopt( for k, v in solver_options.items(): m.setParam(k, v) + if warmstart_fn: + try: + m.read(warmstart_fn) + except mindoptpy.MindoptError as err: + logger.info("Model basis could not be read. Raised error:", err) + m.optimize() + if basis_fn: + try: + m.write(basis_fn) + except mindoptpy.MindoptError as err: + logger.info("No model basis stored. Raised error:", err) + condition = m.status termination_condition = CONDITION_MAP.get(condition, condition) status = Status.from_termination_condition(termination_condition) From 6500536370bed04b2002a08a65bf454fa5c9b721 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 31 Oct 2023 14:39:56 +0100 Subject: [PATCH 4/4] solvers: allow passing mindopt env --- linopy/solvers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 3a80a884..d4bdb326 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -855,7 +855,8 @@ def run_mindopt( warmstart_fn = maybe_convert_path(warmstart_fn) basis_fn = maybe_convert_path(basis_fn) - env = mindoptpy.Env(log_fn) + if env is None: + env = mindoptpy.Env(log_fn) env.start() m = mindoptpy.read(problem_fn, env)