Skip to content

Axis

axis

Runtime axis types and helpers.

Axis(name, kind, size, labels=None, values=None, domain=None, spacing=None) dataclass

Resolved Runtime Axis.

Describe one named axis after configuration has been validated and converted into runtime metadata.

Attributes:

Name Type Description
name IdentifierString

Stable axis name used by systems and parameters.

kind Literal['continuous', 'categorical']

Whether the axis is continuous or categorical.

size int

Number of positions along the axis.

labels tuple[str, ...] | None

Optional labels for categorical axes.

values tuple[int, ...] | None

Optional integer values associated with categorical labels.

domain tuple[float, float] | None

Closed numeric domain for continuous axes.

spacing Literal['linear', 'log'] | None

Spacing rule for continuous axes.

Notes

Continuous axes intentionally support both point-style and bin-style calculations. The same axis can expose representative points via points() and intervals via bins(), so systems and parameters can use whichever view is appropriate without introducing incompatible axis definitions.

bin_edges()

Continuous Bin Edges.

Partition a continuous axis domain into size bins.

Returns:

Type Description
tuple[float, ...]

The bin-edge coordinates with length size + 1.

Notes

Linear axes use np.linspace(lower, upper, size + 1). Log axes use np.geomspace(lower, upper, size + 1).

Examples:

>>> from flepimop2.axis import Axis
>>> axis = Axis(
...     name="time",
...     kind="continuous",
...     size=4,
...     domain=(0.0, 8.0),
...     spacing="linear",
... )
>>> axis.bin_edges()
(0.0, 2.0, 4.0, 6.0, 8.0)
Source code in src/flepimop2/axis.py
def bin_edges(self) -> tuple[float, ...]:
    """
    Continuous Bin Edges.

    Partition a continuous axis domain into `size` bins.

    Returns:
        The bin-edge coordinates with length `size + 1`.

    Notes:
        Linear axes use `np.linspace(lower, upper, size + 1)`. Log axes use
        `np.geomspace(lower, upper, size + 1)`.

    Examples:
        >>> from flepimop2.axis import Axis
        >>> axis = Axis(
        ...     name="time",
        ...     kind="continuous",
        ...     size=4,
        ...     domain=(0.0, 8.0),
        ...     spacing="linear",
        ... )
        >>> axis.bin_edges()
        (0.0, 2.0, 4.0, 6.0, 8.0)
    """
    lo, hi = self._continuous_domain()
    if self.spacing == "linear":
        edges = np.linspace(lo, hi, self.size + 1, dtype=np.float64)
    else:
        edges = np.geomspace(lo, hi, self.size + 1, dtype=np.float64)
    return tuple(edges.tolist())

bins()

Continuous Bin Intervals.

Return half-open-style interval metadata for each continuous bin.

Returns:

Type Description
tuple[tuple[float, float], ...]

A tuple of (lower, upper) bin intervals with length size.

Examples:

>>> from flepimop2.axis import Axis
>>> axis = Axis(
...     name="time",
...     kind="continuous",
...     size=2,
...     domain=(0.0, 4.0),
...     spacing="linear",
... )
>>> axis.bins()
((0.0, 2.0), (2.0, 4.0))
Source code in src/flepimop2/axis.py
def bins(self) -> tuple[tuple[float, float], ...]:
    """
    Continuous Bin Intervals.

    Return half-open-style interval metadata for each continuous bin.

    Returns:
        A tuple of `(lower, upper)` bin intervals with length `size`.

    Examples:
        >>> from flepimop2.axis import Axis
        >>> axis = Axis(
        ...     name="time",
        ...     kind="continuous",
        ...     size=2,
        ...     domain=(0.0, 4.0),
        ...     spacing="linear",
        ... )
        >>> axis.bins()
        ((0.0, 2.0), (2.0, 4.0))
    """
    self._continuous_domain()
    edges = self.bin_edges()
    return tuple(pairwise(edges))

from_model(name, model) classmethod

Build an Axis from a configuration model.

Returns:

Type Description
Axis

The resolved runtime axis.

Source code in src/flepimop2/axis.py
@classmethod
def from_model(
    cls,
    name: IdentifierString,
    model: ContinuousAxisModel | CategoricalAxisModel,
) -> "Axis":
    """
    Build an `Axis` from a configuration model.

    Returns:
        The resolved runtime axis.
    """
    if isinstance(model, ContinuousAxisModel):
        return cls(
            name=name,
            kind=model.kind,
            size=model.size,
            domain=model.domain,
            spacing=model.spacing,
        )
    return cls(
        name=name,
        kind=model.kind,
        size=len(model.labels),
        labels=model.labels,
        values=model.values,
    )

