-
Notifications
You must be signed in to change notification settings - Fork 134
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
Added support for an indicator constraint to QuadraticProgram #151
Changes from 24 commits
fa25c9d
f13877d
de7ec33
9cac005
fdd5dde
550d0d8
3779736
d9f52b1
aa24a27
606f64a
a43294c
03bddbf
6634027
33b3f88
e512f70
d6d37df
c41e705
5cf98cb
c3ce5c9
f8d5ac9
7662bd1
5588733
989f30b
260f34c
818be2a
58003d9
15d0d96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,6 +73,7 @@ hamiltonians | |
hastings | ||
hoyer | ||
imode | ||
indicatorconstraint | ||
init | ||
initializer | ||
instantiation | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2021. | ||
# | ||
# 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 indicator to linear constraint converter.""" | ||
|
||
from typing import List, Optional, Union | ||
|
||
import numpy as np | ||
|
||
from .quadratic_program_converter import QuadraticProgramConverter | ||
from ..exceptions import QiskitOptimizationError | ||
from ..problems.constraint import Constraint | ||
from ..problems.quadratic_objective import QuadraticObjective | ||
from ..problems.quadratic_program import QuadraticProgram | ||
from ..problems.variable import Variable | ||
|
||
|
||
class IndicatorToInequality(QuadraticProgramConverter): | ||
"""Convert indicator constraints into inequality constraints by using a big-M formulation. | ||
e.g. x = 1 -> ax <= b is converted into ax <= b + M * (1 - z) | ||
|
||
Examples: | ||
>>> from qiskit_optimization.problems import QuadraticProgram | ||
>>> from qiskit_optimization.converters import IndicatorToInequality | ||
>>> problem = QuadraticProgram() | ||
>>> # define a problem | ||
>>> conv = IndicatorToInequality() | ||
>>> problem2 = conv.convert(problem) | ||
""" | ||
|
||
_delimiter = "@" # users are supposed not to use this character in variable names | ||
|
||
def __init__(self) -> None: | ||
self._src_num_var = 0 | ||
self._dst = None # type: Optional[QuadraticProgram] | ||
|
||
def convert(self, problem: QuadraticProgram) -> QuadraticProgram: | ||
"""Convert a problem with indicator constraints into one with only inequality constraints. | ||
|
||
Args: | ||
problem: The problem to be solved, that may contain indicator constraints. | ||
|
||
Returns: | ||
The converted problem, that contain only inequality constraints. | ||
|
||
Raises: | ||
QiskitOptimizationError: If a variable type is not supported. | ||
QiskitOptimizationError: If an unsupported mode is selected. | ||
QiskitOptimizationError: If an unsupported sense is specified. | ||
""" | ||
self._src_num_var = problem.get_num_vars() | ||
self._dst = QuadraticProgram(name=problem.name) | ||
|
||
# Copy variables | ||
for x in problem.variables: | ||
if x.vartype == Variable.Type.BINARY: | ||
self._dst.binary_var(name=x.name) | ||
elif x.vartype == Variable.Type.INTEGER: | ||
self._dst.integer_var(name=x.name, lowerbound=x.lowerbound, upperbound=x.upperbound) | ||
elif x.vartype == Variable.Type.CONTINUOUS: | ||
self._dst.continuous_var( | ||
name=x.name, lowerbound=x.lowerbound, upperbound=x.upperbound | ||
) | ||
else: | ||
raise QiskitOptimizationError(f"Unsupported variable type {x.vartype}") | ||
|
||
# Copy the objective function | ||
constant = problem.objective.constant | ||
linear = problem.objective.linear.to_dict(use_name=True) | ||
quadratic = problem.objective.quadratic.to_dict(use_name=True) | ||
if problem.objective.sense == QuadraticObjective.Sense.MINIMIZE: | ||
self._dst.minimize(constant, linear, quadratic) | ||
else: | ||
self._dst.maximize(constant, linear, quadratic) | ||
|
||
# For linear constraints | ||
for l_constraint in problem.linear_constraints: | ||
linear = l_constraint.linear.to_dict(use_name=True) | ||
self._dst.linear_constraint( | ||
linear, l_constraint.sense, l_constraint.rhs, l_constraint.name | ||
) | ||
# For quadratic constraints | ||
for q_constraint in problem.quadratic_constraints: | ||
linear = q_constraint.linear.to_dict(use_name=True) | ||
quadratic = q_constraint.quadratic.to_dict(use_name=True) | ||
self._dst.quadratic_constraint( | ||
linear, | ||
quadratic, | ||
q_constraint.sense, | ||
q_constraint.rhs, | ||
q_constraint.name, | ||
) | ||
# For indicator constraints | ||
for i_constraint in problem.indicator_constraints: | ||
self._convert_indicator_constraint(problem, i_constraint) | ||
|
||
return self._dst | ||
|
||
def _convert_indicator_constraint(self, problem, indicator_const): | ||
# convert indicator constraints to inequality constraints | ||
new_linear = indicator_const.linear.to_dict(use_name=True) | ||
new_rhs = indicator_const.rhs | ||
sense = indicator_const.sense | ||
new_name = indicator_const.name + self._delimiter + "indicator" | ||
if sense == Constraint.Sense.LE: | ||
_, lhs_ub = self._calc_linear_bounds(problem, new_linear) | ||
big_m = lhs_ub - new_rhs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make sure that big_m is a positive value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it does work even when big_m is a negative value in this case. When it's inactive, it becomes lhs <= the upperbounds of lhs. (When rhs is larger than the upper bound of lhs, the constraint doesn't make sense though). |
||
if indicator_const.active_value: | ||
new_linear[indicator_const.binary_var.name] = big_m | ||
new_rhs = new_rhs + big_m | ||
else: | ||
new_linear[indicator_const.binary_var.name] = -big_m | ||
self._dst.linear_constraint(new_linear, "<=", new_rhs, new_name) | ||
elif sense == Constraint.Sense.GE: | ||
lhs_lb, _ = self._calc_linear_bounds(problem, new_linear) | ||
big_m = new_rhs - lhs_lb | ||
if indicator_const.active_value: | ||
new_linear[indicator_const.binary_var.name] = -big_m | ||
new_rhs = new_rhs - big_m | ||
else: | ||
new_linear[indicator_const.binary_var.name] = big_m | ||
self._dst.linear_constraint(new_linear, ">=", new_rhs, new_name) | ||
elif sense == Constraint.Sense.EQ: | ||
# for equality constraints, add both GE and LE constraints. | ||
# new_linear2, new_rhs2, and big_m2 are for a >= constraint | ||
new_linear2 = indicator_const.linear.to_dict(use_name=True) | ||
new_rhs2 = indicator_const.rhs | ||
lhs_lb, lhs_ub = self._calc_linear_bounds(problem, new_linear) | ||
big_m = lhs_ub - new_rhs | ||
big_m2 = new_rhs - lhs_lb | ||
if indicator_const.active_value: | ||
new_linear[indicator_const.binary_var.name] = big_m | ||
new_rhs = new_rhs + big_m | ||
new_linear2[indicator_const.binary_var.name] = -big_m2 | ||
new_rhs2 = new_rhs2 - big_m2 | ||
else: | ||
new_linear[indicator_const.binary_var.name] = -big_m | ||
new_linear2[indicator_const.binary_var.name] = big_m2 | ||
self._dst.linear_constraint(new_linear, "<=", new_rhs, new_name + "_LE") | ||
self._dst.linear_constraint(new_linear2, ">=", new_rhs2, new_name + "_GE") | ||
|
||
def _calc_linear_bounds(self, problem, linear): | ||
lhs_lb, lhs_ub = 0, 0 | ||
for var_name, v in linear.items(): | ||
x = problem.get_variable(var_name) | ||
lhs_lb += min(x.lowerbound * v, x.upperbound * v) | ||
lhs_ub += max(x.lowerbound * v, x.upperbound * v) | ||
return lhs_lb, lhs_ub | ||
|
||
def interpret(self, x: Union[np.ndarray, List[float]]) -> np.ndarray: | ||
"""Convert the result of the converted problem back to that of the original problem | ||
|
||
Args: | ||
x: The result of the converted problem or the given result in case of FAILURE. | ||
|
||
Returns: | ||
The result of the original problem. | ||
|
||
Raises: | ||
QiskitOptimizationError: if the number of variables in the result differs from | ||
that of the original problem. | ||
""" | ||
if len(x) != self._src_num_var: | ||
raise QiskitOptimizationError( | ||
"The number of variables in the passed result differs from " | ||
a-matsuo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
f"that of the original problem. Original: {self._src_num_var}, Given: {len(x)}" | ||
) | ||
return np.asarray(x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest
IndicatorToLinear
because this converts not only inequality constraints but also equality constraints. Another reason is that there is another way to convert indicator constraints into quadratic constraints.