Skip to content

Reference

Registry

Runtime registry of system classes. Every concrete subclass of a family base auto-registers at class-definition time — the 149 built-ins and your own classes alike. The registry is what the bulk test suite and the documentation generator iterate over.

from tsdynamics import registry

registry.families()                       # {'ode': 118, 'dde': 5, 'map': 26}
registry.categories(family="map")         # {'chaotic_maps': 9, ...}
entry = registry.get("Lorenz")            # SystemEntry
entry.cls, entry.family, entry.category   # (<class Lorenz>, 'ode', 'chaotic_attractors')

registry

Runtime registry of dynamical-system classes.

Every concrete subclass of :class:~tsdynamics.families.base.SystemBase is registered automatically at class-definition time (via SystemBase.__init_subclass__) — built-in systems and user-defined ones alike. Built-in systems (those defined under tsdynamics.systems) are what the bulk test-suite and the documentation generator iterate over; user-defined classes are registered too but excluded from iteration by default.

Alongside the system registry, this module hosts two reserved generic name registries — :data:analyses and :data:transforms — :class:Registry containers (name → object + metadata) for the analysis and transform streams to register into. Solvers are not registered here: they live in the richer :mod:tsdynamics.solvers registry (a name → SolverSpec table carrying capability flags, populated by that package's directory scan + the :mod:~tsdynamics.plugins entry-point loader).

This module must stay import-light: it is imported while the tsdynamics package itself is still initialising, so it may only depend on the standard library.

Examples:

>>> from tsdynamics import registry
>>> registry.families()
{'ode': 118, 'dde': 5, 'map': 26}
>>> lorenz = registry.get("Lorenz")
>>> lorenz.family, lorenz.category
('ode', 'chaotic_attractors')
>>> [e.name for e in registry.all_systems(family="dde")]
['MackeyGlass', 'IkedaDelay', 'SprottDelay', 'ScrollDelay', 'PiecewiseCircuit']

SystemEntry dataclass

SystemEntry(
    name: str,
    cls: type,
    family: Family,
    category: str,
    module: str,
    dim: int | None,
    params: Mapping[str, Any],
    is_builtin: bool,
    reference: str | None = None,
    known_lyapunov: Mapping[str, Any] | None = None,
)

One registered system class plus the metadata the tooling needs.

RegistryEntry dataclass

RegistryEntry(
    name: str, obj: Any, metadata: Mapping[str, Any]
)

One named, registered object plus its metadata.

Registry

Registry(kind: str)

A minimal, generic name → object registry.

Backs the :data:analyses and :data:transforms registries. It only stores and looks up; the discovery that fills it (directory scans, :mod:~tsdynamics.plugins entry points) is built on top of it elsewhere.

Registration is usable directly or as a decorator::

analyses.register("lyapunov", lyapunov_spectrum)        # direct

@analyses.register("corr_dim", needs="trajectory")      # decorator
def correlation_dimension(traj): ...

Re-registering the same object under a name is idempotent (safe across module re-imports). A clash between two different objects on one name raises unless replace=True.

Source code in src/tsdynamics/registry.py
def __init__(self, kind: str) -> None:
    self._kind = kind
    self._entries: dict[str, RegistryEntry] = {}

kind property

kind: str

What this registry holds ("solver" / "analysis" / "transform").

register

register(
    name: str,
    obj: Any = None,
    *,
    replace: bool = False,
    **metadata: Any,
) -> Any

Register obj under name.

Called with obj it registers and returns it; called without it returns a decorator (so it can wrap a class/function definition). Extra keyword arguments are stored as the entry's metadata.

Source code in src/tsdynamics/registry.py
def register(
    self,
    name: str,
    obj: Any = None,
    *,
    replace: bool = False,
    **metadata: Any,
) -> Any:
    """
    Register ``obj`` under ``name``.

    Called with ``obj`` it registers and returns it; called without it
    returns a decorator (so it can wrap a class/function definition).
    Extra keyword arguments are stored as the entry's ``metadata``.
    """
    if obj is None:

        def _decorator(target: Any) -> Any:
            self._insert(name, target, replace=replace, metadata=metadata)
            return target

        return _decorator
    self._insert(name, obj, replace=replace, metadata=metadata)
    return obj

get

get(name: str) -> Any

Return the object registered under name (raises KeyError with hints).

Source code in src/tsdynamics/registry.py
def get(self, name: str) -> Any:
    """Return the object registered under ``name`` (raises ``KeyError`` with hints)."""
    entry = self._entries.get(name)
    if entry is not None:
        return entry.obj
    close = [n for n in self._entries if n.lower() == name.lower()]
    hint = f" Did you mean {close[0]!r}?" if close else ""
    raise KeyError(f"No {self._kind} registered as {name!r}.{hint}")

entry

entry(name: str) -> RegistryEntry

Return the full :class:RegistryEntry (object + metadata) for name.

Source code in src/tsdynamics/registry.py
def entry(self, name: str) -> RegistryEntry:
    """Return the full :class:`RegistryEntry` (object + metadata) for ``name``."""
    try:
        return self._entries[name]
    except KeyError:
        raise KeyError(f"No {self._kind} registered as {name!r}.") from None

names

names() -> list[str]

Return registered names, in registration order.

Source code in src/tsdynamics/registry.py
def names(self) -> list[str]:
    """Return registered names, in registration order."""
    return list(self._entries)

all

all() -> list[RegistryEntry]

All entries, in registration order.

Source code in src/tsdynamics/registry.py
def all(self) -> list[RegistryEntry]:
    """All entries, in registration order."""
    return list(self._entries.values())

unregister

unregister(name: str) -> None

Remove name (raises KeyError if absent).

Source code in src/tsdynamics/registry.py
def unregister(self, name: str) -> None:
    """Remove ``name`` (raises ``KeyError`` if absent)."""
    del self._entries[name]

clear

clear() -> None

Drop every entry (mainly for tests).

Source code in src/tsdynamics/registry.py
def clear(self) -> None:
    """Drop every entry (mainly for tests)."""
    self._entries.clear()

register_class

register_class(cls: type) -> None

Register a system class. Called from SystemBase.__init_subclass__.

Classes without a concrete _equations/_step (abstract intermediate bases) are ignored. Two built-in classes sharing a name is a bug and raises immediately; user classes may freely shadow builtin names.

Source code in src/tsdynamics/registry.py
def register_class(cls: type) -> None:
    """
    Register a system class.  Called from ``SystemBase.__init_subclass__``.

    Classes without a concrete ``_equations``/``_step`` (abstract intermediate
    bases) are ignored.  Two *built-in* classes sharing a name is a bug and
    raises immediately; user classes may freely shadow builtin names.
    """
    if not _has_concrete_rhs(cls):
        return

    is_builtin = cls.__module__.startswith(_BUILTIN_PREFIX)
    entry = SystemEntry(
        name=cls.__name__,
        cls=cls,
        family=_family_of(cls),
        category=cls.__module__.rsplit(".", 1)[-1],
        module=cls.__module__,
        dim=getattr(cls, "dim", None),
        params=MappingProxyType(dict(getattr(cls, "params", {}))),
        is_builtin=is_builtin,
        reference=getattr(cls, "reference", None),
        known_lyapunov=getattr(cls, "known_lyapunov", None),
    )

    bucket = _BY_NAME.setdefault(entry.name, [])
    # Re-importing/reloading the SAME builtin module is fine (the entry below
    # replaces the stale one); only a different module is a duplicate bug.
    if is_builtin and any(e.is_builtin and e.module != entry.module for e in bucket):
        other = next(e for e in bucket if e.is_builtin and e.module != entry.module)
        raise TypeError(
            f"Duplicate built-in system name {entry.name!r}: "
            f"already defined in {other.module}, redefined in {entry.module}."
        )
    # Re-definition of the *same* user class (e.g. re-running a notebook cell
    # or pytest re-importing a module) replaces the stale entry.
    bucket[:] = [e for e in bucket if e.module != entry.module] + [entry]

all_systems

all_systems(
    *,
    family: str | None = None,
    category: str | None = None,
    builtin: bool | None = True,
) -> list[SystemEntry]

Return registered systems, in registration (= import) order.

PARAMETER DESCRIPTION
family

Keep only one family.

TYPE: ('ode', 'dde', 'map', 'sde', 'other') DEFAULT: "ode"

category

Keep only one category (module stem, e.g. "chaotic_attractors").

TYPE: str DEFAULT: None

builtin

True → only built-in systems (the default for tests/docs); False → only user-defined classes; None → everything.

TYPE: bool or None DEFAULT: True

Source code in src/tsdynamics/registry.py
def all_systems(
    *,
    family: str | None = None,
    category: str | None = None,
    builtin: bool | None = True,
) -> list[SystemEntry]:
    """
    Return registered systems, in registration (= import) order.

    Parameters
    ----------
    family : {"ode", "dde", "map", "sde", "other"}, optional
        Keep only one family.
    category : str, optional
        Keep only one category (module stem, e.g. ``"chaotic_attractors"``).
    builtin : bool or None, default True
        ``True`` → only built-in systems (the default for tests/docs);
        ``False`` → only user-defined classes; ``None`` → everything.
    """
    out: list[SystemEntry] = []
    for bucket in _BY_NAME.values():
        for e in bucket:
            if builtin is not None and e.is_builtin is not builtin:
                continue
            if family is not None and e.family != family:
                continue
            if category is not None and e.category != category:
                continue
            out.append(e)
    return out

by_family

by_family(family: str, **kwargs: Any) -> list[SystemEntry]

Shorthand for :func:all_systems filtered to one family.

Source code in src/tsdynamics/registry.py
def by_family(family: str, **kwargs: Any) -> list[SystemEntry]:
    """Shorthand for :func:`all_systems` filtered to one family."""
    return all_systems(family=family, **kwargs)

get

get(name: str, *, builtin: bool = True) -> SystemEntry

Look up a single system by class name.

Prefers the built-in entry when builtin is True (the default); raises KeyError with name suggestions otherwise.

Source code in src/tsdynamics/registry.py
def get(name: str, *, builtin: bool = True) -> SystemEntry:
    """
    Look up a single system by class name.

    Prefers the built-in entry when ``builtin`` is True (the default);
    raises ``KeyError`` with name suggestions otherwise.
    """
    bucket = _BY_NAME.get(name, [])
    for e in bucket:
        if e.is_builtin == builtin:
            return e
    if bucket:  # exists, but with the other builtin-ness
        return bucket[-1]
    close = [n for n in _BY_NAME if n.lower() == name.lower()]
    hint = f" Did you mean {close[0]!r}?" if close else ""
    raise KeyError(f"No registered system named {name!r}.{hint}")

families

families(*, builtin: bool | None = True) -> dict[str, int]

Return {family: count} for the registered systems.

Source code in src/tsdynamics/registry.py
def families(*, builtin: bool | None = True) -> dict[str, int]:
    """Return ``{family: count}`` for the registered systems."""
    counts: dict[str, int] = {}
    for e in all_systems(builtin=builtin):
        counts[e.family] = counts.get(e.family, 0) + 1
    return counts

categories

categories(
    family: str | None = None,
    *,
    builtin: bool | None = True,
) -> dict[str, int]

Return {category: count}, optionally restricted to one family.

Source code in src/tsdynamics/registry.py
def categories(family: str | None = None, *, builtin: bool | None = True) -> dict[str, int]:
    """Return ``{category: count}``, optionally restricted to one family."""
    counts: dict[str, int] = {}
    for e in all_systems(family=family, builtin=builtin):
        counts[e.category] = counts.get(e.category, 0) + 1
    return counts