From 63ed807de0312181fa6791679944ffac20622670 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 13 Aug 2026 10:19:56 -0700 Subject: [PATCH] Remove Symbol.equals() and Symbol.maybe_equals() methods --- dwave/optimization/_model.pyi | 2 - dwave/optimization/_model.pyx | 134 +----------------- dwave/optimization/symbols/constants.pyx | 13 -- dwave/optimization/symbols/manipulation.pyx | 26 ---- ...be_equals-and-equals-573362d8619b53bd.yaml | 5 + tests/test_model.py | 4 +- tests/test_serialization.py | 2 - tests/utils.py | 19 +-- 8 files changed, 10 insertions(+), 195 deletions(-) create mode 100644 releasenotes/notes/remove-maybe_equals-and-equals-573362d8619b53bd.yaml diff --git a/dwave/optimization/_model.pyi b/dwave/optimization/_model.pyi index 2a42f830c..8fa823f2b 100644 --- a/dwave/optimization/_model.pyi +++ b/dwave/optimization/_model.pyi @@ -74,13 +74,11 @@ class _Graph: class Symbol: def __init__(self) -> typing.NoReturn: ... - def equals(self, other: Symbol) -> bool: ... def expired(self) -> bool: ... def has_state(self, index: int = 0) -> bool: ... def id(self) -> int: ... def iter_predecessors(self) -> collections.abc.Iterator[Symbol]: ... def iter_successors(self) -> collections.abc.Iterator[Symbol]: ... - def maybe_equals(self, other: Symbol) -> int: ... def reset_state(self, index: int) -> None: ... def shares_memory(self, other: Symbol) -> bool: ... def state_size(self) -> int: ... diff --git a/dwave/optimization/_model.pyx b/dwave/optimization/_model.pyx index 6b7c9e7e7..99fc8997a 100644 --- a/dwave/optimization/_model.pyx +++ b/dwave/optimization/_model.pyx @@ -1140,46 +1140,6 @@ cdef class Symbol: """ return self.node_ptr.deterministic_state() - def equals(self, other): - """Compare whether two symbols are identical. - - Equal symbols represent the same quantity in the model. - - Args: - other (:class:`.Symbol`): A symbol for comparison. - - Returns: - bool: True if the symbols are identical. - - Note that comparing symbols across models is expensive. - - Examples: - This example creates two symbols that are the sum of the same two - :class:`~dwave.optimization.symbols.IntegerVariable` symbols - and a third that is the difference between them, and checks - equality. - - >>> from dwave.optimization import Model - >>> model = Model() - >>> i = model.integer(3) - >>> j = model.integer(3) - >>> a = i + j - >>> b = i + j - >>> c = i - j - >>> print(a.equals(a), a.equals(b), a.equals(c)) - True True False - - See Also: - :meth:`~Symbol.maybe_equals`: A faster alternative for equality - testing but that can return false positives. - """ - cdef Py_ssize_t maybe = self.maybe_equals(other) - if maybe != 1: - return True if maybe else False - - # todo: caching - return all(p.equals(q) for p, q in zip(self.iter_predecessors(), other.iter_predecessors())) - cpdef bool expired(self) noexcept: return deref(self.expired_ptr) @@ -1306,9 +1266,6 @@ cdef class Symbol: See Also: * :meth:`.shares_memory`: ``a.shares_memory(b)`` is equivalent to ``a.id() == b.id()``. - * :meth:`.equals`: ``a.equals(b)`` returns ``True`` if - ``a.id() == b.id()``; the inverse is not necessarily true. - * :meth:`~Symbol.maybe_equals` """ # We refer to the node_ptr, which is not necessarily the address of the @@ -1352,7 +1309,7 @@ cdef class Symbol: >>> c = model.constant([[21, 11], [10, 4]]) >>> a = c * i >>> b = a.sum() - >>> a.equals(next(b.iter_predecessors())) + >>> a.id() == next(b.iter_predecessors()).id() True .. figure:: /_images/optimization/iter_predecessors.svg @@ -1387,7 +1344,7 @@ cdef class Symbol: >>> model = Model() >>> x = model.binary() >>> y = x + 5 - >>> y.equals(next(x.iter_successors())) + >>> y.id() == next(x.iter_successors()).id() True .. figure:: /_images/optimization/iter_successors.svg @@ -1408,73 +1365,6 @@ cdef class Symbol: yield symbol_from_ptr(self.model, deref(it).ptr) inc(it) - def maybe_equals(self, other): - """Compare to another symbol. - - This method exists because a complete equality test can be expensive. - - Args: - other (:class:`.Symbol`): Another symbol in the model's - :term:`directed acyclic graph`. - - Returns: - int: Supported return values are the following. - - * ``0``---Not equal (with certainty) - * ``1``---Might be equal (no guarantees); a complete equality test - is necessary - * ``2``---Are equal (with certainty) - - Examples: - This example compares - :class:`~dwave.optimization.symbols.IntegerVariable` symbols - of different sizes. - - >>> from dwave.optimization import Model - >>> model = Model() - >>> i = model.integer(3, lower_bound=0, upper_bound=20) - >>> j = model.integer(3, lower_bound=-10, upper_bound=10) - >>> k = model.integer(5, upper_bound=55) - >>> i.maybe_equals(j) - 1 - >>> i.maybe_equals(k) - 0 - - See Also: - :meth:`.equals`: A guaranteed but more expensive equality test. - """ - cdef Py_ssize_t NOT = 0 - cdef Py_ssize_t MAYBE = 1 - cdef Py_ssize_t DEFINITELY = 2 - - # If we're the same object, then we're equal - if self is other: - return DEFINITELY - - if not isinstance(other, Symbol): - return NOT - - # Should we require identical types? - if not isinstance(self, type(other)) and not isinstance(other, type(self)): - return NOT - - cdef Symbol rhs = other - - if self.shares_memory(rhs): - return DEFINITELY - - # Check is that we have the right number of predecessors - if self.node_ptr.predecessors().size() != rhs.node_ptr.predecessors().size(): - return NOT - - # Finally, out prdecessors should have the same types in the same order - for p, q in zip(self.iter_predecessors(), rhs.iter_predecessors()): - # Should we require identical types? - if not isinstance(p, type(q)) and not isinstance(q, type(p)): - return NOT - - return MAYBE - def reset_state(self, Py_ssize_t index): """Reset the state of a symbol and any successor symbols. @@ -2135,26 +2025,6 @@ cdef class ArraySymbol(Symbol): from dwave.optimization.symbols import Max # avoid circular import return Max(self, axis=axis, initial=initial) - def maybe_equals(self, other): - # note: docstring inherited from Symbol.maybe_equal() - cdef Py_ssize_t maybe = super().maybe_equals(other) - cdef Py_ssize_t NOT = 0 - cdef Py_ssize_t MAYBE = 1 - cdef Py_ssize_t DEFINITELY = 2 - - if maybe != 1: - return DEFINITELY if maybe else NOT - - if not isinstance(other, ArraySymbol): - return NOT - - if self.shape() != other.shape(): - return NOT - - # I guess we don't care about strides - - return MAYBE - def min(self, *, axis=None, initial=_NoValue): r"""Create a symbol that returns the minimum value of the array. diff --git a/dwave/optimization/symbols/constants.pyx b/dwave/optimization/symbols/constants.pyx index 49e0b886b..5de892830 100644 --- a/dwave/optimization/symbols/constants.pyx +++ b/dwave/optimization/symbols/constants.pyx @@ -228,19 +228,6 @@ cdef class Constant(ArraySymbol): # any noticeable impact on performance (numpy==1.26.3). np.save(f, np.asarray(self), allow_pickle=False) - def maybe_equals(self, other): - cdef Py_ssize_t maybe = super().maybe_equals(other) - cdef Py_ssize_t NOT = 0 - cdef Py_ssize_t MAYBE = 1 - cdef Py_ssize_t DEFINITELY = 2 - if maybe != MAYBE: - return DEFINITELY if maybe else NOT - - # avoid NumPy deprecation warning by casting to bool. But also - # `bool` in this namespace is a C++ class so we do an explicit if else - equal = (np.asarray(self) == np.asarray(other)).all() - return DEFINITELY if equal else NOT - def state(self, Py_ssize_t index=0, *, bool copy = True): """Return the state of the symbol. diff --git a/dwave/optimization/symbols/manipulation.pyx b/dwave/optimization/symbols/manipulation.pyx index 4cf81fad7..9e1ed95ac 100644 --- a/dwave/optimization/symbols/manipulation.pyx +++ b/dwave/optimization/symbols/manipulation.pyx @@ -422,32 +422,6 @@ cdef class Roll(ArraySymbol): axes.append(cppaxes[i]) zf.writestr(directory + "axis.json", encoder.encode(axes)) - def maybe_equals(self, other): - # inherit docstring from ArraySymbol - cdef Py_ssize_t NOT = 0 - cdef Py_ssize_t MAYBE = 1 - cdef Py_ssize_t DEFINITELY = 2 - - equality = super().maybe_equals(other) - if (equality != MAYBE): - return equality - - if not isinstance(other, Roll): - return NOT - - # check the axis parameter - if not equal(self.ptr.axes().begin(), self.ptr.axes().end(), (other).ptr.axes().begin()): - return NOT - - # check the shift - # if we have two predecessors than so must other (based on the super() check earlier) - # and the predecessor equality will be checked later. - if self.node_ptr.predecessors().size() != 2: - if self.ptr.shift() != (other).ptr.shift(): - return NOT - - return MAYBE - cdef RollNode* ptr _register(Roll, typeid(RollNode)) diff --git a/releasenotes/notes/remove-maybe_equals-and-equals-573362d8619b53bd.yaml b/releasenotes/notes/remove-maybe_equals-and-equals-573362d8619b53bd.yaml new file mode 100644 index 000000000..954dcf8e4 --- /dev/null +++ b/releasenotes/notes/remove-maybe_equals-and-equals-573362d8619b53bd.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Remove ``Symbol.equals()`` and ``Symbol.maybe_equals()`` methods. + Use ``Symbol.id()`` to test whether two symbols are identical. diff --git a/tests/test_model.py b/tests/test_model.py index 91deacbd4..2ab770607 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -259,8 +259,8 @@ def test_inputs(self): inputs = list(model.iter_inputs()) self.assertEqual(len(inputs), 2) - self.assertTrue(i0.equals(inputs[0])) - self.assertTrue(i1.equals(inputs[1])) + self.assertEqual(i0.id(), inputs[0].id()) + self.assertEqual(i1.id(), inputs[1].id()) def test_lock(self): model = Model() diff --git a/tests/test_serialization.py b/tests/test_serialization.py index e3d0cf708..0d23124c7 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -45,8 +45,6 @@ def assertModelEqual(self, lhs: Model, rhs: Model): # All nodes in the model need to match, and to have the same states for ls, rs in zip(lhs.iter_symbols(), rhs.iter_symbols()): - self.assertTrue(ls.maybe_equals(rs)) - # If the nodes have states, check that they are all equal # If all are maybe_equal then this amounts to a full equality # check for the model as a whole. diff --git a/tests/utils.py b/tests/utils.py index a0a750402..a6c6395ca 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -164,22 +164,6 @@ def generate_symbols(self): The symbols must all be unique from eachother. """ - def test_equality(self): - DEFINITELY = 2 - for x in self.generate_symbols(): - self.assertEqual(DEFINITELY, x.maybe_equals(x)) - self.assertTrue(x.equals(x)) - - for x, y in zip(self.generate_symbols(), self.generate_symbols()): - self.assertTrue(DEFINITELY, x.maybe_equals(y)) - self.assertTrue(x.equals(y)) - - def test_inequality(self): - MAYBE = 1 - for x, y in itertools.combinations(self.generate_symbols(), 2): - self.assertLessEqual(x.maybe_equals(y), MAYBE) - self.assertFalse(x.equals(y)) - def test_iter_symbols(self): for x in self.generate_symbols(): model = x.model @@ -190,7 +174,7 @@ def test_iter_symbols(self): self.assertTrue(x.shares_memory(y)) self.assertIs(type(x), type(y)) - self.assertTrue(x.equals(y)) + self.assertEqual(x.id(), y.id()) def test_namespace(self): x = next(self.generate_symbols()) @@ -213,7 +197,6 @@ def test_serialization(self): self.assertFalse(x.shares_memory(y)) self.assertIs(type(x), type(y)) - self.assertTrue(x.equals(y)) def test_state_serialization(self): for version in dwave.optimization._model.KNOWN_SERIALIZATION_VERSIONS: