Skip to content

Reference

Data & state-space

The lingua franca every analysis consumes. Trajectory is the result type all families produce (documented with the base classes); the regions, samplers and distances below are the state-space primitives the attractor/basin layer is built on — Monte-Carlo and full-grid sampling, and the attractor-matching distance used by continuation.

Regions

A Region is a Box or a Ball; both describe a bounded subset of state space and can draw uniform samples from it.

Box dataclass

Box(lo: ndarray, hi: ndarray)

Axis-aligned box [lo_i, hi_i] per dimension.

A closed, axis-aligned hyper-rectangle of state space: a point lies in the box iff lo_i <= u_i <= hi_i for every axis i. It is one of the region primitives (alongside :class:Ball and :class:Grid) that the sampler / basin / attractor layer is built on.

PARAMETER DESCRIPTION
lo

Per-axis lower and upper corners. Coerced to float arrays; hi must be >= lo componentwise.

TYPE: (array_like, shape(dim))

hi

Per-axis lower and upper corners. Coerced to float arrays; hi must be >= lo componentwise.

TYPE: (array_like, shape(dim))

RAISES DESCRIPTION
ValueError

If lo and hi differ in shape, or hi < lo on any axis.

Examples:

>>> b = Box([-1.0, -1.0], [1.0, 1.0])
>>> b.contains([0.0, 0.0])
True
>>> b.contains([[0.0, 0.0], [2.0, 0.0]])      # batch query → per-row mask
array([ True, False])

dim property

dim: int

State-space dimension.

contains

contains(u: Any) -> Any

Whether point(s) u lie in the box.

PARAMETER DESCRIPTION
u

A single point of shape (dim,) or a batch of shape (n, dim). Any other shape raises (so a batch can never silently collapse into one conflated truth value).

TYPE: array_like

RETURNS DESCRIPTION
bool or ndarray of bool

A scalar bool for a single point, or an (n,) boolean mask — row i is True iff point i lies in the box.

RAISES DESCRIPTION
ValueError

If u is neither a (dim,) point nor an (n, dim) batch.

Source code in src/tsdynamics/data/sampling.py
def contains(self, u: Any) -> Any:
    """Whether point(s) ``u`` lie in the box.

    Parameters
    ----------
    u : array_like
        A single point of shape ``(dim,)`` **or** a batch of shape
        ``(n, dim)``.  Any other shape raises (so a batch can never silently
        collapse into one conflated truth value).

    Returns
    -------
    bool or ndarray of bool
        A scalar ``bool`` for a single point, or an ``(n,)`` boolean mask —
        row ``i`` is ``True`` iff point ``i`` lies in the box.

    Raises
    ------
    ValueError
        If ``u`` is neither a ``(dim,)`` point nor an ``(n, dim)`` batch.
    """
    pts, scalar = _coerce_query(u, self.dim)
    mask = np.all((pts >= self.lo) & (pts <= self.hi), axis=1)
    return bool(mask[0]) if scalar else mask

Ball dataclass

Ball(center: ndarray, r: float)

Closed Euclidean ball of radius r about center.

The set of points within Euclidean distance r of center (inclusive). Together with :class:Box and :class:Grid it is one of the region primitives the sampler / basin / attractor layer is built on.

PARAMETER DESCRIPTION
center

Ball centre. Coerced to a float array.

TYPE: (array_like, shape(dim))

r

Radius; must be strictly positive.

TYPE: float

RAISES DESCRIPTION
ValueError

If r <= 0.

Examples:

>>> ball = Ball([0.0, 0.0], r=1.0)
>>> ball.contains([0.5, 0.5])
True
>>> ball.contains([[0.0, 0.0], [2.0, 0.0]])    # batch query → per-row mask
array([ True, False])

dim property

dim: int

State-space dimension.

contains

contains(u: Any) -> Any

Whether point(s) u lie in the ball.

PARAMETER DESCRIPTION
u

A single point of shape (dim,) or a batch of shape (n, dim). Any other shape raises.

TYPE: array_like

RETURNS DESCRIPTION
bool or ndarray of bool

A scalar bool for a single point, or an (n,) boolean mask.

RAISES DESCRIPTION
ValueError

If u is neither a (dim,) point nor an (n, dim) batch.

Source code in src/tsdynamics/data/sampling.py
def contains(self, u: Any) -> Any:
    """Whether point(s) ``u`` lie in the ball.

    Parameters
    ----------
    u : array_like
        A single point of shape ``(dim,)`` **or** a batch of shape
        ``(n, dim)``.  Any other shape raises.

    Returns
    -------
    bool or ndarray of bool
        A scalar ``bool`` for a single point, or an ``(n,)`` boolean mask.

    Raises
    ------
    ValueError
        If ``u`` is neither a ``(dim,)`` point nor an ``(n, dim)`` batch.
    """
    pts, scalar = _coerce_query(u, self.dim)
    mask = np.linalg.norm(pts - self.center, axis=1) <= self.r
    return bool(mask[0]) if scalar else mask

