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

Added support for an indicator constraint to QuadraticProgram #151

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fa25c9d
added the indicator constraint class
a-matsuo May 31, 2021
f13877d
added from_docplex
a-matsuo May 31, 2021
de7ec33
added IndicatorToInequality
a-matsuo Jun 2, 2021
9cac005
added unittest for indi2ineq converter
a-matsuo Jun 2, 2021
fdd5dde
add evaluate_indicator
a-matsuo Jun 2, 2021
550d0d8
added unittests
a-matsuo Jun 4, 2021
3779736
Merge remote-tracking branch 'upstream/main' into indicator_const
a-matsuo Jun 4, 2021
d9f52b1
fix linting
a-matsuo Jun 4, 2021
aa24a27
added a release note
a-matsuo Jun 4, 2021
606f64a
added test for from_docplex and to_docplex
a-matsuo Jun 4, 2021
a43294c
fix linting
a-matsuo Jun 4, 2021
03bddbf
added reno
a-matsuo Jun 4, 2021
6634027
Merge branch 'main' into indicator_const
t-imamichi Jun 4, 2021
33b3f88
fix based on comments
a-matsuo Jun 7, 2021
e512f70
fixed link to bib file (#154)
paniash Jun 4, 2021
d6d37df
update docplex version (#155)
t-imamichi Jun 5, 2021
c41e705
add tutorial
a-matsuo Jun 7, 2021
5cf98cb
Merge branch 'indicator_const' of github.com:a-matsuo/qiskit-optimiza…
a-matsuo Jun 7, 2021
c3ce5c9
fix
a-matsuo Jun 7, 2021
f8d5ac9
add tutorial
a-matsuo Jun 7, 2021
7662bd1
Merge branch 'indicator_const' of github.com:a-matsuo/qiskit-optimiza…
a-matsuo Jun 7, 2021
5588733
fix kernel
a-matsuo Jun 7, 2021
989f30b
fix f string
a-matsuo Jun 7, 2021
260f34c
fix typo and a commnet
a-matsuo Jun 7, 2021
818be2a
Merge branch 'main' into indicator_const
t-imamichi Jun 7, 2021
58003d9
changed its name to IndicatorToLinear
a-matsuo Jun 7, 2021
15d0d96
update tutorial
a-matsuo Jun 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .pylintdict
Expand Up @@ -73,6 +73,7 @@ hamiltonians
hastings
hoyer
imode
indicatorconstraint
init
initializer
instantiation
Expand Down
303 changes: 96 additions & 207 deletions docs/tutorials/02_converters_for_quadratic_programs.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions qiskit_optimization/converters/__init__.py
Expand Up @@ -46,6 +46,7 @@

"""

from .indicator_to_inequality import IndicatorToLinear
from .integer_to_binary import IntegerToBinary
from .inequality_to_equality import InequalityToEquality
from .linear_equality_to_penalty import LinearEqualityToPenalty
Expand All @@ -55,6 +56,7 @@
from .quadratic_program_converter import QuadraticProgramConverter

__all__ = [
"IndicatorToLinear",
"InequalityToEquality",
"IntegerToBinary",
"LinearEqualityToPenalty",
Expand Down
176 changes: 176 additions & 0 deletions qiskit_optimization/converters/indicator_to_linear.py
@@ -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 IndicatorToLinear(QuadraticProgramConverter):
"""Convert indicator constraints into linear 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 IndicatorToLinear
>>> problem = QuadraticProgram()
>>> # define a problem
>>> conv = IndicatorToLinear()
>>> 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 linear constraints.

Args:
problem: The problem to be solved, that may contain indicator constraints.

Returns:
The converted problem, that contain only linear 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 linear 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
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 "
f"that of the original problem. Original: {self._src_num_var}, Given: {len(x)}"
)
return np.asarray(x)
10 changes: 8 additions & 2 deletions qiskit_optimization/converters/quadratic_program_to_qubo.py
Expand Up @@ -40,6 +40,7 @@ def __init__(self, penalty: Optional[float] = None) -> None:
If None is passed, a penalty factor will be automatically calculated on every
conversion.
"""
from ..converters.indicator_to_inequality import IndicatorToLinear
from ..converters.integer_to_binary import IntegerToBinary
from ..converters.inequality_to_equality import InequalityToEquality
from ..converters.linear_equality_to_penalty import LinearEqualityToPenalty
Expand All @@ -49,6 +50,7 @@ def __init__(self, penalty: Optional[float] = None) -> None:
self._ineq_to_eq = InequalityToEquality(mode="integer")
self._penalize_lin_eq_constraints = LinearEqualityToPenalty(penalty=penalty)
self._max_to_min = MaximizeToMinimize()
self._indic_to_ineq = IndicatorToLinear()

def convert(self, problem: QuadraticProgram) -> QuadraticProgram:
"""Convert a problem with linear constraints into new one with a QUBO form.
Expand All @@ -66,10 +68,13 @@ def convert(self, problem: QuadraticProgram) -> QuadraticProgram:
# analyze compatibility of problem
msg = self.get_compatibility_msg(problem)
if len(msg) > 0:
raise QiskitOptimizationError("Incompatible problem: {}".format(msg))
raise QiskitOptimizationError(f"Incompatible problem: {msg}")

# Convert indicator constraints into inequality constraints by using a big-M formulation
problem_ = self._indic_to_ineq.convert(problem)

# Convert inequality constraints into equality constraints by adding slack variables
problem_ = self._ineq_to_eq.convert(problem)
problem_ = self._ineq_to_eq.convert(problem_)

# Map integer variables to binary variables
problem_ = self._int_to_bin.convert(problem_)
Expand All @@ -96,6 +101,7 @@ def interpret(self, x: Union[np.ndarray, List[float]]) -> np.ndarray:
x = self._penalize_lin_eq_constraints.interpret(x)
x = self._int_to_bin.interpret(x)
x = self._ineq_to_eq.interpret(x)
x = self._indic_to_ineq.interpret(x)
return x

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions qiskit_optimization/problems/__init__.py
Expand Up @@ -46,6 +46,7 @@
"""

from .constraint import Constraint
from .indicator_constraint import IndicatorConstraint
from .linear_constraint import LinearConstraint
from .linear_expression import LinearExpression
from .quadratic_constraint import QuadraticConstraint
Expand All @@ -57,6 +58,7 @@

__all__ = [
"Constraint",
"IndicatorConstraint",
"LinearExpression",
"LinearConstraint",
"QuadraticExpression",
Expand Down