Implementation of the AxesImpl abstract base class.

AxesImpl

Bases: Impl[R]

Base class for axis metadata containers.

AxesImpl is a lightweight container for axis metadata (names, bounds, periodicity flags, and units). It does NOT store or manipulate the underlying numerical data; instead it standardizes how set representations (of type R) expose axis-aware interactions (selection, projection, formatting).

Construction (new):

AxesImpl([
    dict(name=   'x', bounds=[xmin, xmax], unit='m'),
    dict(name='*phi', bounds=[0, 2*pi],    unit='rad'),
    dict(name=   'y', bounds=[-5, 5]),  # unit optional
])

Also accepted (shorthand): AxesImpl(['x', 'y', 'z']) # all (-inf,+inf), no units

Conventions
  • Leading '' in name => periodic axis (name stored without '').
  • bounds omitted or Ellipsis => (-inf, +inf)
  • unit omitted => ''
  • Each axis spec must be a dict with at minimum a 'name' key (unless list[str] form used).

Key properties / methods: - ndim: number of axes - axis(ax): resolve axis identifier (int index or name) to int - axis_name(i): canonical name at index i - axis_bounds(ax): (min, max) tuple for the resolved axis - axis_is_periodic(ax): True if marked periodic (via leading '*') - project_onto(inp, axes): subclasses implement projection onto one or multiple axes.

