From 4f62c447fbdd1be1ec466c1eb366ac59f9122688 Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Thu, 29 Jun 2023 14:15:19 +0200 Subject: [PATCH 1/8] Adding the unbalanced penalization method Following recent thread about the unbalanced penalization equation to encode inequality constraints. #1339 --- dimod/binary/binary_quadratic_model.py | 71 +++++++++++++++----------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index 628ba674b..8cadb322d 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -703,12 +703,13 @@ def add_linear_equality_constraint( def add_linear_inequality_constraint( self, terms: Iterable[Tuple[Variable, int]], - lagrange_multiplier: Bias, + lagrange_multiplier: Union[Bias, Iterable], label: str, constant: int = 0, lb: int = np.iinfo(np.int64).min, ub: int = 0, - cross_zero: bool = False + cross_zero: bool = False, + penalization_method: str = "slack" ) -> Iterable[Tuple[Variable, int]]: """Add a linear inequality constraint as a quadratic objective. @@ -739,6 +740,10 @@ def add_linear_inequality_constraint( 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 @@ -772,35 +777,41 @@ def add_linear_inequality_constraint( 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) + 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,lagrange_multiplier[0], -ub_c) + + return slack_terms + + elif penalization_method == "unbalanced": + self.add_linear_equality_constraint(terms,lagrange_multiplier[0], -ub_c) + for v, bias in terms: + self.add_linear(v, -lagrange_multiplier[1] * bias) 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 def add_linear_from_array(self, linear: Sequence): """Add linear biases from an array-like to a binary quadratic model. From 5cac8c801cef83769ce6d92941ca308b60788f7f Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Thu, 29 Jun 2023 14:30:02 +0200 Subject: [PATCH 2/8] Update binary_quadratic_model.py --- dimod/binary/binary_quadratic_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index 8cadb322d..0d36d3e9a 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -803,15 +803,15 @@ def add_linear_inequality_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,lagrange_multiplier[0], -ub_c) - - return slack_terms + self.add_linear_equality_constraint(terms,lagrange_multiplier, -ub_c) elif penalization_method == "unbalanced": self.add_linear_equality_constraint(terms,lagrange_multiplier[0], -ub_c) for v, bias in terms: self.add_linear(v, -lagrange_multiplier[1] * bias) - return [] + slack_terms = [] + + return slack_terms def add_linear_from_array(self, linear: Sequence): """Add linear biases from an array-like to a binary quadratic model. From d96576c8b617689148c2a853ba5873f1c0b8947c Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Wed, 26 Jul 2023 09:19:19 +0200 Subject: [PATCH 3/8] Update binary_quadratic_model.py --- dimod/binary/binary_quadratic_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index 0d36d3e9a..db9b96b12 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -777,6 +777,7 @@ def add_linear_inequality_constraint( 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: @@ -806,9 +807,13 @@ def add_linear_inequality_constraint( self.add_linear_equality_constraint(terms,lagrange_multiplier, -ub_c) elif penalization_method == "unbalanced": - self.add_linear_equality_constraint(terms,lagrange_multiplier[0], -ub_c) + 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[1] * bias) + self.add_linear(v, -lagrange_multiplier[0] * bias) + self.add_linear_equality_constraint(terms, lagrange_multiplier[1], -ub_c) slack_terms = [] return slack_terms From cb0668c3df9ccb0427cc3b03166733fc622a76e7 Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Wed, 26 Jul 2023 10:34:10 +0200 Subject: [PATCH 4/8] solving some issues --- dimod/binary/binary_quadratic_model.py | 226 +++++++++++++------------ 1 file changed, 115 insertions(+), 111 deletions(-) diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index db9b96b12..e198b49cc 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -702,121 +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: Union[Bias, Iterable], - label: str, - constant: int = 0, - lb: int = np.iinfo(np.int64).min, - ub: int = 0, - cross_zero: bool = False, - penalization_method: str = "slack" - ) -> 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. - 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 + 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", + ) -> Iterable[Tuple[Variable, int]]: + """Add a linear inequality constraint as a quadratic objective. - 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) + The linear inequality constraint is of the form: + :math:`lb <= \sum_{i,k} a_{i,k} x_{i,k} + constant <= ub`. - for j, s in enumerate(slack_coefficients): - sv = self.add_variable(f'slack_{label}_{j}') - slack_terms.append((sv, s)) + For constraints with fractional coefficients, multiply both sides of the + inequality by an appropriate factor of ten to attain or approximate + integer coefficients. - if zero_constraint: - sv = self.add_variable(f'slack_{label}_{num_slack + 1}') - slack_terms.append((sv, ub_c - slack_upper_bound)) + 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 - self.add_linear_equality_constraint(terms,lagrange_multiplier, -ub_c) - - 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.add_linear_equality_constraint(terms, lagrange_multiplier[1], -ub_c) - slack_terms = [] - - return slack_terms + 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. From b47e37f9bf588f1d20077dd530e15d11c7b895e1 Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Wed, 26 Jul 2023 11:38:16 +0200 Subject: [PATCH 5/8] Adding test and release note --- ...balanced-penalization-e16af2362227bcdb.yaml | 5 +++++ tests/test_bqm.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml diff --git a/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml new file mode 100644 index 000000000..aabbe922c --- /dev/null +++ b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml @@ -0,0 +1,5 @@ +--- +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. \ No newline at end of file diff --git a/tests/test_bqm.py b/tests/test_bqm.py index 6e6c040cb..5dd98f7c5 100644 --- a/tests/test_bqm.py +++ b/tests/test_bqm.py @@ -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') From 4582259d7cfb0442f1c916906961b32d4d7a34d2 Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Wed, 26 Jul 2023 11:46:49 +0200 Subject: [PATCH 6/8] Update unbalanced-penalization-e16af2362227bcdb.yaml --- .../notes/unbalanced-penalization-e16af2362227bcdb.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml index aabbe922c..38245f419 100644 --- a/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml +++ b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml @@ -1,5 +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. \ No newline at end of file + - 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. \ No newline at end of file From 2fe34ae77757b35e563d52bf0abcb85d2d78fda0 Mon Sep 17 00:00:00 2001 From: Alejandro Montanez Date: Thu, 17 Aug 2023 13:36:00 +0200 Subject: [PATCH 7/8] Update binary_quadratic_model.py --- dimod/binary/binary_quadratic_model.py | 216 ++++++++++++------------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index e198b49cc..5698d9cf7 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -40,7 +40,7 @@ from typing import (Any, BinaryIO, ByteString, Callable, Dict, Hashable, Iterable, Iterator, Mapping, MutableMapping, Optional, Sequence, - Tuple, Union, + Tuple, Union, Literal ) import numpy as np @@ -709,118 +709,118 @@ def add_linear_inequality_constraint( lb: int = np.iinfo(np.int64).min, ub: int = 0, cross_zero: bool = False, - penalization_method: str = "slack", + penalization_method: Literal["slack", "unbalanced"] = "slack", ) -> 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. - 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) + """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. + 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 - 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. ") + 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) - 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) + for j, s in enumerate(slack_coefficients): + sv = self.add_variable(f'slack_{label}_{j}') + slack_terms.append((sv, s)) - 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 zero_constraint: + sv = self.add_variable(f'slack_{label}_{num_slack + 1}') + slack_terms.append((sv, ub_c - slack_upper_bound)) - 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"]') + 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. From a2668cb2096986894eab79f71bb43389b2ded8ce Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 17 Aug 2023 08:09:42 -0700 Subject: [PATCH 8/8] Make minor edits to unbalance penalization method release notes --- .../notes/unbalanced-penalization-e16af2362227bcdb.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml index 38245f419..7cf5533f6 100644 --- a/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml +++ b/releasenotes/notes/unbalanced-penalization-e16af2362227bcdb.yaml @@ -1,3 +1,4 @@ --- 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. \ No newline at end of file + - Add ``penalization_method`` parameter to ``BinaryQuadraticModel.add_linear_inequality_constraint()``. It allows the use of unbalanced penalization https://arxiv.org/abs/2211.13914 instead of the slack variables method for the inequality constraints. +