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 ScipyMilpOptimizer #496

Merged
merged 20 commits into from
May 11, 2023
Merged
1 change: 1 addition & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ maxcut
maxfun
maxiter
mdl
milp
minimizer
minimumeigenoptimizer
multiset
Expand Down
3 changes: 3 additions & 0 deletions qiskit_optimization/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
OptimizationResultStatus
RecursiveMinimumEigenOptimizationResult
RecursiveMinimumEigenOptimizer
ScipyMilpOptimizer
SlsqpOptimizationResult
SlsqpOptimizer
SolutionSample
Expand Down Expand Up @@ -93,6 +94,7 @@
RecursiveMinimumEigenOptimizationResult,
IntermediateResult,
)
from .scipy_milp_optimizer import ScipyMilpOptimizer
from .slsqp_optimizer import SlsqpOptimizer, SlsqpOptimizationResult
from .warm_start_qaoa_optimizer import (
BaseAggregator,
Expand Down Expand Up @@ -124,6 +126,7 @@
"RecursiveMinimumEigenOptimizer",
"RecursiveMinimumEigenOptimizationResult",
"IntermediateResult",
"ScipyMilpOptimizer",
"SlsqpOptimizer",
"SlsqpOptimizationResult",
"SolutionSample",
Expand Down
187 changes: 187 additions & 0 deletions qiskit_optimization/algorithms/scipy_milp_optimizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# This code is part of Qiskit.
#
# (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.

"""The SciPy MILP optimizer wrapped to be used within Qiskit's optimization module."""

from warnings import warn

import numpy as np

import qiskit_optimization.optionals as _optionals
from qiskit_optimization import INFINITY
from qiskit_optimization.algorithms.optimization_algorithm import (
OptimizationAlgorithm,
OptimizationResult,
OptimizationResultStatus,
)
from qiskit_optimization.problems.quadratic_program import (
ConstraintSense,
QuadraticProgram,
VarType,
)


def _conv_inf(val):
# Note: qiskit-optimization treats INFINITY as infinity
# while scipy treats np.inf as infinity
if val <= -INFINITY:
return -np.inf
elif val >= INFINITY:
return np.inf
woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved
else:
return val


@_optionals.HAS_SCIPY_MILP.require_in_instance
class ScipyMilpOptimizer(OptimizationAlgorithm):
"""The MILP optimizer from Scipy wrapped as a Qiskit :class:`OptimizationAlgorithm`.

This class provides a wrapper for ``scipy.milp``
(https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html)
to be used within the optimization module.
"""

def __init__(self, disp: bool = False) -> None:
"""Initializes the ScipyMILPOptimizer.

Args:
disp: Whether to print MILP output or not.
"""
self._disp = disp

@property
def disp(self) -> bool:
woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the display setting.

Returns:
Whether to print scipy.milp information or not.
"""
return self._disp

@disp.setter
def disp(self, disp: bool):
"""Set the display setting.

Args:
disp: The display setting.
"""
self._disp = disp

# pylint:disable=unused-argument
def get_compatibility_msg(self, problem: QuadraticProgram) -> str:
"""Checks whether a given problem can be solved with this optimizer.

Checks if the problem has only linear objective function and linear constraints.
The ``scipy.milp`` supports only linear objective function and linear constraints.

Args:
problem: The optimization problem to check compatibility.

Returns:
An empty string (if compatible) or a string describing the incompatibility.
"""
msg = []
if problem.objective.quadratic.to_dict():
msg.append("scipy.milp supports only linear objective function")
if problem.quadratic_constraints:
msg.append("scipy.milp supports only linear constraints")
return "; ".join(msg)

def _generate_problem(self, problem: QuadraticProgram):
from scipy.optimize import Bounds, LinearConstraint
from scipy.sparse import lil_array # pylint: disable=no-name-in-module

sense = problem.objective.sense.value
objective = problem.objective.linear.to_array() * sense

integrality = []
for variable in problem.variables:
if variable.vartype == VarType.CONTINUOUS:
integrality.append(0)
else:
integrality.append(1)

lower_bounds = [_conv_inf(variable.lowerbound) for variable in problem.variables]
upper_bounds = [_conv_inf(variable.upperbound) for variable in problem.variables]
bounds = Bounds(lb=lower_bounds, ub=upper_bounds)

lower_bounds = []
upper_bounds = []
mat = lil_array((problem.get_num_linear_constraints(), problem.get_num_vars()))
for i, constraint in enumerate(problem.linear_constraints):
for variable_id, val in constraint.linear.to_dict().items():
mat[i, variable_id] = val

rhs_val = _conv_inf(constraint.rhs)
if constraint.sense == ConstraintSense.GE:
lower_bounds.append(rhs_val)
upper_bounds.append(np.inf)
elif constraint.sense == ConstraintSense.LE:
lower_bounds.append(-np.inf)
upper_bounds.append(rhs_val)
else:
# ConstraintSense.EQ
lower_bounds.append(rhs_val)
upper_bounds.append(rhs_val)

constraints = LinearConstraint(mat, lower_bounds, upper_bounds)

return objective, integrality, bounds, constraints

def solve(self, problem: QuadraticProgram) -> OptimizationResult:
"""Tries to solve the given problem using the optimizer.

Runs the optimizer to try to solve the optimization problem. If problem is not convex,
this optimizer may raise an exception due to incompatibility, depending on the settings.

Args:
problem: The problem to be solved.

Returns:
The result of the optimizer applied to the problem.

Raises:
QiskitOptimizationError: If the problem is incompatible with the optimizer.
"""
from scipy.optimize import milp # pylint: disable=no-name-in-module

woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved
self._verify_compatibility(problem)

objective, integrality, bounds, constraints = self._generate_problem(problem)
raw_result = milp(
c=objective,
integrality=integrality,
bounds=bounds,
constraints=constraints,
options={"disp": self._disp},
)

if raw_result.x is None:
warn("scipy.milp cannot solve the model. See `raw_results` for details.")
x = [0.0] * problem.get_num_vars()
status = OptimizationResultStatus.FAILURE
else:
x = []
for i, ele in enumerate(raw_result.x):
if integrality[i]:
x.append(round(ele))
else:
x.append(ele)
status = self._get_feasibility_status(problem, x)

return OptimizationResult(
x=x,
fval=problem.objective.evaluate(x),
variables=problem.variables,
status=status,
raw_results=raw_result,
)
10 changes: 9 additions & 1 deletion qiskit_optimization/optionals.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 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
Expand Down Expand Up @@ -47,3 +47,11 @@
name="Matplotlib",
install="pip install 'qiskit-optimization[matplotlib]'",
)

HAS_SCIPY_MILP = LazyImportTester(
{
"scipy.optimize": ("milp",),
},
name="SciPy MILP",
install="pip install scipy>=1.9.0'",
)
9 changes: 9 additions & 0 deletions releasenotes/notes/add-scipy-milp-7d78a5ae8c893cc5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
features:
- |
Adds :class:`~qiskit_optimization.algorithms.ScipyMilpOptimizer`
as a classical solver based on
`scipy.optimize.milp <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html>`_,
which can solve mixed-integer linear program (MILP) problems.
Note that ``scipy.optimize.milp`` has been introduced by
`SciPy 1.9.0 <https://docs.scipy.org/doc/scipy/release.1.9.0.html>`_.