Skip to content

CoordinateAxis.__eq__ compares only unit; ArrayWithNamedDims.__eq__ is shadowed #255

Description

@cboulay

Summary

CoordinateAxis.__eq__ resolves through the MRO to the dataclass-generated AxisBase.__eq__, which compares only unit. ArrayWithNamedDims.__eq__ — written to compare dims and data — is never reached, so two coordinate axes with different data, different dtypes, or different lengths compare equal.

Reproducer

import numpy as np
from ezmsg.util.messages.axisarray import AxisArray

C = AxisArray.CoordinateAxis

print(C.__eq__.__qualname__)
# AxisBase.__eq__
print([c.__name__ for c in C.__mro__])
# ['CoordinateAxis', 'AxisBase', 'ABC', 'ArrayWithNamedDims', 'object']

a = C(data=np.zeros(3), dims=['ch'], unit='')
b = C(data=np.ones(5),  dims=['ch'], unit='')
print(a == b)   # True   <-- different values AND different lengths

# The case that bites in practice: a structured channel axis being relabelled.
D = np.dtype([('label', 'U8')])
x = np.zeros(2, dtype=D); x['label'] = ['a', 'b']
y = np.zeros(2, dtype=D); y['label'] = ['c', 'd']
print(C(data=x, dims=['ch'], unit='') == C(data=y, dims=['ch'], unit=''))   # True

Observed on ezmsg 3.9.0 (v3.9.0-10-gc9572db).

Cause

AxisBase is a plain @dataclass, so it gets a generated __eq__ comparing its own fields (unit). CoordinateAxis(AxisBase, ArrayWithNamedDims) is declared @dataclass(eq=False), which correctly stops it generating its own __eq__ — but AxisBase's generated one still precedes ArrayWithNamedDims in the MRO, so the data-aware implementation is dead code for this class.

ArrayWithNamedDims.__eq__ looks intentional and correct:

def __eq__(self, other):
    if self is other:
        return True
    if other.__class__ is self.__class__:
        xp = get_namespace(self.data)
        if self.dims == other.dims and xp.array_equal(self.data, other.data):
            return True
    return NotImplemented

so this reads as an MRO accident rather than a deliberate choice.

AxisArray is unaffected — it defines its own __eq__. LinearAxis is fine, since unit/gain/offset are all it has.

Impact

Any change detection built on == over coordinate axes silently never fires. We hit this building a metadata side channel over the shared-memory bridge in ezmsg-tools: comparing the incoming ch axis against the last published one to decide whether to republish per-channel labels. With ==, a relabelling — or an entirely different channel set of a different length — was reported as "unchanged", so the labels downstream were never updated. We worked around it by comparing dims/dtype/shape/data explicitly, but anything else in the ecosystem using == here is silently wrong in the same way.

Suggested fix

Either give CoordinateAxis an explicit __eq__ (delegating to the ArrayWithNamedDims implementation), or declare AxisBase as @dataclass(eq=False) so it stops shadowing subclass implementations. The second is tidier but worth checking against anything relying on LinearAxis equality, which the generated __eq__ currently provides.

Worth a regression test asserting inequality for coordinate axes differing in data, in dtype, and in length — the current behaviour returns True for all three.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions