From 41cb93c1681bbf5a6e95e4b25c95e4084f3ff81c Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 16:30:38 +0100 Subject: [PATCH 01/21] Initial support fro reading/writing mps and gz files. --- .../problems/quadratic_program.py | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index bcbfc5d10..9590e967b 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -16,8 +16,11 @@ from collections.abc import Sequence from enum import Enum from math import isclose -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import Dict, List, Optional, Tuple, Union, cast, Callable, List from warnings import warn +from gzip import open as gzip_open +from pathlib import Path +from tempfile import TemporaryFile import numpy as np from docplex.mp.model_reader import ModelReader @@ -921,16 +924,68 @@ def export_as_lp_string(self) -> str: from ..translators.docplex_mp import to_docplex_mp return to_docplex_mp(self).export_as_lp_string() + + def export_as_mps_string(self) -> str: + """Returns the quadratic program as a string of LP format. + + Returns: + A string representing the quadratic program. + """ + # pylint: disable=cyclic-import + from ..translators.docplex_mp import to_docplex_mp + + return to_docplex_mp(self).export_as_mps_string() + + @_optionals.HAS_CPLEX.require_in_call + def _read_from_file(self, filename : str, extensions : List[str], name_parse_fun : Callable) -> None: + """Loads a quadratic program from an LP or MPS file. Also deals with + gzipped files. + + Args: + filename: The filename of the file to be loaded. + name_parse_fun: Function that parses the model name from the input file. + + Raises: + FileNotFoundError: If the file does not exist. + IOError: If the file type is not recognized or not supported. + + Note: + This method requires CPLEX to be installed and present in ``PYTHONPATH``. + """ + + # check whether this file type is supported + extension = "".join(Path(filename).suffixes) + if extension.removesuffix(".gz") not in extensions: + raise IOError("File type not supported for model reading.") + + # uncompress and parse + if extension.endswith(".gz"): + with gzip_open(filename, "rb") as compressed: + with TemporaryFile(suffix=extension[:-2]) as uncompressed: + uncompressed.write(compressed.read()) + + model = ModelReader().read(filename, + model_name=name_parse_fun(uncompressed.name) if name_parse_fun is not None else None) + else: + model = ModelReader().read(filename, + model_name=name_parse_fun(uncompressed.name) if name_parse_fun is not None else None) + + # pylint: disable=cyclic-import + from ..translators.docplex_mp import from_docplex_mp + + other = from_docplex_mp(model) + self._copy_from(other, include_name=True) @_optionals.HAS_CPLEX.require_in_call def read_from_lp_file(self, filename: str) -> None: - """Loads the quadratic program from a LP file. + """Loads the quadratic program from a LP file (may be gzip'ed). Args: filename: The filename of the file to be loaded. Raises: FileNotFoundError: If the file does not exist. + IOError: If the file type is not recognized or not supported. Note: This method requires CPLEX to be installed and present in ``PYTHONPATH``. @@ -950,12 +1005,23 @@ def _parse_problem_name(filename: str) -> str: break return model_name - # pylint: disable=cyclic-import - from ..translators.docplex_mp import from_docplex_mp + self._read_from_file(filename, [".lp"], _parse_problem_name) - model = ModelReader().read(filename, model_name=_parse_problem_name(filename)) - other = from_docplex_mp(model) - self._copy_from(other, include_name=True) + @_optionals.HAS_CPLEX.require_in_call + def read_from_mps_file(self, filename: str) -> None: + """Loads the quadratic program from a MPS file (may be gzip'ed). + + Args: + filename: The filename of the file to be loaded. + + Raises: + FileNotFoundError: If the file does not exist. + IOError: If the file type is not recognized or not supported. + + Note: + This method requires CPLEX to be installed and present in ``PYTHONPATH``. + """ + self._read_from_file(filename, [".mps"], None) def write_to_lp_file(self, filename: str) -> None: """Writes the quadratic program to an LP file. @@ -975,6 +1041,24 @@ def write_to_lp_file(self, filename: str) -> None: mdl = to_docplex_mp(self) mdl.export_as_lp(filename) + def write_to_mps_file(self, filename: str) -> None: + """Writes the quadratic program to an MPS file. + + Args: + filename: The filename of the file the model is written to. + If filename is a directory, file name 'my_problem.mps' is appended. + If filename does not end with '.mps', suffix '.mps' is appended. + + Raises: + OSError: If this cannot open a file. + DOcplexException: If filename is an empty string + """ + # pylint: disable=cyclic-import + from ..translators.docplex_mp import to_docplex_mp + + mdl = to_docplex_mp(self) + mdl.export_as_mps(filename) + def substitute_variables( self, constants: Optional[Dict[Union[str, int], float]] = None, From b45438f55b723a00595e81372a69dd336798ba96 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 16:49:28 +0100 Subject: [PATCH 02/21] Ran black. --- .../problems/quadratic_program.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 9590e967b..80dbb77b1 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -924,7 +924,7 @@ def export_as_lp_string(self) -> str: from ..translators.docplex_mp import to_docplex_mp return to_docplex_mp(self).export_as_lp_string() - + def export_as_mps_string(self) -> str: """Returns the quadratic program as a string of LP format. @@ -935,16 +935,18 @@ def export_as_mps_string(self) -> str: from ..translators.docplex_mp import to_docplex_mp return to_docplex_mp(self).export_as_mps_string() - + @_optionals.HAS_CPLEX.require_in_call - def _read_from_file(self, filename : str, extensions : List[str], name_parse_fun : Callable) -> None: - """Loads a quadratic program from an LP or MPS file. Also deals with + def _read_from_file( + self, filename: str, extensions: List[str], name_parse_fun: Callable + ) -> None: + """Loads a quadratic program from an LP or MPS file. Also deals with gzipped files. Args: filename: The filename of the file to be loaded. name_parse_fun: Function that parses the model name from the input file. - + Raises: FileNotFoundError: If the file does not exist. IOError: If the file type is not recognized or not supported. @@ -964,15 +966,23 @@ def _read_from_file(self, filename : str, extensions : List[str], name_parse_fun with TemporaryFile(suffix=extension[:-2]) as uncompressed: uncompressed.write(compressed.read()) - model = ModelReader().read(filename, - model_name=name_parse_fun(uncompressed.name) if name_parse_fun is not None else None) + model = ModelReader().read( + filename, + model_name=name_parse_fun(uncompressed.name) + if name_parse_fun is not None + else None, + ) else: - model = ModelReader().read(filename, - model_name=name_parse_fun(uncompressed.name) if name_parse_fun is not None else None) - + model = ModelReader().read( + filename, + model_name=name_parse_fun(uncompressed.name) + if name_parse_fun is not None + else None, + ) + # pylint: disable=cyclic-import from ..translators.docplex_mp import from_docplex_mp - + other = from_docplex_mp(model) self._copy_from(other, include_name=True) From 6f6db0ba9f460c7efce62e26aa7300eb881e7046 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 16:55:39 +0100 Subject: [PATCH 03/21] Ran tox; added unit test. --- .../problems/quadratic_program.py | 2 +- test/translators/test_docplex_mp.py | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 80dbb77b1..3b89989d6 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -16,7 +16,7 @@ from collections.abc import Sequence from enum import Enum from math import isclose -from typing import Dict, List, Optional, Tuple, Union, cast, Callable, List +from typing import Dict, List, Optional, Tuple, Union, cast, Callable from warnings import warn from gzip import open as gzip_open from pathlib import Path diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index 3dbd7c6b2..a7af18d94 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -24,7 +24,7 @@ class TestDocplexMpTranslator(QiskitOptimizationTestCase): """Test from_docplex_mp and to_docplex_mp""" - def test_from_and_to(self): + def test_from_and_to_lp(self): """test from_docplex_mp and to_docplex_mp""" q_p = QuadraticProgram("test") q_p.binary_var(name="x") @@ -49,6 +49,31 @@ def test_from_and_to(self): mod.add(2 * x - z + 3 * y * z == 1, "q0") self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) + def test_from_and_to_mps(self): + """test from_docplex_mp and to_docplex_mp""" + q_p = QuadraticProgram("test") + q_p.binary_var(name="x") + q_p.integer_var(name="y", lowerbound=-2, upperbound=4) + q_p.continuous_var(name="z", lowerbound=-1.5, upperbound=3.2) + q_p.minimize( + constant=1, + linear={"x": 1, "y": 2}, + quadratic={("x", "y"): -1, ("z", "z"): 2}, + ) + q_p.linear_constraint({"x": 2, "z": -1}, "==", 1) + q_p.quadratic_constraint({"x": 2, "z": -1}, {("y", "z"): 3}, "==", 1) + q_p2 = from_docplex_mp(to_docplex_mp(q_p)) + self.assertEqual(q_p.export_as_mps_string(), q_p2.export_as_mps_string()) + + mod = Model("test") + x = mod.binary_var("x") + y = mod.integer_var(-2, 4, "y") + z = mod.continuous_var(-1.5, 3.2, "z") + mod.minimize(1 + x + 2 * y - x * y + 2 * z * z) + mod.add(2 * x - z == 1, "c0") + mod.add(2 * x - z + 3 * y * z == 1, "q0") + self.assertEqual(q_p.export_as_mps_string(), mod.export_as_mps_string()) + def test_from_without_variable_names(self): """test from_docplex_mp without explicit variable names""" mod = Model() From 7077fbc01e63be525cd7ea7f95c1f9950c1b778f Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 17:47:33 +0100 Subject: [PATCH 04/21] Make mps tests optional; support for 3.8. --- apps/mip2qubo.py | 18 ++++++++++++++++++ .../problems/quadratic_program.py | 8 ++++++-- test/problems/test_quadratic_program.py | 6 ++++-- test/translators/test_docplex_mp.py | 4 ++++ 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 apps/mip2qubo.py diff --git a/apps/mip2qubo.py b/apps/mip2qubo.py new file mode 100644 index 000000000..b1cdd0b71 --- /dev/null +++ b/apps/mip2qubo.py @@ -0,0 +1,18 @@ +import sys +import argparse + +from qiskit_optimization import QuadraticProgram + +### +# Read a larger LP file and convert it to other formats +### + +options = argparse.ArgumentParser("mip2qubo") +options.add_argument("problem") + +args = options.parse_args() + +# read LP file in Qiskit format +qp = QuadraticProgram() +qp.read_from_mps_file(args.problem) +qp.write_to_lp_file(str(args.problem).removesuffix(".mps.gz")) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 3b89989d6..05d65709a 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -925,6 +925,7 @@ def export_as_lp_string(self) -> str: return to_docplex_mp(self).export_as_lp_string() + @_optionals.HAS_CPLEX.require_in_call def export_as_mps_string(self) -> str: """Returns the quadratic program as a string of LP format. @@ -957,7 +958,10 @@ def _read_from_file( # check whether this file type is supported extension = "".join(Path(filename).suffixes) - if extension.removesuffix(".gz") not in extensions: + main_extension = extension + if main_extension.endswith('.gz'): + main_extension = main_extension[:-3] + if main_extension not in extensions: raise IOError("File type not supported for model reading.") # uncompress and parse @@ -975,7 +979,7 @@ def _read_from_file( else: model = ModelReader().read( filename, - model_name=name_parse_fun(uncompressed.name) + model_name=name_parse_fun(filename) if name_parse_fun is not None else None, ) diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index a98aba9e8..06a941359 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -740,10 +740,12 @@ def test_read_from_lp_file(self): """test read lp file""" try: q_p = QuadraticProgram() - with self.assertRaises(FileNotFoundError): + with self.assertRaises(IOError): q_p.read_from_lp_file("") - with self.assertRaises(FileNotFoundError): + with self.assertRaises(IOError): q_p.read_from_lp_file("no_file.txt") + with self.assertRaises(FileNotFoundError): + q_p.read_from_lp_file("no_file.lp") lp_file = self.get_resource_path("test_quadratic_program.lp", "problems/resources") q_p.read_from_lp_file(lp_file) self.assertEqual(q_p.name, "my problem") diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index a7af18d94..7a0b3390a 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -12,6 +12,9 @@ """Test from_docplex_mp and to_docplex_mp""" +import unittest +import qiskit_optimization.optionals as _optionals + from test.optimization_test_case import QiskitOptimizationTestCase from docplex.mp.model import Model @@ -49,6 +52,7 @@ def test_from_and_to_lp(self): mod.add(2 * x - z + 3 * y * z == 1, "q0") self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_from_and_to_mps(self): """test from_docplex_mp and to_docplex_mp""" q_p = QuadraticProgram("test") From 60abd5fc2b803b4426dc50a3c958349f73d69309 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 17:54:06 +0100 Subject: [PATCH 05/21] Fix spelling. --- .pylintdict | 3 +++ qiskit_optimization/problems/quadratic_program.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pylintdict b/.pylintdict index 9230a0687..6252e2003 100644 --- a/.pylintdict +++ b/.pylintdict @@ -81,6 +81,7 @@ gurobi gurobioptimizer gurobipy gutmann +gzip'ed hamilton hamiltonian hamiltonians @@ -125,6 +126,7 @@ minimizer minimumeigenoptimizer mmp mpm +mps multiset mypy nannicini @@ -233,6 +235,7 @@ transpiling travelling troyer tsplib +uncompress undirected upperbound variational diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 05d65709a..9edd198bf 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -942,7 +942,7 @@ def _read_from_file( self, filename: str, extensions: List[str], name_parse_fun: Callable ) -> None: """Loads a quadratic program from an LP or MPS file. Also deals with - gzipped files. + gzip'ed files. Args: filename: The filename of the file to be loaded. From 0779c55f3c304f49f4b520d82f4cd9f59b213a2c Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 17:54:27 +0100 Subject: [PATCH 06/21] Ran black. --- qiskit_optimization/problems/quadratic_program.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 9edd198bf..4b08b0247 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -959,7 +959,7 @@ def _read_from_file( # check whether this file type is supported extension = "".join(Path(filename).suffixes) main_extension = extension - if main_extension.endswith('.gz'): + if main_extension.endswith(".gz"): main_extension = main_extension[:-3] if main_extension not in extensions: raise IOError("File type not supported for model reading.") @@ -979,9 +979,7 @@ def _read_from_file( else: model = ModelReader().read( filename, - model_name=name_parse_fun(filename) - if name_parse_fun is not None - else None, + model_name=name_parse_fun(filename) if name_parse_fun is not None else None, ) # pylint: disable=cyclic-import From 81977fe94eb1e3c687c5c0011be30a54360b5afc Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 18:08:59 +0100 Subject: [PATCH 07/21] Changing imports for linter. --- test/translators/test_docplex_mp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index 7a0b3390a..f2154cb09 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -13,12 +13,12 @@ """Test from_docplex_mp and to_docplex_mp""" import unittest -import qiskit_optimization.optionals as _optionals from test.optimization_test_case import QiskitOptimizationTestCase from docplex.mp.model import Model +import qiskit_optimization.optionals as _optionals from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems import Constraint, QuadraticProgram from qiskit_optimization.translators.docplex_mp import from_docplex_mp, to_docplex_mp From 6a69aec0a566e523784e62c5d3cfb0bcc7a8f257 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 11 Dec 2023 23:53:02 +0100 Subject: [PATCH 08/21] Added fixes for gzip reading and tests. --- .../problems/quadratic_program.py | 43 +++- .../resources/test_quadratic_program.mps | 62 +++++ .../resources/test_quadratic_program.mps.gz | Bin 0 -> 411 bytes test/problems/test_quadratic_program.py | 221 ++++++++++++------ 4 files changed, 245 insertions(+), 81 deletions(-) create mode 100644 test/problems/resources/test_quadratic_program.mps create mode 100644 test/problems/resources/test_quadratic_program.mps.gz diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 4b08b0247..e4e955f7b 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -20,7 +20,7 @@ from warnings import warn from gzip import open as gzip_open from pathlib import Path -from tempfile import TemporaryFile +from tempfile import NamedTemporaryFile import numpy as np from docplex.mp.model_reader import ModelReader @@ -949,8 +949,7 @@ def _read_from_file( name_parse_fun: Function that parses the model name from the input file. Raises: - FileNotFoundError: If the file does not exist. - IOError: If the file type is not recognized or not supported. + IOError: If the file type is not recognized, not supported or the file is not found. Note: This method requires CPLEX to be installed and present in ``PYTHONPATH``. @@ -967,11 +966,13 @@ def _read_from_file( # uncompress and parse if extension.endswith(".gz"): with gzip_open(filename, "rb") as compressed: - with TemporaryFile(suffix=extension[:-2]) as uncompressed: + with NamedTemporaryFile(suffix=extension[:-3]) as uncompressed: uncompressed.write(compressed.read()) + uncompressed.seek(0) + uncompressed.flush() model = ModelReader().read( - filename, + uncompressed.name, model_name=name_parse_fun(uncompressed.name) if name_parse_fun is not None else None, @@ -996,8 +997,7 @@ def read_from_lp_file(self, filename: str) -> None: filename: The filename of the file to be loaded. Raises: - FileNotFoundError: If the file does not exist. - IOError: If the file type is not recognized or not supported. + IOError: If the file type is not recognized, not supported or the file is not found. Note: This method requires CPLEX to be installed and present in ``PYTHONPATH``. @@ -1033,7 +1033,21 @@ def read_from_mps_file(self, filename: str) -> None: Note: This method requires CPLEX to be installed and present in ``PYTHONPATH``. """ - self._read_from_file(filename, [".mps"], None) + + def _parse_problem_name(filename: str) -> str: + # Because docplex model reader uses the base name as model name, + # we parse the model name in the LP file manually. + # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html + prefix = "NAME " + model_name = "" + with open(filename, encoding="utf8") as file: + for line in file: + if line.startswith(prefix): + model_name = line[len(prefix) :].strip() + break + return model_name + + self._read_from_file(filename, [".mps"], _parse_problem_name) def write_to_lp_file(self, filename: str) -> None: """Writes the quadratic program to an LP file. @@ -1069,7 +1083,18 @@ def write_to_mps_file(self, filename: str) -> None: from ..translators.docplex_mp import to_docplex_mp mdl = to_docplex_mp(self) - mdl.export_as_mps(filename) + full_path = mdl.export_as_mps(filename) + + # docplex does not write the model's name out, so we do this here manually + with open(full_path, "r", encoding="utf8") as mps_file: + txt = mps_file.read() + + with open(full_path, "w", encoding="utf8") as mps_file: + for line in txt.splitlines(): + if line.startswith("NAME"): + mps_file.write(f"NAME {self._name}\n") + else: + mps_file.write(line + "\n") def substitute_variables( self, diff --git a/test/problems/resources/test_quadratic_program.mps b/test/problems/resources/test_quadratic_program.mps new file mode 100644 index 000000000..927c4023b --- /dev/null +++ b/test/problems/resources/test_quadratic_program.mps @@ -0,0 +1,62 @@ +* ENCODING=ISO-8859-1 +NAME my problem +ROWS + N obj1 + E lin_eq + L lin_leq + G lin_geq + E quad_eq + L quad_leq + G quad_geq +COLUMNS + MARK0000 'MARKER' 'INTORG' + x obj1 1 + x lin_eq 1 + x lin_leq 1 + x lin_geq 1 + x quad_eq 1 + x quad_leq 1 + x quad_geq 1 + y obj1 -1 + y lin_eq 2 + y lin_leq 2 + y lin_geq 2 + y quad_eq 1 + y quad_leq 1 + y quad_geq 1 + MARK0001 'MARKER' 'INTEND' + z obj1 10 +RHS + rhs obj1 -1 + rhs lin_eq 1 + rhs lin_leq 1 + rhs lin_geq 1 + rhs quad_eq 1 + rhs quad_leq 1 + rhs quad_geq 1 +BOUNDS + BV bnd x + LO bnd y -1 + UP bnd y 5 + LO bnd z -1 + UP bnd z 5 +QMATRIX + x x 1 + y z -1 + z y -1 +QCMATRIX quad_eq + x x 1 + y z -0.5 + z y -0.5 + z z 2 +QCMATRIX quad_leq + x x 1 + y z -0.5 + z y -0.5 + z z 2 +QCMATRIX quad_geq + x x 1 + y z -0.5 + z y -0.5 + z z 2 +ENDATA diff --git a/test/problems/resources/test_quadratic_program.mps.gz b/test/problems/resources/test_quadratic_program.mps.gz new file mode 100644 index 0000000000000000000000000000000000000000..d7a8b427629a6c4565aa505598bdbf4727a30ce9 GIT binary patch literal 411 zcmV;M0c8FkiwForh<9ZG19W9`bYF3GVPtY)bZKK>aB^>Fa$#*QZE$k{-IPIZf-n@v z@BI|-MzVwjOi0{e2L!^%pq&VAH#4GPTTrLV67kcoP(Z2J!eJNRiT}&5<@Mk9dIt`+ zi0xtb(VG!gH44e=3;_n%CSN+xK;QsmpPR*MR|ubpj4A{! z6KN?Zw)=fwE=u87f^sER3d$ji_;G;gFK7g2_^iN3{RDch~!x0HxrQ{Dyx-RZo z=p0|yKZMT&3%dAp0ACw5h!+&p$qNc%eBNk@pDNGG{2X=gB0sx$k)J%@&rgn*`pNN9 zKdXM$CH?GRoAvXtNlX0KH55Lvnc08*^jlNi*@^Er18rVD18rVD11G-UopHSM&NyCp zXA=@*o7y^=L$X;`>3WFh8%EC3dB+ZCQMety^yk6g_CE^g%l#!h-G3UGQRuyKFT>&L z3Ko3l_o%By$$gqytT1R=;6F=LwR>Yoi5e0oKvKu^8dIM*oFRvMy=nt`xw73ch F007dJ#9aUY literal 0 HcmV?d00001 diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 06a941359..e4235e252 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -735,6 +735,79 @@ def test_empty_objective(self): q_p.binary_var_list(3) _ = q_p.objective.evaluate_gradient({}) + def helper_test_read_problem_file(self, q_p: QuadraticProgram): + """evaluates the quadratic program read in file reading tests""" + self.assertEqual(q_p.name, "my problem") + self.assertEqual(q_p.get_num_vars(), 3) + self.assertEqual(q_p.get_num_binary_vars(), 1) + self.assertEqual(q_p.get_num_integer_vars(), 1) + self.assertEqual(q_p.get_num_continuous_vars(), 1) + self.assertEqual(q_p.get_num_linear_constraints(), 3) + self.assertEqual(q_p.get_num_quadratic_constraints(), 3) + + self.assertEqual(q_p.variables[0].name, "x") + self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) + self.assertEqual(q_p.variables[0].lowerbound, 0) + self.assertEqual(q_p.variables[0].upperbound, 1) + self.assertEqual(q_p.variables[1].name, "y") + self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) + self.assertEqual(q_p.variables[1].lowerbound, -1) + self.assertEqual(q_p.variables[1].upperbound, 5) + self.assertEqual(q_p.variables[2].name, "z") + self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) + self.assertEqual(q_p.variables[2].lowerbound, -1) + self.assertEqual(q_p.variables[2].upperbound, 5) + + self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) + self.assertEqual(q_p.objective.constant, 1) + self.assertDictEqual( + q_p.objective.linear.to_dict(use_name=True), {"x": 1, "y": -1, "z": 10} + ) + self.assertDictEqual( + q_p.objective.quadratic.to_dict(use_name=True), + {("x", "x"): 0.5, ("y", "z"): -1}, + ) + + cst = q_p.linear_constraints + self.assertEqual(cst[0].name, "lin_eq") + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, "lin_leq") + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, "lin_geq") + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + + cst = q_p.quadratic_constraints + self.assertEqual(cst[0].name, "quad_eq") + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + cst[0].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, "quad_leq") + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + cst[1].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, "quad_geq") + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + cst[2].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_read_from_lp_file(self): """test read lp file""" @@ -744,85 +817,50 @@ def test_read_from_lp_file(self): q_p.read_from_lp_file("") with self.assertRaises(IOError): q_p.read_from_lp_file("no_file.txt") - with self.assertRaises(FileNotFoundError): + with self.assertRaises(IOError): q_p.read_from_lp_file("no_file.lp") lp_file = self.get_resource_path("test_quadratic_program.lp", "problems/resources") q_p.read_from_lp_file(lp_file) - self.assertEqual(q_p.name, "my problem") - self.assertEqual(q_p.get_num_vars(), 3) - self.assertEqual(q_p.get_num_binary_vars(), 1) - self.assertEqual(q_p.get_num_integer_vars(), 1) - self.assertEqual(q_p.get_num_continuous_vars(), 1) - self.assertEqual(q_p.get_num_linear_constraints(), 3) - self.assertEqual(q_p.get_num_quadratic_constraints(), 3) - - self.assertEqual(q_p.variables[0].name, "x") - self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) - self.assertEqual(q_p.variables[0].lowerbound, 0) - self.assertEqual(q_p.variables[0].upperbound, 1) - self.assertEqual(q_p.variables[1].name, "y") - self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) - self.assertEqual(q_p.variables[1].lowerbound, -1) - self.assertEqual(q_p.variables[1].upperbound, 5) - self.assertEqual(q_p.variables[2].name, "z") - self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) - self.assertEqual(q_p.variables[2].lowerbound, -1) - self.assertEqual(q_p.variables[2].upperbound, 5) - - self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) - self.assertEqual(q_p.objective.constant, 1) - self.assertDictEqual( - q_p.objective.linear.to_dict(use_name=True), {"x": 1, "y": -1, "z": 10} - ) - self.assertDictEqual( - q_p.objective.quadratic.to_dict(use_name=True), - {("x", "x"): 0.5, ("y", "z"): -1}, - ) + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) - cst = q_p.linear_constraints - self.assertEqual(cst[0].name, "lin_eq") - self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[0].sense, Constraint.Sense.EQ) - self.assertEqual(cst[0].rhs, 1) - self.assertEqual(cst[1].name, "lin_leq") - self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[1].sense, Constraint.Sense.LE) - self.assertEqual(cst[1].rhs, 1) - self.assertEqual(cst[2].name, "lin_geq") - self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[2].sense, Constraint.Sense.GE) - self.assertEqual(cst[2].rhs, 1) - - cst = q_p.quadratic_constraints - self.assertEqual(cst[0].name, "quad_eq") - self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - cst[0].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(cst[0].sense, Constraint.Sense.EQ) - self.assertEqual(cst[0].rhs, 1) - self.assertEqual(cst[1].name, "quad_leq") - self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - cst[1].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(cst[1].sense, Constraint.Sense.LE) - self.assertEqual(cst[1].rhs, 1) - self.assertEqual(cst[2].name, "quad_geq") - self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - cst[2].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(cst[2].sense, Constraint.Sense.GE) - self.assertEqual(cst[2].rhs, 1) + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_mps_file(self): + """test read mps file""" + try: + q_p = QuadraticProgram() + with self.assertRaises(IOError): + q_p.read_from_mps_file("") + with self.assertRaises(IOError): + q_p.read_from_mps_file("no_file.txt") + with self.assertRaises(IOError): + q_p.read_from_mps_file("no_file.mps") + mps_file = self.get_resource_path("test_quadratic_program.mps", "problems/resources") + q_p.read_from_mps_file(mps_file) + self.helper_test_read_problem_file(q_p) except RuntimeError as ex: self.fail(str(ex)) - def test_write_to_lp_file(self): - """test write problem""" + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_mps_gz_file(self): + """test read mps file""" + try: + q_p = QuadraticProgram() + with self.assertRaises(IOError): + q_p.read_from_mps_file("") + with self.assertRaises(IOError): + q_p.read_from_mps_file("no_file.txt") + with self.assertRaises(IOError): + q_p.read_from_mps_file("no_file.mps.gz") + mps_file = self.get_resource_path("test_quadratic_program.mps.gz", "problems/resources") + q_p.read_from_mps_file(mps_file) + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + + def helper_test_write_problem_file(self): + """creates the quadratic program used in file write tests""" q_p = QuadraticProgram("my problem") q_p.binary_var("x") q_p.integer_var(-1, 5, "y") @@ -853,6 +891,12 @@ def test_write_to_lp_file(self): "quad_geq", ) + return q_p + + def test_write_to_lp_file(self): + """test write problem to lp file""" + q_p = self.helper_test_write_problem_file() + reference_file_name = self.get_resource_path( "test_quadratic_program.lp", "problems/resources" ) @@ -881,6 +925,39 @@ def test_write_to_lp_file(self): with self.assertRaises(DOcplexException): q_p.write_to_lp_file("") + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_write_to_mps_file(self): + """test write problem to mps file""" + q_p = self.helper_test_write_problem_file() + + reference_file_name = self.get_resource_path( + "test_quadratic_program.mps", "problems/resources" + ) + with tempfile.TemporaryDirectory() as tmp: + temp_output_path = path.join(tmp, "temp.mps") + q_p.write_to_mps_file(temp_output_path) + with open(reference_file_name, encoding="utf8") as reference, open( + temp_output_path, encoding="utf8" + ) as temp_output_file: + lines1 = temp_output_file.readlines() + lines2 = reference.readlines() + self.assertListEqual(lines1, lines2) + + with tempfile.TemporaryDirectory() as temp_problem_dir: + q_p.write_to_mps_file(temp_problem_dir) + with open( + path.join(temp_problem_dir, "my_problem.mps"), encoding="utf8" + ) as file1, open(reference_file_name, encoding="utf8") as file2: + lines1 = file1.readlines() + lines2 = file2.readlines() + self.assertListEqual(lines1, lines2) + + with self.assertRaises(OSError): + q_p.write_to_mps_file("/cannot/write/this/file.mps") + + with self.assertRaises(DOcplexException): + q_p.write_to_mps_file("") + def test_substitute_variables(self): """test substitute variables""" q_p = QuadraticProgram("test") From 7d4012054123b0d1fd0d14c21d5e752448650945 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Tue, 12 Dec 2023 00:01:40 +0100 Subject: [PATCH 09/21] Fixing mypy. --- test/problems/test_quadratic_program.py | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index e4235e252..b809c6e08 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -782,31 +782,31 @@ def helper_test_read_problem_file(self, q_p: QuadraticProgram): self.assertEqual(cst[2].sense, Constraint.Sense.GE) self.assertEqual(cst[2].rhs, 1) - cst = q_p.quadratic_constraints - self.assertEqual(cst[0].name, "quad_eq") - self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + qst = q_p.quadratic_constraints + self.assertEqual(qst[0].name, "quad_eq") + self.assertDictEqual(qst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) self.assertDictEqual( - cst[0].quadratic.to_dict(use_name=True), + qst[0].quadratic.to_dict(use_name=True), {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, ) - self.assertEqual(cst[0].sense, Constraint.Sense.EQ) - self.assertEqual(cst[0].rhs, 1) - self.assertEqual(cst[1].name, "quad_leq") - self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertEqual(qst[0].sense, Constraint.Sense.EQ) + self.assertEqual(qst[0].rhs, 1) + self.assertEqual(qst[1].name, "quad_leq") + self.assertDictEqual(qst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) self.assertDictEqual( - cst[1].quadratic.to_dict(use_name=True), + qst[1].quadratic.to_dict(use_name=True), {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, ) - self.assertEqual(cst[1].sense, Constraint.Sense.LE) - self.assertEqual(cst[1].rhs, 1) - self.assertEqual(cst[2].name, "quad_geq") - self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertEqual(qst[1].sense, Constraint.Sense.LE) + self.assertEqual(qst[1].rhs, 1) + self.assertEqual(qst[2].name, "quad_geq") + self.assertDictEqual(qst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) self.assertDictEqual( - cst[2].quadratic.to_dict(use_name=True), + qst[2].quadratic.to_dict(use_name=True), {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, ) - self.assertEqual(cst[2].sense, Constraint.Sense.GE) - self.assertEqual(cst[2].rhs, 1) + self.assertEqual(qst[2].sense, Constraint.Sense.GE) + self.assertEqual(qst[2].rhs, 1) @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_read_from_lp_file(self): From d64866ac98941877031ec4322b496feedf38eb39 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Tue, 12 Dec 2023 00:15:48 +0100 Subject: [PATCH 10/21] Add missing mode specifiers. --- qiskit_optimization/problems/quadratic_program.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index e4e955f7b..0118d00e6 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -1009,7 +1009,7 @@ def _parse_problem_name(filename: str) -> str: # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html prefix = "\\Problem name:" model_name = "" - with open(filename, encoding="utf8") as file: + with open(filename, "r", encoding="utf8") as file: for line in file: if line.startswith(prefix): model_name = line[len(prefix) :].strip() @@ -1040,7 +1040,7 @@ def _parse_problem_name(filename: str) -> str: # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html prefix = "NAME " model_name = "" - with open(filename, encoding="utf8") as file: + with open(filename, "r", encoding="utf8") as file: for line in file: if line.startswith(prefix): model_name = line[len(prefix) :].strip() @@ -1059,7 +1059,7 @@ def write_to_lp_file(self, filename: str) -> None: Raises: OSError: If this cannot open a file. - DOcplexException: If filename is an empty string + DOcplexException: If filename is an empty stringakefile """ # pylint: disable=cyclic-import from ..translators.docplex_mp import to_docplex_mp From 6291712d60dbd7562d709c4bf2b776cb56a2cdf3 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Tue, 12 Dec 2023 00:36:15 +0100 Subject: [PATCH 11/21] Manual management of tmp-file for Windows. --- qiskit_optimization/problems/quadratic_program.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 0118d00e6..d83aaeea9 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -12,6 +12,7 @@ """Quadratic Program.""" +import os import logging from collections.abc import Sequence from enum import Enum @@ -966,7 +967,8 @@ def _read_from_file( # uncompress and parse if extension.endswith(".gz"): with gzip_open(filename, "rb") as compressed: - with NamedTemporaryFile(suffix=extension[:-3]) as uncompressed: + # requires delete=False to avoid permission issues under windows + with NamedTemporaryFile(suffix=extension[:-3], delete=False) as uncompressed: uncompressed.write(compressed.read()) uncompressed.seek(0) uncompressed.flush() @@ -977,6 +979,9 @@ def _read_from_file( if name_parse_fun is not None else None, ) + uncompressed.close() + os.unlink(uncompressed.name) + else: model = ModelReader().read( filename, @@ -1009,7 +1014,7 @@ def _parse_problem_name(filename: str) -> str: # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html prefix = "\\Problem name:" model_name = "" - with open(filename, "r", encoding="utf8") as file: + with open(filename, encoding="utf8") as file: for line in file: if line.startswith(prefix): model_name = line[len(prefix) :].strip() @@ -1040,7 +1045,7 @@ def _parse_problem_name(filename: str) -> str: # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html prefix = "NAME " model_name = "" - with open(filename, "r", encoding="utf8") as file: + with open(filename, encoding="utf8") as file: for line in file: if line.startswith(prefix): model_name = line[len(prefix) :].strip() @@ -1059,7 +1064,7 @@ def write_to_lp_file(self, filename: str) -> None: Raises: OSError: If this cannot open a file. - DOcplexException: If filename is an empty stringakefile + DOcplexException: If filename is an empty string """ # pylint: disable=cyclic-import from ..translators.docplex_mp import to_docplex_mp From 03fea8ef6eddd4c72da8aef6835d8c2edfed80b1 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Tue, 12 Dec 2023 00:47:24 +0100 Subject: [PATCH 12/21] Deleted files that were accidentally pushed. --- apps/mip2qubo.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 apps/mip2qubo.py diff --git a/apps/mip2qubo.py b/apps/mip2qubo.py deleted file mode 100644 index b1cdd0b71..000000000 --- a/apps/mip2qubo.py +++ /dev/null @@ -1,18 +0,0 @@ -import sys -import argparse - -from qiskit_optimization import QuadraticProgram - -### -# Read a larger LP file and convert it to other formats -### - -options = argparse.ArgumentParser("mip2qubo") -options.add_argument("problem") - -args = options.parse_args() - -# read LP file in Qiskit format -qp = QuadraticProgram() -qp.read_from_mps_file(args.problem) -qp.write_to_lp_file(str(args.problem).removesuffix(".mps.gz")) From aaa88a9d5e2b52d19a806597ff5e7162798dd47c Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Wed, 27 Dec 2023 15:53:11 +0100 Subject: [PATCH 13/21] Moving mps/lp IO functions into translators. --- docs/tutorials/01_quadratic_program.ipynb | 14 +- ...ical_optimization_solvers_and_models.ipynb | 5 +- .../algorithms/admm_optimizer.py | 9 +- .../problems/quadratic_program.py | 180 ++---------- qiskit_optimization/translators/__init__.py | 14 + qiskit_optimization/translators/file_io.py | 225 ++++++++++++++ test/algorithms/test_cplex_optimizer.py | 5 +- test/algorithms/test_gurobi_optimizer.py | 3 +- test/algorithms/test_min_eigen_optimizer.py | 5 +- .../algorithms/test_recursive_optimization.py | 7 +- .../resources/test_quadratic_program.lp.gz | Bin 0 -> 285 bytes test/problems/test_quadratic_program.py | 218 +++++--------- test/translators/test_docplex_mp.py | 33 +-- test/translators/test_file_io.py | 274 ++++++++++++++++++ 14 files changed, 651 insertions(+), 341 deletions(-) create mode 100644 qiskit_optimization/translators/file_io.py create mode 100644 test/problems/resources/test_quadratic_program.lp.gz create mode 100644 test/translators/test_file_io.py diff --git a/docs/tutorials/01_quadratic_program.ipynb b/docs/tutorials/01_quadratic_program.ipynb index 2a6295c17..2ce094b71 100644 --- a/docs/tutorials/01_quadratic_program.ipynb +++ b/docs/tutorials/01_quadratic_program.ipynb @@ -119,6 +119,7 @@ "source": [ "# Make a Docplex model\n", "from docplex.mp.model import Model\n", + "from qiskit_optimization.translators import export_as_lp_string\n", "\n", "mdl = Model(\"docplex model\")\n", "x = mdl.binary_var(\"x\")\n", @@ -126,7 +127,7 @@ "mdl.minimize(x + 2 * y)\n", "mdl.add_constraint(x - y == 3)\n", "mdl.add_constraint((x + y) * (x - y) <= 1)\n", - "print(mdl.export_as_lp_string())" + "print(export_as_lp_string(mdl))" ] }, { @@ -791,7 +792,8 @@ "mod.binary_var(name=\"f\")\n", "mod.continuous_var(name=\"g\")\n", "mod.minimize(linear=[1, 2, 3])\n", - "print(mod.export_as_lp_string())" + "\n", + "print(export_as_lp_string(mod))" ] }, { @@ -840,8 +842,14 @@ } ], "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "name": "python", + "version": "3.10.13" } }, "nbformat": 4, diff --git a/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb b/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb index 7702a8390..b8611b03a 100644 --- a/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb +++ b/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb @@ -417,6 +417,7 @@ ], "source": [ "from qiskit_optimization.translators import to_gurobipy, to_docplex_mp\n", + "from qiskit_optimization.translators import export_as_lp_string\n", "\n", "gmod = to_gurobipy(from_docplex_mp(docplex_model))\n", "print(\"convert docplex to gurobipy via QuadraticProgram\")\n", @@ -424,7 +425,7 @@ "\n", "dmod = to_docplex_mp(from_gurobipy(gurobipy_model))\n", "print(\"\\nconvert gurobipy to docplex via QuadraticProgram\")\n", - "print(dmod.export_as_lp_string())" + "print(export_as_lp_string(dmod))" ] }, { @@ -470,6 +471,8 @@ } ], "source": [ + "from qiskit_optimization.translators import export_as_lp_string\n", + "\n", "ind_mod = Model(\"docplex\")\n", "x = ind_mod.binary_var(\"x\")\n", "y = ind_mod.integer_var(-1, 2, \"y\")\n", diff --git a/qiskit_optimization/algorithms/admm_optimizer.py b/qiskit_optimization/algorithms/admm_optimizer.py index e837ee278..7e6ad8b9d 100644 --- a/qiskit_optimization/algorithms/admm_optimizer.py +++ b/qiskit_optimization/algorithms/admm_optimizer.py @@ -25,6 +25,7 @@ from ..problems.linear_expression import LinearExpression from ..problems.quadratic_program import QuadraticProgram from ..problems.variable import Variable, VarType +from ..translators import export_as_lp_string from .minimum_eigen_optimizer import MinimumEigenOptimizer from .optimization_algorithm import ( OptimizationAlgorithm, @@ -282,7 +283,7 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: self._verify_compatibility(problem) # debug - self._log.debug("Initial problem: %s", problem.export_as_lp_string()) + self._log.debug("Initial problem: %s", export_as_lp_string(problem)) # map integer variables to binary variables from ..converters.integer_to_binary import IntegerToBinary @@ -321,7 +322,7 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: op1 = self._create_step1_problem() self._state.x0 = self._update_x0(op1) # debug - self._log.debug("Step 1 sub-problem: %s", op1.export_as_lp_string()) + self._log.debug("Step 1 sub-problem: %s", export_as_lp_string(op1)) # else, no binary variables exist, and no update to be done in this case. # debug self._log.debug("x0=%s", self._state.x0) @@ -329,7 +330,7 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: op2 = self._create_step2_problem() self._state.u, self._state.z = self._update_x1(op2) # debug - self._log.debug("Step 2 sub-problem: %s", op2.export_as_lp_string()) + self._log.debug("Step 2 sub-problem: %s", export_as_lp_string(op2)) self._log.debug("u=%s", self._state.u) self._log.debug("z=%s", self._state.z) @@ -338,7 +339,7 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: op3 = self._create_step3_problem() self._state.y = self._update_y(op3) # debug - self._log.debug("Step 3 sub-problem: %s", op3.export_as_lp_string()) + self._log.debug("Step 3 sub-problem: %s", export_as_lp_string(op3)) # debug self._log.debug("y=%s", self._state.y) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index d83aaeea9..565ab7368 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -12,19 +12,14 @@ """Quadratic Program.""" -import os import logging from collections.abc import Sequence from enum import Enum from math import isclose -from typing import Dict, List, Optional, Tuple, Union, cast, Callable +from typing import Dict, List, Optional, Tuple, Union, cast from warnings import warn -from gzip import open as gzip_open -from pathlib import Path -from tempfile import NamedTemporaryFile import numpy as np -from docplex.mp.model_reader import ModelReader from numpy import ndarray from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -921,78 +916,19 @@ def export_as_lp_string(self) -> str: Returns: A string representing the quadratic program. """ - # pylint: disable=cyclic-import - from ..translators.docplex_mp import to_docplex_mp - - return to_docplex_mp(self).export_as_lp_string() - - @_optionals.HAS_CPLEX.require_in_call - def export_as_mps_string(self) -> str: - """Returns the quadratic program as a string of LP format. - - Returns: - A string representing the quadratic program. - """ - # pylint: disable=cyclic-import - from ..translators.docplex_mp import to_docplex_mp - - return to_docplex_mp(self).export_as_mps_string() - - @_optionals.HAS_CPLEX.require_in_call - def _read_from_file( - self, filename: str, extensions: List[str], name_parse_fun: Callable - ) -> None: - """Loads a quadratic program from an LP or MPS file. Also deals with - gzip'ed files. - - Args: - filename: The filename of the file to be loaded. - name_parse_fun: Function that parses the model name from the input file. - - Raises: - IOError: If the file type is not recognized, not supported or the file is not found. - - Note: - This method requires CPLEX to be installed and present in ``PYTHONPATH``. - """ - - # check whether this file type is supported - extension = "".join(Path(filename).suffixes) - main_extension = extension - if main_extension.endswith(".gz"): - main_extension = main_extension[:-3] - if main_extension not in extensions: - raise IOError("File type not supported for model reading.") - - # uncompress and parse - if extension.endswith(".gz"): - with gzip_open(filename, "rb") as compressed: - # requires delete=False to avoid permission issues under windows - with NamedTemporaryFile(suffix=extension[:-3], delete=False) as uncompressed: - uncompressed.write(compressed.read()) - uncompressed.seek(0) - uncompressed.flush() - - model = ModelReader().read( - uncompressed.name, - model_name=name_parse_fun(uncompressed.name) - if name_parse_fun is not None - else None, - ) - uncompressed.close() - os.unlink(uncompressed.name) - - else: - model = ModelReader().read( - filename, - model_name=name_parse_fun(filename) if name_parse_fun is not None else None, - ) + warn( + "The QuadraticProgram.export_as_lp_string function is pending deprecation. " + "Please use export_as_lp_string from qiskit_optimization.translators.file_io instead." + "This function will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + stacklevel=2, + ) # pylint: disable=cyclic-import - from ..translators.docplex_mp import from_docplex_mp + from ..translators.file_io import export_as_lp_string - other = from_docplex_mp(model) - self._copy_from(other, include_name=True) + return export_as_lp_string(self) @_optionals.HAS_CPLEX.require_in_call def read_from_lp_file(self, filename: str) -> None: @@ -1008,51 +944,19 @@ def read_from_lp_file(self, filename: str) -> None: This method requires CPLEX to be installed and present in ``PYTHONPATH``. """ - def _parse_problem_name(filename: str) -> str: - # Because docplex model reader uses the base name as model name, - # we parse the model name in the LP file manually. - # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html - prefix = "\\Problem name:" - model_name = "" - with open(filename, encoding="utf8") as file: - for line in file: - if line.startswith(prefix): - model_name = line[len(prefix) :].strip() - if not line.startswith("\\"): - break - return model_name - - self._read_from_file(filename, [".lp"], _parse_problem_name) - - @_optionals.HAS_CPLEX.require_in_call - def read_from_mps_file(self, filename: str) -> None: - """Loads the quadratic program from a MPS file (may be gzip'ed). - - Args: - filename: The filename of the file to be loaded. - - Raises: - FileNotFoundError: If the file does not exist. - IOError: If the file type is not recognized or not supported. + warn( + "The QuadraticProgram.read_from_lp_file function is pending deprecation. " + "Please use read_from_lp_file from qiskit_optimization.translators.file_io instead." + "This function will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + stacklevel=2, + ) - Note: - This method requires CPLEX to be installed and present in ``PYTHONPATH``. - """ + # pylint: disable=cyclic-import + from ..translators.file_io import read_from_lp_file - def _parse_problem_name(filename: str) -> str: - # Because docplex model reader uses the base name as model name, - # we parse the model name in the LP file manually. - # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html - prefix = "NAME " - model_name = "" - with open(filename, encoding="utf8") as file: - for line in file: - if line.startswith(prefix): - model_name = line[len(prefix) :].strip() - break - return model_name - - self._read_from_file(filename, [".mps"], _parse_problem_name) + self._copy_from(read_from_lp_file(filename), include_name=True) def write_to_lp_file(self, filename: str) -> None: """Writes the quadratic program to an LP file. @@ -1066,40 +970,20 @@ def write_to_lp_file(self, filename: str) -> None: OSError: If this cannot open a file. DOcplexException: If filename is an empty string """ - # pylint: disable=cyclic-import - from ..translators.docplex_mp import to_docplex_mp - - mdl = to_docplex_mp(self) - mdl.export_as_lp(filename) - def write_to_mps_file(self, filename: str) -> None: - """Writes the quadratic program to an MPS file. - - Args: - filename: The filename of the file the model is written to. - If filename is a directory, file name 'my_problem.mps' is appended. - If filename does not end with '.mps', suffix '.mps' is appended. + warn( + "The QuadraticProgram.write_to_lp_file function is pending deprecation. " + "Please use write_to_lp_file from qiskit_optimization.translators.file_io instead." + "This function will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + stacklevel=2, + ) - Raises: - OSError: If this cannot open a file. - DOcplexException: If filename is an empty string - """ # pylint: disable=cyclic-import - from ..translators.docplex_mp import to_docplex_mp + from ..translators.file_io import write_to_lp_file - mdl = to_docplex_mp(self) - full_path = mdl.export_as_mps(filename) - - # docplex does not write the model's name out, so we do this here manually - with open(full_path, "r", encoding="utf8") as mps_file: - txt = mps_file.read() - - with open(full_path, "w", encoding="utf8") as mps_file: - for line in txt.splitlines(): - if line.startswith("NAME"): - mps_file.write(f"NAME {self._name}\n") - else: - mps_file.write(line + "\n") + write_to_lp_file(self, filename) def substitute_variables( self, diff --git a/qiskit_optimization/translators/__init__.py b/qiskit_optimization/translators/__init__.py index 913c23922..e30294d3a 100644 --- a/qiskit_optimization/translators/__init__.py +++ b/qiskit_optimization/translators/__init__.py @@ -34,6 +34,14 @@ """ from .docplex_mp import from_docplex_mp, to_docplex_mp +from .file_io import ( + export_as_lp_string, + export_as_mps_string, + read_from_lp_file, + read_from_mps_file, + write_to_lp_file, + write_to_mps_file, +) from .gurobipy import from_gurobipy, to_gurobipy from .ising import from_ising, to_ising @@ -44,4 +52,10 @@ "to_gurobipy", "from_ising", "to_ising", + "export_as_lp_string", + "export_as_mps_string", + "read_from_lp_file", + "read_from_mps_file", + "write_to_lp_file", + "write_to_mps_file", ] diff --git a/qiskit_optimization/translators/file_io.py b/qiskit_optimization/translators/file_io.py new file mode 100644 index 000000000..593056476 --- /dev/null +++ b/qiskit_optimization/translators/file_io.py @@ -0,0 +1,225 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""LP/MPS-file import / export for Quadratic Programs.""" + +import os +import logging +from typing import List, Callable +from gzip import open as gzip_open +from pathlib import Path +from tempfile import NamedTemporaryFile + +from docplex.mp.model_reader import ModelReader + +import qiskit_optimization.optionals as _optionals +from ..problems.quadratic_program import QuadraticProgram + +logger = logging.getLogger(__name__) + + +def export_as_lp_string(q_p: QuadraticProgram) -> str: + """Returns the quadratic program as a string of LP format. + + Args: + q_p: The quadratic program to be exported. + + Returns: + A string representing the quadratic program. + """ + from .docplex_mp import to_docplex_mp + + return to_docplex_mp(q_p).export_as_lp_string() + + +@_optionals.HAS_CPLEX.require_in_call +def export_as_mps_string(q_p: QuadraticProgram) -> str: + """Returns the quadratic program as a string of LP format. + + Args: + q_p: The quadratic program to be exported. + + Returns: + A string representing the quadratic program. + """ + from .docplex_mp import to_docplex_mp + + return to_docplex_mp(q_p).export_as_mps_string() + + +@_optionals.HAS_CPLEX.require_in_call +def _read_from_file( + filename: str, extensions: List[str], name_parse_fun: Callable +) -> QuadraticProgram: + """Loads a quadratic program from an LP or MPS file. Also deals with + gzip'ed files. + + Args: + filename: The filename of the file to be loaded. + name_parse_fun: Function that parses the model name from the input file. + + Raises: + IOError: If the file type is not recognized, not supported or the file is not found. + + Note: + This method requires CPLEX to be installed and present in ``PYTHONPATH``. + """ + + # check whether this file type is supported + extension = "".join(Path(filename).suffixes) + main_extension = extension + if main_extension.endswith(".gz"): + main_extension = main_extension[:-3] + if main_extension not in extensions: + raise IOError("File type not supported for model reading.") + + # uncompress and parse + if extension.endswith(".gz"): + with gzip_open(filename, "rb") as compressed: + # requires delete=False to avoid permission issues under windows + with NamedTemporaryFile(suffix=extension[:-3], delete=False) as uncompressed: + uncompressed.write(compressed.read()) + uncompressed.seek(0) + uncompressed.flush() + + model = ModelReader().read( + uncompressed.name, + model_name=name_parse_fun(uncompressed.name) + if name_parse_fun is not None + else None, + ) + uncompressed.close() + os.unlink(uncompressed.name) + + else: + model = ModelReader().read( + filename, + model_name=name_parse_fun(filename) if name_parse_fun is not None else None, + ) + + # pylint: disable=cyclic-import + from ..translators.docplex_mp import from_docplex_mp + + return from_docplex_mp(model) + + +@_optionals.HAS_CPLEX.require_in_call +def read_from_lp_file(filename: str) -> QuadraticProgram: + """Loads the quadratic program from a LP file (may be gzip'ed). + + Args: + filename: The filename of the file to be loaded. + + Raises: + IOError: If the file type is not recognized, not supported or the file is not found. + + Note: + This method requires CPLEX to be installed and present in ``PYTHONPATH``. + """ + + def _parse_problem_name(filename: str) -> str: + # Because docplex model reader uses the base name as model name, + # we parse the model name in the LP file manually. + # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html + prefix = "\\Problem name:" + model_name = "" + with open(filename, encoding="utf8") as file: + for line in file: + if line.startswith(prefix): + model_name = line[len(prefix) :].strip() + if not line.startswith("\\"): + break + return model_name + + return _read_from_file(filename, [".lp"], _parse_problem_name) + + +@_optionals.HAS_CPLEX.require_in_call +def read_from_mps_file(filename: str) -> QuadraticProgram: + """Loads the quadratic program from a MPS file (may be gzip'ed). + + Args: + filename: The filename of the file to be loaded. + + Raises: + FileNotFoundError: If the file does not exist. + IOError: If the file type is not recognized or not supported. + + Note: + This method requires CPLEX to be installed and present in ``PYTHONPATH``. + """ + + def _parse_problem_name(filename: str) -> str: + # Because docplex model reader uses the base name as model name, + # we parse the model name in the LP file manually. + # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html + prefix = "NAME " + model_name = "" + with open(filename, encoding="utf8") as file: + for line in file: + if line.startswith(prefix): + model_name = line[len(prefix) :].strip() + break + return model_name + + return _read_from_file(filename, [".mps"], _parse_problem_name) + + +@_optionals.HAS_CPLEX.require_in_call +def write_to_lp_file(q_p: QuadraticProgram, filename: str): + """Writes the quadratic program to an LP file. + + Args: + q_p: The quadratic program to be exported. + filename: The filename of the file the model is written to. + If filename is a directory, file name 'my_problem.lp' is appended. + If filename does not end with '.lp', suffix '.lp' is appended. + + Raises: + OSError: If this cannot open a file. + DOcplexException: If filename is an empty string + """ + from .docplex_mp import to_docplex_mp + + mdl = to_docplex_mp(q_p) + mdl.export_as_lp(filename) + + +@_optionals.HAS_CPLEX.require_in_call +def write_to_mps_file(q_p: QuadraticProgram, filename: str): + """Writes the quadratic program to an MPS file. + + Args: + q_p: The quadratic program to be exported. + filename: The filename of the file the model is written to. + If filename is a directory, file name 'my_problem.mps' is appended. + If filename does not end with '.mps', suffix '.mps' is appended. + + Raises: + OSError: If this cannot open a file. + DOcplexException: If filename is an empty string + """ + from .docplex_mp import to_docplex_mp + + mdl = to_docplex_mp(q_p) + full_path = mdl.export_as_mps(filename) + + # docplex does not write the model's name out, so we do this here manually + with open(full_path, "r", encoding="utf8") as mps_file: + txt = mps_file.read() + + with open(full_path, "w", encoding="utf8") as mps_file: + for line in txt.splitlines(): + if line.startswith("NAME"): + mps_file.write(f"NAME {q_p.name}\n") + else: + mps_file.write(line + "\n") diff --git a/test/algorithms/test_cplex_optimizer.py b/test/algorithms/test_cplex_optimizer.py index ecbba6d2f..01e37e798 100644 --- a/test/algorithms/test_cplex_optimizer.py +++ b/test/algorithms/test_cplex_optimizer.py @@ -21,6 +21,7 @@ import qiskit_optimization.optionals as _optionals from qiskit_optimization.algorithms import CplexOptimizer, OptimizationResultStatus from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.translators import read_from_lp_file @ddt @@ -44,7 +45,7 @@ def test_cplex_optimizer(self, config): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with cplex result = cplex_optimizer.solve(problem) @@ -69,7 +70,7 @@ def test_cplex_optimizer_no_solution(self, config): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with cplex result = cplex_optimizer.solve(problem) diff --git a/test/algorithms/test_gurobi_optimizer.py b/test/algorithms/test_gurobi_optimizer.py index 713a5f3f2..4c6b3fd81 100644 --- a/test/algorithms/test_gurobi_optimizer.py +++ b/test/algorithms/test_gurobi_optimizer.py @@ -21,6 +21,7 @@ import qiskit_optimization.optionals as _optionals from qiskit_optimization.algorithms import GurobiOptimizer from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.translators import read_from_lp_file @ddt @@ -43,7 +44,7 @@ def test_gurobi_optimizer(self, config): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with gurobi result = gurobi_optimizer.solve(problem) diff --git a/test/algorithms/test_min_eigen_optimizer.py b/test/algorithms/test_min_eigen_optimizer.py index c1251d6bb..1abba87a2 100644 --- a/test/algorithms/test_min_eigen_optimizer.py +++ b/test/algorithms/test_min_eigen_optimizer.py @@ -35,6 +35,7 @@ ) from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.translators import read_from_lp_file @ddt @@ -94,7 +95,7 @@ def test_min_eigen_optimizer(self, min_eigen_solver_name, shots, filename): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with cplex cplex = CplexOptimizer(cplex_parameters={"threads": 1, "randomseed": 1}) @@ -137,7 +138,7 @@ def filter_criterion(x, v, aux): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem result = min_eigen_optimizer.solve(problem) diff --git a/test/algorithms/test_recursive_optimization.py b/test/algorithms/test_recursive_optimization.py index b3970563a..68c90792e 100644 --- a/test/algorithms/test_recursive_optimization.py +++ b/test/algorithms/test_recursive_optimization.py @@ -37,6 +37,7 @@ QuadraticProgramToQubo, ) from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.translators import read_from_lp_file class TestRecursiveMinEigenOptimizer(QiskitOptimizationTestCase): @@ -58,7 +59,7 @@ def test_recursive_min_eigen_optimizer(self): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with cplex cplex = CplexOptimizer() @@ -78,7 +79,7 @@ def test_recursive_history(self): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path(filename, "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # get minimum eigen solver min_eigen_solver = NumPyMinimumEigensolver() @@ -144,7 +145,7 @@ def test_recursive_warm_qaoa(self): # load optimization problem problem = QuadraticProgram() lp_file = self.get_resource_path("op_ip1.lp", "algorithms/resources") - problem.read_from_lp_file(lp_file) + problem = read_from_lp_file(lp_file) # solve problem with cplex cplex = CplexOptimizer(cplex_parameters={"threads": 1, "randomseed": 1}) diff --git a/test/problems/resources/test_quadratic_program.lp.gz b/test/problems/resources/test_quadratic_program.lp.gz new file mode 100644 index 0000000000000000000000000000000000000000..93980b0baa91f174f9bf80bced3ccbad50d0b775 GIT binary patch literal 285 zcmV+&0pk82iwFqWC2(Z`19W9`bYF3GVPtY)bZKK>aB^>Fa$#*QY;XXDk-=`lFc3uV z`xSF8ib^6OQ3O3u5aLi$32qIo9I_2Yjsppi;(UE=11&dB-g!GayOI-Z_Oiy0RE)i- zkuq!Ktho|r&X5|+;@wekO9|$yw|KT(Ehfu#91Mqpmms9%qbgIy1+*x5gu>u>0YvXo z%R+WcDAU6TEdm(y5cbj8=M(K$WKjh3)M5J^xfZRP^uW6_HYGtx{mmyg*mEvSY~Mq5 zPse{azsIj0pBj<*jN3N1%NaVWwzhKiYqIyM$L8_BJAdr`V literal 0 HcmV?d00001 diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index b809c6e08..cd2833968 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -735,79 +735,6 @@ def test_empty_objective(self): q_p.binary_var_list(3) _ = q_p.objective.evaluate_gradient({}) - def helper_test_read_problem_file(self, q_p: QuadraticProgram): - """evaluates the quadratic program read in file reading tests""" - self.assertEqual(q_p.name, "my problem") - self.assertEqual(q_p.get_num_vars(), 3) - self.assertEqual(q_p.get_num_binary_vars(), 1) - self.assertEqual(q_p.get_num_integer_vars(), 1) - self.assertEqual(q_p.get_num_continuous_vars(), 1) - self.assertEqual(q_p.get_num_linear_constraints(), 3) - self.assertEqual(q_p.get_num_quadratic_constraints(), 3) - - self.assertEqual(q_p.variables[0].name, "x") - self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) - self.assertEqual(q_p.variables[0].lowerbound, 0) - self.assertEqual(q_p.variables[0].upperbound, 1) - self.assertEqual(q_p.variables[1].name, "y") - self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) - self.assertEqual(q_p.variables[1].lowerbound, -1) - self.assertEqual(q_p.variables[1].upperbound, 5) - self.assertEqual(q_p.variables[2].name, "z") - self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) - self.assertEqual(q_p.variables[2].lowerbound, -1) - self.assertEqual(q_p.variables[2].upperbound, 5) - - self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) - self.assertEqual(q_p.objective.constant, 1) - self.assertDictEqual( - q_p.objective.linear.to_dict(use_name=True), {"x": 1, "y": -1, "z": 10} - ) - self.assertDictEqual( - q_p.objective.quadratic.to_dict(use_name=True), - {("x", "x"): 0.5, ("y", "z"): -1}, - ) - - cst = q_p.linear_constraints - self.assertEqual(cst[0].name, "lin_eq") - self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[0].sense, Constraint.Sense.EQ) - self.assertEqual(cst[0].rhs, 1) - self.assertEqual(cst[1].name, "lin_leq") - self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[1].sense, Constraint.Sense.LE) - self.assertEqual(cst[1].rhs, 1) - self.assertEqual(cst[2].name, "lin_geq") - self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 2}) - self.assertEqual(cst[2].sense, Constraint.Sense.GE) - self.assertEqual(cst[2].rhs, 1) - - qst = q_p.quadratic_constraints - self.assertEqual(qst[0].name, "quad_eq") - self.assertDictEqual(qst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - qst[0].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(qst[0].sense, Constraint.Sense.EQ) - self.assertEqual(qst[0].rhs, 1) - self.assertEqual(qst[1].name, "quad_leq") - self.assertDictEqual(qst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - qst[1].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(qst[1].sense, Constraint.Sense.LE) - self.assertEqual(qst[1].rhs, 1) - self.assertEqual(qst[2].name, "quad_geq") - self.assertDictEqual(qst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) - self.assertDictEqual( - qst[2].quadratic.to_dict(use_name=True), - {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, - ) - self.assertEqual(qst[2].sense, Constraint.Sense.GE) - self.assertEqual(qst[2].rhs, 1) - @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_read_from_lp_file(self): """test read lp file""" @@ -821,46 +748,82 @@ def test_read_from_lp_file(self): q_p.read_from_lp_file("no_file.lp") lp_file = self.get_resource_path("test_quadratic_program.lp", "problems/resources") q_p.read_from_lp_file(lp_file) - self.helper_test_read_problem_file(q_p) - except RuntimeError as ex: - self.fail(str(ex)) - @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") - def test_read_from_mps_file(self): - """test read mps file""" - try: - q_p = QuadraticProgram() - with self.assertRaises(IOError): - q_p.read_from_mps_file("") - with self.assertRaises(IOError): - q_p.read_from_mps_file("no_file.txt") - with self.assertRaises(IOError): - q_p.read_from_mps_file("no_file.mps") - mps_file = self.get_resource_path("test_quadratic_program.mps", "problems/resources") - q_p.read_from_mps_file(mps_file) - self.helper_test_read_problem_file(q_p) - except RuntimeError as ex: - self.fail(str(ex)) + self.assertEqual(q_p.name, "my problem") + self.assertEqual(q_p.get_num_vars(), 3) + self.assertEqual(q_p.get_num_binary_vars(), 1) + self.assertEqual(q_p.get_num_integer_vars(), 1) + self.assertEqual(q_p.get_num_continuous_vars(), 1) + self.assertEqual(q_p.get_num_linear_constraints(), 3) + self.assertEqual(q_p.get_num_quadratic_constraints(), 3) + + self.assertEqual(q_p.variables[0].name, "x") + self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) + self.assertEqual(q_p.variables[0].lowerbound, 0) + self.assertEqual(q_p.variables[0].upperbound, 1) + self.assertEqual(q_p.variables[1].name, "y") + self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) + self.assertEqual(q_p.variables[1].lowerbound, -1) + self.assertEqual(q_p.variables[1].upperbound, 5) + self.assertEqual(q_p.variables[2].name, "z") + self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) + self.assertEqual(q_p.variables[2].lowerbound, -1) + self.assertEqual(q_p.variables[2].upperbound, 5) + + self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) + self.assertEqual(q_p.objective.constant, 1) + self.assertDictEqual( + q_p.objective.linear.to_dict(use_name=True), {"x": 1, "y": -1, "z": 10} + ) + self.assertDictEqual( + q_p.objective.quadratic.to_dict(use_name=True), + {("x", "x"): 0.5, ("y", "z"): -1}, + ) - @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") - def test_read_from_mps_gz_file(self): - """test read mps file""" - try: - q_p = QuadraticProgram() - with self.assertRaises(IOError): - q_p.read_from_mps_file("") - with self.assertRaises(IOError): - q_p.read_from_mps_file("no_file.txt") - with self.assertRaises(IOError): - q_p.read_from_mps_file("no_file.mps.gz") - mps_file = self.get_resource_path("test_quadratic_program.mps.gz", "problems/resources") - q_p.read_from_mps_file(mps_file) - self.helper_test_read_problem_file(q_p) + cst = q_p.linear_constraints + self.assertEqual(cst[0].name, "lin_eq") + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, "lin_leq") + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, "lin_geq") + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + + qst = q_p.quadratic_constraints + self.assertEqual(qst[0].name, "quad_eq") + self.assertDictEqual(qst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[0].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[0].sense, Constraint.Sense.EQ) + self.assertEqual(qst[0].rhs, 1) + self.assertEqual(qst[1].name, "quad_leq") + self.assertDictEqual(qst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[1].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[1].sense, Constraint.Sense.LE) + self.assertEqual(qst[1].rhs, 1) + self.assertEqual(qst[2].name, "quad_geq") + self.assertDictEqual(qst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[2].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[2].sense, Constraint.Sense.GE) + self.assertEqual(qst[2].rhs, 1) except RuntimeError as ex: self.fail(str(ex)) - def helper_test_write_problem_file(self): - """creates the quadratic program used in file write tests""" + def test_write_to_lp_file(self): + """test write problem to lp file""" q_p = QuadraticProgram("my problem") q_p.binary_var("x") q_p.integer_var(-1, 5, "y") @@ -891,12 +854,6 @@ def helper_test_write_problem_file(self): "quad_geq", ) - return q_p - - def test_write_to_lp_file(self): - """test write problem to lp file""" - q_p = self.helper_test_write_problem_file() - reference_file_name = self.get_resource_path( "test_quadratic_program.lp", "problems/resources" ) @@ -925,39 +882,6 @@ def test_write_to_lp_file(self): with self.assertRaises(DOcplexException): q_p.write_to_lp_file("") - @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") - def test_write_to_mps_file(self): - """test write problem to mps file""" - q_p = self.helper_test_write_problem_file() - - reference_file_name = self.get_resource_path( - "test_quadratic_program.mps", "problems/resources" - ) - with tempfile.TemporaryDirectory() as tmp: - temp_output_path = path.join(tmp, "temp.mps") - q_p.write_to_mps_file(temp_output_path) - with open(reference_file_name, encoding="utf8") as reference, open( - temp_output_path, encoding="utf8" - ) as temp_output_file: - lines1 = temp_output_file.readlines() - lines2 = reference.readlines() - self.assertListEqual(lines1, lines2) - - with tempfile.TemporaryDirectory() as temp_problem_dir: - q_p.write_to_mps_file(temp_problem_dir) - with open( - path.join(temp_problem_dir, "my_problem.mps"), encoding="utf8" - ) as file1, open(reference_file_name, encoding="utf8") as file2: - lines1 = file1.readlines() - lines2 = file2.readlines() - self.assertListEqual(lines1, lines2) - - with self.assertRaises(OSError): - q_p.write_to_mps_file("/cannot/write/this/file.mps") - - with self.assertRaises(DOcplexException): - q_p.write_to_mps_file("") - def test_substitute_variables(self): """test substitute variables""" q_p = QuadraticProgram("test") diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index f2154cb09..87a3f5161 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -12,8 +12,6 @@ """Test from_docplex_mp and to_docplex_mp""" -import unittest - from test.optimization_test_case import QiskitOptimizationTestCase from docplex.mp.model import Model @@ -22,6 +20,7 @@ from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems import Constraint, QuadraticProgram from qiskit_optimization.translators.docplex_mp import from_docplex_mp, to_docplex_mp +from qiskit_optimization.translators import export_as_lp_string class TestDocplexMpTranslator(QiskitOptimizationTestCase): @@ -41,33 +40,7 @@ def test_from_and_to_lp(self): q_p.linear_constraint({"x": 2, "z": -1}, "==", 1) q_p.quadratic_constraint({"x": 2, "z": -1}, {("y", "z"): 3}, "==", 1) q_p2 = from_docplex_mp(to_docplex_mp(q_p)) - self.assertEqual(q_p.export_as_lp_string(), q_p2.export_as_lp_string()) - - mod = Model("test") - x = mod.binary_var("x") - y = mod.integer_var(-2, 4, "y") - z = mod.continuous_var(-1.5, 3.2, "z") - mod.minimize(1 + x + 2 * y - x * y + 2 * z * z) - mod.add(2 * x - z == 1, "c0") - mod.add(2 * x - z + 3 * y * z == 1, "q0") - self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) - - @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") - def test_from_and_to_mps(self): - """test from_docplex_mp and to_docplex_mp""" - q_p = QuadraticProgram("test") - q_p.binary_var(name="x") - q_p.integer_var(name="y", lowerbound=-2, upperbound=4) - q_p.continuous_var(name="z", lowerbound=-1.5, upperbound=3.2) - q_p.minimize( - constant=1, - linear={"x": 1, "y": 2}, - quadratic={("x", "y"): -1, ("z", "z"): 2}, - ) - q_p.linear_constraint({"x": 2, "z": -1}, "==", 1) - q_p.quadratic_constraint({"x": 2, "z": -1}, {("y", "z"): 3}, "==", 1) - q_p2 = from_docplex_mp(to_docplex_mp(q_p)) - self.assertEqual(q_p.export_as_mps_string(), q_p2.export_as_mps_string()) + self.assertEqual(export_as_lp_string(q_p), export_as_lp_string(q_p2)) mod = Model("test") x = mod.binary_var("x") @@ -76,7 +49,7 @@ def test_from_and_to_mps(self): mod.minimize(1 + x + 2 * y - x * y + 2 * z * z) mod.add(2 * x - z == 1, "c0") mod.add(2 * x - z + 3 * y * z == 1, "q0") - self.assertEqual(q_p.export_as_mps_string(), mod.export_as_mps_string()) + self.assertEqual(export_as_lp_string(q_p), mod.export_as_lp_string()) def test_from_without_variable_names(self): """test from_docplex_mp without explicit variable names""" diff --git a/test/translators/test_file_io.py b/test/translators/test_file_io.py new file mode 100644 index 000000000..2857d7dd8 --- /dev/null +++ b/test/translators/test_file_io.py @@ -0,0 +1,274 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test file_io's LP/MPS import/export functions""" + +import tempfile +import unittest +from os import path + +from test.optimization_test_case import QiskitOptimizationTestCase + +from docplex.mp.model import DOcplexException + +import qiskit_optimization.optionals as _optionals +from qiskit_optimization.problems import QuadraticProgram, Constraint, QuadraticObjective, Variable +from qiskit_optimization.translators import ( + read_from_lp_file, + read_from_mps_file, + write_to_lp_file, + write_to_mps_file, +) + + +class TestFileIOTranslator(QiskitOptimizationTestCase): + """Test Ex/Importers for LP and MPS files (including compressed forms).""" + + def helper_test_read_problem_file(self, q_p: QuadraticProgram): + """evaluates the quadratic program read in file reading tests""" + self.assertEqual(q_p.name, "my problem") + self.assertEqual(q_p.get_num_vars(), 3) + self.assertEqual(q_p.get_num_binary_vars(), 1) + self.assertEqual(q_p.get_num_integer_vars(), 1) + self.assertEqual(q_p.get_num_continuous_vars(), 1) + self.assertEqual(q_p.get_num_linear_constraints(), 3) + self.assertEqual(q_p.get_num_quadratic_constraints(), 3) + + self.assertEqual(q_p.variables[0].name, "x") + self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) + self.assertEqual(q_p.variables[0].lowerbound, 0) + self.assertEqual(q_p.variables[0].upperbound, 1) + self.assertEqual(q_p.variables[1].name, "y") + self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) + self.assertEqual(q_p.variables[1].lowerbound, -1) + self.assertEqual(q_p.variables[1].upperbound, 5) + self.assertEqual(q_p.variables[2].name, "z") + self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) + self.assertEqual(q_p.variables[2].lowerbound, -1) + self.assertEqual(q_p.variables[2].upperbound, 5) + + self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) + self.assertEqual(q_p.objective.constant, 1) + self.assertDictEqual( + q_p.objective.linear.to_dict(use_name=True), {"x": 1, "y": -1, "z": 10} + ) + self.assertDictEqual( + q_p.objective.quadratic.to_dict(use_name=True), + {("x", "x"): 0.5, ("y", "z"): -1}, + ) + + cst = q_p.linear_constraints + self.assertEqual(cst[0].name, "lin_eq") + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, "lin_leq") + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, "lin_geq") + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {"x": 1, "y": 2}) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + + qst = q_p.quadratic_constraints + self.assertEqual(qst[0].name, "quad_eq") + self.assertDictEqual(qst[0].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[0].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[0].sense, Constraint.Sense.EQ) + self.assertEqual(qst[0].rhs, 1) + self.assertEqual(qst[1].name, "quad_leq") + self.assertDictEqual(qst[1].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[1].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[1].sense, Constraint.Sense.LE) + self.assertEqual(qst[1].rhs, 1) + self.assertEqual(qst[2].name, "quad_geq") + self.assertDictEqual(qst[2].linear.to_dict(use_name=True), {"x": 1, "y": 1}) + self.assertDictEqual( + qst[2].quadratic.to_dict(use_name=True), + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ) + self.assertEqual(qst[2].sense, Constraint.Sense.GE) + self.assertEqual(qst[2].rhs, 1) + + def helper_test_write_problem_file(self): + """creates the quadratic program used in file write tests""" + q_p = QuadraticProgram("my problem") + q_p.binary_var("x") + q_p.integer_var(-1, 5, "y") + q_p.continuous_var(-1, 5, "z") + q_p.minimize(1, {"x": 1, "y": -1, "z": 10}, {("x", "x"): 0.5, ("y", "z"): -1}) + q_p.linear_constraint({"x": 1, "y": 2}, "==", 1, "lin_eq") + q_p.linear_constraint({"x": 1, "y": 2}, "<=", 1, "lin_leq") + q_p.linear_constraint({"x": 1, "y": 2}, ">=", 1, "lin_geq") + q_p.quadratic_constraint( + {"x": 1, "y": 1}, + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + "==", + 1, + "quad_eq", + ) + q_p.quadratic_constraint( + {"x": 1, "y": 1}, + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + "<=", + 1, + "quad_leq", + ) + q_p.quadratic_constraint( + {"x": 1, "y": 1}, + {("x", "x"): 1, ("y", "z"): -1, ("z", "z"): 2}, + ">=", + 1, + "quad_geq", + ) + + return q_p + + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_lp_file(self): + """test read lp file""" + try: + with self.assertRaises(IOError): + read_from_lp_file("") + with self.assertRaises(IOError): + read_from_lp_file("no_file.txt") + with self.assertRaises(IOError): + read_from_lp_file("no_file.lp") + lp_file = self.get_resource_path("test_quadratic_program.lp", "problems/resources") + q_p = read_from_lp_file(lp_file) + + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_mps_file(self): + """test read mps file""" + try: + with self.assertRaises(IOError): + read_from_mps_file("") + with self.assertRaises(IOError): + read_from_mps_file("no_file.txt") + with self.assertRaises(IOError): + read_from_mps_file("no_file.mps") + mps_file = self.get_resource_path("test_quadratic_program.mps", "problems/resources") + q_p = read_from_mps_file(mps_file) + + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_lp_gz_file(self): + """test read compressed lp file""" + try: + with self.assertRaises(IOError): + read_from_mps_file("") + with self.assertRaises(IOError): + read_from_mps_file("no_file.txt") + with self.assertRaises(IOError): + read_from_mps_file("no_file.lp.gz") + mps_file = self.get_resource_path("test_quadratic_program.lp.gz", "problems/resources") + q_p = read_from_lp_file(mps_file) + + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_read_from_mps_gz_file(self): + """test read compressed mps file""" + try: + with self.assertRaises(IOError): + read_from_mps_file("") + with self.assertRaises(IOError): + read_from_mps_file("no_file.txt") + with self.assertRaises(IOError): + read_from_mps_file("no_file.mps.gz") + mps_file = self.get_resource_path("test_quadratic_program.mps.gz", "problems/resources") + q_p = read_from_mps_file(mps_file) + + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + + def test_write_to_lp_file(self): + """test write problem to lp file""" + q_p = self.helper_test_write_problem_file() + + reference_file_name = self.get_resource_path( + "test_quadratic_program.lp", "problems/resources" + ) + with tempfile.TemporaryDirectory() as tmp: + temp_output_path = path.join(tmp, "temp.lp") + write_to_lp_file(q_p, temp_output_path) + with open(reference_file_name, encoding="utf8") as reference, open( + temp_output_path, encoding="utf8" + ) as temp_output_file: + lines1 = temp_output_file.readlines() + lines2 = reference.readlines() + self.assertListEqual(lines1, lines2) + + with tempfile.TemporaryDirectory() as temp_problem_dir: + write_to_lp_file(q_p, temp_problem_dir) + with open(path.join(temp_problem_dir, "my_problem.lp"), encoding="utf8") as file1, open( + reference_file_name, encoding="utf8" + ) as file2: + lines1 = file1.readlines() + lines2 = file2.readlines() + self.assertListEqual(lines1, lines2) + + with self.assertRaises(OSError): + write_to_lp_file(q_p, "/cannot/write/this/file.lp") + + with self.assertRaises(DOcplexException): + write_to_lp_file(q_p, "") + + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_write_to_mps_file(self): + """test write problem to mps file""" + q_p = self.helper_test_write_problem_file() + + reference_file_name = self.get_resource_path( + "test_quadratic_program.mps", "problems/resources" + ) + with tempfile.TemporaryDirectory() as tmp: + temp_output_path = path.join(tmp, "temp.mps") + write_to_mps_file(q_p, temp_output_path) + with open(reference_file_name, encoding="utf8") as reference, open( + temp_output_path, encoding="utf8" + ) as temp_output_file: + lines1 = temp_output_file.readlines() + lines2 = reference.readlines() + self.assertListEqual(lines1, lines2) + + with tempfile.TemporaryDirectory() as temp_problem_dir: + write_to_mps_file(q_p, temp_problem_dir) + with open( + path.join(temp_problem_dir, "my_problem.mps"), encoding="utf8" + ) as file1, open(reference_file_name, encoding="utf8") as file2: + lines1 = file1.readlines() + lines2 = file2.readlines() + self.assertListEqual(lines1, lines2) + + with self.assertRaises(OSError): + write_to_mps_file(q_p, "/cannot/write/this/file.mps") + + with self.assertRaises(DOcplexException): + write_to_mps_file(q_p, "") From e45be75a72d578a3966b9e263ac2a2bc0c6cf4a7 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Wed, 27 Dec 2023 16:07:24 +0100 Subject: [PATCH 14/21] Fix tests. --- docs/tutorials/01_quadratic_program.ipynb | 42 +++++++++++----------- qiskit_optimization/translators/file_io.py | 1 - 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/docs/tutorials/01_quadratic_program.ipynb b/docs/tutorials/01_quadratic_program.ipynb index 2ce094b71..a5f3c56db 100644 --- a/docs/tutorials/01_quadratic_program.ipynb +++ b/docs/tutorials/01_quadratic_program.ipynb @@ -119,7 +119,6 @@ "source": [ "# Make a Docplex model\n", "from docplex.mp.model import Model\n", - "from qiskit_optimization.translators import export_as_lp_string\n", "\n", "mdl = Model(\"docplex model\")\n", "x = mdl.binary_var(\"x\")\n", @@ -127,7 +126,7 @@ "mdl.minimize(x + 2 * y)\n", "mdl.add_constraint(x - y == 3)\n", "mdl.add_constraint((x + y) * (x - y) <= 1)\n", - "print(export_as_lp_string(mdl))" + "print(mdl.export_as_lp_string())" ] }, { @@ -760,33 +759,24 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 1, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "\\ This file has been generated by DOcplex\n", - "\\ ENCODING=ISO-8859-1\n", - "\\Problem name: CPLEX\n", - "\n", - "Minimize\n", - " obj: _e + 2 f + 3 g\n", - "Subject To\n", - "\n", - "Bounds\n", - " 0 <= _e <= 1\n", - " 0 <= f <= 1\n", - "\n", - "Binaries\n", - " _e f\n", - "End\n", - "\n" + "ename": "NameError", + "evalue": "name 'QuadraticProgram' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m mod \u001b[38;5;241m=\u001b[39m \u001b[43mQuadraticProgram\u001b[49m()\n\u001b[1;32m 2\u001b[0m mod\u001b[38;5;241m.\u001b[39mbinary_var(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124me\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3\u001b[0m mod\u001b[38;5;241m.\u001b[39mbinary_var(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'QuadraticProgram' is not defined" ] } ], "source": [ + "from qiskit_optimization.translators import export_as_lp_string\n", + "\n", "mod = QuadraticProgram()\n", "mod.binary_var(name=\"e\")\n", "mod.binary_var(name=\"f\")\n", @@ -848,7 +838,15 @@ "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", "version": "3.10.13" } }, diff --git a/qiskit_optimization/translators/file_io.py b/qiskit_optimization/translators/file_io.py index 593056476..d6fe02355 100644 --- a/qiskit_optimization/translators/file_io.py +++ b/qiskit_optimization/translators/file_io.py @@ -174,7 +174,6 @@ def _parse_problem_name(filename: str) -> str: return _read_from_file(filename, [".mps"], _parse_problem_name) -@_optionals.HAS_CPLEX.require_in_call def write_to_lp_file(q_p: QuadraticProgram, filename: str): """Writes the quadratic program to an LP file. From caf8ec97c6f2bf83ad92f652b7a6b3f446d77ceb Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Wed, 27 Dec 2023 16:13:49 +0100 Subject: [PATCH 15/21] Fix export in Tutorial 11. --- .../11_using_classical_optimization_solvers_and_models.ipynb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb b/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb index b8611b03a..7702a8390 100644 --- a/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb +++ b/docs/tutorials/11_using_classical_optimization_solvers_and_models.ipynb @@ -417,7 +417,6 @@ ], "source": [ "from qiskit_optimization.translators import to_gurobipy, to_docplex_mp\n", - "from qiskit_optimization.translators import export_as_lp_string\n", "\n", "gmod = to_gurobipy(from_docplex_mp(docplex_model))\n", "print(\"convert docplex to gurobipy via QuadraticProgram\")\n", @@ -425,7 +424,7 @@ "\n", "dmod = to_docplex_mp(from_gurobipy(gurobipy_model))\n", "print(\"\\nconvert gurobipy to docplex via QuadraticProgram\")\n", - "print(export_as_lp_string(dmod))" + "print(dmod.export_as_lp_string())" ] }, { @@ -471,8 +470,6 @@ } ], "source": [ - "from qiskit_optimization.translators import export_as_lp_string\n", - "\n", "ind_mod = Model(\"docplex\")\n", "x = ind_mod.binary_var(\"x\")\n", "y = ind_mod.integer_var(-1, 2, \"y\")\n", From f9e418cde56c021cfa2febbfde65e91d966cd167 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 5 Feb 2024 11:10:12 +0100 Subject: [PATCH 16/21] Re-ran tutorial; added deprecation notes. --- docs/tutorials/01_quadratic_program.ipynb | 35 +++++++++----- .../problems/quadratic_program.py | 48 +++++++++---------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/docs/tutorials/01_quadratic_program.ipynb b/docs/tutorials/01_quadratic_program.ipynb index a5f3c56db..eeae827fc 100644 --- a/docs/tutorials/01_quadratic_program.ipynb +++ b/docs/tutorials/01_quadratic_program.ipynb @@ -390,7 +390,7 @@ "\n", "quadratic dict w/ index:\t {(0, 1): 2, (2, 2): -1}\n", "quadratic dict w/ name:\t\t {('x', 'y'): 2, ('z', 'z'): -1}\n", - "symmetric quadratic dict w/ name:\t {('y', 'x'): 1, ('x', 'y'): 1, ('z', 'z'): -1}\n", + "symmetric quadratic dict w/ name:\t {('x', 'y'): 1, ('y', 'x'): 1, ('z', 'z'): -1}\n", "quadratic matrix:\n", " [[ 0 2 0]\n", " [ 0 0 0]\n", @@ -759,18 +759,29 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 16, "metadata": {}, "outputs": [ { - "ename": "NameError", - "evalue": "name 'QuadraticProgram' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m mod \u001b[38;5;241m=\u001b[39m \u001b[43mQuadraticProgram\u001b[49m()\n\u001b[1;32m 2\u001b[0m mod\u001b[38;5;241m.\u001b[39mbinary_var(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124me\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3\u001b[0m mod\u001b[38;5;241m.\u001b[39mbinary_var(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", - "\u001b[0;31mNameError\u001b[0m: name 'QuadraticProgram' is not defined" + "name": "stdout", + "output_type": "stream", + "text": [ + "\\ This file has been generated by DOcplex\n", + "\\ ENCODING=ISO-8859-1\n", + "\\Problem name: CPLEX\n", + "\n", + "Minimize\n", + " obj: _e + 2 f + 3 g\n", + "Subject To\n", + "\n", + "Bounds\n", + " 0 <= _e <= 1\n", + " 0 <= f <= 1\n", + "\n", + "Binaries\n", + " _e f\n", + "End\n", + "\n" ] } ], @@ -794,7 +805,7 @@ { "data": { "text/html": [ - "

