Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mps support #581

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ gurobi
gurobioptimizer
gurobipy
gutmann
gzip'ed
hamilton
hamiltonian
hamiltonians
Expand Down Expand Up @@ -125,6 +126,7 @@ minimizer
minimumeigenoptimizer
mmp
mpm
mps
multiset
mypy
nannicini
Expand Down Expand Up @@ -233,6 +235,7 @@ transpiling
travelling
troyer
tsplib
uncompress
undirected
upperbound
variational
Expand Down
142 changes: 134 additions & 8 deletions qiskit_optimization/problems/quadratic_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@

"""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
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
from tempfile import NamedTemporaryFile

import numpy as np
from docplex.mp.model_reader import ModelReader
Expand Down Expand Up @@ -922,15 +926,83 @@ 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.

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,
)

# 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, not supported or the file is not found.

Note:
This method requires CPLEX to be installed and present in ``PYTHONPATH``.
Expand All @@ -950,12 +1022,37 @@ 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``.
"""

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.
Expand All @@ -975,6 +1072,35 @@ 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)
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,
constants: Optional[Dict[Union[str, int], float]] = None,
Expand Down
62 changes: 62 additions & 0 deletions test/problems/resources/test_quadratic_program.mps
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
Loading