Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dwave/optimization/_model.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
134 changes: 2 additions & 132 deletions dwave/optimization/_model.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
13 changes: 0 additions & 13 deletions dwave/optimization/symbols/constants.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 0 additions & 26 deletions dwave/optimization/symbols/manipulation.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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(), (<Roll>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() != (<Roll>other).ptr.shift():
return NOT

return MAYBE

cdef RollNode* ptr

_register(Roll, typeid(RollNode))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
upgrade:
- |
Remove ``Symbol.equals()`` and ``Symbol.maybe_equals()`` methods.
Use ``Symbol.id()`` to test whether two symbols are identical.
4 changes: 2 additions & 2 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions tests/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

@arcondello arcondello Aug 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I should do type(ls) is type(rs) here at least


# 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.
Expand Down
19 changes: 1 addition & 18 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -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:
Expand Down