points()

Representative Continuous Points.

Return one representative point for each continuous bin.

Returns:

Type Description
tuple[float, ...]

A tuple of representative point coordinates with length size.

Notes

Points are the centroids of the bins returned by Axis.bins, arithmetic midpoints for linear spacing and geometric means for log spacing.

Examples:

>>> from pprint import pp
>>> from flepimop2.axis import Axis
>>> Axis(
...     name="time",
...     kind="continuous",
...     size=4,
...     domain=(0.0, 8.0),
...     spacing="linear",
... ).points()
(1.0, 3.0, 5.0, 7.0)
>>> pp(
...     Axis(
...         name="time",
...         kind="continuous",
...         size=4,
...         domain=(1e-9, 1e-3),
...         spacing="log",
...     ).points()
... )
(5.623413251903491e-09,
 1.7782794100389227e-07,
 5.623413251903491e-06,
 0.0001778279410038923)
Source code in src/flepimop2/axis.py
def points(self) -> tuple[float, ...]:
    """
    Representative Continuous Points.

    Return one representative point for each continuous bin.

    Returns:
        A tuple of representative point coordinates with length `size`.

    Notes:
        Points are the centroids of the bins returned by
        [Axis.bins][flepimop2.axis.Axis.bins], arithmetic midpoints for linear
        spacing and geometric means for log spacing.

    Examples:
        >>> from pprint import pp
        >>> from flepimop2.axis import Axis
        >>> Axis(
        ...     name="time",
        ...     kind="continuous",
        ...     size=4,
        ...     domain=(0.0, 8.0),
        ...     spacing="linear",
        ... ).points()
        (1.0, 3.0, 5.0, 7.0)
        >>> pp(
        ...     Axis(
        ...         name="time",
        ...         kind="continuous",
        ...         size=4,
        ...         domain=(1e-9, 1e-3),
        ...         spacing="log",
        ...     ).points()
        ... )
        (5.623413251903491e-09,
         1.7782794100389227e-07,
         5.623413251903491e-06,
         0.0001778279410038923)
    """
    lo, hi = self._continuous_domain()
    if self.spacing == "linear":
        edges = np.linspace(lo, hi, self.size + 1, dtype=np.float64)
        points = 0.5 * (edges[:-1] + edges[1:])
    else:
        edges = np.geomspace(lo, hi, self.size + 1, dtype=np.float64)
        points = np.sqrt(edges[:-1] * edges[1:])
    return tuple(points.tolist())

AxisCollection(axes=None)

Bases: Mapping[IdentifierString, Axis]

Runtime Axis Collection.

Store resolved axes and provide lookup and named-shape helpers for systems, parameters, and engines.

Notes

AxisCollection is the runtime entry point for working with named axes. Systems typically use it to resolve requested parameter shapes, while parameter modules use it to interpret declared axis names and inspect axis metadata such as labels or bin edges.

Examples:

>>> from flepimop2.axis import AxisCollection
>>> axes = AxisCollection.from_config({
...     "age": {
...         "kind": "categorical",
...         "labels": ["age0_17", "age18_64", "age65_plus"],
...     },
...     "time": {
...         "kind": "continuous",
...         "domain": (0.0, 12.0),
...         "size": 4,
...     },
... })
>>> axes.size("age")
3
>>> axes["age"].labels
('age0_17', 'age18_64', 'age65_plus')
>>> axes["time"].bin_edges()
(0.0, 3.0, 6.0, 9.0, 12.0)
>>> axes.resolve_shape(("age", "time")).sizes
(3, 4)
>>> axes.resolve_shape(("region",))
Traceback (most recent call last):
    ...
KeyError: "Unknown axis names requested: ('region',)."

Initialize the collection with an optional axis mapping.

Source code in src/flepimop2/axis.py
def __init__(self, axes: Mapping[IdentifierString, Axis] | None = None) -> None:
    """Initialize the collection with an optional axis mapping."""
    self._axes = dict(axes or {})

__getitem__(key)

Return the axis for a given name.

Source code in src/flepimop2/axis.py
def __getitem__(self, key: IdentifierString) -> Axis:
    """Return the axis for a given name."""
    return self._axes[key]

__iter__()

