Skip to content
Merged
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
11 changes: 7 additions & 4 deletions src/ezmsg/sigproc/math/abs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
"""

import ezmsg.core as ez
from array_api_compat import get_namespace
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
from ezmsg.util.messages.axisarray import AxisArray
from ezmsg.util.messages.util import replace


class AbsSettings:
pass
class AbsSettings(ez.Settings):
"""This transform takes no parameters. See :obj:`AnscombeSettings` for why
an empty settings class is needed rather than ``SETTINGS = None``."""


class AbsTransformer(BaseTransformer[None, AxisArray, AxisArray]):
class AbsTransformer(BaseTransformer[AbsSettings, AxisArray, AxisArray]):
def _process(self, message: AxisArray) -> AxisArray:
xp = get_namespace(message.data)
return replace(message, data=xp.abs(message.data))


class Abs(BaseTransformerUnit[None, AxisArray, AxisArray, AbsTransformer]): ... # SETTINGS = None
class Abs(BaseTransformerUnit[AbsSettings, AxisArray, AxisArray, AbsTransformer]):
SETTINGS = AbsSettings


def abs() -> AbsTransformer:
Expand Down
15 changes: 13 additions & 2 deletions src/ezmsg/sigproc/math/anscombe.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,24 @@
_SQRT_1P5 = math.sqrt(1.5)


class AnscombeTransformer(BaseTransformer[None, AxisArray, AxisArray]):
class AnscombeSettings(ez.Settings):
"""The forward transform takes no parameters.

This empty class exists because a Unit cannot express "no settings":
``SETTINGS = None`` is rejected by ezmsg's Unit metaclass, and omitting
``SETTINGS`` makes the metaclass substitute a bare :obj:`ez.Settings`,
which the transformer then rejects.
"""


class AnscombeTransformer(BaseTransformer[AnscombeSettings, AxisArray, AxisArray]):
def _process(self, message: AxisArray) -> AxisArray:
xp = get_namespace(message.data)
return replace(message, data=2.0 * xp.sqrt(message.data + _OFFSET))


class Anscombe(BaseTransformerUnit[None, AxisArray, AxisArray, AnscombeTransformer]): ... # SETTINGS = None
class Anscombe(BaseTransformerUnit[AnscombeSettings, AxisArray, AxisArray, AnscombeTransformer]):
SETTINGS = AnscombeSettings


class InverseMethod(OptionsEnum):
Expand Down
11 changes: 9 additions & 2 deletions src/ezmsg/sigproc/math/invert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
"""

import ezmsg.core as ez
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
from ezmsg.util.messages.axisarray import AxisArray
from ezmsg.util.messages.util import replace


class InvertTransformer(BaseTransformer[None, AxisArray, AxisArray]):
class InvertSettings(ez.Settings):
"""This transform takes no parameters. See :obj:`AnscombeSettings` for why
an empty settings class is needed rather than ``SETTINGS = None``."""


class InvertTransformer(BaseTransformer[InvertSettings, AxisArray, AxisArray]):
def _process(self, message: AxisArray) -> AxisArray:
return replace(message, data=1 / message.data)


class Invert(BaseTransformerUnit[None, AxisArray, AxisArray, InvertTransformer]): ... # SETTINGS = None
class Invert(BaseTransformerUnit[InvertSettings, AxisArray, AxisArray, InvertTransformer]):
SETTINGS = InvertSettings


def invert() -> InvertTransformer:
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,54 @@ def test_inverse_anscombe_mlx_matches_numpy(method: InverseMethod):
out_mx = np.asarray(xformer(AxisArray(mx.array(in_dat), dims=["time", "ch"])).data)

assert np.allclose(out_np, out_mx, rtol=1e-5, atol=1e-5)


def test_math_unit_settings_type_matches_transformer():
"""Every math Unit must declare a SETTINGS whose type the paired transformer accepts.

A Unit that declares no SETTINGS gets a bare ``ez.Settings()`` from ezmsg's
metaclass, which then trips the ``isinstance(settings, settings_type)``
assert in ``_unify_settings`` the moment the Unit builds its processor --
i.e. at ``initialize()``, inside a running pipeline. Parameterless
transforms are the ones at risk, since ``SETTINGS = None`` is not
expressible (the metaclass requires an ``ez.Settings`` subclass), which
makes it tempting to leave SETTINGS off entirely.

The whole package is swept in one test, at call time rather than at
collection time: importing every ``ezmsg.sigproc.math`` submodule during
collection perturbs import order for the rest of the suite.
"""
import importlib
import pkgutil

from ezmsg.baseproc import BaseTransformerUnit
from ezmsg.baseproc.units import get_base_transformer_type

import ezmsg.sigproc.math as math_pkg

units = []
for mod_info in pkgutil.iter_modules(math_pkg.__path__):
mod = importlib.import_module(f"{math_pkg.__name__}.{mod_info.name}")
units.extend(
obj
for obj in vars(mod).values()
if isinstance(obj, type) and issubclass(obj, BaseTransformerUnit) and obj.__module__ == mod.__name__
)
assert units, "no math Units discovered; the sweep is not testing anything"

problems = []
for unit_cls in sorted(units, key=lambda c: (c.__module__, c.__name__)):
settings_type = get_base_transformer_type(unit_cls).get_settings_type()
if settings_type is type(None):
problems.append(f"{unit_cls.__name__}: transformer is parameterized with None, needs an ez.Settings")
continue
if not issubclass(unit_cls.SETTINGS, settings_type):
problems.append(f"{unit_cls.__name__}: SETTINGS is {unit_cls.SETTINGS.__name__}, want {settings_type}")
continue
# The failure mode is at processor construction, so exercise it directly.
unit = unit_cls()
unit.create_processor()
if not isinstance(unit.processor.settings, settings_type):
problems.append(f"{unit_cls.__name__}: processor got {type(unit.processor.settings)}")

assert not problems, "Units with unusable SETTINGS:\n " + "\n ".join(problems)
Loading