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

Adding the unbalanced penalization method #1347

Merged
218 changes: 119 additions & 99 deletions dimod/binary/binary_quadratic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,105 +702,125 @@ def add_linear_equality_constraint(
self.offset += lagrange_multiplier * constant * constant

def add_linear_inequality_constraint(
self, terms: Iterable[Tuple[Variable, int]],
lagrange_multiplier: Bias,
label: str,
constant: int = 0,
lb: int = np.iinfo(np.int64).min,
ub: int = 0,
cross_zero: bool = False
) -> Iterable[Tuple[Variable, int]]:
"""Add a linear inequality constraint as a quadratic objective.

The linear inequality constraint is of the form:
:math:`lb <= \sum_{i,k} a_{i,k} x_{i,k} + constant <= ub`.

For constraints with fractional coefficients, multiply both sides of the
inequality by an appropriate factor of ten to attain or approximate
integer coefficients.

Args:
terms:
Values of the :math:`\sum_{i} a_{i} x_{i}` term as an
:math:`i`--length iterable of 2-tuples, ``(variable, bias)``, with
each tuple constituting a term in the summation.
lagrange_multiplier:
Weight or penalty strength. The linear constraint is multiplied
by this value (which does not appear explicitly in the above
equation) when added to the binary quadratic model.
label:
Prefix for labels of any slack variables used in the added
objective.
constant:
Value of the constant term of the linear constraint.
lb:
Lower bound for the constraint.
ub:
Upper bound for the constraint.
cross_zero:
When True, adds zero to the domain of constraint.

Returns:
slack_terms: Values of :math:`\sum_{i} b_{i} slack_{i}` as an
:math:`i`--length iterable of 2-tuples, ``(slack variable, bias)``,
with each tuple constituting a term in the summation.
"""

if isinstance(terms, Iterator):
terms = list(terms)

if int(constant) != constant or int(lb) != lb or int(ub) != ub or any(
int(bias) != bias for _, bias in terms):
warnings.warn("For constraints with fractional coefficients, "
"multiply both sides of the inequality by an "
"appropriate factor of ten to attain or "
"approximate integer coefficients. ")

terms_upper_bound = sum(v for _, v in terms if v > 0)
terms_lower_bound = sum(v for _, v in terms if v < 0)
ub_c = min(terms_upper_bound, ub - constant)
lb_c = max(terms_lower_bound, lb - constant)

if terms_upper_bound <= ub_c and terms_lower_bound >= lb_c:
warnings.warn(
f'Did not add constraint {label}.'
' This constraint is feasible'
' with any value for state variables.')
return []

if ub_c < lb_c:
raise ValueError(
f'The given constraint ({label}) is infeasible with any value'
' for state variables.')

slack_upper_bound = int(ub_c - lb_c)
if slack_upper_bound == 0:
self.add_linear_equality_constraint(terms, lagrange_multiplier, -ub_c)
return []
else:
slack_terms = []
zero_constraint = False
if cross_zero:
if lb_c > 0 or ub_c < 0:
if ub_c-slack_upper_bound > 0:
zero_constraint = True

num_slack = int(np.floor(np.log2(slack_upper_bound)))
slack_coefficients = [2 ** j for j in range(num_slack)]
if slack_upper_bound - 2 ** num_slack >= 0:
slack_coefficients.append(slack_upper_bound - 2 ** num_slack + 1)

for j, s in enumerate(slack_coefficients):
sv = self.add_variable(f'slack_{label}_{j}')
slack_terms.append((sv, s))

if zero_constraint:
sv = self.add_variable(f'slack_{label}_{num_slack + 1}')
slack_terms.append((sv, ub_c - slack_upper_bound))

self.add_linear_equality_constraint(terms + slack_terms,
lagrange_multiplier, -ub_c)
return slack_terms
self, terms: Iterable[Tuple[Variable, int]],
lagrange_multiplier: Bias,
label: str,
constant: int = 0,
lb: int = np.iinfo(np.int64).min,
ub: int = 0,
cross_zero: bool = False,
penalization_method: str = "slack",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
penalization_method: str = "slack",
penalization_method: typing.Literal["slack", "unbalanced"] = "slack",

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added @arcondello

) -> Iterable[Tuple[Variable, int]]:
"""Add a linear inequality constraint as a quadratic objective.
Copy link
Member

Choose a reason for hiding this comment

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

It looks like everything got indented by an additional tab?

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 fixed it! @arcondello


The linear inequality constraint is of the form:
:math:`lb <= \sum_{i,k} a_{i,k} x_{i,k} + constant <= ub`.

For constraints with fractional coefficients, multiply both sides of the
inequality by an appropriate factor of ten to attain or approximate
integer coefficients.

Args:
terms:
Values of the :math:`\sum_{i} a_{i} x_{i}` term as an
:math:`i`--length iterable of 2-tuples, ``(variable, bias)``, with
each tuple constituting a term in the summation.
lagrange_multiplier:
Weight or penalty strength. The linear constraint is multiplied
by this value (which does not appear explicitly in the above
equation) when added to the binary quadratic model.
label:
Prefix for labels of any slack variables used in the added
objective.
constant:
Value of the constant term of the linear constraint.
lb:
Lower bound for the constraint.
ub:
Upper bound for the constraint.
cross_zero:
When True, adds zero to the domain of constraint.
penalization_method:
Whether to use slack variables or the unbalanced penalization method [1].
("slack", "unbalanced")
[1] https://arxiv.org/abs/2211.13914

Returns:
slack_terms: Values of :math:`\sum_{i} b_{i} slack_{i}` as an
:math:`i`--length iterable of 2-tuples, ``(slack variable, bias)``,
with each tuple constituting a term in the summation.
"""