Iterate over axis names in the collection.

Returns:

Type Description
Iterator[IdentifierString]

An iterator over axis names.

Source code in src/flepimop2/axis.py
def __iter__(self) -> Iterator[IdentifierString]:
    """
    Iterate over axis names in the collection.

    Returns:
        An iterator over axis names.
    """
    return iter(self._axes)

__len__()

Return the number of axes in the collection.

Source code in src/flepimop2/axis.py
def __len__(self) -> int:
    """Return the number of axes in the collection."""
    return len(self._axes)

from_config(config) classmethod

Construct a runtime axis collection from configuration data.

Returns:

Type Description
AxisCollection

The resolved runtime axis collection.

Source code in src/flepimop2/axis.py
@classmethod
def from_config(cls, config: ResolvedAxisConfig) -> "AxisCollection":
    """
    Construct a runtime axis collection from configuration data.

    Returns:
        The resolved runtime axis collection.
    """
    validated = TypeAdapter(AxesGroupModel).validate_python(config)
    return cls({
        name: Axis.from_model(name, axis) for name, axis in validated.items()
    })

get(name, /, default=None)

get(name: IdentifierString) -> Axis | None
get(name: IdentifierString, /, default: Axis) -> Axis
get(name: IdentifierString, /, default: T) -> Axis | T

Return a named axis, optionally providing a default.

Returns:

Type Description
Axis | T | None

The resolved axis, or the provided default when missing.

Source code in src/flepimop2/axis.py
def get(
    self, name: IdentifierString, /, default: T | None = None
) -> Axis | T | None:
    """
    Return a named axis, optionally providing a default.

    Returns:
        The resolved axis, or the provided default when missing.
    """
    return self._axes.get(name, default)

resolve_shape(axis_names)

Resolve a tuple of axis names into concrete dimension sizes.

Returns:

Type Description
ResolvedShape

The resolved named shape.

Source code in src/flepimop2/axis.py
def resolve_shape(self, axis_names: Sequence[IdentifierString]) -> ResolvedShape:
    """
    Resolve a tuple of axis names into concrete dimension sizes.

    Returns:
        The resolved named shape.
    """
    names = tuple(axis_names)
    self._validate_axis_names(names)
    return ResolvedShape(axis_names=names, sizes=self.sizes(*names))

size(name)

Return the size for a named axis.

Source code in src/flepimop2/axis.py
def size(self, name: IdentifierString) -> int:
    """Return the size for a named axis."""
    return self[name].size

sizes(*names)

Return the sizes for a sequence of named axes.

Source code in src/flepimop2/axis.py
def sizes(self, *names: IdentifierString) -> tuple[int, ...]:
    """Return the sizes for a sequence of named axes."""
    return tuple(self.size(name) for name in names)

ResolvedShape(axis_names=(), sizes=()) dataclass

A named runtime shape resolved against a concrete axis collection.

__post_init__()

Validate the number of axis names and sizes match.

Raises:

Type Description
ValueError

If axis_names and sizes do not have matching lengths.

Examples:

>>> from flepimop2.axis import ResolvedShape
>>> ResolvedShape(axis_names=("age", "time"), sizes=(3, 4))
ResolvedShape(axis_names=('age', 'time'), sizes=(3, 4))
>>> ResolvedShape(axis_names=("age",), sizes=(3, 4))
Traceback (most recent call last):
    ...
ValueError: ResolvedShape axis_names and sizes must have matching lengths; got 1 names and 2 sizes.
Source code in src/flepimop2/axis.py
def __post_init__(self) -> None:
    """
    Validate the number of axis names and sizes match.

    Raises:
        ValueError: If `axis_names` and `sizes` do not have matching lengths.

    Examples:
        >>> from flepimop2.axis import ResolvedShape
        >>> ResolvedShape(axis_names=("age", "time"), sizes=(3, 4))
        ResolvedShape(axis_names=('age', 'time'), sizes=(3, 4))
        >>> ResolvedShape(axis_names=("age",), sizes=(3, 4))
        Traceback (most recent call last):
            ...
        ValueError: ResolvedShape axis_names and sizes must have matching lengths; got 1 names and 2 sizes.
    """  # noqa: E501
    if len(self.axis_names) != len(self.sizes):
        msg = (
            "ResolvedShape axis_names and sizes must have matching lengths; "
            f"got {len(self.axis_names)} names and {len(self.sizes)} sizes."
        )
        raise ValueError(msg)