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 13 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
2 changes: 2 additions & 0 deletions qiskit_optimization/converters/__init__.py
Expand Up @@ -46,6 +46,7 @@

"""

from .indicator_to_inequality import IndicatorToInequality
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__ = [
"IndicatorToInequality",
"InequalityToEquality",
"IntegerToBinary",
"LinearEqualityToPenalty",
Expand Down
175 changes: 175 additions & 0 deletions qiskit_optimization/converters/indicator_to_inequality.py
@@ -0,0 +1,175 @@
# 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 inequality to equality converter."""
a-matsuo marked this conversation as resolved.
Show resolved Hide resolved

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):
Copy link
Collaborator

@t-imamichi t-imamichi Jun 7, 2021

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.

"""Convert inequality constraints into equality constraints by introducing slack variables.

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("Unsupported variable type {}".format(x.vartype))
a-matsuo marked this conversation as resolved.
Show resolved Hide resolved

# 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure that big_m is a positive value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
"that of the original problem."
)
return np.asarray(x)
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
181 changes: 181 additions & 0 deletions qiskit_optimization/problems/indicator_constraint.py
@@ -0,0 +1,181 @@
# 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.

"""Quadratic Constraint."""

from typing import Union, List, Dict, Any

from numpy import ndarray
from scipy.sparse import spmatrix

from ..exceptions import QiskitOptimizationError
from .constraint import Constraint, ConstraintSense
from .linear_expression import LinearExpression
from .variable import Variable


class IndicatorConstraint(Constraint):
"""Representation of an indicator constraint."""

# Note: added, duplicating in effect that in Constraint, to avoid issues with Sphinx
Sense = ConstraintSense

def __init__(
self,
quadratic_program: Any,
name: str,
binary_var: Union[str, int, Variable],
linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]],
sense: ConstraintSense,
rhs: float,
active_value: int = 1,
) -> None:
"""Constructs an indicator constraint, consisting of a binary indicator variable and
linear terms.

Args:
quadratic_program: The parent quadratic program.
name: The name of the constraint.
binary_var: The binary indicator variable.
linear: The coefficients specifying the linear part of the constraint.
sense: The sense of the constraint.
rhs: The right-hand-side of the constraint.
active_value: The value of the binary variable is used to force the satisfaction of the
linear constraint. Default is 1.

Raises:
QiskitOptimizationError: If given binary_var is not Variable, the index, or the name
of the variable.
QiskitOptimizationError: If binary_var is not a binary variable
QiskitOptimizationError: If active_value is not 0 or 1.
"""
# Type check for the arguments
super().__init__(quadratic_program, name, sense, rhs)
self._linear = LinearExpression(quadratic_program, linear)
if isinstance(binary_var, Variable):
binary_var_ = binary_var
elif isinstance(binary_var, (int, str)):
binary_var_ = quadratic_program.get_variable(binary_var)
else:
raise QiskitOptimizationError(
"Unsupported format for binary_var. It must be \
Variable, the index, or the name: {}".format(
type(binary_var)
)
)
if binary_var_.vartype != Variable.Type.BINARY:
raise QiskitOptimizationError(
"binary_var must be a binary variable: {}".format(binary_var_.vartype)
)
self._binary_var = binary_var_
if active_value not in (0, 1):
raise QiskitOptimizationError("Active value must be 1 or 0: {}".format(active_value))
self._active_value = active_value

@property
def active_value(self) -> int:
"""Returns the active value for the binary indicator variable of the constraint.

Args:
The active value of the binary indicator variable
"""
return self._active_value

@active_value.setter
def active_value(self, active_value: int) -> None:
"""Set the active value for the binary indicator variable of the constraint.

Returns:
The active value
"""
self._active_value = active_value

@property
def binary_var(self) -> Variable:
"""Returns the binary indicator variable of the constraint.

Returns:
The binary indicator variable
"""
return self._binary_var

@binary_var.setter
def binary_var(self, binary_var: Variable) -> None:
"""Set the binary indicator variable of the constraint.

Args:
binary_var: The binary indicator variable of the constraint.
"""
self._binary_var = binary_var

@property
def linear(self) -> LinearExpression:
"""Returns the linear expression corresponding to the left-hand-side of the constraint.

Returns:
The left-hand-side linear expression.
"""
return self._linear

@linear.setter
def linear(
self,
linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]],
) -> None:
"""Sets the linear expression corresponding to the left-hand-side of the constraint.
The coefficients can either be given by an array, a (sparse) 1d matrix, a list or a
dictionary.

Args:
linear: The linear coefficients of the left-hand-side.
"""

self._linear = LinearExpression(self.quadratic_program, linear)

def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float:
"""Evaluate the left-hand-side of the constraint.

Args:
x: The values of the variables to be evaluated.

Returns:
The left-hand-side of the constraint given the variable values.
"""
return self.linear.evaluate(x)
a-matsuo marked this conversation as resolved.
Show resolved Hide resolved

def evaluate_indicator(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> bool:
"""Evaluate the binary indicator var using the active value.

Args:
x: The values of the variables to be evaluated.

Returns:
If x equals to the active value, return True. Otherwise, return False.

Raises:
QiskitOptimizationError: if the given variable index does not match to the index of
self.binary_var.
QiskitOptimizationError: if x is given in unsupported format.
"""
index = self.quadratic_program.variables_index[self.binary_var.name]
if isinstance(x, (list, ndarray)):
val = x[index]
elif isinstance(x, dict):
for ind, value in x.items():
if isinstance(ind, str):
ind = self.quadratic_program.variables_index[ind]
if index == ind:
val = value
else:
raise QiskitOptimizationError("Unsupported format for x.")
a-matsuo marked this conversation as resolved.
Show resolved Hide resolved

return val == self.active_value