Source code in src/pyspect/impls/axes.py
class AxesImpl[R](Impl[R]):
    """
    Base class for axis metadata containers.

    AxesImpl is a lightweight container for axis metadata (names, bounds,
    periodicity flags, and units). It does NOT store or manipulate the underlying
    numerical data; instead it standardizes how set representations (of type `R`)
    expose axis-aware interactions (selection, projection, formatting).

    Construction (new):
    ```python
    AxesImpl([
        dict(name=   'x', bounds=[xmin, xmax], unit='m'),
        dict(name='*phi', bounds=[0, 2*pi],    unit='rad'),
        dict(name=   'y', bounds=[-5, 5]),  # unit optional
    ])
    ```

    Also accepted (shorthand): `AxesImpl(['x', 'y', 'z'])  # all (-inf,+inf), no units`

    Conventions:
        - Leading '*' in name => periodic axis (name stored without '*').
        - bounds omitted or Ellipsis => (-inf, +inf)
        - unit omitted => ''
        - Each axis spec must be a dict with at minimum a 'name' key (unless list[str] form used).

    Key properties / methods:
        - `ndim`: number of axes
        - `axis(ax)`: resolve axis identifier (int index or name) to int
        - `axis_name(i)`: canonical name at index i
        - `axis_bounds(ax)`: (min, max) tuple for the resolved axis
        - `axis_is_periodic(ax)`: True if marked periodic (via leading '*')
        - `project_onto(inp, axes)`: subclasses implement projection onto one or multiple axes.
    """

    def __init__(self, specs: list[dict] | list[str]):
        """Construct AxesImpl.

        Parameters:
            axes: Either a list of axis spec dicts (preferred) or list of axis name strings.
                  Dict form keys: name (required), bounds (optional), unit (optional), periodic (optional bool).
                  Periodicity may also be encoded by leading '*' in the name.
        """

        if not isinstance(specs, list):
            raise TypeError("AxesImpl expects a list[dict] or list[str]")

        names: list[str] = []
        periodicity: list[bool] = []
        min_bounds: list[float] = []
        max_bounds: list[float] = []
        units: list[str] = []

        for spec in specs:
            if isinstance(spec, str):
                spec = dict(name=spec)
            elif not isinstance(spec, dict):
                raise TypeError(f"Axis spec must be dict, got {type(spec)}")
            elif 'name' not in spec:
                raise ValueError("Axis spec missing 'name'")

            if not isinstance(spec['name'], str) or spec['name'] == '':
                raise TypeError("Axis 'name' must be non-empty str")
            is_periodic = spec['name'].startswith('*')
            name = spec['name'][1:] if is_periodic else spec['name']

            if bounds := spec.get('bounds', None):
                try:
                    mn, mx = float(bounds[0]), float(bounds[1])
                except (TypeError, ValueError, IndexError):
                    raise ValueError(f"Axis '{name}' bounds must be a 2-element list/tuple of numbers")
            else:
                mn, mx = -math.inf, math.inf

            unit = spec.get('unit', '')
            if not isinstance(unit, str):
                raise TypeError(f"Axis '{name}' unit must be a string")

            names.append(name)
            periodicity.append(is_periodic)
            min_bounds.append(mn)
            max_bounds.append(mx)
            units.append(unit)

        self._ndim = len(names)
        self._axis_name = tuple(names)
        self._axis_isperiodic = tuple(periodicity)
        self._min_bounds = tuple(min_bounds)
        self._max_bounds = tuple(max_bounds)
        self._units = tuple(units)

    def _axes_from_lists(self,
                         names: list[str],
                         min_bounds: list[float] = ...,
                         max_bounds: list[float] = ...,
                         units: list[str] | None = None):
        """Legacy helper: build list[dict] from separate lists and re-init."""
        if min_bounds is Ellipsis:
            min_bounds = [-float('inf')] * len(names)
        if max_bounds is Ellipsis:
            max_bounds = [ float('inf')] * len(names)
        if units is None:
            units = [''] * len(names)
        specs = [
            dict(name=name, bounds=[mn, mx], unit=unit)
            for name, mn, mx, unit in zip(names, min_bounds, max_bounds, units)
        ]
        self.__init__(specs)

    @property
    def ndim(self):
        return self._ndim

    def assert_axis(self, ax: Axis) -> None:
        """Assert that the given axis identifier is valid."""
        match ax:
            case int(i):
                assert -len(self._axis_name) <= i < len(self._axis_name), \
                    f'Axis ({i=}) does not exist.'
            case str(name):
                assert name in self._axis_name, \
                    f'Axis ({name=}) does not exist.'

    def axis(self, ax: Axis) -> int:
        """Resolve the given axis identifier to an integer index."""
        self.assert_axis(ax)
        match ax:
            case int(i):
                return i
            case str(name):
                return self._axis_name.index(name)

    def axis_name(self, i: int) -> str:
        """Get the canonical name of the axis at the given index."""
        self.assert_axis(i)
        return self._axis_name[i]

    def axis_unit(self, ax: Axis) -> str:
        """Get the unit string of the axis at the given index."""
        i = self.axis(ax)
        return self._units[i]

    def axis_bounds(self, ax: Axis) -> bool:
        """Get the (min, max) bounds tuple of the given axis."""
        i = self.axis(ax)
        amin = self._min_bounds[i]
        amax = self._max_bounds[i]
        return amin, amax

    def axis_is_periodic(self, ax: Axis) -> bool:
        """Return True if the given axis is marked periodic."""
        i = self.axis(ax)
        return self._axis_isperiodic[i]

    def project_onto(self, inp: R, axes: Axis | tuple[Axis, ...], **kwds) -> R:
        """Project the input set representation onto the specified axes.

        This method should be implemented in subclasses.
        The `axes` argument may be a single axis identifier or a tuple of them.
        Additional keyword arguments may be accepted by subclasses.

        Parameters:
            inp (R): The input set to project.
            axes (Axis | tuple[Axis, ...]): The axis or axes to project onto.
            **kwds: Additional keyword arguments for subclass-specific behavior.

        Returns:
            The projected set.
        """
        raise NotImplementedError("project_onto not implemented")

__init__(specs)

Construct AxesImpl.

Parameters:

Name Type Description Default
axes

Either a list of axis spec dicts (preferred) or list of axis name strings. Dict form keys: name (required), bounds (optional), unit (optional), periodic (optional bool). Periodicity may also be encoded by leading '*' in the name.