if isinstance(terms, Iterator):
terms = list(terms)

if int(constant) != constant or int(lb) != lb or int(ub) != ub or any(
int(bias) != bias for _, bias in terms):
warnings.warn("For constraints with fractional coefficients, "
"multiply both sides of the inequality by an "
"appropriate factor of ten to attain or "
"approximate integer coefficients. ")

terms_upper_bound = sum(v for _, v in terms if v > 0)
terms_lower_bound = sum(v for _, v in terms if v < 0)
ub_c = min(terms_upper_bound, ub - constant)
lb_c = max(terms_lower_bound, lb - constant)

if terms_upper_bound <= ub_c and terms_lower_bound >= lb_c:
warnings.warn(
f'Did not add constraint {label}.'
' This constraint is feasible'
' with any value for state variables.')
return []

if ub_c < lb_c:
raise ValueError(
f'The given constraint ({label}) is infeasible with any value'
' for state variables.')
if penalization_method == "slack":
slack_upper_bound = int(ub_c - lb_c)
if slack_upper_bound == 0:
self.add_linear_equality_constraint(terms, lagrange_multiplier, -ub_c)
return []
else:
slack_terms = []
zero_constraint = False
if cross_zero:
if lb_c > 0 or ub_c < 0:
if ub_c-slack_upper_bound > 0:
zero_constraint = True

num_slack = int(np.floor(np.log2(slack_upper_bound)))
slack_coefficients = [2 ** j for j in range(num_slack)]
if slack_upper_bound - 2 ** num_slack >= 0:
slack_coefficients.append(slack_upper_bound - 2 ** num_slack + 1)

for j, s in enumerate(slack_coefficients):
sv = self.add_variable(f'slack_{label}_{j}')
slack_terms.append((sv, s))

if zero_constraint:
sv = self.add_variable(f'slack_{label}_{num_slack + 1}')
slack_terms.append((sv, ub_c - slack_upper_bound))

self.add_linear_equality_constraint(terms + slack_terms,
lagrange_multiplier, -ub_c)
return slack_terms

elif penalization_method == "unbalanced":
if not isinstance(lagrange_multiplier, Iterable):
raise TypeError('A list with two lagrange_multiplier are needed'
' for the unbalanced penalization method.')

for v, bias in terms:
self.add_linear(v, lagrange_multiplier[0] * bias)
self.offset += -ub_c
self.add_linear_equality_constraint(terms, lagrange_multiplier[1], -ub_c)

return []
else:
raise ValueError(f"The method {penalization_method} is not a valid method."
' Choose between ["slack", "unbalanced"]')

def add_linear_from_array(self, linear: Sequence):
"""Add linear biases from an array-like to a binary quadratic model.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
features:
- Add `BinaryQuadraticModel::add_linear_inequality_constraint:penalization_method` parameter. It allows the use of unbalanced penalization https://arxiv.org/abs/2211.13914 instead of the slack variables method for the inequality constraints.
18 changes: 18 additions & 0 deletions tests/test_bqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3643,6 +3643,24 @@ def test_inequality_equality(self, name, BQM):
self.assertEqual(bqm_equal, bqm1)
self.assertEqual(bqm_equal, bqm2)

@parameterized.expand(BQMs.items())
def test_inequality_constraint_unbalanced(self, name, BQM):
bqm = BQM('BINARY')
num_variables = 3
x = {}
for i in range(num_variables):
x[i] = bqm.add_variable('x_{i}'.format(i=i))
terms = iter([(x[i], 2.0) for i in range(num_variables)])
unbalanced_terms = bqm.add_linear_inequality_constraint(
terms, lagrange_multiplier=[1.0, 1.0], label='inequality0', constant=0.0, ub=5,
penalization_method="unbalanced")
self.assertTrue(len(unbalanced_terms) == 0)
for i in x:
self.assertEqual(bqm.get_linear(x[i]), -14.0)
for j in x:
if j > i:
self.assertEqual(bqm.get_quadratic(x[i], x[j]), 8.0)

@parameterized.expand(BQMs.items())
def test_simple_constraint_iterator(self, name, BQM):
bqm = BQM('BINARY')
Expand Down