Architecture and API layers¶
This page is the maintainer-facing contract for how KoopmanGraph is structured. It exists so contributors and agents do not reintroduce style drift across releases. Read it when changing package layout, exports, device handling, or shared helpers.
Design philosophy¶
KoopmanGraph combines four complementary styles:
Composition — users assemble an encoder, decoder, and Koopman operator (discrete or continuous) into
GraphKoopmanModel. Tutorials typically omitkoopman=and let the model factory-build a built-in operator fromdynamics_mode/koopman_parameterizationstrings. Advanced callers may inject a customKoopmanOperatorContractnn.Moduleviakoopman=...(mutually exclusive with non-default factory kwargs). Custom operators are not checkpoint-round-trippable; see serialization notes below.Sklearn-like façade — the model exposes
fit/predict/evaluate/save/loadas the primary workflow.Functional training and evaluation — lower-level helpers in
koopman_graph.trainingandkoopman_graph.metricsimplement losses, epoch loops, and forecast metrics without requiring subclassing. The multi-epoch fit loop lives inrun_fit_loop();fit()validates sequences / controls and delegates to that helper.String-mode configuration — behavioral modes are selected with explicit strings (for example
dynamics_mode="discrete"|"continuous"andkoopman_parameterization="dense"|"structural"|"odo") rather than parallel class hierarchies.
Prefer extending these patterns over introducing a fifth style (global device managers, deep inheritance trees, or underscore helpers used as a second public API).
Package layout¶
Target style is capability packages at one level (similar to
torch_geometric.nn or sklearn.linear_model): group related peers under
a single shallow package when growth warrants it. Do not over-nest “because
PyG does,” invent one folder per tiny module, or introduce a fifth architectural
style beyond composition / sklearn façade / functional helpers / string-mode
configuration.
When to nest¶
Convert a flat module into a capability package when any of these hold:
Multi-concern grab-bag — one file mixes several cohesive jobs (orchestration + schedules + history types, containers + splits + sampling, spectrum + anomaly + similarity) and is already ~1k+ lines of maintainable surface.
Peer implementations — two or more sibling implementations share a contract (discrete/continuous operators; encoder/decoder/GNN primitives; classical DMD-family baselines).
Room to grow peers — a new classical method or GNN variant should land as a sibling module, not another flat mega-file.
Phase 8 capability packages: training/, data/, operators/,
nn/, analysis/, and baselines/ (all landed).
koopman_graph.training package layout:
history—LossWeights,FitHistory,TrainingLossBreakdown, fit input type aliasesschedules— constant / ramp loss-weight schedulesdevice—resolve_device,sequence_to_deviceobjectives— training-side composition over top-levelkoopman_graph.losses(left flat; no cycle benefit from nesting)loop— epoch helpers, input resolution,run_fit_loop
koopman_graph.data package layout (kept separate from
koopman_graph.datasets):
containers—GraphSnapshotSequence,MultiTrajectory,as_multi_trajectory,resolve_sequence,resolve_pair_delta_tsampling—WindowSamplersplits—TemporalSplit,temporal_splitrollout—resolve_rollout_start_indicesand related type aliases
koopman_graph.operators package layout (peer discrete/continuous pair):
contract—KoopmanOperatorContract,Parameterization,DynamicsMode,StabilityCertificate, shared structural helpersdiscrete—KoopmanOperatorcontinuous—ContinuousKoopmanOperatorand Van Loan helpers
Prefer from koopman_graph.operators import … (or the root façade for public
operator classes). Former root modules koopman_graph.operator and
koopman_graph.continuous were removed in v0.3.0.
koopman_graph.nn package layout (peer encoder / decoder / GNN primitives;
PyG-style nn capability package, no conv/ subtree):
gnn—BaseGNNModule, activation typing, validators, GCN/GAT convolution builders (power-user)encoder—GNNEncoder/GATEncoderdecoder—GNNDecoder/GATDecoder
Prefer from koopman_graph.nn import … (or the root façade for public
classes). Former root modules koopman_graph.encoder,
koopman_graph.decoder, and koopman_graph.gnn were removed in v0.3.0.
Encoder and decoder remain peers: both import from nn.gnn;
neither imports the other.
koopman_graph.analysis package layout (spectrum / similarity / anomaly /
plotting):
spectrum—compute_spectrum,compute_generator_spectrum,discrete_spectrum_at_delta_t,decode_mode_shapessimilarity—spectrum_distance,koopman_std,resolve_spectrum,dynamical_similarityanomaly—AnomalyDetectionResult,calibrate_anomaly_threshold,detect_anomalyplotting—plot_spectrum(discrete unit-disk / data-zoom complex-plane figures; Matplotlib call-site import)
koopman_graph.spectrum_types remains a top-level neutral leaf so
koopman_graph.protocols never imports heavy analysis code.
KoopmanSpectrum is re-exported from koopman_graph.analysis and the
package root for the public API.
koopman_graph.baselines package layout (peer DMD-family methods behind
ClassicalBaseline):
base—ClassicalBaselineand shared flattening / least-squares / topology-control guard helpersdmd—DMDBaselinedmdc—DMDcBaselineedmd—EDMDBaseline
Deep imports continue to use from koopman_graph.training import …,
from koopman_graph.data import …, from koopman_graph.analysis import …,
and from koopman_graph.baselines import … via package __init__
re-exports (same-named package compatibility).
When to stay flat¶
Keep a single module (or leave an existing small package alone) when:
The module is small and single-purpose (for example
metrics,losses,env,adaptation,observables,serialization,protocols,graph_utils,spectrum_types).koopman_graph.datasetsis already the correct benchmark/load subpackage — do not merge it intodata/(data structures vs datasets, same distinction as sklearn/PyG).Nesting would only rename a path without clarifying capability boundaries.
Explicit non-goals unless a later audit shows multi-peer growth: nesting
env, adaptation, observables, metrics, serialization,
protocols, or graph_utils.
model.py stays at the package root¶
GraphKoopmanModel and koopman_graph.model
remain top-level. Do not split the façade across packages or bury the
primary workflow class under nn/, training/, or similar.
No deep trees¶
One nesting level is enough (koopman_graph.nn, koopman_graph.operators).
Do not add deep trees such as nn/conv/... or operators/discrete/...
unless a future audit shows a genuine multi-layer library surface. Prefer peer
modules inside the capability package.
Three-layer API regardless of folders¶
Moving files must not change the layering contract above:
Public façade — symbols in package
__all__/from koopman_graph import …Power-user modules — importable packages or modules documented here but not newly promoted into
__all__Private helpers — leading-
_names, same-module only
Folders are an organization tool; they do not create a fourth API tier.
Compatibility contract¶
When splitting or nesting modules:
Root façade is stable —
from koopman_graph import …symbols and package__all__must keep the same public names and semantics.Power-user deep imports — for each move, choose one and document it in this page (and a short migration note if paths change):
Same-named package —
koopman_graph.training,data,analysis,baselineskeep working via package__init__re-exports when the old flat module is replaced by a package of the same name; orMigrate in-repo — update every docs/tests/notebook deep import to the new path and record the rename here. Do not add long-lived root shim modules for renamed paths (
operator→operators,encoder→nn.encoder, …). v0.3.0 removed those temporary root shims.
Do not leave half-migrated import paths. Serialization type strings and
checkpoint reconstruct rules stay intact unless a task explicitly bumps
FORMAT_VERSION.
API layers¶
The package has three intentional layers.
Public façade¶
Stable, supported entry points re-exported from koopman_graph (see
__all__). Prefer these in tutorials, notebooks, and application code.
v0.3.0 uses a thin root façade: keep the core encode → evolve → decode / fit / evaluate workflow at the package root; import specialized helpers from capability modules. Demotions are a hard cut (no root deprecation aliases), consistent with the shim-removal policy above.
Keep in koopman_graph.__all__ (core workflow):
GNNEncoder,GATEncoder,GNNDecoder,GATDecoder(also viakoopman_graph.nn)Classical baselines (
DMDBaseline,EDMDBaseline,DMDcBaseline)Data I/O for
fit:GraphSnapshotSequence,MultiTrajectory,TemporalSplit,temporal_split(),WindowSamplerTraining knobs:
ForwardConsistencyLoss,BackwardConsistencyLoss,EigenvalueRegularizationLoss,FitHistory,LossWeightsFeatured Phase 6 surfaces:
RecursiveKoopmanAdapter,GraphKoopmanEnvPrimary eval / spectrum entrypoints:
evaluate_forecast(),EvaluationResult,KoopmanSpectrum,compute_spectrum()__version__
Dataset generators remain via koopman_graph.datasets (not root
__all__ members; use from koopman_graph.datasets import …).
Demote to module imports (still public and documented; not in root
__all__):
koopman_graph.metrics—mae,rmse,mape,HorizonMetricskoopman_graph.analysis—compute_generator_spectrum,discrete_spectrum_at_delta_t,decode_mode_shapes,spectrum_distance,koopman_std,dynamical_similarity,detect_anomaly,calibrate_anomaly_threshold,AnomalyDetectionResultkoopman_graph.data—as_multi_trajectorykoopman_graph.adaptation—AdaptationStepResultkoopman_graph.observables—graph_laplacian_features
from koopman_graph import GNNDecoder, GNNEncoder, GraphKoopmanModel
from koopman_graph.analysis import dynamical_similarity
from koopman_graph.data import as_multi_trajectory
from koopman_graph.metrics import rmse
from koopman_graph.observables import graph_laplacian_features
Power-user modules¶
Importable modules that are documented and useful for advanced workflows, but
not advertised as a stable public contract. They may change without a major
version bump. Do not re-export new power-user symbols from
koopman_graph __all__ unless a blueprint task explicitly promotes
them.
Examples:
koopman_graph.training— loss schedules,train_one_epoch,run_fit_loop,resolve_device, and related helperskoopman_graph.serialization— checkpoint build/load internals behindGraphKoopmanModel.save/loadkoopman_graph.datasets.dynamics— Laplacian diffusion primitives and shared generation validators used by benchmark generators (dense step operators; sharesL_symweights withkoopman_graph.graph_utils)koopman_graph.graph_utils— shared graph-input resolution, latent propagation, and symmetric-normalized adjacency helpers (not in__all__)koopman_graph.nn— encoder / decoder / shared GNN primitives capability package (BaseGNNModule, activation typing, GCN/GAT builders). The package is power-user; public encoder/decoder classes remain in__all__. Peers import fromnn.gnnonly (no encoder↔decoder inversion).koopman_graph.spectrum_types— neutralKoopmanSpectrumvalue type (re-exported fromkoopman_graph.analysisand the package root)KoopmanOperatorContract— shared Protocol for discrete and continuous operators (matrix,advance,inverse_advance,bound_metric); importable fromkoopman_graph.operators/ used asKoopmanPropagatorinkoopman_graph.graph_utilsForecastModel— loose Protocol forfit/predict/spectrumon classical baselines andGraphKoopmanModel(method presence only; not drop-in interchangeable at call sites)SpectrumProvider— spectrum-only surface used bydynamical_similarity()(baselines and neural models; optionaldelta_tonly when the implementer accepts it)UncontrolledForecastModel— narrower peer for autonomouspredict(initial_graph: Data, steps: int); useaccepts_uncontrolled_data_predict()to reject controlled-only implementers at runtimeTrainableKoopmanModel— extendsForecastModelwith the encode / operator / Module members thatkoopman_graph.trainingandevaluate_forecast()duck-type
from koopman_graph.training import resolve_device, train_one_epoch
from koopman_graph.datasets.dynamics import laplacian_diffusion_rollout
from koopman_graph.protocols import (
ForecastModel,
SpectrumProvider,
TrainableKoopmanModel,
UncontrolledForecastModel,
)
Phase 8 note: shared utilities live in koopman_graph.graph_utils —
documented, importable as koopman_graph.graph_utils, and not listed in
__all__. Prefer this module over importing leading-underscore helpers from
peer packages. Typing Protocols
(ForecastModel,
SpectrumProvider,
UncontrolledForecastModel,
TrainableKoopmanModel,
KoopmanOperatorContract) and neutral value
types (koopman_graph.spectrum_types) stay power-user and are likewise
omitted from __all__.
Dependency direction. Prefer
types/protocols → data / graph_utils → feature modules → façade:
koopman_graph.protocolsimportsKoopmanSpectrumfromkoopman_graph.spectrum_types, not fromkoopman_graph.analysis.resolve_rollout_start_indices()lives next toGraphSnapshotSequenceand is shared bykoopman_graph.trainingandkoopman_graph.metrics.Forecast metrics reuse
masked_mse_loss()for masked RMSE so training and evaluation share one masked-MSE reduction.
Encode API¶
Prefer encode() for latent lifting.
It accepts a Data snapshot or explicit (x, edge_index, edge_weight)
tensors and applies hybrid physics observables when configured.
Training helpers require encode via
TrainableKoopmanModel (no encoder-only
fallback).
Private helpers¶
Names that begin with a leading underscore (_helper, _validate_…) are
not a public contract. They are implementation details of their enclosing
module.
Rules:
Call private helpers only from the same module (or tightly coupled tests).
Do not import leading-
_symbols across modules. Cross-module reuse must go through a documented internal (power-user) module without a leading underscore (seekoopman_graph.graph_utils).Autodoc may still render some private members for maintainers; that does not make them supported API.
Duck-typed training vs hard-typed env / checkpoints¶
Two coupling styles coexist on purpose:
Duck-typed training and evaluation —
train_one_epoch(),rollout_sequence_loss()/rollout_multi_start_loss(), other loss helpers, andevaluate_forecast()annotate againstTrainableKoopmanModel. They requireencode,predict,encoder/koopman/decoder,time_step,dynamics_mode,control_dim,resolve_delta_t, and thenn.Moduletrain/eval/parameters façade (no encoder-only fallback). Classical baselines satisfy onlyForecastModeland are not training targets for these helpers.TrainableKoopmanModeldisables runtimeisinstance(inherited@runtime_checkablewould be unreliable because submodule attributes live innn.Module._modules). Use static typing or structural smoke tests instead.Hard-typed Gymnasium and serialization —
GraphKoopmanEnvand checkpoint reconstruct inkoopman_graph.serializationrequireGraphKoopmanModelspecifically. They freeze or rebuild concrete encoder/decoder classes and architecture config fields that a structural Protocol cannot safely express. Env is not required to accept arbitrary Protocol implementers.
Topology capability matrix¶
Dynamic topology is first-class in the data container and neural model path, but not every consumer supports it. Callers must not assume silent freeze/flatten behavior:
Surface |
Dynamic topology ( |
|---|---|
Supported via |
|
Model |
Supported: hold-last topology (optional |
Training losses ( |
Supported: teacher-target topology from observed snapshots |
Rejected at construction (episode topology frozen from reset) |
|
Classical baselines (DMD / EDMD / DMDc) |
Rejected at |
Control layout capability matrix¶
GraphSnapshotSequence stores either global
(T, control_dim) or per-node (T, N, control_dim) controls. Consumers
must not assume the same physics for both layouts:
Surface |
Supported control layouts |
|---|---|
Global |
|
|
Global broadcast and per-node row matching
( |
Same as neural: global or per-node rows aligned to latent samples |
|
Global only |
|
Global only; per-node (3-D) |
Dynamics mode and stability-bound vocabulary¶
DynamicsMode is the single
Literal["discrete", "continuous"] type, defined next to
Parameterization in
koopman_graph.operators and re-exported from
koopman_graph.protocols. GraphKoopmanModel imports it from
protocols; AdaptationMode is an alias of
the same type for the RLS adapter API. Do not redefine the literals in call
sites.
Factory / checkpoint construction uses one neutral stability knob. Domain
operator attributes keep their physics-accurate names; use
resolve_factory_stability_bound() when reading
the bound for serialization or factory reconstruction:
Surface |
Discrete |
Continuous |
|---|---|---|
Factory / checkpoint key |
|
|
Built-in operator attribute |
|
|
Optional operator injection¶
GraphKoopmanModel accepts an optional
koopman= argument satisfying
KoopmanOperatorContract (must be an
nn.Module). Use this to compose a pre-built or custom propagator without
editing the model class. String-mode factory construction remains the default
when koopman is omitted.
Enforced capability tier
Protocol-complete (any contract
nn.Module): latent propagation (propagate_latent()/inverse_propagate_latent()always call unifiedadvance/inverse_advancewith a resolveddelta_t; discrete operators ignore the interval), encode/decode training andpredict,spectrum()viakoopman.matrix, and eigenvalue regularization viaeigvals(matrix)fordense/odoandbound_metricfor structural modes, plus the model’sdynamics_mode. Discrete dense backward-consistency training optionally reuses a built-indense_inverse_matrix()when present; operators without that helper still invert viainverse_advance(inverse_matrix=None).Built-in-only: checkpoint
save/loadround-tripsKoopmanOperatorandContinuousKoopmanOperatoronly; RLSRecursiveKoopmanAdapterseed and write-back likewise. Custom injected operators raise on save; reconstruct withkoopman=...after loading encoder/decoder state separately if needed.
Factory and dimension rules:
Factory kwargs (
koopman_init_mode,koopman_init_scale,koopman_parameterization,koopman_max_spectral_radius) must stay at their defaults when injecting.latent_dim/control_dimmust match the injected operator.Built-in
KoopmanOperatorrequiresdynamics_mode="discrete";ContinuousKoopmanOperatorrequires"continuous". For custom operators, setdynamics_modeto match the operator’s semantics (spectrum and eigenvalue hinge follow the model flag; propagation always suppliesdelta_t).
from koopman_graph import GNNDecoder, GNNEncoder, GraphKoopmanModel
from koopman_graph.operators import KoopmanOperator
operator = KoopmanOperator(latent_dim=64, parameterization="odo")
model = GraphKoopmanModel(
encoder=encoder,
decoder=decoder,
latent_dim=64,
time_step=0.1,
koopman=operator,
)
from koopman_graph.graph_utils import (
autoregressive_latent_rollout,
propagate_latent,
resolve_graph_inputs,
)
Optional dependencies and result types¶
Optional extras¶
Optional dependencies must not break importing koopman_graph. Prefer
fail-at-call with an ImportError that names the install extra or
package:
Soft import + call guard (Gymnasium / ``[rl]``) —
koopman_graph.envsoft-importsgymnasiumsoGraphKoopmanEnvcan subclassgym.Envwhen present. Construction calls_require_gymnasium()and raises withpip install koopman-graph[rl]guidance when the extra is missing.Call-site import (e.g. ``h5py`` for METR-LA) — import inside the function that needs the dependency and re-raise
ImportErrorwith install guidance (seekoopman_graph.datasets.metr_la).
Do not fail at import of core modules for optional extras.
Public result types¶
Value / result objects (snapshots returned to callers or passed between
pure helpers) should be @dataclass(frozen=True) with attribute access
(examples:
EvaluationResult,
StabilityCertificate,
FitHistory,
AdaptationStepResult,
and the module-local
TrainingLossBreakdown). Prefer optional fields
with None defaults over mapping/TypedDict styles for new APIs.
Collection-valued fields should be immutable sequences (tuple) so callers
cannot mutate nested contents in place.
Stateful workflow objects (samplers, optimizers, live training loops, accumulators that update in place) remain plain / mutable classes. Do not freeze types whose primary job is mutation across steps.
GraphSnapshotSequence follows the same collection
rule: snapshots is a
tuple[Data, ...] after construction (no .append / in-place replace).
Individual Data objects are borrowed, not cloned — in-place mutation of
node features or topology remains possible. Clone explicitly when isolation is
required. MultiTrajectory stores its trajectories
as a frozen tuple of sequences.
TrainingLossBreakdown is a frozen internal snapshot (batch/epoch loss
terms), not a package __all__ export. Epoch aggregation uses a local dict
accumulator, then constructs a new breakdown.
Multi-trajectory fit input¶
fit() accepts one trajectory or
several trajectories of the same system. Prefer the explicit wrapper:
from koopman_graph import MultiTrajectory
from koopman_graph.data import as_multi_trajectory
model.fit(MultiTrajectory((trajectory_a, trajectory_b)), epochs=50)
# equivalent helper
model.fit(as_multi_trajectory(trajectory_a, trajectory_b), epochs=50)
Discrimination rules (used by
resolve_training_sequences()):
MultiTrajectory— multi-trajectory (required)GraphSnapshotSequence— single trajectorynon-empty
list/tupleof onlyData— single trajectory of snapshotsbare
list/tupleofGraphSnapshotSequence—TypeError(wrap inMultiTrajectory)empty list or mixed
GraphSnapshotSequence/Data—ValueError
Validation input follows the same rules; a multi-trajectory validation container must match the training trajectory count, while a single validation sequence is reused for every training trajectory.
Device and tensor conventions¶
There is no global device manager. Device placement is local and explicit.
Who moves tensors¶
``GraphKoopmanModel.fit`` validates inputs then delegates to
run_fit_loop(), which resolves the training device withresolve_device()(explicitdevice=argument, else the model’s parameter device, else CPU), moves the model withto(device), and copies training / validation sequences viasequence_to_device().``GraphKoopmanEnv`` takes an explicit
device(or falls back to the wrapped model’s parameter device) and keeps graph / latent state there for stepping.Callers of ``predict`` / ``evaluate`` are responsible for placing the model and inputs consistently. These APIs do not silently relocate the model the way
fitdoes; keep inputs on the same device as the model parameters.
Façade vs functional training ownership¶
Model façade —
fit()owns user-facing validation (epoch bounds, control layouts via_validate_sequence_controls, multi-trajectory resolve, snapshot-length checks) and keeps the public signature /FitHistoryreturn type.Functional loop —
run_fit_loop()owns device placement, optimizer / scheduler setup, epoch iteration (train_one_epoch/train_windowed_epoch/eval_one_epoch), early stopping, best-weight tracking, optional checkpoint writes, and history assembly. Prefer extending that helper over growing the model class.
Online adaptation¶
RecursiveKoopmanAdapter keeps RLS regression
state on CPU. Operator matrices are detached and copied to CPU when the
adapter is constructed from a live operator. apply_to()
copies adapted weights back onto the target operator’s device and dtype.
Classical baselines and datasets¶
DMD-family baselines (DMDBaseline,
EDMDBaseline,
DMDcBaseline) share
ClassicalBaseline for time_step /
rank, fitted metadata, and _check_fitted scaffolding. They remain
siblings under that ABC (DMDc does not subclass DMD) and satisfy
ForecastModel structurally.
EDMDBaseline stores a least-squares
reconstruction_matrix (not a GNN decoder).
Dataset / benchmark generators and these baselines default to CPU
float32 tensors unless a docstring or parameter explicitly says otherwise.
Move results to a training device only when feeding a GPU model.
Sequences with is_dynamic_topology=True are rejected at fit (see
Topology capability matrix). Per-node (3-D) controls are rejected by
DMDcBaseline (see Control layout capability
matrix); neural and adaptation paths keep per-node row matching.
ForecastModel call-site matrix¶
ForecastModel is a loose façade
(*args / **kwargs). Peers share method names, not interchangeable
signatures. Prefer
UncontrolledForecastModel (and
accepts_uncontrolled_data_predict()) when code
must call predict(data, steps) portably.
Implementer |
|
|
|
|
|---|---|---|---|---|
|
Optional |
Optional |
||
|
None (uncontrolled peer) |
None |
|
|
|
None (uncontrolled peer) |
None |
|
|
|
Required |
None |
|
``fit`` return divergence. Classical baselines return self for chaining
(baseline.fit(seq).predict(...)).
fit() returns a frozen
FitHistory with per-epoch losses and early-stop
metadata. Do not assume a portable fit return type across
ForecastModel peers; this is intentional for
v0.3.0 (document rather than unify).
Spectral similarity vs mode shapes¶
dynamical_similarity() compares spectra, not
concrete model classes. Documented call patterns:
Precomputed spectra —
dynamical_similarity(spectrum_a, spectrum_b)Classical baselines —
dynamical_similarity(dmd_a, dmd_b)Mixed peers —
dynamical_similarity(dmd, neural_model)Continuous neural horizon —
dynamical_similarity(model_a, model_b, delta_t=0.1)(delta_tis forwarded only whenspectrumaccepts it; ignored for precomputedKoopmanSpectrumvalues and classical baselines)
resolve_spectrum() is the shared resolver.
decode_mode_shapes() stays hard-typed to
GraphKoopmanModel because it needs encode /
decode and a GNN decoder; do not widen it to SpectrumProvider.
Interchangeable under the uncontrolled peer contract:
DMDBaseline,
EDMDBaseline, and
GraphKoopmanModel when called as
predict(data, steps).
Dataset factory idioms¶
koopman_graph.datasets uses two complementary factory styles:
Simulated dynamics —
Benchmark.generate(...)synthesizes aGraphSnapshotSequence(synthetic path/ring, grid, IEEE 118 voltage/load diffusion). Prefer these classmethods in tutorials and application code.Real telemetry —
Benchmark.load_topology/load_sequence(METR-LA) read cached downloaded artifacts. There is nogeneratebecause the time series is observed, not simulated.
Seed defaults. All simulated generate methods default seed=None
(unseeded RNG). Pass an explicit integer for reproducible runs; tutorial
notebooks and quickstart examples use seed=42.
Topology payloads. IEEE 118 and METR-LA load_topology return a frozen
TopologyPayload (attribute access preferred).
Mapping-style access (payload["edge_index"]) remains supported for existing
notebooks. On-disk caches stay plain dict; the typed payload is the public
Python return type.
IEEE 118 also exposes a module-level load_topology free function used by
download scripts; prefer IEEE118DynamicBenchmark.load_topology.
Generation validators. Shared physical parameters for Laplacian-diffusion
benchmarks (synthetic, grid, IEEE 118) route through
validate_diffusion_generation_params()
(diffusion_rate ∈ [0, 1], decay_rate > 0, noise_std ≥ 0, optional
initial_state). Anisotropic advection uses the same helper for noise /
initial state, but its self-retention factor is validated separately by
validate_advection_decay_rate()
(decay_rate ∈ (0, 1)) — same parameter name, stricter open interval.
Benchmark-specific knobs (load ramp, grid size, neighbor weights) stay local.
Laplacian helpers: sparse vs dense¶
The symmetric normalized Laplacian L_sym = I - D^{-1/2} A D^{-1/2} has one
shared weight core in koopman_graph.graph_utils
(symmetric_normalized_adjacency_edge_weights()):
Sparse —
graph_laplacian_features()appliesL_sym @ xvia matvec for hybrid physics lifting during training.Dense —
koopman_graph.datasets.dynamicsbuildsI - alpha * L_symstep operators for offline benchmark rollouts.
Keep both surfaces: they are numerically aligned on the same L_sym but
serve different performance and API roles.
Hybrid physics checkpoint schema¶
When hybrid observables are enabled, checkpoint config.physics owns
dim, preset, and position. build_model_config()
writes position from model.physics_position;
reconstruct_model() reads and validates it via
resolve_physics_position() (missing position
defaults to "prepend"). Only "prepend" is supported today — unsupported
values raise on load so a future "append" mode cannot silently mis-restore.
No FORMAT_VERSION bump was required: the field was already emitted in v2.
v0.3.0 architectural consistency outcomes¶
Phase 8 standardized style and release quality without bumping the package
beyond 0.3.0 or FORMAT_VERSION 2. Outcomes folded into the first public
v0.3.0 cut:
Highest-impact API remediations (second style audit)
Shared rollout —
predict/ training losses / the RL env shareautoregressive_latent_rollout()(and one-stepadvance_and_decode), with hold-last vs teacher-target topology policies documented above.``ForecastModel`` call sites — loose Protocol for method presence;
UncontrolledForecastModelfor portablepredict(data, steps)among autonomous peers (see call-site matrix).Optional operator injection —
koopman=composes a customKoopmanOperatorContractnn.Modulewith Protocol-complete propagate / spectrum / eigenvalue paths; RLS and checkpoints remain built-in-only (see Optional operator injection).Classical baseline scaffolding —
ClassicalBaselineABC; EDMD storesreconstruction_matrix(not a GNNdecoder).Frozen public result types —
FitHistory,AdaptationStepResult, and related dataclasses usefrozen=Truewith tuple series;TrainingLossBreakdownis a frozen internal snapshot (not package-exported).
Contract and boundary follow-ups
Honest continuous/discrete dispatch for injected operators; sequence immutability; topology and control-layout capability guards; shared GNN primitives; thin
fitorchestration; spectrum-based dynamical similarity; shared dataset validators; centralized dynamics/stability vocabulary; hybridphysics.positionround-trip; loss-breakdown hygiene.
Tutorial claim↔result integrity
Notebooks 02 / 03 / 06 / 14 / 15 remediations so scientific claims match stored outputs (history-constrained Vm scope, METR-LA baseline protocol, epidemic spectrum wording, physics-informed evidence scope, RL takeaway metrics).
Capability packages and thin façade
Layout policy plus
training/,data/,operators/,nn/,analysis/, andbaselines/capability packages; deep-import shim hard cut; thin root__all__with metrics / analysis / data / adaptation / observables secondaries demoted to capability imports.
Analysis UX and release quality
plot_spectrum()withlimits="unit_disk"/"data"(capability-module import only).Enforced pytest coverage gate of 90% (
fail_under/ CI--cov-fail-under); branch-aware suite remains well above the floor.