required
Source code in src/pyspect/impls/axes.py
def __init__(self, specs: list[dict] | list[str]):
    """Construct AxesImpl.

    Parameters:
        axes: Either a list of axis spec dicts (preferred) or list of axis name strings.
              Dict form keys: name (required), bounds (optional), unit (optional), periodic (optional bool).
              Periodicity may also be encoded by leading '*' in the name.
    """

    if not isinstance(specs, list):
        raise TypeError("AxesImpl expects a list[dict] or list[str]")

    names: list[str] = []
    periodicity: list[bool] = []
    min_bounds: list[float] = []
    max_bounds: list[float] = []
    units: list[str] = []

    for spec in specs:
        if isinstance(spec, str):
            spec = dict(name=spec)
        elif not isinstance(spec, dict):
            raise TypeError(f"Axis spec must be dict, got {type(spec)}")
        elif 'name' not in spec:
            raise ValueError("Axis spec missing 'name'")

        if not isinstance(spec['name'], str) or spec['name'] == '':
            raise TypeError("Axis 'name' must be non-empty str")
        is_periodic = spec['name'].startswith('*')
        name = spec['name'][1:] if is_periodic else spec['name']

        if bounds := spec.get('bounds', None):
            try:
                mn, mx = float(bounds[0]), float(bounds[1])
            except (TypeError, ValueError, IndexError):
                raise ValueError(f"Axis '{name}' bounds must be a 2-element list/tuple of numbers")
        else:
            mn, mx = -math.inf, math.inf

        unit = spec.get('unit', '')
        if not isinstance(unit, str):
            raise TypeError(f"Axis '{name}' unit must be a string")

        names.append(name)
        periodicity.append(is_periodic)
        min_bounds.append(mn)
        max_bounds.append(mx)
        units.append(unit)

    self._ndim = len(names)
    self._axis_name = tuple(names)
    self._axis_isperiodic = tuple(periodicity)
    self._min_bounds = tuple(min_bounds)
    self._max_bounds = tuple(max_bounds)
    self._units = tuple(units)

assert_axis(ax)

Assert that the given axis identifier is valid.

Source code in src/pyspect/impls/axes.py
def assert_axis(self, ax: Axis) -> None:
    """Assert that the given axis identifier is valid."""
    match ax:
        case int(i):
            assert -len(self._axis_name) <= i < len(self._axis_name), \
                f'Axis ({i=}) does not exist.'
        case str(name):
            assert name in self._axis_name, \
                f'Axis ({name=}) does not exist.'

axis(ax)

Resolve the given axis identifier to an integer index.

Source code in src/pyspect/impls/axes.py
def axis(self, ax: Axis) -> int:
    """Resolve the given axis identifier to an integer index."""
    self.assert_axis(ax)
    match ax:
        case int(i):
            return i
        case str(name):
            return self._axis_name.index(name)

axis_bounds(ax)

Get the (min, max) bounds tuple of the given axis.

Source code in src/pyspect/impls/axes.py
def axis_bounds(self, ax: Axis) -> bool:
    """Get the (min, max) bounds tuple of the given axis."""
    i = self.axis(ax)
    amin = self._min_bounds[i]
    amax = self._max_bounds[i]
    return amin, amax

axis_is_periodic(ax)

Return True if the given axis is marked periodic.

Source code in src/pyspect/impls/axes.py
def axis_is_periodic(self, ax: Axis) -> bool:
    """Return True if the given axis is marked periodic."""
    i = self.axis(ax)
    return self._axis_isperiodic[i]

axis_name(i)

Get the canonical name of the axis at the given index.

Source code in src/pyspect/impls/axes.py
def axis_name(self, i: int) -> str:
    """Get the canonical name of the axis at the given index."""
    self.assert_axis(i)
    return self._axis_name[i]

axis_unit(ax)

Get the unit string of the axis at the given index.

Source code in src/pyspect/impls/axes.py
def axis_unit(self, ax: Axis) -> str:
    """Get the unit string of the axis at the given index."""
    i = self.axis(ax)
    return self._units[i]

project_onto(inp, axes, **kwds)

Project the input set representation onto the specified axes.

This method should be implemented in subclasses. The axes argument may be a single axis identifier or a tuple of them. Additional keyword arguments may be accepted by subclasses.

Parameters:

Name Type Description Default
inp R

The input set to project.

required
axes Axis | tuple[Axis, ...]

The axis or axes to project onto.

required
**kwds

Additional keyword arguments for subclass-specific behavior.

{}

Returns:

Type Description
R

The projected set.

Source code in src/pyspect/impls/axes.py
def project_onto(self, inp: R, axes: Axis | tuple[Axis, ...], **kwds) -> R:
    """Project the input set representation onto the specified axes.

    This method should be implemented in subclasses.
    The `axes` argument may be a single axis identifier or a tuple of them.
    Additional keyword arguments may be accepted by subclasses.

    Parameters:
        inp (R): The input set to project.
        axes (Axis | tuple[Axis, ...]): The axis or axes to project onto.
        **kwds: Additional keyword arguments for subclass-specific behavior.

    Returns:
        The projected set.
    """
    raise NotImplementedError("project_onto not implemented")