Skip to content

AlssmPolyLegendre

AlssmPolyLegendre(poly_degree: int, a_seg: int = 0, b_seg: int = None, **kwargs)

Bases: ModelBase


              flowchart TD
              lmlib.statespace.model.AlssmPolyLegendre[AlssmPolyLegendre]
              lmlib.statespace.model.ModelBase[ModelBase]

                              lmlib.statespace.model.ModelBase --> lmlib.statespace.model.AlssmPolyLegendre
                


              click lmlib.statespace.model.AlssmPolyLegendre href "" "lmlib.statespace.model.AlssmPolyLegendre"
              click lmlib.statespace.model.ModelBase href "" "lmlib.statespace.model.ModelBase"
            

ALSSM whose output basis is the discrete Legendre polynomials on a finite window.

Unlike AlssmPoly (Pascal/monomial basis) and AlssmPolyJordan (Jordan/binomial basis), the Legendre ALSSM maps time indices to the interval \([-1, +1]\) and uses the classical Legendre polynomials \(P_0, P_1, \ldots, P_D\) as its basis functions. This keeps the Gram matrix \(W\) well-conditioned regardless of the window length \(W_{\rm size}\):

\[ \kappa(W_{\rm Legendre}) \approx 2D + 1 \qquad \text{vs.} \qquad \kappa(W_{\rm Pascal}) = \mathcal{O}\!\left(W_{\rm size}^{2D}\right) \]

For a window of 500 samples and polynomial degree 4, the Gram matrix condition number is \(\approx 9\) instead of \(\approx 10^{22}\), an improvement of more than 20 orders of magnitude.

State-space parametrisation

The window of \(W_{\rm size}\) samples is mapped to \(t_{\rm sc} \in [-1, +1]\) via

\[ t_{\rm sc}(j) = \frac{2j}{W_{\rm size}-1} - 1, \qquad j = 0 \;(\text{newest}) \;\ldots\; W_{\rm size}-1 \;(\text{oldest}). \]

The state vector \(x \in \mathbb{R}^N\) (with \(N = D+1\)) holds the Legendre expansion coefficients \([c_0, \ldots, c_D]\) of the fitted polynomial:

\[ \hat{y}(t_{\rm sc}) = \sum_{n=0}^{D} c_n P_n(t_{\rm sc}). \]

Transition matrix \(A\)

Advancing one step from the newest sample toward the past corresponds to shifting \(t_{\rm sc}\) by \(h = 2/(W_{\rm size}-1)\). The resulting constant upper-triangular shift matrix \(L\) satisfies

\[ \phi(t_{\rm sc} + h) = \phi(t_{\rm sc})\,L, \qquad \phi(t) = [P_0(t),\, P_1(t),\, \ldots,\, P_D(t)], \]

and is computed analytically via a term-by-term Taylor expansion in the Legendre basis using legder:

\[ L_{:,n} = \sum_{m=0}^{n} \frac{h^m}{m!} \bigl[\text{Legendre coefficients of } P_n^{(m)}\bigr]. \]

\(\kappa(L) \approx 1\) for all practical window sizes.

Output vector \(C\)

The newest sample (reference point, \(j = 0 \Rightarrow t_{\rm sc} = -1\)) is evaluated by

\[ C = \phi(-1) = \bigl[P_0(-1),\, P_1(-1),\, \ldots,\, P_D(-1)\bigr] = \bigl[1,\, {-1},\, 1,\, {-1},\, \ldots\bigr]. \]

Compatibility

The state vector is in Legendre coefficient space, not monomial coefficient space, so the numerical values of \(x[k]\) differ from those returned by AlssmPoly. The output \(\hat{y}[k] = Cx[k]\) and the full-window trajectory via eval_output with js are identical in meaning (predicted signal value at each lag).

Notes

N : ALSSM system order, corresponding to the number of state variables

The Legendre polynomials used here are the standard (unnormalised) Legendre polynomials satisfying \(P_n(1) = 1\), identical to those returned by legval. They are not normalised to \(\|P_n\|_{L^2} = 1\); the orthonormality factor is \(\sqrt{(2n+1)/2}\). Because the RLS filter works with \(W = V^\top V\) (where \(V\) contains the Legendre design-matrix rows), the normalisation cancels out in the coefficient recovery and does not need to be applied explicitly.

