Linear Gaussian predictive model
sc_neurocore.world_model.predictive_model exposes filtering, smoothing,
learning, and planning-compatible forecasts for a controlled linear Gaussian
state-space model (LGSSM).
Model
For latent state (x_t \in \mathbb{R}^d), observation
(y_t \in \mathbb{R}^p), and control (u_t \in \mathbb{R}^m):
[
x_{t+1} = A x_t + B u_t + w_t, \qquad w_t \sim \mathcal{N}(0, Q),
]
[
y_t = C x_t + D u_t + v_t, \qquad v_t \sim \mathcal{N}(0, R),
]
with prior (x_0 \sim \mathcal{N}(\mu_0, \Sigma_0)).
LinearGaussianSSM copies every parameter into a finite, C-contiguous
float64 array. It validates all dimensions, requires Q to be symmetric
positive semidefinite, and requires R and Sigma_0 to be symmetric positive
definite. A positive diagonal alone is not accepted as a covariance proof.
Public API and compatibility
Pythonfrom sc_neurocore.world_model.predictive_model import (
EMLearner,
FilterResult,
KalmanFilter,
LinearGaussianSSM,
PredictiveWorldModel,
RTSSmoother,
SmoothResult,
)
The import path, constructor parameter names, public class identities, and
pickle references remain sc_neurocore.world_model.predictive_model. The
implementation is partitioned by responsibility behind that facade:
| Responsibility |
Owner |
| parameter and result contracts |
_lgssm_types.py |
| native discovery, selection, and FFI marshalling |
_lgssm_backends.py |
| Python and native forward-filter dispatch |
_lgssm_filter.py |
| RTS backward recursion |
_lgssm_smoothing.py |
| controlled EM learning |
_lgssm_em.py |
| planning-compatible state forecasts |
_predictive_world_model.py |
The private import graph is one-way and acyclic. The facade contains no
algorithm implementation.
Forward filtering
KalmanFilter.filter(observations, controls=None, backend="auto") returns:
- filtered state means and covariances;
- one-step predicted means and covariances before each observation; and
- the sequence log-likelihood.
Observations must have shape (T, p) with T > 0. Controls are required for a
model with m > 0 and must have shape (T, m). Every input and returned moment
must be finite. Result covariance stacks are checked in one vectorised
symmetry/eigenvalue pass before being exposed to the caller.
The Python update factors each innovation covariance once with Cholesky. It
uses triangular solves for the innovation quadratic form and Kalman gain; it
does not form a matrix inverse. The covariance update uses Joseph form:
[
P_{t|t} = (I-K_t C)P_{t|t-1}(I-K_t C)^T + K_t R K_t^T.
]
A non-positive-definite innovation covariance fails closed with
numpy.linalg.LinAlgError.
Native forward-filter chain
The maintained forward filter has five execution paths:
| Backend |
Boundary |
Numerical source |
| Python |
NumPy |
_lgssm_filter.py |
| Mojo |
C ABI via ctypes |
accel/mojo/world_model/lgssm.mojo |
| Go |
C shared library via ctypes |
accel/go/lgssm/lgssm.go |
| Rust |
PyO3 |
engine/src/lgssm.rs |
| Julia |
juliacall |
accel/julia/world_model/predictive_model.jl |
An explicit unavailable backend raises RuntimeError; it never silently
changes language. backend="auto" follows the stable availability- and
initialisation-aware order:
Text OnlyMojo -> Go -> Rust -> Julia -> Python
The source-bound controlled workload described below rejects any unexplained
ordering with a material warm-timing inversion greater than 10%. Rust is the
one declared exception: it precedes Julia because it is loaded during package
import, while probing Julia may initialise a separate runtime. Adjacent timings
inside the loaded-host noise band retain their stable order. The artifact
records both post-import probe cost and the exact warm median ranking. Python is
always the final maintained fallback. All native results pass through the same
FilterResult validation boundary as Python results.
The Mojo kernel solves each row of the (d \times p) gain workspace
independently; it never indexes that workspace as though it were
(p \times p). A dedicated (d = 1, p = 3) test exercises the
observation-wider-than-state case through all five backends and verifies
complete moment and likelihood parity.
RTS smoothing
RTSSmoother.smooth(filter_result) performs the Rauch-Tung-Striebel backward
recursion with positive-definite solves. It returns full-sequence posterior
moments and lag-one covariance blocks oriented as:
[
\operatorname{Cov}[x_t, x_{t+1} \mid y_{0:T-1}].
]
That orientation is part of the public result contract. The EM transition
update explicitly transposes each lag block when it needs
(E[x_{t+1}x_t^T]). A multivariate exact batch-conditioning test verifies the
means, covariance blocks, and lag orientation.
RTS smoothing is currently a Python/NumPy responsibility. The native chain
accelerates only the forward filter.
Controlled expectation-maximisation
EMLearner.fit updates A, C, Q, R, mu_0, and Sigma_0. B and D
are treated as known parameters and are preserved exactly.
Controls still participate in the M-step. The transition statistics subtract
B @ u_t, and the observation statistics subtract D @ u_t, before solving
for A and C. Omitting those terms biases a controlled fit even when B and
D themselves are fixed.
All normal-equation right solves use Cholesky factors rather than explicit
inverses. Learned covariance matrices are symmetrised and projected only as
needed to restore the documented positive-semidefinite or positive-definite
contract. Every M-step candidate, including the candidate from the final
allowed iteration, is filtered and checked before it becomes the returned
model. A material likelihood decrease raises RuntimeError; convergence uses
the configured absolute tolerance. log_likelihood_history starts with the
initial model and records each evaluated candidate.
The default EM forward-filter backend is Python, making the complete learning
workload deterministic and explicit. A caller may select another maintained
forward backend for the E-step, but the smoother and M-step remain Python.
Latent-state bases are not uniquely identifiable: an invertible change of
basis can alter A and C while preserving the observation distribution.
Accordingly, recovery tests judge held-out likelihood and posterior moments,
not raw parameter equality alone.
Planning-compatible forecasts
PredictiveWorldModel preserves the historical planning surface:
Pythonimport numpy as np
from sc_neurocore.world_model import PredictiveWorldModel
world_model = PredictiveWorldModel(state_dim=4, action_dim=2, seed=42)
mean, covariance = world_model.predict_next_state_with_cov(
current_state=np.zeros(4),
current_cov=np.eye(4),
action=np.array([0.25, -0.10]),
)
predict_next_state returns (A x_t + B u_t).
predict_next_state_with_cov additionally returns
(A P_t A^T + Q). forecast and forecast_with_cov return independent arrays
for every step, so mutating one returned state cannot alter another.
Benchmark evidence
benchmarks/bench_predictive_model.py runs one controlled workload through all
five forward backends and fails if array or likelihood parity exceeds the
declared tolerances. It warms every backend before timing, then rotates the
starting backend across interleaved sampling rounds so changing host load does
not systematically favour a later block. The same run separately measures the
Python-only RTS and EM workloads; unsupported native RTS/EM rows are not
represented as skips.
The committed artifact is
benchmarks/results/bench_predictive_model.json. It records:
- every timing sample, median, minimum, maximum, post-import probe cost, exact
warm measured rank, stable dispatch policy, and interleaving policy;
- parity deltas against the Python result;
- source SHA-256 hashes for the facade, responsibility modules, native
implementations, bridge, engine registration, and benchmark harness;
- binary hashes for the selected Rust, Go, and Mojo runtimes;
- language versions, CPU model, affinity, frequency governor, and load averages;
- the exact invocation and explicit heavy-job/isolation disclosure.
Reproduce the local evidence with:
Bashtaskset -c 2 env PYTHONPATH=src .venv/bin/python \
benchmarks/bench_predictive_model.py \
--backends python rust julia go mojo \
--steps 200 \
--repeats 25 \
--em-iterations 10 \
--other-heavy-jobs-running yes \
--other-heavy-jobs-note "shared workstation with concurrent repository work" \
--isolation-note "single-CPU affinity; no exclusive-core reservation" \
--json benchmarks/results/bench_predictive_model.json
The measurements are loaded-host local-regression evidence. CPU affinity does
not imply an exclusive core, so the artifact does not support a promotion-grade
cross-host performance claim.
Latest committed local run
The 2026-07-14 source-bound run used 200 time steps, 25 interleaved samples per
backend, four latent states, three observations, and two controls. It ran on
logical CPU 2 of an Intel i5-11600K under the powersave governor while other
heavy repository work was active. The core was affinity-pinned but not
reserved. These numbers are regression evidence for this host only.
| Backend |
Probe (ms) |
Median (ms) |
Min (ms) |
Max (ms) |
Python / median |
Max array delta |
Likelihood delta |
| Python |
0.005959 |
13.847860 |
12.020334 |
15.800676 |
1.000000 |
0 |
0 |
| Rust |
0.003617 |
2.806096 |
2.519172 |
5.343638 |
4.934920 |
6.22e-15 |
1.71e-13 |
| Julia |
6259.491127 |
2.140998 |
1.976630 |
136.903510 |
6.467946 |
5.33e-15 |
1.14e-13 |
| Go |
1.802784 |
1.827930 |
1.572094 |
3.094846 |
7.575706 |
5.33e-15 |
2.27e-13 |
| Mojo |
67.868924 |
1.553183 |
1.382646 |
2.595167 |
8.915794 |
4.00e-15 |
8.52e-08 |
The exact warm ranking was Mojo, Go, Julia, Rust, Python. The stable dispatch
remains Mojo, Go, Rust, Julia, Python: the artifact records Rust-before-Julia
as the sole declared warm-order exception because Rust is already imported and
the Julia probe incurred 6.26 seconds of runtime initialisation. All parity
deltas remain inside the committed 1e-9 array and 1e-7 likelihood limits.
Python-only RTS smoothing measured 6.222437 ms median
(5.728943–9.343452 ms). Ten EM iterations measured 234.127058 ms median
(217.413576–265.916984 ms). The artifact records every raw sample, so timing
dispersion remains visible rather than being discarded.
Verification surfaces
test_linear_gaussian_ssm_parameters.py: parameter and covariance contracts.
test_linear_gaussian_ssm_random.py: random-model dimension and stability contracts.
test_linear_gaussian_filter_result.py: filtering-result contracts.
test_linear_gaussian_smooth_result.py: smoothing-result contracts.
test_kalman_filter.py: analytic updates, controls, covariance stability,
validation, and installed-backend parity.
test_rts_smoother.py: exact multivariate batch conditioning and lag
orientation.
test_em_learner.py: controlled sufficient statistics, monotonicity,
held-out likelihood, convergence, and fail-closed behavior.
test_predictive_world_model.py: planning-facing mean/covariance forecasts.
test_predictive_model_backends.py: loader and FFI boundaries.
test_predictive_model_architecture.py: facade identity, pickle compatibility,
responsibility ownership, import DAG, structured solves, and module bounds.
test_predictive_model_benchmark.py: artifact provenance, all-backend parity,
loaded-host disclosure, and the reduced real CLI.
Focused tests cover every executable line in the seven Python responsibility
modules. The only uncovered branch arcs are the exits of static Protocol
declarations; no coverage exclusions or test skips are used.
API reference
sc_neurocore.world_model.predictive_model
Linear Gaussian filtering, smoothing, learning, and state forecasting.
The model follows Kalman (1960), Rauch, Tung, and Striebel (1965), and the
controlled expectation-maximisation formulation of Shumway and Stoffer
(1982). Native acceleration applies to the forward Kalman filter; RTS
smoothing and the EM M-step remain NumPy implementations.
EMLearner
Estimate selected LGSSM parameters by expectation-maximisation.
Parameters
max_iter : int, default=50
Positive maximum number of E/M iterations.
tol : float, default=1e-4
Non-negative absolute log-likelihood convergence threshold.
Notes
The M-step updates A, C, Q, R, mu_0, and
Sigma_0. B and D are treated as known, but their control
contributions are subtracted from the transition and observation
sufficient statistics as required by Shumway and Stoffer (1982).
Raises
ValueError
If max_iter or tol is outside its documented domain.
Source code in src/sc_neurocore/world_model/_lgssm_em.py
| Python |
|---|
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270 | class EMLearner:
"""Estimate selected LGSSM parameters by expectation-maximisation.
Parameters
----------
max_iter : int, default=50
Positive maximum number of E/M iterations.
tol : float, default=1e-4
Non-negative absolute log-likelihood convergence threshold.
Notes
-----
The M-step updates ``A``, ``C``, ``Q``, ``R``, ``mu_0``, and
``Sigma_0``. ``B`` and ``D`` are treated as known, but their control
contributions are subtracted from the transition and observation
sufficient statistics as required by Shumway and Stoffer (1982).
Raises
------
ValueError
If ``max_iter`` or ``tol`` is outside its documented domain.
"""
def __init__(self, max_iter: int = 50, tol: float = 1e-4) -> None:
if isinstance(max_iter, bool) or not isinstance(max_iter, Integral):
raise ValueError("max_iter must be a positive integer")
if int(max_iter) <= 0:
raise ValueError("max_iter must be a positive integer")
if not isfinite(tol) or tol < 0.0:
raise ValueError("tol must be finite and non-negative")
self.max_iter: int = int(max_iter)
self.tol: float = float(tol)
self.log_likelihood_history: list[float] = []
def fit(
self,
observations: npt.ArrayLike,
initial_model: LinearGaussianSSM,
controls: npt.ArrayLike | None = None,
backend: str = "python",
) -> LinearGaussianSSM:
"""Estimate model parameters from one observation sequence.
Parameters
----------
observations : array-like, shape (T, p)
Finite observation sequence with ``T >= 2``.
initial_model : LinearGaussianSSM
Starting parameters. The returned model preserves its ``B`` and
``D`` values exactly.
controls : array-like, shape (T, m), optional
Controls paired with the observations.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}, default="python"
Forward-filter backend used in each E-step. The deterministic
default keeps the entire learning benchmark on the Python path.
Returns
-------
LinearGaussianSSM
Last accepted M-step model.
Raises
------
ValueError
If data shapes or values are invalid or fewer than two samples
are supplied.
RuntimeError
If a likelihood decrease exceeds accumulated float64 round-off.
"""
observation_array = _normalise_observations(
observations,
obs_dim=initial_model.obs_dim,
)
if observation_array.shape[0] < 2:
raise ValueError("EM requires at least two observation time steps")
control_array = _normalise_controls(
controls,
time_steps=observation_array.shape[0],
control_dim=initial_model.control_dim,
)
model = initial_model
filter_result = KalmanFilter(model).filter(
observation_array,
controls=control_array,
backend=backend,
)
previous_likelihood = filter_result.log_likelihood
self.log_likelihood_history = [previous_likelihood]
for _ in range(self.max_iter):
smoothed = RTSSmoother(model).smooth(filter_result)
candidate = _maximisation_step(
model,
observation_array,
control_array,
smoothed,
)
candidate_result = KalmanFilter(candidate).filter(
observation_array,
controls=control_array,
backend=backend,
)
candidate_likelihood = candidate_result.log_likelihood
self.log_likelihood_history.append(candidate_likelihood)
decrease = previous_likelihood - candidate_likelihood
if decrease > _likelihood_tolerance(previous_likelihood):
raise RuntimeError(
"EM log-likelihood decreased beyond float64 round-off: "
f"{previous_likelihood} -> {candidate_likelihood}"
)
model = candidate
if abs(candidate_likelihood - previous_likelihood) <= self.tol:
break
filter_result = candidate_result
previous_likelihood = candidate_likelihood
return model
|
fit(observations, initial_model, controls=None, backend='python')
Estimate model parameters from one observation sequence.
Parameters
observations : array-like, shape (T, p)
Finite observation sequence with T >= 2.
initial_model : LinearGaussianSSM
Starting parameters. The returned model preserves its B and
D values exactly.
controls : array-like, shape (T, m), optional
Controls paired with the observations.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}, default="python"
Forward-filter backend used in each E-step. The deterministic
default keeps the entire learning benchmark on the Python path.
Returns
LinearGaussianSSM
Last accepted M-step model.
Raises
ValueError
If data shapes or values are invalid or fewer than two samples
are supplied.
RuntimeError
If a likelihood decrease exceeds accumulated float64 round-off.
Source code in src/sc_neurocore/world_model/_lgssm_em.py
| Python |
|---|
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270 | def fit(
self,
observations: npt.ArrayLike,
initial_model: LinearGaussianSSM,
controls: npt.ArrayLike | None = None,
backend: str = "python",
) -> LinearGaussianSSM:
"""Estimate model parameters from one observation sequence.
Parameters
----------
observations : array-like, shape (T, p)
Finite observation sequence with ``T >= 2``.
initial_model : LinearGaussianSSM
Starting parameters. The returned model preserves its ``B`` and
``D`` values exactly.
controls : array-like, shape (T, m), optional
Controls paired with the observations.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}, default="python"
Forward-filter backend used in each E-step. The deterministic
default keeps the entire learning benchmark on the Python path.
Returns
-------
LinearGaussianSSM
Last accepted M-step model.
Raises
------
ValueError
If data shapes or values are invalid or fewer than two samples
are supplied.
RuntimeError
If a likelihood decrease exceeds accumulated float64 round-off.
"""
observation_array = _normalise_observations(
observations,
obs_dim=initial_model.obs_dim,
)
if observation_array.shape[0] < 2:
raise ValueError("EM requires at least two observation time steps")
control_array = _normalise_controls(
controls,
time_steps=observation_array.shape[0],
control_dim=initial_model.control_dim,
)
model = initial_model
filter_result = KalmanFilter(model).filter(
observation_array,
controls=control_array,
backend=backend,
)
previous_likelihood = filter_result.log_likelihood
self.log_likelihood_history = [previous_likelihood]
for _ in range(self.max_iter):
smoothed = RTSSmoother(model).smooth(filter_result)
candidate = _maximisation_step(
model,
observation_array,
control_array,
smoothed,
)
candidate_result = KalmanFilter(candidate).filter(
observation_array,
controls=control_array,
backend=backend,
)
candidate_likelihood = candidate_result.log_likelihood
self.log_likelihood_history.append(candidate_likelihood)
decrease = previous_likelihood - candidate_likelihood
if decrease > _likelihood_tolerance(previous_likelihood):
raise RuntimeError(
"EM log-likelihood decreased beyond float64 round-off: "
f"{previous_likelihood} -> {candidate_likelihood}"
)
model = candidate
if abs(candidate_likelihood - previous_likelihood) <= self.tol:
break
filter_result = candidate_result
previous_likelihood = candidate_likelihood
return model
|
KalmanFilter
Forward Kalman filter for a linear Gaussian state-space model.
Parameters
model : LinearGaussianSSM
Validated model parameters shared by the Python and native paths.
Source code in src/sc_neurocore/world_model/_lgssm_filter.py
| Python |
|---|
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155 | class KalmanFilter:
"""Forward Kalman filter for a linear Gaussian state-space model.
Parameters
----------
model : LinearGaussianSSM
Validated model parameters shared by the Python and native paths.
"""
def __init__(self, model: LinearGaussianSSM) -> None:
self.model = model
def filter(
self,
observations: npt.ArrayLike,
controls: npt.ArrayLike | None = None,
backend: str = "auto",
) -> FilterResult:
"""Filter an observation sequence.
Parameters
----------
observations : array-like, shape (T, p)
Finite observations ordered by time.
controls : array-like, shape (T, m), optional
Finite controls. They are required when ``m > 0`` and may be
omitted only for a model with no control input.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}
Execution backend. ``auto`` follows the availability- and
initialisation-aware order Mojo, Go, Rust, Julia, then Python.
Returns
-------
FilterResult
Filtered and predicted moments plus the sequence log-likelihood.
Raises
------
ValueError
If sequence shapes, values, or the backend name are invalid.
RuntimeError
If an explicitly requested native backend is unavailable.
numpy.linalg.LinAlgError
If an innovation covariance is not positive definite.
"""
observation_array = _normalise_observations(
observations,
obs_dim=self.model.obs_dim,
)
control_array = _normalise_controls(
controls,
time_steps=observation_array.shape[0],
control_dim=self.model.control_dim,
)
selected_backend = resolve_backend(backend)
if selected_backend == "python":
return self._filter_python(observation_array, control_array)
return filter_native(selected_backend, self.model, observation_array, control_array)
def _filter_python(
self,
observations: FloatArray,
controls: FloatArray,
) -> FilterResult:
time_steps, obs_dim = observations.shape
state_dim = self.model.state_dim
A, B, C, D = self.model.A, self.model.B, self.model.C, self.model.D
Q, R = self.model.Q, self.model.R
means = np.zeros((time_steps, state_dim), dtype=np.float64)
covariances = np.zeros((time_steps, state_dim, state_dim), dtype=np.float64)
pred_means = np.zeros((time_steps, state_dim), dtype=np.float64)
pred_covariances = np.zeros((time_steps, state_dim, state_dim), dtype=np.float64)
predicted_mean = self.model.mu_0.copy()
predicted_covariance = self.model.Sigma_0.copy()
identity = np.eye(state_dim, dtype=np.float64)
gaussian_constant = obs_dim * np.log(2.0 * np.pi)
log_likelihood = 0.0
for time_index in range(time_steps):
pred_means[time_index] = predicted_mean
pred_covariances[time_index] = predicted_covariance
control = controls[time_index]
innovation = observations[time_index] - C @ predicted_mean - D @ control
innovation_covariance = _symmetrise(C @ predicted_covariance @ C.T + R)
try:
lower = np.asarray(
np.linalg.cholesky(innovation_covariance),
dtype=np.float64,
)
except np.linalg.LinAlgError as exc:
raise np.linalg.LinAlgError(
"innovation covariance must be positive definite"
) from exc
solved_innovation = _solve_cholesky(lower, innovation)
log_determinant = 2.0 * float(np.sum(np.log(np.diag(lower))))
log_likelihood -= 0.5 * (
gaussian_constant + log_determinant + float(innovation @ solved_innovation)
)
covariance_observation = predicted_covariance @ C.T
gain = _solve_cholesky(lower, covariance_observation.T).T
filtered_mean = predicted_mean + gain @ innovation
residual_operator = identity - gain @ C
filtered_covariance = _symmetrise(
residual_operator @ predicted_covariance @ residual_operator.T + gain @ R @ gain.T
)
means[time_index] = filtered_mean
covariances[time_index] = filtered_covariance
predicted_mean = A @ filtered_mean + B @ control
predicted_covariance = _symmetrise(A @ filtered_covariance @ A.T + Q)
return FilterResult(
means=means,
covariances=covariances,
pred_means=pred_means,
pred_covariances=pred_covariances,
log_likelihood=log_likelihood,
)
|
filter(observations, controls=None, backend='auto')
Filter an observation sequence.
Parameters
observations : array-like, shape (T, p)
Finite observations ordered by time.
controls : array-like, shape (T, m), optional
Finite controls. They are required when m > 0 and may be
omitted only for a model with no control input.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}
Execution backend. auto follows the availability- and
initialisation-aware order Mojo, Go, Rust, Julia, then Python.
Returns
FilterResult
Filtered and predicted moments plus the sequence log-likelihood.
Raises
ValueError
If sequence shapes, values, or the backend name are invalid.
RuntimeError
If an explicitly requested native backend is unavailable.
numpy.linalg.LinAlgError
If an innovation covariance is not positive definite.
Source code in src/sc_neurocore/world_model/_lgssm_filter.py
| Python |
|---|
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 | def filter(
self,
observations: npt.ArrayLike,
controls: npt.ArrayLike | None = None,
backend: str = "auto",
) -> FilterResult:
"""Filter an observation sequence.
Parameters
----------
observations : array-like, shape (T, p)
Finite observations ordered by time.
controls : array-like, shape (T, m), optional
Finite controls. They are required when ``m > 0`` and may be
omitted only for a model with no control input.
backend : {"auto", "mojo", "go", "rust", "julia", "python"}
Execution backend. ``auto`` follows the availability- and
initialisation-aware order Mojo, Go, Rust, Julia, then Python.
Returns
-------
FilterResult
Filtered and predicted moments plus the sequence log-likelihood.
Raises
------
ValueError
If sequence shapes, values, or the backend name are invalid.
RuntimeError
If an explicitly requested native backend is unavailable.
numpy.linalg.LinAlgError
If an innovation covariance is not positive definite.
"""
observation_array = _normalise_observations(
observations,
obs_dim=self.model.obs_dim,
)
control_array = _normalise_controls(
controls,
time_steps=observation_array.shape[0],
control_dim=self.model.control_dim,
)
selected_backend = resolve_backend(backend)
if selected_backend == "python":
return self._filter_python(observation_array, control_array)
return filter_native(selected_backend, self.model, observation_array, control_array)
|
RTSSmoother
Rauch-Tung-Striebel backward smoother.
Parameters
model : LinearGaussianSSM
Model used to produce the corresponding forward-filter result.
Notes
The recursion follows Rauch, Tung, and Striebel (1965). The returned
lag-one covariance is oriented as Cov[x_t, x_{t+1} | y].
Source code in src/sc_neurocore/world_model/_lgssm_smoothing.py
| Python |
|---|
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100 | class RTSSmoother:
"""Rauch-Tung-Striebel backward smoother.
Parameters
----------
model : LinearGaussianSSM
Model used to produce the corresponding forward-filter result.
Notes
-----
The recursion follows Rauch, Tung, and Striebel (1965). The returned
lag-one covariance is oriented as ``Cov[x_t, x_{t+1} | y]``.
"""
def __init__(self, model: LinearGaussianSSM) -> None:
self.model = model
def smooth(self, filter_result: FilterResult) -> SmoothResult:
"""Smooth every state in a validated forward-filter result.
Parameters
----------
filter_result : FilterResult
Forward moments for at least one time step.
Returns
-------
SmoothResult
Full-sequence posterior means, covariances, and lag-one
cross-covariances.
Raises
------
ValueError
If the filter-result state dimension differs from the model.
numpy.linalg.LinAlgError
If a predicted covariance required by the recursion is singular.
"""
time_steps, state_dim = filter_result.means.shape
if state_dim != self.model.state_dim:
raise ValueError(
"filter_result state dimension "
f"{state_dim} does not match model state dimension {self.model.state_dim}"
)
smoothed_means = filter_result.means.copy()
smoothed_covariances = filter_result.covariances.copy()
cross_covariances = np.zeros(
(time_steps - 1, state_dim, state_dim),
dtype=np.float64,
)
for time_index in range(time_steps - 2, -1, -1):
predicted_next = filter_result.pred_covariances[time_index + 1]
covariance_transition = filter_result.covariances[time_index] @ self.model.A.T
smoothing_gain = _solve_positive_definite(
predicted_next,
covariance_transition.T,
).T
smoothed_means[time_index] = filter_result.means[time_index] + smoothing_gain @ (
smoothed_means[time_index + 1] - filter_result.pred_means[time_index + 1]
)
smoothed_covariances[time_index] = _symmetrise(
filter_result.covariances[time_index]
+ smoothing_gain
@ (smoothed_covariances[time_index + 1] - predicted_next)
@ smoothing_gain.T
)
cross_covariances[time_index] = smoothing_gain @ smoothed_covariances[time_index + 1]
return SmoothResult(
means=smoothed_means,
covariances=smoothed_covariances,
cross_covariances=cross_covariances,
)
|
smooth(filter_result)
Smooth every state in a validated forward-filter result.
Parameters
filter_result : FilterResult
Forward moments for at least one time step.
Returns
SmoothResult
Full-sequence posterior means, covariances, and lag-one
cross-covariances.
Raises
ValueError
If the filter-result state dimension differs from the model.
numpy.linalg.LinAlgError
If a predicted covariance required by the recursion is singular.
Source code in src/sc_neurocore/world_model/_lgssm_smoothing.py
| Python |
|---|
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100 | def smooth(self, filter_result: FilterResult) -> SmoothResult:
"""Smooth every state in a validated forward-filter result.
Parameters
----------
filter_result : FilterResult
Forward moments for at least one time step.
Returns
-------
SmoothResult
Full-sequence posterior means, covariances, and lag-one
cross-covariances.
Raises
------
ValueError
If the filter-result state dimension differs from the model.
numpy.linalg.LinAlgError
If a predicted covariance required by the recursion is singular.
"""
time_steps, state_dim = filter_result.means.shape
if state_dim != self.model.state_dim:
raise ValueError(
"filter_result state dimension "
f"{state_dim} does not match model state dimension {self.model.state_dim}"
)
smoothed_means = filter_result.means.copy()
smoothed_covariances = filter_result.covariances.copy()
cross_covariances = np.zeros(
(time_steps - 1, state_dim, state_dim),
dtype=np.float64,
)
for time_index in range(time_steps - 2, -1, -1):
predicted_next = filter_result.pred_covariances[time_index + 1]
covariance_transition = filter_result.covariances[time_index] @ self.model.A.T
smoothing_gain = _solve_positive_definite(
predicted_next,
covariance_transition.T,
).T
smoothed_means[time_index] = filter_result.means[time_index] + smoothing_gain @ (
smoothed_means[time_index + 1] - filter_result.pred_means[time_index + 1]
)
smoothed_covariances[time_index] = _symmetrise(
filter_result.covariances[time_index]
+ smoothing_gain
@ (smoothed_covariances[time_index + 1] - predicted_next)
@ smoothing_gain.T
)
cross_covariances[time_index] = smoothing_gain @ smoothed_covariances[time_index + 1]
return SmoothResult(
means=smoothed_means,
covariances=smoothed_covariances,
cross_covariances=cross_covariances,
)
|
FilterResult
dataclass
Forward-filter posterior and one-step prediction moments.
Parameters
means : numpy.ndarray, shape (T, d)
Filtered state means.
covariances : numpy.ndarray, shape (T, d, d)
Filtered state covariances.
pred_means : numpy.ndarray, shape (T, d)
One-step predicted state means before observing each sample.
pred_covariances : numpy.ndarray, shape (T, d, d)
One-step predicted state covariances.
log_likelihood : float
Sequence log-likelihood under the model.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 | @dataclass
class FilterResult:
"""Forward-filter posterior and one-step prediction moments.
Parameters
----------
means : numpy.ndarray, shape (T, d)
Filtered state means.
covariances : numpy.ndarray, shape (T, d, d)
Filtered state covariances.
pred_means : numpy.ndarray, shape (T, d)
One-step predicted state means before observing each sample.
pred_covariances : numpy.ndarray, shape (T, d, d)
One-step predicted state covariances.
log_likelihood : float
Sequence log-likelihood under the model.
"""
means: FloatArray
covariances: FloatArray
pred_means: FloatArray
pred_covariances: FloatArray
log_likelihood: float
def __post_init__(self) -> None:
"""Validate result shapes, finiteness, symmetry, and covariance signs."""
self.means = _as_float_array(self.means, name="means", ndim=2)
self.covariances = _as_float_array(self.covariances, name="covariances", ndim=3)
self.pred_means = _as_float_array(self.pred_means, name="pred_means", ndim=2)
self.pred_covariances = _as_float_array(
self.pred_covariances, name="pred_covariances", ndim=3
)
time_steps, state_dim = self.means.shape
if time_steps == 0 or state_dim == 0:
raise ValueError("means must have non-zero time and state dimensions")
expected_vector_shape = (time_steps, state_dim)
expected_matrix_shape = (time_steps, state_dim, state_dim)
if self.pred_means.shape != expected_vector_shape:
raise ValueError(
f"pred_means must have shape {expected_vector_shape}, got {self.pred_means.shape}"
)
for name in ("covariances", "pred_covariances"):
covariance_stack = getattr(self, name)
if covariance_stack.shape != expected_matrix_shape:
raise ValueError(
f"{name} must have shape {expected_matrix_shape}, got {covariance_stack.shape}"
)
_require_positive_semidefinite_stack(covariance_stack, name=name)
self.log_likelihood = float(self.log_likelihood)
if not isfinite(self.log_likelihood):
raise ValueError("log_likelihood must be finite")
|
__post_init__()
Validate result shapes, finiteness, symmetry, and covariance signs.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 | def __post_init__(self) -> None:
"""Validate result shapes, finiteness, symmetry, and covariance signs."""
self.means = _as_float_array(self.means, name="means", ndim=2)
self.covariances = _as_float_array(self.covariances, name="covariances", ndim=3)
self.pred_means = _as_float_array(self.pred_means, name="pred_means", ndim=2)
self.pred_covariances = _as_float_array(
self.pred_covariances, name="pred_covariances", ndim=3
)
time_steps, state_dim = self.means.shape
if time_steps == 0 or state_dim == 0:
raise ValueError("means must have non-zero time and state dimensions")
expected_vector_shape = (time_steps, state_dim)
expected_matrix_shape = (time_steps, state_dim, state_dim)
if self.pred_means.shape != expected_vector_shape:
raise ValueError(
f"pred_means must have shape {expected_vector_shape}, got {self.pred_means.shape}"
)
for name in ("covariances", "pred_covariances"):
covariance_stack = getattr(self, name)
if covariance_stack.shape != expected_matrix_shape:
raise ValueError(
f"{name} must have shape {expected_matrix_shape}, got {covariance_stack.shape}"
)
_require_positive_semidefinite_stack(covariance_stack, name=name)
self.log_likelihood = float(self.log_likelihood)
if not isfinite(self.log_likelihood):
raise ValueError("log_likelihood must be finite")
|
LinearGaussianSSM
dataclass
Parameters of a discrete-time linear Gaussian state-space model.
Parameters
A : numpy.ndarray, shape (d, d)
State-transition matrix.
B : numpy.ndarray, shape (d, m)
Control-input matrix. Use an empty second dimension when m = 0.
C : numpy.ndarray, shape (p, d)
Observation matrix.
D : numpy.ndarray, shape (p, m)
Direct control-to-observation matrix.
Q : numpy.ndarray, shape (d, d)
Symmetric positive-semidefinite process covariance.
R : numpy.ndarray, shape (p, p)
Symmetric positive-definite observation covariance.
mu_0 : numpy.ndarray, shape (d,)
Prior state mean.
Sigma_0 : numpy.ndarray, shape (d, d)
Symmetric positive-definite prior covariance.
Raises
ValueError
If a parameter has an incompatible shape, non-finite value, or invalid
covariance contract.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 | @dataclass
class LinearGaussianSSM:
"""Parameters of a discrete-time linear Gaussian state-space model.
Parameters
----------
A : numpy.ndarray, shape (d, d)
State-transition matrix.
B : numpy.ndarray, shape (d, m)
Control-input matrix. Use an empty second dimension when ``m = 0``.
C : numpy.ndarray, shape (p, d)
Observation matrix.
D : numpy.ndarray, shape (p, m)
Direct control-to-observation matrix.
Q : numpy.ndarray, shape (d, d)
Symmetric positive-semidefinite process covariance.
R : numpy.ndarray, shape (p, p)
Symmetric positive-definite observation covariance.
mu_0 : numpy.ndarray, shape (d,)
Prior state mean.
Sigma_0 : numpy.ndarray, shape (d, d)
Symmetric positive-definite prior covariance.
Raises
------
ValueError
If a parameter has an incompatible shape, non-finite value, or invalid
covariance contract.
"""
A: FloatArray
B: FloatArray
C: FloatArray
D: FloatArray
Q: FloatArray
R: FloatArray
mu_0: FloatArray
Sigma_0: FloatArray
def __post_init__(self) -> None:
"""Copy parameters into finite, C-contiguous float64 arrays and validate them."""
self.A = _as_float_array(self.A, name="A", ndim=2)
self.B = _as_float_array(self.B, name="B", ndim=2)
self.C = _as_float_array(self.C, name="C", ndim=2)
self.D = _as_float_array(self.D, name="D", ndim=2)
self.Q = _as_float_array(self.Q, name="Q", ndim=2)
self.R = _as_float_array(self.R, name="R", ndim=2)
self.mu_0 = _as_float_array(self.mu_0, name="mu_0", ndim=1)
self.Sigma_0 = _as_float_array(self.Sigma_0, name="Sigma_0", ndim=2)
d = self.A.shape[0]
if d == 0 or self.A.shape != (d, d):
raise ValueError(f"A must be a non-empty square matrix, got {self.A.shape}")
if self.B.shape[0] != d:
raise ValueError(f"B must have {d} rows, got {self.B.shape}")
m = self.B.shape[1]
p = self.C.shape[0]
if p == 0 or self.C.shape != (p, d):
raise ValueError(f"C must have shape (p, {d}) with p > 0, got {self.C.shape}")
expected_shapes = {
"D": (p, m),
"Q": (d, d),
"R": (p, p),
"mu_0": (d,),
"Sigma_0": (d, d),
}
for name, expected_shape in expected_shapes.items():
actual_shape = getattr(self, name).shape
if actual_shape != expected_shape:
raise ValueError(f"{name} must have shape {expected_shape}, got {actual_shape}")
_require_positive_semidefinite(self.Q, name="Q")
_require_positive_definite(self.R, name="R")
_require_positive_definite(self.Sigma_0, name="Sigma_0")
@property
def state_dim(self) -> int:
"""Return the latent-state dimension ``d``."""
return int(self.A.shape[0])
@property
def obs_dim(self) -> int:
"""Return the observation dimension ``p``."""
return int(self.C.shape[0])
@property
def control_dim(self) -> int:
"""Return the control-input dimension ``m``."""
return int(self.B.shape[1])
@classmethod
def random(
cls,
state_dim: int,
obs_dim: int,
control_dim: int = 0,
seed: int = 42,
) -> LinearGaussianSSM:
"""Construct a stable random model for initialisation and examples.
Parameters
----------
state_dim : int
Positive latent-state dimension.
obs_dim : int
Positive observation dimension.
control_dim : int, default=0
Non-negative control dimension.
seed : int, default=42
NumPy random-generator seed.
Returns
-------
LinearGaussianSSM
Model whose state-transition spectral radius is ``0.95``.
Raises
------
ValueError
If a dimension is not an integer in its documented domain.
"""
d = _require_dimension(state_dim, name="state_dim", allow_zero=False)
p = _require_dimension(obs_dim, name="obs_dim", allow_zero=False)
m = _require_dimension(control_dim, name="control_dim", allow_zero=True)
rng = np.random.default_rng(seed)
raw = rng.standard_normal((d, d)) * 0.5
spectral_radius = float(np.max(np.abs(np.linalg.eigvals(raw))))
minimum_scale = float(np.finfo(np.float64).tiny)
A = np.asarray(raw * (0.95 / max(spectral_radius, minimum_scale)))
B = rng.standard_normal((d, m)) if m > 0 else np.zeros((d, 0))
C = rng.standard_normal((p, d)) * 0.5
D = rng.standard_normal((p, m)) if m > 0 else np.zeros((p, 0))
return cls(
A=A,
B=B,
C=C,
D=D,
Q=np.eye(d) * 0.1,
R=np.eye(p) * 0.1,
mu_0=np.zeros(d),
Sigma_0=np.eye(d),
)
|
state_dim
property
Return the latent-state dimension d.
obs_dim
property
Return the observation dimension p.
control_dim
property
Return the control-input dimension m.
__post_init__()
Copy parameters into finite, C-contiguous float64 arrays and validate them.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257 | def __post_init__(self) -> None:
"""Copy parameters into finite, C-contiguous float64 arrays and validate them."""
self.A = _as_float_array(self.A, name="A", ndim=2)
self.B = _as_float_array(self.B, name="B", ndim=2)
self.C = _as_float_array(self.C, name="C", ndim=2)
self.D = _as_float_array(self.D, name="D", ndim=2)
self.Q = _as_float_array(self.Q, name="Q", ndim=2)
self.R = _as_float_array(self.R, name="R", ndim=2)
self.mu_0 = _as_float_array(self.mu_0, name="mu_0", ndim=1)
self.Sigma_0 = _as_float_array(self.Sigma_0, name="Sigma_0", ndim=2)
d = self.A.shape[0]
if d == 0 or self.A.shape != (d, d):
raise ValueError(f"A must be a non-empty square matrix, got {self.A.shape}")
if self.B.shape[0] != d:
raise ValueError(f"B must have {d} rows, got {self.B.shape}")
m = self.B.shape[1]
p = self.C.shape[0]
if p == 0 or self.C.shape != (p, d):
raise ValueError(f"C must have shape (p, {d}) with p > 0, got {self.C.shape}")
expected_shapes = {
"D": (p, m),
"Q": (d, d),
"R": (p, p),
"mu_0": (d,),
"Sigma_0": (d, d),
}
for name, expected_shape in expected_shapes.items():
actual_shape = getattr(self, name).shape
if actual_shape != expected_shape:
raise ValueError(f"{name} must have shape {expected_shape}, got {actual_shape}")
_require_positive_semidefinite(self.Q, name="Q")
_require_positive_definite(self.R, name="R")
_require_positive_definite(self.Sigma_0, name="Sigma_0")
|
random(state_dim, obs_dim, control_dim=0, seed=42)
classmethod
Construct a stable random model for initialisation and examples.
Parameters
state_dim : int
Positive latent-state dimension.
obs_dim : int
Positive observation dimension.
control_dim : int, default=0
Non-negative control dimension.
seed : int, default=42
NumPy random-generator seed.
Returns
LinearGaussianSSM
Model whose state-transition spectral radius is 0.95.
Raises
ValueError
If a dimension is not an integer in its documented domain.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 | @classmethod
def random(
cls,
state_dim: int,
obs_dim: int,
control_dim: int = 0,
seed: int = 42,
) -> LinearGaussianSSM:
"""Construct a stable random model for initialisation and examples.
Parameters
----------
state_dim : int
Positive latent-state dimension.
obs_dim : int
Positive observation dimension.
control_dim : int, default=0
Non-negative control dimension.
seed : int, default=42
NumPy random-generator seed.
Returns
-------
LinearGaussianSSM
Model whose state-transition spectral radius is ``0.95``.
Raises
------
ValueError
If a dimension is not an integer in its documented domain.
"""
d = _require_dimension(state_dim, name="state_dim", allow_zero=False)
p = _require_dimension(obs_dim, name="obs_dim", allow_zero=False)
m = _require_dimension(control_dim, name="control_dim", allow_zero=True)
rng = np.random.default_rng(seed)
raw = rng.standard_normal((d, d)) * 0.5
spectral_radius = float(np.max(np.abs(np.linalg.eigvals(raw))))
minimum_scale = float(np.finfo(np.float64).tiny)
A = np.asarray(raw * (0.95 / max(spectral_radius, minimum_scale)))
B = rng.standard_normal((d, m)) if m > 0 else np.zeros((d, 0))
C = rng.standard_normal((p, d)) * 0.5
D = rng.standard_normal((p, m)) if m > 0 else np.zeros((p, 0))
return cls(
A=A,
B=B,
C=C,
D=D,
Q=np.eye(d) * 0.1,
R=np.eye(p) * 0.1,
mu_0=np.zeros(d),
Sigma_0=np.eye(d),
)
|
SmoothResult
dataclass
Rauch-Tung-Striebel smoothed state moments.
Parameters
means : numpy.ndarray, shape (T, d)
Smoothed state means.
covariances : numpy.ndarray, shape (T, d, d)
Smoothed state covariances.
cross_covariances : numpy.ndarray, shape (T - 1, d, d)
Lag-one covariances Cov[x_t, x_{t+1} | y_{0:T-1}].
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426 | @dataclass
class SmoothResult:
"""Rauch-Tung-Striebel smoothed state moments.
Parameters
----------
means : numpy.ndarray, shape (T, d)
Smoothed state means.
covariances : numpy.ndarray, shape (T, d, d)
Smoothed state covariances.
cross_covariances : numpy.ndarray, shape (T - 1, d, d)
Lag-one covariances ``Cov[x_t, x_{t+1} | y_{0:T-1}]``.
"""
means: FloatArray
covariances: FloatArray
cross_covariances: FloatArray
def __post_init__(self) -> None:
"""Validate smoothed moment shapes, finiteness, and covariance signs."""
self.means = _as_float_array(self.means, name="means", ndim=2)
self.covariances = _as_float_array(self.covariances, name="covariances", ndim=3)
self.cross_covariances = _as_float_array(
self.cross_covariances, name="cross_covariances", ndim=3
)
time_steps, state_dim = self.means.shape
if time_steps == 0 or state_dim == 0:
raise ValueError("means must have non-zero time and state dimensions")
covariance_shape = (time_steps, state_dim, state_dim)
cross_shape = (time_steps - 1, state_dim, state_dim)
if self.covariances.shape != covariance_shape:
raise ValueError(
f"covariances must have shape {covariance_shape}, got {self.covariances.shape}"
)
if self.cross_covariances.shape != cross_shape:
raise ValueError(
"cross_covariances must have shape "
f"{cross_shape}, got {self.cross_covariances.shape}"
)
_require_positive_semidefinite_stack(
self.covariances,
name="covariances",
)
|
__post_init__()
Validate smoothed moment shapes, finiteness, and covariance signs.
Source code in src/sc_neurocore/world_model/_lgssm_types.py
| Python |
|---|
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426 | def __post_init__(self) -> None:
"""Validate smoothed moment shapes, finiteness, and covariance signs."""
self.means = _as_float_array(self.means, name="means", ndim=2)
self.covariances = _as_float_array(self.covariances, name="covariances", ndim=3)
self.cross_covariances = _as_float_array(
self.cross_covariances, name="cross_covariances", ndim=3
)
time_steps, state_dim = self.means.shape
if time_steps == 0 or state_dim == 0:
raise ValueError("means must have non-zero time and state dimensions")
covariance_shape = (time_steps, state_dim, state_dim)
cross_shape = (time_steps - 1, state_dim, state_dim)
if self.covariances.shape != covariance_shape:
raise ValueError(
f"covariances must have shape {covariance_shape}, got {self.covariances.shape}"
)
if self.cross_covariances.shape != cross_shape:
raise ValueError(
"cross_covariances must have shape "
f"{cross_shape}, got {self.cross_covariances.shape}"
)
_require_positive_semidefinite_stack(
self.covariances,
name="covariances",
)
|
PredictiveWorldModel
dataclass
Forecast latent-state means and covariances through an LGSSM.
Parameters
state_dim : int
Positive latent-state dimension.
action_dim : int
Non-negative action dimension.
seed : int, default=42
Seed used to initialise the stable random LGSSM.
Notes
This class preserves the historical planning-facing API. Use
:class:LinearGaussianSSM, :class:KalmanFilter, and
:class:RTSSmoother when observations are available.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221 | @dataclass
class PredictiveWorldModel:
"""Forecast latent-state means and covariances through an LGSSM.
Parameters
----------
state_dim : int
Positive latent-state dimension.
action_dim : int
Non-negative action dimension.
seed : int, default=42
Seed used to initialise the stable random LGSSM.
Notes
-----
This class preserves the historical planning-facing API. Use
:class:`LinearGaussianSSM`, :class:`KalmanFilter`, and
:class:`RTSSmoother` when observations are available.
"""
state_dim: int
action_dim: int
seed: int = 42
def __post_init__(self) -> None:
"""Initialise a validated stable state-transition model."""
self.state_dim = _require_dimension(
self.state_dim,
name="state_dim",
allow_zero=False,
)
self.action_dim = _require_dimension(
self.action_dim,
name="action_dim",
allow_zero=True,
)
self.model = LinearGaussianSSM.random(
state_dim=self.state_dim,
obs_dim=self.state_dim,
control_dim=self.action_dim,
seed=self.seed,
)
self._mu = self.model.mu_0.copy()
self._Sigma = self.model.Sigma_0.copy()
def reset(self) -> None:
"""Reset the stored belief moments to the model prior."""
self._mu = self.model.mu_0.copy()
self._Sigma = self.model.Sigma_0.copy()
def predict_next_state(
self,
current_state: npt.ArrayLike,
action: npt.ArrayLike,
) -> FloatArray:
"""Predict the next latent-state mean.
Parameters
----------
current_state : array-like, shape (state_dim,)
Current state estimate.
action : array-like, shape (action_dim,)
Current control input. A scalar is accepted when ``action_dim=1``.
Returns
-------
numpy.ndarray, shape (state_dim,)
Conditional mean ``A x_t + B u_t``.
Raises
------
ValueError
If an input has an incompatible shape or non-finite value.
"""
state = _normalise_vector(
current_state,
name="current_state",
length=self.state_dim,
)
control = _normalise_vector(
action,
name="action",
length=self.action_dim,
allow_scalar=True,
)
return np.asarray(self.model.A @ state + self.model.B @ control)
def predict_next_state_with_cov(
self,
current_state: npt.ArrayLike,
current_cov: npt.ArrayLike,
action: npt.ArrayLike,
) -> tuple[FloatArray, FloatArray]:
"""Predict the next latent-state mean and covariance.
Parameters
----------
current_state : array-like, shape (state_dim,)
Current state estimate.
current_cov : array-like, shape (state_dim, state_dim)
Symmetric positive-semidefinite current covariance.
action : array-like, shape (action_dim,)
Current control input.
Returns
-------
tuple of numpy.ndarray
Mean ``A x_t + B u_t`` and covariance ``A P_t A^T + Q``.
"""
state_covariance = _normalise_state_covariance(
current_cov,
state_dim=self.state_dim,
name="current_cov",
)
mean = self.predict_next_state(current_state, action)
covariance = _symmetrise(self.model.A @ state_covariance @ self.model.A.T + self.model.Q)
return mean, covariance
def forecast(
self,
initial_state: npt.ArrayLike,
actions: list[npt.ArrayLike],
) -> list[FloatArray]:
"""Forecast a deterministic mean trajectory.
Parameters
----------
initial_state : array-like, shape (state_dim,)
State before the first action.
actions : list of array-like
Ordered actions, one per returned state.
Returns
-------
list of numpy.ndarray
Independent state arrays after each action.
"""
state = _normalise_vector(
initial_state,
name="initial_state",
length=self.state_dim,
)
trajectory: list[FloatArray] = []
for action in actions:
state = self.predict_next_state(state, action)
trajectory.append(state.copy())
return trajectory
def forecast_with_cov(
self,
initial_state: npt.ArrayLike,
initial_cov: npt.ArrayLike,
actions: list[npt.ArrayLike],
) -> list[tuple[FloatArray, FloatArray]]:
"""Forecast a mean and covariance trajectory.
Parameters
----------
initial_state : array-like, shape (state_dim,)
State before the first action.
initial_cov : array-like, shape (state_dim, state_dim)
Initial symmetric positive-semidefinite covariance.
actions : list of array-like
Ordered actions, one per returned state.
Returns
-------
list of tuple of numpy.ndarray
Independent ``(mean, covariance)`` pairs after each action.
"""
state = _normalise_vector(
initial_state,
name="initial_state",
length=self.state_dim,
)
covariance = _normalise_state_covariance(
initial_cov,
state_dim=self.state_dim,
name="initial_cov",
)
trajectory: list[tuple[FloatArray, FloatArray]] = []
for action in actions:
state, covariance = self.predict_next_state_with_cov(
state,
covariance,
action,
)
trajectory.append((state.copy(), covariance.copy()))
return trajectory
|
__post_init__()
Initialise a validated stable state-transition model.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 | def __post_init__(self) -> None:
"""Initialise a validated stable state-transition model."""
self.state_dim = _require_dimension(
self.state_dim,
name="state_dim",
allow_zero=False,
)
self.action_dim = _require_dimension(
self.action_dim,
name="action_dim",
allow_zero=True,
)
self.model = LinearGaussianSSM.random(
state_dim=self.state_dim,
obs_dim=self.state_dim,
control_dim=self.action_dim,
seed=self.seed,
)
self._mu = self.model.mu_0.copy()
self._Sigma = self.model.Sigma_0.copy()
|
reset()
Reset the stored belief moments to the model prior.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
| def reset(self) -> None:
"""Reset the stored belief moments to the model prior."""
self._mu = self.model.mu_0.copy()
self._Sigma = self.model.Sigma_0.copy()
|
predict_next_state(current_state, action)
Predict the next latent-state mean.
Parameters
current_state : array-like, shape (state_dim,)
Current state estimate.
action : array-like, shape (action_dim,)
Current control input. A scalar is accepted when action_dim=1.
Returns
numpy.ndarray, shape (state_dim,)
Conditional mean A x_t + B u_t.
Raises
ValueError
If an input has an incompatible shape or non-finite value.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115 | def predict_next_state(
self,
current_state: npt.ArrayLike,
action: npt.ArrayLike,
) -> FloatArray:
"""Predict the next latent-state mean.
Parameters
----------
current_state : array-like, shape (state_dim,)
Current state estimate.
action : array-like, shape (action_dim,)
Current control input. A scalar is accepted when ``action_dim=1``.
Returns
-------
numpy.ndarray, shape (state_dim,)
Conditional mean ``A x_t + B u_t``.
Raises
------
ValueError
If an input has an incompatible shape or non-finite value.
"""
state = _normalise_vector(
current_state,
name="current_state",
length=self.state_dim,
)
control = _normalise_vector(
action,
name="action",
length=self.action_dim,
allow_scalar=True,
)
return np.asarray(self.model.A @ state + self.model.B @ control)
|
predict_next_state_with_cov(current_state, current_cov, action)
Predict the next latent-state mean and covariance.
Parameters
current_state : array-like, shape (state_dim,)
Current state estimate.
current_cov : array-like, shape (state_dim, state_dim)
Symmetric positive-semidefinite current covariance.
action : array-like, shape (action_dim,)
Current control input.
Returns
tuple of numpy.ndarray
Mean A x_t + B u_t and covariance A P_t A^T + Q.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147 | def predict_next_state_with_cov(
self,
current_state: npt.ArrayLike,
current_cov: npt.ArrayLike,
action: npt.ArrayLike,
) -> tuple[FloatArray, FloatArray]:
"""Predict the next latent-state mean and covariance.
Parameters
----------
current_state : array-like, shape (state_dim,)
Current state estimate.
current_cov : array-like, shape (state_dim, state_dim)
Symmetric positive-semidefinite current covariance.
action : array-like, shape (action_dim,)
Current control input.
Returns
-------
tuple of numpy.ndarray
Mean ``A x_t + B u_t`` and covariance ``A P_t A^T + Q``.
"""
state_covariance = _normalise_state_covariance(
current_cov,
state_dim=self.state_dim,
name="current_cov",
)
mean = self.predict_next_state(current_state, action)
covariance = _symmetrise(self.model.A @ state_covariance @ self.model.A.T + self.model.Q)
return mean, covariance
|
forecast(initial_state, actions)
Forecast a deterministic mean trajectory.
Parameters
initial_state : array-like, shape (state_dim,)
State before the first action.
actions : list of array-like
Ordered actions, one per returned state.
Returns
list of numpy.ndarray
Independent state arrays after each action.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178 | def forecast(
self,
initial_state: npt.ArrayLike,
actions: list[npt.ArrayLike],
) -> list[FloatArray]:
"""Forecast a deterministic mean trajectory.
Parameters
----------
initial_state : array-like, shape (state_dim,)
State before the first action.
actions : list of array-like
Ordered actions, one per returned state.
Returns
-------
list of numpy.ndarray
Independent state arrays after each action.
"""
state = _normalise_vector(
initial_state,
name="initial_state",
length=self.state_dim,
)
trajectory: list[FloatArray] = []
for action in actions:
state = self.predict_next_state(state, action)
trajectory.append(state.copy())
return trajectory
|
forecast_with_cov(initial_state, initial_cov, actions)
Forecast a mean and covariance trajectory.
Parameters
initial_state : array-like, shape (state_dim,)
State before the first action.
initial_cov : array-like, shape (state_dim, state_dim)
Initial symmetric positive-semidefinite covariance.
actions : list of array-like
Ordered actions, one per returned state.
Returns
list of tuple of numpy.ndarray
Independent (mean, covariance) pairs after each action.
Source code in src/sc_neurocore/world_model/_predictive_world_model.py
| Python |
|---|
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221 | def forecast_with_cov(
self,
initial_state: npt.ArrayLike,
initial_cov: npt.ArrayLike,
actions: list[npt.ArrayLike],
) -> list[tuple[FloatArray, FloatArray]]:
"""Forecast a mean and covariance trajectory.
Parameters
----------
initial_state : array-like, shape (state_dim,)
State before the first action.
initial_cov : array-like, shape (state_dim, state_dim)
Initial symmetric positive-semidefinite covariance.
actions : list of array-like
Ordered actions, one per returned state.
Returns
-------
list of tuple of numpy.ndarray
Independent ``(mean, covariance)`` pairs after each action.
"""
state = _normalise_vector(
initial_state,
name="initial_state",
length=self.state_dim,
)
covariance = _normalise_state_covariance(
initial_cov,
state_dim=self.state_dim,
name="initial_cov",
)
trajectory: list[tuple[FloatArray, FloatArray]] = []
for action in actions:
state, covariance = self.predict_next_state_with_cov(
state,
covariance,
action,
)
trajectory.append((state.copy(), covariance.copy()))
return trajectory
|
__getattr__(name)
Return the live historical Rust-availability flag on private access.
Source code in src/sc_neurocore/world_model/predictive_model.py
| Python |
|---|
| def __getattr__(name: str) -> object:
"""Return the live historical Rust-availability flag on private access."""
if name == "_HAS_RUST_LGSSM":
return _backend_runtime._HAS_RUST_LGSSM
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
References
- Kalman, R. E. (1960), “A New Approach to Linear Filtering and Prediction
Problems.”
- Rauch, H. E., Tung, F., and Striebel, C. T. (1965), “Maximum Likelihood
Estimates of Linear Dynamic Systems.”
- Shumway, R. H., and Stoffer, D. S. (1982), “An Approach to Time Series
Smoothing and Forecasting Using the EM Algorithm.”
- Bishop, C. M. (2006), Pattern Recognition and Machine Learning, section
13.3.