Version Information

Qiskit SoftwareVersion
qiskit-terra0.21.0.dev0+dbd3961
qiskit-aer0.10.4
qiskit-ibmq-provider0.19.1
qiskit-optimization0.4.0
System information
Python version3.10.4
Python compilerGCC 11.2.0
Python buildmain, Apr 2 2022 09:04:19
OSLinux
CPUs4
Memory (Gb)14.577545166015625
Wed May 18 16:03:27 2022 JST
" + "

Version Information

SoftwareVersion
qiskit0.45.1
qiskit_optimization0.7.0
System information
Python version3.10.13
Python compilerGCC 13.2.1 20231205 (Red Hat 13.2.1-6)
Python buildmain, Dec 18 2023 00:00:00
OSLinux
CPUs12
Memory (Gb)31.056407928466797
Mon Feb 05 10:25:02 2024 CET
" ], "text/plain": [ "" @@ -806,7 +817,7 @@ { "data": { "text/html": [ - "

This code is a part of Qiskit

© Copyright IBM 2017, 2022.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

" + "

This code is a part of Qiskit

© Copyright IBM 2017, 2024.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

" ], "text/plain": [ "" diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 565ab7368..71d44eff1 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -21,6 +21,7 @@ import numpy as np from numpy import ndarray +from qiskit.utils.deprecation import deprecate_func from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator from scipy.sparse import spmatrix @@ -910,20 +911,19 @@ def _copy_from(self, other: "QuadraticProgram", include_name: bool) -> None: elem.quadratic_program = self setattr(self, attr, val) + @deprecate_func( + additional_msg=( + "Please use export_as_lp_string from qiskit_optimization.translators.file_io instead." + ), + since="0.6.0", + pending=True, + ) def export_as_lp_string(self) -> str: """Returns the quadratic program as a string of LP format. Returns: A string representing the quadratic program. """ - warn( - "The QuadraticProgram.export_as_lp_string function is pending deprecation. " - "Please use export_as_lp_string from qiskit_optimization.translators.file_io instead." - "This function will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - stacklevel=2, - ) # pylint: disable=cyclic-import from ..translators.file_io import export_as_lp_string @@ -931,6 +931,13 @@ def export_as_lp_string(self) -> str: return export_as_lp_string(self) @_optionals.HAS_CPLEX.require_in_call + @deprecate_func( + additional_msg=( + "Please use read_from_lp_file from qiskit_optimization.translators.file_io instead." + ), + since="0.6.0", + pending=True, + ) def read_from_lp_file(self, filename: str) -> None: """Loads the quadratic program from a LP file (may be gzip'ed). @@ -944,20 +951,18 @@ def read_from_lp_file(self, filename: str) -> None: This method requires CPLEX to be installed and present in ``PYTHONPATH``. """ - warn( - "The QuadraticProgram.read_from_lp_file function is pending deprecation. " - "Please use read_from_lp_file from qiskit_optimization.translators.file_io instead." - "This function will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - stacklevel=2, - ) - # pylint: disable=cyclic-import from ..translators.file_io import read_from_lp_file self._copy_from(read_from_lp_file(filename), include_name=True) + @deprecate_func( + additional_msg=( + "Please use write_to_lp_file from qiskit_optimization.translators.file_io instead." + ), + since="0.6.0", + pending=True, + ) def write_to_lp_file(self, filename: str) -> None: """Writes the quadratic program to an LP file. @@ -971,15 +976,6 @@ def write_to_lp_file(self, filename: str) -> None: DOcplexException: If filename is an empty string """ - warn( - "The QuadraticProgram.write_to_lp_file function is pending deprecation. " - "Please use write_to_lp_file from qiskit_optimization.translators.file_io instead." - "This function will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - stacklevel=2, - ) - # pylint: disable=cyclic-import from ..translators.file_io import write_to_lp_file From 2b44785b24cac1392ef8c281bed4e623374a592a Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 5 Feb 2024 11:39:30 +0100 Subject: [PATCH 17/21] Added a release note. --- ...d-mps-gz-translator-support-e9d6750cb401e3c9.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml diff --git a/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml b/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml new file mode 100644 index 000000000..b92bab383 --- /dev/null +++ b/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added a `file_io` unit in `translators` with support for reading and writing + `.mps, .mps.gz, .lp, .lp.gz` files from/to QuadraticPrograms. +deprecations: + - | + Due to the new unit in `translators`, the functions + :meth:`~qiskit_optimization.problems.QuadraticProgram.export_as_lp_string`, + :meth:`~qiskit_optimization.problems.QuadraticProgram.read_from_lp_file`, + :meth:`~qiskit_optimization.problems.QuadraticProgram.write_to_lp_file`, + have been deprecated. From c4893695887970d5641cee900d9af11557fb4d4f Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Mon, 5 Feb 2024 11:43:39 +0100 Subject: [PATCH 18/21] Updated copyright. --- qiskit_optimization/problems/quadratic_program.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 71d44eff1..466c7cac3 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory From ae1d6eae5ae5fe6a5efeb4f4cd26ac0add559ced Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Fri, 22 Mar 2024 10:00:50 +0100 Subject: [PATCH 19/21] Added file_io to documentation, corrected dates. --- .../problems/quadratic_program.py | 6 +++--- qiskit_optimization/translators/__init__.py | 17 +++++++++++++++-- qiskit_optimization/translators/file_io.py | 8 ++++---- ...-gz-translator-support-e9d6750cb401e3c9.yaml | 12 ------------ ...-gz-translator-support-e9d6750cb401e3c9.yaml | 16 ++++++++++++++++ 5 files changed, 38 insertions(+), 21 deletions(-) delete mode 100644 releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml create mode 100644 releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 466c7cac3..ee0fd05e8 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -915,7 +915,7 @@ def _copy_from(self, other: "QuadraticProgram", include_name: bool) -> None: additional_msg=( "Please use export_as_lp_string from qiskit_optimization.translators.file_io instead." ), - since="0.6.0", + since="0.6.1", pending=True, ) def export_as_lp_string(self) -> str: @@ -935,7 +935,7 @@ def export_as_lp_string(self) -> str: additional_msg=( "Please use read_from_lp_file from qiskit_optimization.translators.file_io instead." ), - since="0.6.0", + since="0.6.1", pending=True, ) def read_from_lp_file(self, filename: str) -> None: @@ -960,7 +960,7 @@ def read_from_lp_file(self, filename: str) -> None: additional_msg=( "Please use write_to_lp_file from qiskit_optimization.translators.file_io instead." ), - since="0.6.0", + since="0.6.1", pending=True, ) def write_to_lp_file(self, filename: str) -> None: diff --git a/qiskit_optimization/translators/__init__.py b/qiskit_optimization/translators/__init__.py index e30294d3a..c747e5b2a 100644 --- a/qiskit_optimization/translators/__init__.py +++ b/qiskit_optimization/translators/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -17,7 +17,7 @@ .. currentmodule:: qiskit_optimization.translators Translators between :class:`~qiskit_optimization.problems.QuadraticProgram` and -other optimization models or other objects. +other optimization models or other objects, including loading and writing to/from files. Translators ---------------------- @@ -31,6 +31,19 @@ to_gurobipy from_ising to_ising + +File loading and saving +------------------------- +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + export_as_lp_string + export_as_mps_string + read_from_lp_file + read_from_mps_file + write_to_lp_file + write_to_mps_file """ from .docplex_mp import from_docplex_mp, to_docplex_mp diff --git a/qiskit_optimization/translators/file_io.py b/qiskit_optimization/translators/file_io.py index d6fe02355..1d1c88ebd 100644 --- a/qiskit_optimization/translators/file_io.py +++ b/qiskit_optimization/translators/file_io.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023 +# (C) Copyright IBM 2024 # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -43,7 +43,7 @@ def export_as_lp_string(q_p: QuadraticProgram) -> str: @_optionals.HAS_CPLEX.require_in_call def export_as_mps_string(q_p: QuadraticProgram) -> str: - """Returns the quadratic program as a string of LP format. + """Returns the quadratic program as a string of MPS format. Args: q_p: The quadratic program to be exported. @@ -114,7 +114,7 @@ def _read_from_file( @_optionals.HAS_CPLEX.require_in_call def read_from_lp_file(filename: str) -> QuadraticProgram: - """Loads the quadratic program from a LP file (may be gzip'ed). + """Loads the quadratic program from a LP file ('.lp' or compressed '.lp.gz'). Args: filename: The filename of the file to be loaded. @@ -145,7 +145,7 @@ def _parse_problem_name(filename: str) -> str: @_optionals.HAS_CPLEX.require_in_call def read_from_mps_file(filename: str) -> QuadraticProgram: - """Loads the quadratic program from a MPS file (may be gzip'ed). + """Loads the quadratic program from a MPS file ('.mps' or compressed '.mps.gz'). Args: filename: The filename of the file to be loaded. diff --git a/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml b/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml deleted file mode 100644 index b92bab383..000000000 --- a/releasenotes/notes/0.6/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -features: - - | - Added a `file_io` unit in `translators` with support for reading and writing - `.mps, .mps.gz, .lp, .lp.gz` files from/to QuadraticPrograms. -deprecations: - - | - Due to the new unit in `translators`, the functions - :meth:`~qiskit_optimization.problems.QuadraticProgram.export_as_lp_string`, - :meth:`~qiskit_optimization.problems.QuadraticProgram.read_from_lp_file`, - :meth:`~qiskit_optimization.problems.QuadraticProgram.write_to_lp_file`, - have been deprecated. diff --git a/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml b/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml new file mode 100644 index 000000000..48c1ccbc6 --- /dev/null +++ b/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Support for reading and writing `mps` and `lp` files has moved to functions + :func:`translators.file_io.read_from_mps_file` and :func:`translators.file_io.read_from_lp_file`, + returning `QuadraticProgram` instances. In addition, both functions accept gzipped files as + `lp.gz` and `mps.gz`. In the same unit, :func:`translators.file_io.write_to_mps_file` and + :func:`translators.file_io.write_to_lp_file` have been added to write `QuadraticPrograms` to + `mps` and `lp` files. +deprecations: + - | + Due to the new unit in `translators`, the functions + :meth:`~qiskit_optimization.problems.QuadraticProgram.export_as_lp_string`, + :meth:`~qiskit_optimization.problems.QuadraticProgram.read_from_lp_file`, + :meth:`~qiskit_optimization.problems.QuadraticProgram.write_to_lp_file`, + have been deprecated. From 10218e0a44e02593eaf9b05e0ffbb01e25198f04 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Fri, 22 Mar 2024 12:14:23 +0100 Subject: [PATCH 20/21] Adding tests for deprecated functions. --- .pylintdict | 1 + ...z-translator-support-e9d6750cb401e3c9.yaml | 2 +- test/translators/test_file_io.py | 58 ++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.pylintdict b/.pylintdict index 6252e2003..75685b4ae 100644 --- a/.pylintdict +++ b/.pylintdict @@ -82,6 +82,7 @@ gurobioptimizer gurobipy gutmann gzip'ed +gz hamilton hamiltonian hamiltonians diff --git a/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml b/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml index 48c1ccbc6..d8682c220 100644 --- a/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml +++ b/releasenotes/notes/add-mps-gz-translator-support-e9d6750cb401e3c9.yaml @@ -3,7 +3,7 @@ features: - | Support for reading and writing `mps` and `lp` files has moved to functions :func:`translators.file_io.read_from_mps_file` and :func:`translators.file_io.read_from_lp_file`, - returning `QuadraticProgram` instances. In addition, both functions accept gzipped files as + returning `QuadraticProgram` instances. In addition, both functions accept gzip'ed files as `lp.gz` and `mps.gz`. In the same unit, :func:`translators.file_io.write_to_mps_file` and :func:`translators.file_io.write_to_lp_file` have been added to write `QuadraticPrograms` to `mps` and `lp` files. diff --git a/test/translators/test_file_io.py b/test/translators/test_file_io.py index 2857d7dd8..ad3aa95f3 100644 --- a/test/translators/test_file_io.py +++ b/test/translators/test_file_io.py @@ -140,6 +140,25 @@ def helper_test_write_problem_file(self): return q_p + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") + def test_qp_read_from_lp_file(self): + """test read lp file""" + q_p = QuadraticProgram() + + try: + with self.assertRaises(IOError): + q_p.read_from_lp_file("") + with self.assertRaises(IOError): + q_p.read_from_lp_file("no_file.txt") + with self.assertRaises(IOError): + q_p.read_from_lp_file("no_file.lp") + lp_file = self.get_resource_path("test_quadratic_program.lp", "problems/resources") + q_p.read_from_lp_file(lp_file) + + self.helper_test_read_problem_file(q_p) + except RuntimeError as ex: + self.fail(str(ex)) + @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_read_from_lp_file(self): """test read lp file""" @@ -177,13 +196,14 @@ def test_read_from_mps_file(self): @unittest.skipIf(not _optionals.HAS_CPLEX, "CPLEX not available.") def test_read_from_lp_gz_file(self): """test read compressed lp file""" + q_p = QuadraticProgram() try: with self.assertRaises(IOError): - read_from_mps_file("") + read_from_lp_file("") with self.assertRaises(IOError): - read_from_mps_file("no_file.txt") + read_from_lp_file("no_file.txt") with self.assertRaises(IOError): - read_from_mps_file("no_file.lp.gz") + read_from_lp_file("no_file.lp.gz") mps_file = self.get_resource_path("test_quadratic_program.lp.gz", "problems/resources") q_p = read_from_lp_file(mps_file) @@ -208,6 +228,38 @@ def test_read_from_mps_gz_file(self): except RuntimeError as ex: self.fail(str(ex)) + def test_qp_write_to_lp_file(self): + """test write problem to lp file""" + q_p = self.helper_test_write_problem_file() + + reference_file_name = self.get_resource_path( + "test_quadratic_program.lp", "problems/resources" + ) + with tempfile.TemporaryDirectory() as tmp: + temp_output_path = path.join(tmp, "temp.lp") + q_p.write_to_lp_file(temp_output_path) + with open(reference_file_name, encoding="utf8") as reference, open( + temp_output_path, encoding="utf8" + ) as temp_output_file: + lines1 = temp_output_file.readlines() + lines2 = reference.readlines() + self.assertListEqual(lines1, lines2) + + with tempfile.TemporaryDirectory() as temp_problem_dir: + q_p.write_to_lp_file(temp_problem_dir) + with open(path.join(temp_problem_dir, "my_problem.lp"), encoding="utf8") as file1, open( + reference_file_name, encoding="utf8" + ) as file2: + lines1 = file1.readlines() + lines2 = file2.readlines() + self.assertListEqual(lines1, lines2) + + with self.assertRaises(OSError): + q_p.write_to_lp_file("/cannot/write/this/file.lp") + + with self.assertRaises(DOcplexException): + q_p.write_to_lp_file("") + def test_write_to_lp_file(self): """test write problem to lp file""" q_p = self.helper_test_write_problem_file() From dba2d5413248c3b20bd9ea800d7194ab7be8f3c7 Mon Sep 17 00:00:00 2001 From: Daniel Thuerck Date: Fri, 22 Mar 2024 12:23:09 +0100 Subject: [PATCH 21/21] Bump year. --- test/translators/test_file_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/translators/test_file_io.py b/test/translators/test_file_io.py index ad3aa95f3..b02edc371 100644 --- a/test/translators/test_file_io.py +++ b/test/translators/test_file_io.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023 +# (C) Copyright IBM 2024 # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory