From a0935599b156d0cc9b0d507b9b374474af7f4b07 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 7 Dec 2022 14:11:03 +0100 Subject: [PATCH 1/4] support writing MPS files --- doc/release_notes.rst | 6 ++++-- linopy/io.py | 44 ++++++++++++++++++++++++++++++------------- test/test_io.py | 9 +++++++++ 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index f4444d0c..2718ab26 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -1,8 +1,10 @@ Release Notes ============= -.. Upcoming Release -.. ---------------- +Upcoming Release +---------------- + +* Support exporting problems to MPS file via fast highspy MPS-writer. Version 0.0.15 diff --git a/linopy/io.py b/linopy/io.py index 6d4073bd..b96d08bf 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -165,26 +165,40 @@ def binaries_to_file(m, f, log=False): def to_file(m, fn): """ - Write out a model to a lp file. + Write out a model to a lp or mps file. """ - fn = m.get_problem_file(fn) + fn = Path(m.get_problem_file(fn)) - if os.path.exists(fn): - os.remove(fn) # ensure a clear file + if fn.exists(): + fn.unlink() - log = m._xCounter > 10000 + if fn.suffix == ".lp": - with open(fn, mode="w") as f: + log = m._xCounter > 10000 - start = time.time() + with open(fn, mode="w") as f: - objective_to_file(m, f, log) - constraints_to_file(m, f, log) - bounds_to_file(m, f, log) - binaries_to_file(m, f, log) - f.write("end\n") + start = time.time() - logger.info(f" Writing time: {round(time.time()-start, 2)}s") + objective_to_file(m, f, log) + constraints_to_file(m, f, log) + bounds_to_file(m, f, log) + binaries_to_file(m, f, log) + f.write("end\n") + + logger.info(f" Writing time: {round(time.time()-start, 2)}s") + + elif fn.suffix == ".mps": + # Use very fast highspy implementation + # Might be replaced by custom writer, however needs C bindings for performance + h = m.to_highspy() + h.writeModel(str(fn)) + + else: + + raise ValueError( + f"Cannot write problem to {fn}, file format `{fn.suffix}` not supported." + ) return fn @@ -262,6 +276,10 @@ def to_highspy(m): lower = np.where(M.sense != "<", M.b, -np.inf) upper = np.where(M.sense != ">", M.b, np.inf) h.addRows(num_cons, lower, upper, A.nnz, A.indptr, A.indices, A.data) + lp = h.getLp() + lp.row_names_ = "c" + M.clabels.astype(str).astype(object) + lp.col_names_ = "x" + M.vlabels.astype(str).astype(object) + h.passModel(lp) return h diff --git a/test/test_io.py b/test/test_io.py index e2b778ca..e46a7349 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -84,6 +84,15 @@ def test_to_file(tmp_path): gurobipy.read(str(fn)) + fn = tmp_path / "test.mps" + m.to_file(fn) + + gurobipy.read(str(fn)) + + with pytest.raises(ValueError): + fn = tmp_path / "test.failedtype" + m.to_file(fn) + @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed") def test_to_gurobipy(tmp_path): From 0b3eeb446546325d1c0400cee4af552860e29dde Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 7 Dec 2022 15:20:05 +0100 Subject: [PATCH 2/4] setup.py: include highspy --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a289b0bb..0cf50f99 100755 --- a/setup.py +++ b/setup.py @@ -55,8 +55,7 @@ "pre-commit", "paramiko", "gurobipy", - # until available for windows/mac - # "highspy", + "highspy", "cplex", "xpress", ], From beffc901c654e3600f0332f45394f1a959ed31c2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 9 Dec 2022 14:14:06 +0100 Subject: [PATCH 3/4] make MPS export dependent on whether highspy is available --- linopy/io.py | 15 +++++++++++---- setup.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/linopy/io.py b/linopy/io.py index b96d08bf..5005e683 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -15,6 +15,8 @@ from numpy import asarray, concatenate, ones_like, zeros_like from tqdm import tqdm +from linopy import solvers + logger = logging.getLogger(__name__) @@ -189,10 +191,15 @@ def to_file(m, fn): logger.info(f" Writing time: {round(time.time()-start, 2)}s") elif fn.suffix == ".mps": - # Use very fast highspy implementation - # Might be replaced by custom writer, however needs C bindings for performance - h = m.to_highspy() - h.writeModel(str(fn)) + if "highs" in solvers.available_solvers: + # Use very fast highspy implementation + # Might be replaced by custom writer, however needs C bindings for performance + h = m.to_highspy() + h.writeModel(str(fn)) + else: + raise RuntimeError( + "Package highspy not installed. This is required to exporting to MPS file." + ) else: diff --git a/setup.py b/setup.py index 0cf50f99..a289b0bb 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,8 @@ "pre-commit", "paramiko", "gurobipy", - "highspy", + # until available for windows/mac + # "highspy", "cplex", "xpress", ], From 66404bdc97627ee05418b533d7b09341cb112c25 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 9 Dec 2022 15:16:41 +0100 Subject: [PATCH 4/4] test_io.py: test mps export only when highspy is installed --- test/test_io.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/test/test_io.py b/test/test_io.py index e46a7349..be9018ca 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -15,6 +15,22 @@ from linopy.io import float_to_str, int_to_str +@pytest.fixture +def m(): + import gurobipy + + m = Model() + + x = m.add_variables(4, pd.Series([8, 10])) + y = m.add_variables(0, pd.DataFrame([[1, 2], [3, 4], [5, 6]])) + + m.add_constraints(x + y, "<=", 10) + + m.add_objective(2 * x + 3 * y) + + return m + + def test_str_arrays(): m = Model() @@ -67,28 +83,30 @@ def test_to_netcdf(tmp_path): @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed") -def test_to_file(tmp_path): +def test_to_file_lp(m, tmp_path): import gurobipy - m = Model() - - x = m.add_variables(4, pd.Series([8, 10])) - y = m.add_variables(0, pd.DataFrame([[1, 2], [3, 4], [5, 6]])) - - m.add_constraints(x + y, "<=", 10) - - m.add_objective(2 * x + 3 * y) - fn = tmp_path / "test.lp" m.to_file(fn) gurobipy.read(str(fn)) + +@pytest.mark.skipif( + not {"gurobi", "highs"}.issubset(available_solvers), + reason="Gurobipy of highspy not installed", +) +def test_to_file_mps(m, tmp_path): + import gurobipy + fn = tmp_path / "test.mps" m.to_file(fn) gurobipy.read(str(fn)) + +@pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed") +def test_to_file_invalid(m, tmp_path): with pytest.raises(ValueError): fn = tmp_path / "test.failedtype" m.to_file(fn)