diff --git a/.pylintdict b/.pylintdict index 9230a0687..75685b4ae 100644 --- a/.pylintdict +++ b/.pylintdict @@ -81,6 +81,8 @@ gurobi gurobioptimizer gurobipy gutmann +gzip'ed +gz hamilton hamiltonian hamiltonians @@ -125,6 +127,7 @@ minimizer minimumeigenoptimizer mmp mpm +mps multiset mypy nannicini @@ -233,6 +236,7 @@ transpiling travelling troyer tsplib +uncompress undirected upperbound variational diff --git a/docs/tutorials/01_quadratic_program.ipynb b/docs/tutorials/01_quadratic_program.ipynb index 4eaf3f628..4921fdd0c 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", @@ -786,12 +786,15 @@ } ], "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", "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))" ] }, { @@ -802,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": [ "" @@ -814,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": [ "" @@ -840,8 +843,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "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" } }, "nbformat": 4, 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 bcbfc5d10..ee0fd05e8 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 @@ -20,8 +20,8 @@ from warnings import warn import numpy as np -from docplex.mp.model_reader import ModelReader 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 @@ -911,52 +911,58 @@ 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.1", + 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. """ + # pylint: disable=cyclic-import - from ..translators.docplex_mp import to_docplex_mp + from ..translators.file_io import export_as_lp_string - return to_docplex_mp(self).export_as_lp_string() + 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.1", + pending=True, + ) 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, 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 - # pylint: disable=cyclic-import - from ..translators.docplex_mp import from_docplex_mp + from ..translators.file_io import read_from_lp_file - model = ModelReader().read(filename, model_name=_parse_problem_name(filename)) - other = from_docplex_mp(model) - self._copy_from(other, include_name=True) + 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.1", + pending=True, + ) def write_to_lp_file(self, filename: str) -> None: """Writes the quadratic program to an LP file. @@ -969,11 +975,11 @@ 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 + from ..translators.file_io import write_to_lp_file - mdl = to_docplex_mp(self) - mdl.export_as_lp(filename) + 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..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,9 +31,30 @@ 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 +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 +65,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..1d1c88ebd --- /dev/null +++ b/qiskit_optimization/translators/file_io.py @@ -0,0 +1,224 @@ +# This code is part of a Qiskit project. +# +# (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 +# 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 MPS 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 ('.lp' or compressed '.lp.gz'). + + 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 ('.mps' or compressed '.mps.gz'). + + 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) + + +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/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..d8682c220 --- /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 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. +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/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 000000000..93980b0ba Binary files /dev/null and b/test/problems/resources/test_quadratic_program.lp.gz differ 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 000000000..d7a8b4276 Binary files /dev/null and b/test/problems/resources/test_quadratic_program.mps.gz differ diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index a98aba9e8..cd2833968 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -740,12 +740,15 @@ 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(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) @@ -791,36 +794,36 @@ def test_read_from_lp_file(self): 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) except RuntimeError as ex: self.fail(str(ex)) def test_write_to_lp_file(self): - """test write problem""" + """test write problem to lp file""" q_p = QuadraticProgram("my problem") q_p.binary_var("x") q_p.integer_var(-1, 5, "y") diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index 3dbd7c6b2..87a3f5161 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -16,15 +16,17 @@ 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 qiskit_optimization.translators import export_as_lp_string 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") @@ -38,7 +40,7 @@ def test_from_and_to(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()) + self.assertEqual(export_as_lp_string(q_p), export_as_lp_string(q_p2)) mod = Model("test") x = mod.binary_var("x") @@ -47,7 +49,7 @@ def test_from_and_to(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_lp_string(), mod.export_as_lp_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..b02edc371 --- /dev/null +++ b/test/translators/test_file_io.py @@ -0,0 +1,326 @@ +# This code is part of a Qiskit project. +# +# (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 +# 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_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""" + 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""" + q_p = QuadraticProgram() + 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.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_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() + + 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, "")