To convert recovered Legendre coefficients \(c\) back to standard monomial coefficients, premultiply by the change-of-basis matrix \(T^{-1}\) where \(T\) satisfies \(V_{\rm pascal}\,T = V_{\rm Legendre}\).

Example

Setting up a degree-3 Legendre ALSSM for a 500-sample window:

import lmlib as lm alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=0, b_seg=499, label='legendre') print(alssm) AlssmPolyLegendre(A=..., C=..., label=legendre)

Using it inside a cost segment (drop-in replacement for AlssmPoly):
```python
>>> import numpy as np, lmlib as lm
>>> from lmlib.utils.generator import gen_wgn, gen_rect
>>> K = 1000
>>> y = gen_rect(K, 300, 100) + gen_wgn(K, 0.01)
>>> alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=0,b_seg=199)
>>> cost  = lm.CostSegment(alssm, lm.Segment(0, 199, lm.BW, 500))
>>> rls   = lm.RLSAlssm(cost)
>>> rls.filter(y)
>>> xs = rls.minimize_x()
>>> y_hat = alssm.eval_output(xs)

Parameters:

  • poly_degree (int) –

    Polynomial degree \(D \geq 0\). The model order is \(N = D+1\).

  • a_seg (int, default: 0 ) –

    Left boundary of the target segment (default 0). Together with b_seg this defines the window \([a, b]\):

    • The step size \(h = 2 / (b - a)\) maps \([a, b]\) to \([-1, +1]\) in the Legendre domain.
    • The output vector is shifted to \(C_{\rm new} = \phi(-1)\,A^{-a}\) so that the filter naturally accumulates the segment-relative Gram matrix \(W_{\rm rel}\) with \(\kappa(W_{\rm rel}) \approx 2D+1\) — regardless of where \([a, b]\) sits relative to \(j=0\).
    • The output \(C_{\rm new}\,x[k]\) evaluates the polynomial at \(j = 0\) (the current sample \(y[k]\)).
  • b_seg (int, default: None ) –

    Right boundary of the target segment. Must satisfy b_seg > a_seg. The window width is b_seg - a_seg + 1.

  • **kwargs

    Forwarded to ModelBase.

Examples:

Standard backward window of 501 samples aligned at the current sample::

alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=0, b_seg=500)
seg   = lm.Segment(0, 500, lm.BW, g=100)

Backward window shifted 200 samples into the past — same window size, same \(h\), same \(\kappa(W) \approx 2D+1\)::

alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=-200, b_seg=300)
seg   = lm.Segment(-200, 300, lm.BW, g=100)

Forward window entirely in the past::

alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=-501, b_seg=-1)
seg   = lm.Segment(-501, -1, lm.FW, g=100)

Methods:

  • update

    Recompute \(A\) and \(C\) from poly_degree, a_seg, b_seg.

  • eval_output

    Evaluate the ALSSM output for one or more state vectors.

  • dump_tree

    Return the internal ALSSM tree structure as a string.

  • set_state_var_label

    Register a label for one or more state vector indices.

  • get_state_var_labels

    Return all registered state-variable labels together with their index tuples.

  • get_state_var_indices

    Return the state-vector indices for a state variable identified by its label.

  • get_alssm_output_dimension

    Return the ALSSM output dimension \(Q\) (number of output channels).

Attributes:

  • steady_state_basis
  • poly_degree

    int : Polynomial degree \(D\).

  • a_seg

    int : Left boundary of the target segment.

  • b_seg

    int : Right boundary of the target segment.

  • h

    float : Legendre step size \(h = 2\,/\,(b\_seg - a\_seg)\).

  • label

    str : Label of the model

  • C_init

    ndarray, shape=([Q,] N) : Initialized Output matrix \(C \in \mathbb{R}^{Q \times N}\)

  • force_MC

    bool : If True, a 1-D output vector C is broadcast to a 2-D array of shape (1, N) (multi-channel form).

  • A

    ndarray, shape=(N, N) : State matrix \(A \in \mathbb{R}^{N \times N}\)

  • C

    ndarray, shape=([Q,] N) : Output matrix \(C \in \mathbb{R}^{Q \times N}\)

  • N

    int : Model order \(N\)

  • Q

    int : Number of output channels \(Q\).

  • alssms

    list : Sub-ALSSMs that compose this model (empty for leaf nodes such as Alssm).

  • lambdas

    ndarray : Per-ALSSM scalar output scaling factors \(\lambda_m\) applied to each sub-model's output matrix \(C_m\).

  • is_MC

    bool : True if the output matrix C is 2-D (multi-channel form), False if 1-D (scalar output).

Source code in lmlib/statespace/model.py
def __init__(self, poly_degree: int, a_seg: int = 0, b_seg: int = None,
             **kwargs):
    r"""
    Parameters
    ----------
    poly_degree : int
        Polynomial degree $D \geq 0$.  The model order is $N = D+1$.
    a_seg : int, optional
        Left boundary of the target segment (default ``0``).
        Together with ``b_seg`` this defines the window $[a, b]$:

        * The step size $h = 2 / (b - a)$ maps $[a, b]$ to
          $[-1, +1]$ in the Legendre domain.
        * The output vector is shifted to
          $C_{\rm new} = \phi(-1)\,A^{-a}$ so that the filter
          naturally accumulates the segment-relative Gram matrix
          $W_{\rm rel}$ with $\kappa(W_{\rm rel}) \approx 2D+1$
          — regardless of where $[a, b]$ sits relative to $j=0$.
        * The output $C_{\rm new}\,x[k]$ evaluates the polynomial at
          $j = 0$ (the current sample $y[k]$).

    b_seg : int
        Right boundary of the target segment.  Must satisfy ``b_seg > a_seg``.
        The window width is ``b_seg - a_seg + 1``.
    **kwargs
        Forwarded to [`ModelBase`][lmlib.statespace.model.ModelBase].

    Examples
    --------
    Standard backward window of 501 samples aligned at the current sample::

        alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=0, b_seg=500)
        seg   = lm.Segment(0, 500, lm.BW, g=100)

    Backward window shifted 200 samples into the past — same window size,
    same $h$, same $\kappa(W) \approx 2D+1$::

        alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=-200, b_seg=300)
        seg   = lm.Segment(-200, 300, lm.BW, g=100)

    Forward window entirely in the past::

        alssm = lm.AlssmPolyLegendre(poly_degree=3, a_seg=-501, b_seg=-1)
        seg   = lm.Segment(-501, -1, lm.FW, g=100)
    """
    # ── parameter handling / backward compat ──────────────────────────
    if b_seg is None:
        raise TypeError(
            'AlssmPolyLegendre requires b_seg. '
            'Example: AlssmPolyLegendre(poly_degree=3, a_seg=0, b_seg=500).'
        )

    super().__init__(**kwargs)
    self._poly_degree = int(poly_degree)
    self._a_seg = int(a_seg)
    self._b_seg = int(b_seg)
    assert isinstance(poly_degree, int) and poly_degree >= 0, \
        'poly_degree must be a non-negative int'
    assert self._b_seg > self._a_seg, \
        f'b_seg ({b_seg}) must be strictly greater than a_seg ({a_seg})'
    self.update()

Methods

update

update()

Recompute \(A\) and \(C\) from poly_degree, a_seg, b_seg.

Step 1 — build shift matrix \(A = L\) for step size \(h = 2/(b-a)\).

Step 2 — set \(C = \phi(-1) = [1,\,-1,\,1,\,-1,\,\ldots]\).

Step 3 — apply segment-relative shift (only when a_seg != 0):

\[ C \;\leftarrow\; C\,A^{-a_{\rm seg}} \]

For a_seg < 0 (common backward window case) this is a positive power of \(A\) — always stable.

Source code in lmlib/statespace/model.py
def update(self):
    r"""
    Recompute $A$ and $C$ from ``poly_degree``, ``a_seg``, ``b_seg``.

    Step 1 — build shift matrix $A = L$ for step size $h = 2/(b-a)$.

    Step 2 — set $C = \phi(-1) = [1,\,-1,\,1,\,-1,\,\ldots]$.

    Step 3 — apply segment-relative shift (only when ``a_seg != 0``):

    $$
    C \;\leftarrow\; C\,A^{-a_{\rm seg}}
    $$

    For ``a_seg < 0`` (common backward window case) this is a *positive*
    power of $A$ — always stable.
    """
    N = self._poly_degree + 1
    self.A = self._legendre_shift_matrix(N, self.h)
    self.C = np.array([(-1.0) ** n for n in range(N)])
    if self._a_seg != 0:
        self.C = self.C.astype(float) @ np.linalg.matrix_power(
            self.A.astype(float), -self._a_seg)
    self._init_state_var_labels()
    self._broadcast_C_to_multichannel()

eval_output

eval_output(xs, js=None)

Evaluate the ALSSM output for one or more state vectors.

Without evaluation index (js=None):

\[ s(x) = C x \]

With evaluation indices (js provided):

\[ s_j(x) = C A^j x \]

Parameters:

  • xs (array_like of shape (..., N)) –

    State vector(s). The last dimension must equal the model order N.

  • js (array_like of shape (J,) or None, default: None ) –

    Sequence of integer evaluation indices. If None, evaluates at \(j = 0\) only (i.e. returns \(Cx\)).

Returns:

  • s ( ndarray ) –

    If js is None: shape (..., [Q]). If js is provided: shape (J, ..., [Q]). The [Q] dimension is present only when is_MC is True.

Source code in lmlib/statespace/model.py
def eval_output(self, xs, js=None):
    r"""
    Evaluate the ALSSM output for one or more state vectors.

    Without evaluation index (``js=None``):

    $$
    s(x) = C x
    $$

    With evaluation indices (``js`` provided):

    $$
    s_j(x) = C A^j x
    $$

    Parameters
    ----------
    xs : array_like of shape (..., N)
        State vector(s). The last dimension must equal the model order N.
    js : array_like of shape (J,) or None, optional
        Sequence of integer evaluation indices. If None, evaluates at
        $j = 0$ only (i.e. returns $Cx$).

    Returns
    -------
    s : ndarray
        If ``js`` is None: shape ``(..., [Q])``.
        If ``js`` is provided: shape ``(J, ..., [Q])``.
        The ``[Q]`` dimension is present only when [`is_MC`][lmlib.statespace.model.ModelBase.is_MC] is True.
    """
    xs = np.asarray(xs)

    # Ensure last axis is state dimension
    if xs.shape[-1] != self.N:
        raise ValueError(f"Last dimension of xs must be {self.N}")

    # No propagation: s = C x
    if js is None:
        _subscript = 'ln,...n->...l' if self.is_MC else 'n,...n->...'
        return np.einsum(_subscript, self.C, xs)

    # Propagation: s_j = C A^j x
    A_powers = [matrix_power(self.A, int(j)) for j in js]
    _subscript = 'ln,...n->...l' if self.is_MC else 'n,...n->...'
    return np.asarray([np.einsum(_subscript, self.C @ Aj, xs) for Aj in A_powers])

dump_tree

dump_tree() -> str

Return the internal ALSSM tree structure as a string.

Returns:

  • out ( str ) –

    Multi-line string representing the nested ALSSM structure.

Example
>>> import lmlib as lm
>>> import numpy as np
>>> alssm_poly = lm.AlssmPoly(4, label="high order polynomial")
>>> A = [[1, 1], [0, 1]]
>>> C = [[1, 0]]
>>> alssm_line = lm.Alssm(A, C, label="line")
>>> stacked_alssm = lm.AlssmStacked((alssm_poly, alssm_line), label='stacked model')
>>> print(stacked_alssm.dump_tree())
-AlssmStacked, A: (7, 7), C: (2, 7), label: stacked model
  -AlssmPoly, A: (5, 5), C: (5,), label: high order polynomial
  -Alssm, A: (2, 2), C: (1, 2), label: line
Source code in lmlib/statespace/model.py
def dump_tree(self) -> str:
    """
    Return the internal ALSSM tree structure as a string.

    Returns
    -------
    out : str
        Multi-line string representing the nested ALSSM structure.

    Example
    --------
    ```python
    >>> import lmlib as lm
    >>> import numpy as np
    >>> alssm_poly = lm.AlssmPoly(4, label="high order polynomial")
    >>> A = [[1, 1], [0, 1]]
    >>> C = [[1, 0]]
    >>> alssm_line = lm.Alssm(A, C, label="line")
    >>> stacked_alssm = lm.AlssmStacked((alssm_poly, alssm_line), label='stacked model')
    >>> print(stacked_alssm.dump_tree())
    └-AlssmStacked, A: (7, 7), C: (2, 7), label: stacked model
      └-AlssmPoly, A: (5, 5), C: (5,), label: high order polynomial
      └-Alssm, A: (2, 2), C: (1, 2), label: line
    ```
    """
    return self._rec_tree(level=0)

set_state_var_label

set_state_var_label(label: str, indices: tuple[int])

Register a label for one or more state vector indices.

Labels allow state components to be referenced by name rather than by numeric index; see get_state_var_indices.

Parameters:

  • label (str) –

    Label name to register.

  • indices (tuple of int) –

    State vector indices associated with this label.

Example
>>> import lmlib as lm
>>> alssm = lm.AlssmPoly(poly_degree=1, label='slope_with_offset')
>>> alssm.set_state_var_label('slope', (1,))
>>> alssm.get_state_var_indices('slope_with_offset.slope')
(1,)
Source code in lmlib/statespace/model.py
def set_state_var_label(self, label:str, indices:tuple[int]):
    r"""
    Register a label for one or more state vector indices.

    Labels allow state components to be referenced by name rather than by
    numeric index; see [`get_state_var_indices`][lmlib.statespace.model.ModelBase.get_state_var_indices].

    Parameters
    ----------
    label : str
        Label name to register.
    indices : tuple of int
        State vector indices associated with this label.

    Example
    --------
    ```python
    >>> import lmlib as lm
    >>> alssm = lm.AlssmPoly(poly_degree=1, label='slope_with_offset')
    >>> alssm.set_state_var_label('slope', (1,))
    >>> alssm.get_state_var_indices('slope_with_offset.slope')
    (1,)
    ```
    """
    self._state_var_labels[label] = indices

get_state_var_labels

get_state_var_labels()

Return all registered state-variable labels together with their index tuples.

Labels are accumulated recursively from all nested sub-ALSSMs, with each label prefixed by the current model's label. The state indices are adjusted to reflect the position within the combined (block-diagonal) state vector.

Returns:

  • out ( list of (str, tuple of int) ) –

    List of (label_string, indices) pairs. label_string is a dot-separated path (e.g. 'stacked.poly.x0') and indices is the corresponding tuple of integer state-vector positions.

Source code in lmlib/statespace/model.py
def get_state_var_labels(self):
    r"""
    Return all registered state-variable labels together with their index tuples.

    Labels are accumulated recursively from all nested sub-ALSSMs, with
    each label prefixed by the current model's [`label`][lmlib.statespace.model.ModelBase.label].  The state
    indices are adjusted to reflect the position within the combined
    (block-diagonal) state vector.

    Returns
    -------
    out : list of (str, tuple of int)
        List of ``(label_string, indices)`` pairs.  ``label_string`` is a
        dot-separated path (e.g. ``'stacked.poly.x0'``) and ``indices`` is
        the corresponding tuple of integer state-vector positions.
    """
    state_list = []
    for var_label, indices in self._state_var_labels.items():
        state_list.append((self.label + '.' + var_label, indices))

    N = 0
    for alssm in self.alssms:
        for var_label, indices in alssm.get_state_var_labels():
            state_list.extend([(self.label + '.' + var_label, tuple(i + N for i in indices))])
        N += alssm.N
    return state_list

get_state_var_indices

get_state_var_indices(label)

Return the state-vector indices for a state variable identified by its label.

Parameters:

Returns:

  • out ( tuple of int or list of int ) –

    State-vector indices associated with label. Returns an empty list if label is not found.

Source code in lmlib/statespace/model.py
def get_state_var_indices(self, label):
    r"""
    Return the state-vector indices for a state variable identified by its label.

    Parameters
    ----------
    label : str
        Fully qualified state label (dot-separated path), as returned by
        [`get_state_var_labels`][lmlib.statespace.model.ModelBase.get_state_var_labels].

    Returns
    -------
    out : tuple of int or list of int
        State-vector indices associated with ``label``.
        Returns an empty list if ``label`` is not found.
    """

    for l, indices in self.get_state_var_labels():
        if label == l:
            return indices
    return []

get_alssm_output_dimension

get_alssm_output_dimension()

Return the ALSSM output dimension \(Q\) (number of output channels).

Source code in lmlib/statespace/model.py
def get_alssm_output_dimension(self):
    r"""Return the ALSSM output dimension $Q$ (number of output channels)."""
    return self.Q