Skip to main content

Δt-native Legendre Memory Unit (Approach-C) and recurrent model core for the juniper-recurrence application

Project description

juniper-recurrence-model

The model-specific core for the juniper-recurrence application — the selected model P3-C (LMU + Approach-C).

This package ships the Δt-native Legendre Memory Unit (Approach-C) — a closed-form, variable-step LMU discretisation that is the only first-principles-clean ("C1") option natively handling irregularly-sampled time series — and FixedOrderLMURegressor, the recurrent model implementing the shared juniper-model-core TrainableModel interface (now that that package has landed). The regressor keeps the LMU memory fixed and trains only a linear readout in closed form (least squares — no BPTT, fully deterministic); it passes model-core's conformance kit unchanged, making it the WS-4 refactor template (a non-cascor model on the shared model seam).

Design of record (in juniper-ml): notes/JUNIPER_RECURRENCE_MODEL_DETAILED_DESIGN_2026-06-14.md.

Why Approach-C

An LMU's linear memory obeys theta * m'(t) = A·m(t) + B·u(t) with fixed, closed-form matrices. Because the system is linear, its exact discretisation is a matrix exponential — no ODE solver, no autodiff-through-solver. For irregular sampling, the discrete update is simply evaluated at the real per-step gap dt: the dataset's dt channel is the discretisation step. A/B are never trained; only the read-in/readout are. That is the entire C1-clean, irregular-Δt-native story.

Install

pip install juniper-recurrence-model          # once published
pip install -e ".[test]"                       # local development

numpy-only at the core (the memory is a fixed linear recurrence requiring no autodiff).

Quick start

import numpy as np
from juniper_recurrence_model import VariableStepLMUMemory

mem = VariableStepLMUMemory(d=16, theta=1.0)   # order 16, window 1.0 (same unit as dt)

# Irregularly-sampled input: u driven on a non-uniform time grid
t = np.cumsum(np.r_[0.0, np.random.default_rng(0).uniform(0.02, 0.08, 239)])
dt = np.empty_like(t); dt[0] = 0.0; dt[1:] = np.diff(t)
u = np.sin(2.0 * t)

m = mem.rollout(u, dt)                          # (240, 16) memory trajectory
w = mem.decode_weights(rho=1.0)                 # read the input one full window ago
reconstruction = m @ w

Trainable model (FixedOrderLMURegressor)

The package also exposes FixedOrderLMURegressor, a juniper-model-core TrainableModel. The LMU memory is fixed; only a linear readout is fit, in closed form (least squares — no BPTT, fully deterministic). It is Δt-native: pass per-step gaps dt ((n, T)) and an optional readout_mask to fit / predict; both default to uniform gaps and the final step, so the bare ABC predict(X) works too. It reports canonical regression metrics (mse, rmse, mae, r2).

import numpy as np
from juniper_recurrence_model import FixedOrderLMURegressor, LMURegressorSerializer

n, T, F = 48, 6, 3
X = np.random.default_rng(0).normal(size=(n, T, F))
y = X.reshape(n, -1) @ np.random.default_rng(1).normal(size=(T * F, 1))
dt = np.zeros((n, T)); dt[:, 1:] = np.random.default_rng(2).integers(1, 4, size=(n, T - 1))

model = FixedOrderLMURegressor(d=6)             # theta resolved data-driven from dt at fit time
result = model.fit(X, y, dt=dt)                 # closed-form readout solve
preds = model.predict(X, dt=dt)                 # (n, 1)
print(result.final_metrics["r2"], model.describe_topology()["model_type"])

LMURegressorSerializer().save(model, "/tmp/lmu")   # writes /tmp/lmu.npz (lossless round-trip)

FixedOrderLMURegressor passes model-core's conformance kit unchanged (tests/test_lmu_conformance.py), proving the WS-4 refactor template.

Verified behaviour

Check Result
A (d=16) max eigenvalue real part −6.49 (< 0 → stable)
Reconstruction RMSE e_reg (regular grid) ≈ 0.035 (< 0.05)
Grid-invariance e_irr (irregular grid) ≈ 0.039–0.043 (≈1.15× e_reg; < 3·e_reg + 0.02)

Pinned by tests/test_lmu_grid_invariance.py. Numerics match the reference util/ad-hoc/verify_delta_t_reference_code.py in juniper-ml.

Numerical guardrails

  • Keep d ≲ 64 — the eigenvector matrix of A becomes ill-conditioned for large d (Padé scaling-and-squaring is the documented fallback for larger orders).
  • Stability is automatic for dt > 0 (Re(λ) < 0 ⇒ |e^z| < 1).
  • dt may be quantised (e.g. integer calendar-day gaps) and Abar/Bbar cached per bucket.

Versioning

PEP 440 + Keep a Changelog. Consumers should pin juniper-recurrence-model>=A.B,<A+1. See CHANGELOG.md.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

juniper_recurrence_model-0.1.0.tar.gz (23.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

juniper_recurrence_model-0.1.0-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file juniper_recurrence_model-0.1.0.tar.gz.

File metadata

  • Download URL: juniper_recurrence_model-0.1.0.tar.gz
  • Upload date:
  • Size: 23.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for juniper_recurrence_model-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bcb05b3f634eea17dcbc651e42ef4aa3a3bf4a4626426e18f5109ae09485e050
MD5 b95e390de87f258a6e1241d969db2e5a
BLAKE2b-256 6f1c247ee717aa9da1385c03f5294e7f2961383295ec49ef825bb1c0fedbfcbd

See more details on using hashes here.

Provenance

The following attestation bundles were made for juniper_recurrence_model-0.1.0.tar.gz:

Publisher: publish-recurrence-model.yml on pcalnon/juniper-recurrence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file juniper_recurrence_model-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for juniper_recurrence_model-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e49574108a0af0166d8bb6c33cfffca98ae4f64d931c2fa4b0a071da17659f7c
MD5 bc93c3487261669a95db8f05e15363ca
BLAKE2b-256 b6fe77a0400fb2ffb1fbc2067c8db1c4ea76c21eaf6b1999ec8ccedf845c2a7f

See more details on using hashes here.

Provenance

The following attestation bundles were made for juniper_recurrence_model-0.1.0-py3-none-any.whl:

Publisher: publish-recurrence-model.yml on pcalnon/juniper-recurrence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page