Grid dataclass

Grid(lo: ndarray, hi: ndarray, counts: tuple[int, ...])

Regular grid: counts[i] points spanning [lo_i, hi_i] per axis.

A regular Cartesian lattice over an axis-aligned box: axis i carries counts[i] evenly spaced nodes spanning [lo_i, hi_i] (inclusive of both endpoints). Use :func:grid_points to enumerate the lattice nodes and :func:region for a terse (lo, hi, n)-per-axis constructor.

PARAMETER DESCRIPTION
lo

Per-axis lower and upper bounds of the bounding box.

TYPE: (array_like, shape(dim))

hi

Per-axis lower and upper bounds of the bounding box.

TYPE: (array_like, shape(dim))

counts

Number of nodes per axis; each must be >= 1.

TYPE: tuple of int

RAISES DESCRIPTION
ValueError

If lo, hi, and counts disagree in length, or any count is < 1.

Examples:

>>> g = Grid([-1.0, -1.0], [1.0, 1.0], (3, 3))
>>> g.shape
(3, 3)
>>> g.contains([0.0, 0.0])
True

dim property

dim: int

State-space dimension.

shape property

shape: tuple[int, ...]

Per-axis node counts.

axes

axes() -> list[ndarray]

Per-axis coordinate vectors (dim arrays, counts[i] long).

Source code in src/tsdynamics/data/sampling.py
def axes(self) -> list[np.ndarray]:
    """Per-axis coordinate vectors (``dim`` arrays, ``counts[i]`` long)."""
    return [np.linspace(self.lo[i], self.hi[i], self.counts[i]) for i in range(self.dim)]

contains

contains(u: Any) -> Any

Whether point(s) u lie in the grid's bounding box.

Membership is against the bounding box [lo_i, hi_i]not exact coincidence with a lattice node.

PARAMETER DESCRIPTION
u

A single point of shape (dim,) or a batch of shape (n, dim). Any other shape raises.

TYPE: array_like

RETURNS DESCRIPTION
bool or ndarray of bool

A scalar bool for a single point, or an (n,) boolean mask.

RAISES DESCRIPTION
ValueError

If u is neither a (dim,) point nor an (n, dim) batch.

Source code in src/tsdynamics/data/sampling.py
def contains(self, u: Any) -> Any:
    """Whether point(s) ``u`` lie in the grid's bounding box.

    Membership is against the bounding box ``[lo_i, hi_i]`` — *not* exact
    coincidence with a lattice node.

    Parameters
    ----------
    u : array_like
        A single point of shape ``(dim,)`` **or** a batch of shape
        ``(n, dim)``.  Any other shape raises.

    Returns
    -------
    bool or ndarray of bool
        A scalar ``bool`` for a single point, or an ``(n,)`` boolean mask.

    Raises
    ------
    ValueError
        If ``u`` is neither a ``(dim,)`` point nor an ``(n, dim)`` batch.
    """
    pts, scalar = _coerce_query(u, self.dim)
    mask = np.all((pts >= self.lo) & (pts <= self.hi), axis=1)
    return bool(mask[0]) if scalar else mask

Sampling

sampler

sampler(
    region: Region, *, seed: int | None = None
) -> Callable[[], ndarray]

Return a reproducible 0-argument sampler drawing points from region.

Box → uniform in the box; Ball → uniform in the ball (radial CDF, not the biased "uniform radius" trick); Grid → uniform over its bounding box (use :func:grid_points for the lattice itself).

PARAMETER DESCRIPTION
region

TYPE: Box, Ball, or Grid

seed

Seeds a private numpy.random.Generator so draws are reproducible and independent of global RNG state.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
callable

draw() -> ndarray of shape (region.dim,).

Examples:

>>> draw = sampler(Box([-1, -1], [1, 1]), seed=0)
>>> draw().shape
(2,)
Source code in src/tsdynamics/data/sampling.py
def sampler(region: Region, *, seed: int | None = None) -> Callable[[], np.ndarray]:
    """
    Return a reproducible 0-argument sampler drawing points from ``region``.

    Box → uniform in the box; Ball → uniform in the ball (radial CDF, not the
    biased "uniform radius" trick); Grid → uniform over its bounding box (use
    :func:`grid_points` for the lattice itself).

    Parameters
    ----------
    region : Box, Ball, or Grid
    seed : int, optional
        Seeds a private ``numpy.random.Generator`` so draws are reproducible
        and independent of global RNG state.

    Returns
    -------
    callable
        ``draw() -> ndarray`` of shape ``(region.dim,)``.

    Examples
    --------
    >>> draw = sampler(Box([-1, -1], [1, 1]), seed=0)
    >>> draw().shape
    (2,)
    """
    rng = np.random.default_rng(seed)

    if isinstance(region, Box):
        lo, hi = region.lo, region.hi

        def draw_box() -> np.ndarray:
            return rng.uniform(lo, hi)

        return draw_box

    if isinstance(region, Ball):
        c, r, d = region.center, region.r, region.dim

        def draw_ball() -> np.ndarray:
            v = rng.standard_normal(d)
            v /= np.linalg.norm(v)
            radius = r * rng.uniform() ** (1.0 / d)  # uniform-in-volume
            return np.asarray(c + radius * v)

        return draw_ball

    if isinstance(region, Grid):
        lo, hi = region.lo, region.hi

        def draw_grid() -> np.ndarray:
            return rng.uniform(lo, hi)

        return draw_grid

    raise TypeError(f"unknown region type {type(region).__name__}")

grid_points

grid_points(grid: Grid) -> ndarray

Enumerate every lattice point of grid (row-major / C order).

RETURNS DESCRIPTION
ndarray, shape ``(prod(counts), dim)``

One row per grid node; reshape to grid.shape + (dim,) for a basin map laid out over the grid.

Source code in src/tsdynamics/data/sampling.py
def grid_points(grid: Grid) -> np.ndarray:
    """
    Enumerate every lattice point of ``grid`` (row-major / C order).

    Returns
    -------
    ndarray, shape ``(prod(counts), dim)``
        One row per grid node; reshape to ``grid.shape + (dim,)`` for a basin
        map laid out over the grid.
    """
    axes = grid.axes()
    if grid.dim == 1:
        return axes[0][:, None]
    mesh = np.meshgrid(*axes, indexing="ij")
    return np.stack([m.ravel() for m in mesh], axis=-1)

Set distances

set_distance

set_distance(
    a: Any, b: Any, *, method: _SetMethod = "centroid"
) -> float

Distance between two point sets a and b (each (n, dim)).

Methods (Datseris & Wagemakers-style matching primitives):

  • "centroid" — Euclidean distance between the set centroids. O(n); the cheap default used for attractor matching across a continuation.
  • "hausdorff" — symmetric Hausdorff distance, a true metric: max(sup_a inf_b ‖a-b‖, sup_b inf_a ‖a-b‖). KD-tree accelerated.
  • "minimum" — the smallest pairwise distance (do the sets touch?). KD-tree accelerated.

Accepts :class:~tsdynamics.data.Trajectory (uses .y), arrays, or any array-like.

Source code in src/tsdynamics/data/sampling.py
def set_distance(
    a: Any,
    b: Any,
    *,
    method: _SetMethod = "centroid",
) -> float:
    """
    Distance between two point sets ``a`` and ``b`` (each ``(n, dim)``).

    Methods (Datseris & Wagemakers-style matching primitives):

    - ``"centroid"`` — Euclidean distance between the set centroids. O(n);
      the cheap default used for attractor matching across a continuation.
    - ``"hausdorff"`` — symmetric Hausdorff distance, a true metric:
      ``max(sup_a inf_b ‖a-b‖, sup_b inf_a ‖a-b‖)``. KD-tree accelerated.
    - ``"minimum"`` — the smallest pairwise distance (do the sets touch?).
      KD-tree accelerated.

    Accepts :class:`~tsdynamics.data.Trajectory` (uses ``.y``), arrays, or any
    array-like.
    """
    A = _as_points(a)
    B = _as_points(b)
    if A.shape[1] != B.shape[1]:
        raise ValueError(f"point sets live in different dimensions: {A.shape[1]} vs {B.shape[1]}")

    if method == "centroid":
        return float(np.linalg.norm(A.mean(axis=0) - B.mean(axis=0)))

    from scipy.spatial import cKDTree

    tree_a, tree_b = cKDTree(A), cKDTree(B)
    if method == "minimum":
        d_ab, _ = tree_b.query(A, k=1)
        return float(np.min(d_ab))
    if method == "hausdorff":
        d_ab, _ = tree_b.query(A, k=1)
        d_ba, _ = tree_a.query(B, k=1)
        return float(max(np.max(d_ab), np.max(d_ba)))

    raise ValueError(f"unknown method {method!r}; use centroid, hausdorff, or minimum")