From a63c0c341e0729838e7e68edd7a8530f6f94b41d Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 29 Aug 2025 08:21:59 -0700 Subject: [PATCH] Add divisor argument to BQM.scale() --- dimod/binary/binary_quadratic_model.py | 54 ++++++++++++------- dimod/cyqmbase/cyqmbase_template.pyx.pxi | 10 ++-- dimod/include/dimod/abc.h | 15 ++++-- dimod/libcpp/abc.pxd | 3 +- ...eature-scale-divisor-7820d25c8fefe1dc.yaml | 5 ++ tests/test_bqm.py | 16 +++++- 6 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/feature-scale-divisor-7820d25c8fefe1dc.yaml diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index d66833ee7..38420e80a 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -1797,15 +1797,21 @@ def min_and_max(iterable): and (b, a) not in ignored_interactions)]) - inv_scalar = max(lin_min / lin_range[0], lin_max / lin_range[1], - quad_min / quad_range[0], quad_max / quad_range[1]) - - if inv_scalar != 0: - self.scale(1 / inv_scalar, ignored_variables=ignored_variables, + # Figure out what multiplier/divisor will get us into range + divisor, multiplier = max( + [(lin_min, lin_range[0]), + (lin_max, lin_range[1]), + (quad_min, quad_range[0]), + (quad_max, quad_range[1])], + key=lambda tpl: tpl[0] / tpl[1], + ) + + if divisor != 0: + self.scale(multiplier=multiplier, divisor=divisor, ignored_variables=ignored_variables, ignored_interactions=ignored_interactions, ignore_offset=ignore_offset) - return 1.0 / inv_scalar + return multiplier / divisor else: return 1.0 @@ -1992,30 +1998,39 @@ def resize(self, n: int): """ return self.data.resize - def scale(self, scalar, ignored_variables=None, ignored_interactions=None, - ignore_offset=False): - """Multiply all biases by the specified scalar. + def scale( + self, + multiplier: float, + divisor: float = 1, + *, + ignored_variables: Optional[abc.Iterable[Variable]] = None, + ignored_interactions: Optional[abc.Iterable[tuple[Variable, Variable]]] = None, + ignore_offset: bool = False, + ): + """Multiply all biases by the specified values. Args: - scalar (number): - Value by which to scale the energy range of the binary - quadratic model. + multiplier: + Value by which to multiply all biases in the model. + + divisor: + Value by which to divide all biases in the model. - ignored_variables (iterable, optional): + ignored_variables: Biases associated with these variables are not scaled. - ignored_interactions (iterable[tuple], optional): + ignored_interactions: Biases associated with these interactions, formatted as an iterable of 2-tuples, are not scaled. - ignore_offset (bool, default=False): + ignore_offset: If True, the offset is not scaled. """ if ignored_variables is None and ignored_interactions is None \ and ignore_offset is False: try: - self.data.scale(scalar) + self.data.scale(multiplier, divisor) return except AttributeError: pass @@ -2033,15 +2048,16 @@ def scale(self, scalar, ignored_variables=None, ignored_interactions=None, for v in self.variables: if v in ignored_variables: continue - self.set_linear(v, scalar*self.get_linear(v)) + self.set_linear(v, multiplier * self.get_linear(v) / divisor) for u, v, bias in self.iter_quadratic(): if (u, v) in ignored_interactions or (v, u) in ignored_interactions: continue - self.set_quadratic(u, v, scalar*self.get_quadratic(u, v)) + self.set_quadratic(u, v, multiplier * self.get_quadratic(u, v) / divisor) if not ignore_offset: - self.offset *= scalar + self.offset *= multiplier + self.offset /= divisor @forwarding_method def set_linear(self, v: Variable, bias: Bias): diff --git a/dimod/cyqmbase/cyqmbase_template.pyx.pxi b/dimod/cyqmbase/cyqmbase_template.pyx.pxi index a090225be..cb2615015 100644 --- a/dimod/cyqmbase/cyqmbase_template.pyx.pxi +++ b/dimod/cyqmbase/cyqmbase_template.pyx.pxi @@ -23,7 +23,7 @@ from dimod.libcpp.vartypes cimport Vartype as cppVartype from dimod.cyutilities cimport as_numpy_float from dimod.sampleset import as_samples -from dimod.typing cimport Numeric, float64_t, int8_t +from dimod.typing cimport Numeric, float64_t, int8_t, int64_t from dimod.variables import Variables from dimod.vartypes import Vartype @@ -405,8 +405,12 @@ cdef class cyQMBase_template: return v - def scale(self, bias_type scalar): - self.base.scale(scalar) + def scale(self, multiplier, divisor=1): + dtype = np.result_type(multiplier, divisor) + if isinstance(dtype, np.integer): + self.base.scale[int64_t](multiplier, divisor) + else: + self.base.scale[double](multiplier, divisor) def upper_bound(self, v): cdef Py_ssize_t vi = self.variables.index(v) diff --git a/dimod/include/dimod/abc.h b/dimod/include/dimod/abc.h index 356ac8666..7ec2e0576 100644 --- a/dimod/include/dimod/abc.h +++ b/dimod/include/dimod/abc.h @@ -349,7 +349,8 @@ class QuadraticModelBase { virtual void remove_variables(const std::vector& variables); /// Multiply all biases by the value of `scalar`. - void scale(bias_type scalar); + template + void scale(T multiplier, T divisor = 1); /// Set the linear bias of variable `v`. void set_linear(index_type v, bias_type bias); @@ -1034,19 +1035,23 @@ void QuadraticModelBase::resize(index_type n) { } template -void QuadraticModelBase::scale(bias_type scalar) { - offset_ *= scalar; +template +void QuadraticModelBase::scale(T multiplier, T divisor) { + offset_ *= multiplier; + offset_ /= divisor; // linear biases for (bias_type& bias : linear_biases_) { - bias *= scalar; + bias *= multiplier; + bias /= divisor; } if (has_adj()) { // quadratic biases for (auto& n : (*adj_ptr_)) { for (auto& term : n) { - term.bias *= scalar; + term.bias *= multiplier; + term.bias /= divisor; } } } diff --git a/dimod/libcpp/abc.pxd b/dimod/libcpp/abc.pxd index eac4d19cd..a2e7fdd88 100644 --- a/dimod/libcpp/abc.pxd +++ b/dimod/libcpp/abc.pxd @@ -116,7 +116,8 @@ cdef extern from "dimod/abc.h" namespace "dimod::abc" nogil: bias_type quadratic_at(index_type, index_type) except+ bint remove_interaction(index_type, index_type) void remove_variable(index_type) - void scale(bias_type) + void scale[T](T) + void scale[T](T, T) void set_linear(index_type, bias_type) void set_offset(bias_type) void set_quadratic(index_type, index_type, bias_type) except+ diff --git a/releasenotes/notes/feature-scale-divisor-7820d25c8fefe1dc.yaml b/releasenotes/notes/feature-scale-divisor-7820d25c8fefe1dc.yaml new file mode 100644 index 000000000..e056e64c0 --- /dev/null +++ b/releasenotes/notes/feature-scale-divisor-7820d25c8fefe1dc.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``divisor`` argument to ``BinaryQuadraticModel.scale()``. +upgrade: + - Rename ``scalar`` argument of ``BinaryQuadraticModel.scale()`` to ``multiplier``. diff --git a/tests/test_bqm.py b/tests/test_bqm.py index 466219df3..887303367 100644 --- a/tests/test_bqm.py +++ b/tests/test_bqm.py @@ -1875,6 +1875,13 @@ def test_return_value(self, name, BQM): self.assertEqual(bqm.normalize([-1, 1]), .5) + @parameterized.expand(BQMs.items()) + def test_numeric(self, name, BQM): + # chosen because 187. * (6. / 187.) == 6.000000000000001 + bqm = dimod.BQM({0: 187}, {}, 0, "SPIN") + bqm.normalize(bias_range=[-6, 6]) + self.assertEqual(bqm.linear[0], 6) + class TestObjectDtype(unittest.TestCase): def test_dtypes_array_like_ints(self): @@ -2325,7 +2332,14 @@ def test_typical(self, name, BQM): assert_consistent_bqm(bqm) with self.assertRaises(TypeError): - bqm.scale('a') + bqm.scale('hithere') + + @parameterized.expand(BQMs.items()) + def test_numeric(self, name, BQM): + # chosen because 187. * (6. / 187.) == 6.000000000000001 + bqm = dimod.BQM({0: 187}, {}, 0, "SPIN") + bqm.scale(multiplier=6, divisor=187) + self.assertEqual(bqm.linear[0], 6) class TestSetLinear(unittest.TestCase):