From 4d8d70ca9de1e70d46f0c08345bb00981dc1d32d Mon Sep 17 00:00:00 2001 From: EddieCunningham Date: Thu, 11 Jun 2026 12:25:51 -0400 Subject: [PATCH 1/2] Agent skills for paper preproduction --- agent_skills/grill-with-docs/ADR-FORMAT.md | 47 +++++ agent_skills/grill-with-docs/ATTRIBUTION.md | 10 ++ .../grill-with-docs/CONTEXT-FORMAT.md | 60 +++++++ agent_skills/grill-with-docs/SKILL.md | 88 ++++++++++ agent_skills/implement-with-dynestyx/SKILL.md | 45 +++++ .../inference-filtering.md | 71 ++++++++ .../implement-with-dynestyx/inference-mcmc.md | 44 +++++ .../inference-smoothing.md | 34 ++++ .../implement-with-dynestyx/inference-svi.md | 50 ++++++ .../implement-with-dynestyx/reference.md | 160 ++++++++++++++++++ 10 files changed, 609 insertions(+) create mode 100644 agent_skills/grill-with-docs/ADR-FORMAT.md create mode 100644 agent_skills/grill-with-docs/ATTRIBUTION.md create mode 100644 agent_skills/grill-with-docs/CONTEXT-FORMAT.md create mode 100644 agent_skills/grill-with-docs/SKILL.md create mode 100644 agent_skills/implement-with-dynestyx/SKILL.md create mode 100644 agent_skills/implement-with-dynestyx/inference-filtering.md create mode 100644 agent_skills/implement-with-dynestyx/inference-mcmc.md create mode 100644 agent_skills/implement-with-dynestyx/inference-smoothing.md create mode 100644 agent_skills/implement-with-dynestyx/inference-svi.md create mode 100644 agent_skills/implement-with-dynestyx/reference.md diff --git a/agent_skills/grill-with-docs/ADR-FORMAT.md b/agent_skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 00000000..da7e78ec --- /dev/null +++ b/agent_skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/agent_skills/grill-with-docs/ATTRIBUTION.md b/agent_skills/grill-with-docs/ATTRIBUTION.md new file mode 100644 index 00000000..95e588ab --- /dev/null +++ b/agent_skills/grill-with-docs/ATTRIBUTION.md @@ -0,0 +1,10 @@ +# Attribution + +This skill was written by Matt Pocock and is vendored here unmodified from his skills repository, which is distributed under the MIT License. + +- Author. Matt Pocock +- Repository. https://github.com/mattpocock/skills +- Skill source. https://github.com/mattpocock/skills/tree/main/skills/engineering/grill-with-docs +- License. MIT + +The vendored files are `SKILL.md`, `CONTEXT-FORMAT.md`, and `ADR-FORMAT.md`. diff --git a/agent_skills/grill-with-docs/CONTEXT-FORMAT.md b/agent_skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 00000000..eaf2a185 --- /dev/null +++ b/agent_skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/agent_skills/grill-with-docs/SKILL.md b/agent_skills/grill-with-docs/SKILL.md new file mode 100644 index 00000000..5ea0aa91 --- /dev/null +++ b/agent_skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/agent_skills/implement-with-dynestyx/SKILL.md b/agent_skills/implement-with-dynestyx/SKILL.md new file mode 100644 index 00000000..a20a8841 --- /dev/null +++ b/agent_skills/implement-with-dynestyx/SKILL.md @@ -0,0 +1,45 @@ +--- +name: implement-with-dynestyx +description: Re-implements an existing piece of work, usually a research paper, as a minimal and tested dynestyx notebook or sub-repo. Use when the user wants to reproduce or port a dynamical-systems or state-space paper into dynestyx, points at a paper PDF or an arXiv or biorxiv link and asks for a dynestyx version, or asks to build a state-space, filtering, particle-filter, smoothing, pseudo-marginal MCMC, or SVI model in dynestyx from a described model. Use this even when the user does not name the skill but describes a dynamical-systems modeling task to carry out in dynestyx. +--- + +# Implement with dynestyx + +`dynestyx` is a NumPyro extension for Bayesian inference in dynamical systems. A model is a NumPyro model function that builds a `DynamicalModel` and returns `dsx.sample(...)`. Inference is selected by wrapping that same model in a handler (`Filter`, `Smoother`, or a simulator) and running it through NumPyro's `Predictive`, `MCMC`, or `SVI`. The same model serves every inference method. + +## Workflow + +Copy this checklist into your response and tick items off as you go. + +``` +- [ ] 1. Acquire the source +- [ ] 2. Extract the model spec +- [ ] 3. Grill the user on requirements +- [ ] 4. Map the spec to dynestyx +- [ ] 5. Build, validate, deliver +``` + +### 1. Acquire the source + +If the input is a PDF, first ask the user whether they can provide the TeX source. TeX gives exact equations, while a PDF forces error-prone extraction of math. If the user cannot provide it, read the PDF and find the source online, and fall back to the PDF when no source exists. If the paper ships code, pull that codebase into a scratch location and keep it as a reference for the dynamics and the data, without letting its style drive yours. + +### 2. Extract the model spec + +Distill a precise spec that the later phases can implement directly. A complete spec states the latent state and its dimension, the dynamics and whether they are discrete or continuous in time and deterministic or stochastic, the observation model, the parameters and which of them are unknown, the priors on the unknowns, and the inference target of states or parameters or both. The discrete-versus-continuous and linear-Gaussian-versus-nonlinear choices decide most of the downstream API, so settle them here. + +### 3. Grill the user on requirements + +Invoke the `grill-with-docs` skill to settle the open requirements with the user. Cover the deliverable shape of a single notebook versus a sub-repo and where it lands, whether to use simulated data or the paper's real data, which model hypotheses from the paper to include, and the concrete success signal that will count as done. + +### 4. Map the spec to dynestyx + +Read [reference.md](reference.md), then exactly one inference file picked from the spec. Translate the spec into idiomatic dynestyx using only those files. Avoid reading the wider `docs/` tree. + +- Online state estimates or the marginal likelihood, via Kalman-family, particle, or HMM filters → [inference-filtering.md](inference-filtering.md) +- State estimates that use the full observation window → [inference-smoothing.md](inference-smoothing.md) +- Posterior over unknown parameters, pseudo-marginal or joint MCMC → [inference-mcmc.md](inference-mcmc.md) +- Fast approximate posterior for larger problems → [inference-svi.md](inference-svi.md) + +### 5. Build, validate, deliver + +Write the deliverable and run it end to end. Show one correctness signal, such as recovering known generating parameters on simulated data or reproducing the paper's qualitative result. Each inference file names the natural signal for its method. Do not hand over code that has not been executed. diff --git a/agent_skills/implement-with-dynestyx/inference-filtering.md b/agent_skills/implement-with-dynestyx/inference-filtering.md new file mode 100644 index 00000000..94e806fc --- /dev/null +++ b/agent_skills/implement-with-dynestyx/inference-filtering.md @@ -0,0 +1,71 @@ +# Filtering + +Filters compute the filtering distribution over the latent state given past observations and return the marginal log-likelihood `f_marginal_loglik`. Read [reference.md](reference.md) first. + +## Choosing a filter + +Match the filter to the model. + +- Linear dynamics with Gaussian noise → `KFConfig`, the exact Kalman filter. +- Nonlinear dynamics, roughly Gaussian → `EKFConfig` for a Jacobian linearization or `UKFConfig` for a derivative-free sigma-point version. +- Nonlinear dynamics, higher dimension, ensemble approach → `EnKFConfig`. A bare `Filter()` defaults to this. +- Nonlinear and non-Gaussian, arbitrary likelihoods, tracking → `PFConfig`, the bootstrap particle filter. The right default whenever the observation model is not Gaussian, which is the common reason a Kalman variant fails. +- Discrete latent regimes → `HMMConfig`. + +Every Gaussian filter and the particle filter has a continuous-time variant for models with an SDE between observations, named `ContinuousTimeKFConfig`, `ContinuousTimeEKFConfig`, `ContinuousTimeUKFConfig`, `ContinuousTimeEnKFConfig`, and `ContinuousTimeDPFConfig`. Use these when the dynamics are continuous in time and you do not want to discretize first. + +Import configs from `dynestyx.inference.filters`. `Filter` is exported from the top level. + +## Likelihood sweeps and recording states + +Wrap the conditioning pattern from [reference.md](reference.md) in a function and `vmap` it to profile the marginal log-likelihood over a parameter. The argmax is a quick parameter estimate and a good first correctness check on simulated data. + +```python +import jax.random as jr +from jax import vmap +from numpyro.infer import Predictive +from dynestyx import Filter +from dynestyx.inference.filters import EKFConfig, PFConfig + +def get_mll(rho, filter_config): + with Filter(filter_config): + out = Predictive(model, params={"rho": rho}, num_samples=1, + exclude_deterministic=False)( + jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) + return out["f_marginal_loglik"] + +rho_grid = jnp.linspace(-0.8, 0.8, 50) +mll_pf = vmap(lambda r: get_mll(r, PFConfig(n_particles=1000)))(rho_grid) +rho_hat = rho_grid[jnp.argmax(mll_pf)] +``` + +Filtered state estimates are recorded on request and then read from the trace. + +```python +with Filter(filter_config=EKFConfig(record_filtered_states_mean=True)): + out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, + exclude_deterministic=False)( + jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) +filtered_means = out["f_filtered_states_mean"] # (num_samples, T, state_dim) +``` + +## Particle filter notes + +`PFConfig(n_particles=...)` runs the bootstrap particle filter and handles arbitrary nonlinear dynamics and non-Gaussian observation models. Set `n_particles` from the difficulty of the problem, in the thousands for hard tracking. Resampling is configured through `PFResamplingConfig`. For a continuous-time SDE with discrete observations, use `ContinuousTimeDPFConfig` instead. + +## Forecasting + +To roll a fitted model forward, nest a simulator outside the filter and pass `predict_times`. The filter conditions on the training window and the simulator predicts beyond it. + +```python +from dynestyx import DiscreteTimeSimulator + +with DiscreteTimeSimulator(n_simulations=20): + with Filter(filter_config=EKFConfig(record_filtered_states_mean=True)): + out = Predictive(model, params={"rho": rho_hat}, num_samples=1, + exclude_deterministic=False)( + jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values, + ctrl_times=ctrl_times_full, ctrl_values=ctrl_values_full, + predict_times=future_times) +pred_states = out["f_predicted_states"] # (num_samples, n_sim, T_pred, state_dim) +``` diff --git a/agent_skills/implement-with-dynestyx/inference-mcmc.md b/agent_skills/implement-with-dynestyx/inference-mcmc.md new file mode 100644 index 00000000..2617ce5b --- /dev/null +++ b/agent_skills/implement-with-dynestyx/inference-mcmc.md @@ -0,0 +1,44 @@ +# MCMC + +MCMC draws a posterior over the unknown parameters. Read [reference.md](reference.md) first. There are two modes, and the only difference is the handler you wrap around the model. + +## Pseudo-marginal MCMC + +Wrap the model in a `Filter`. The filter integrates out the latent states and contributes the marginal log-likelihood, so NUTS explores the parameters with the states marginalized. This is the method for nonlinear or non-Gaussian models where the states are nuisance variables. + +```python +import jax.random as jr +from numpyro.infer import MCMC, NUTS +from dynestyx import Filter +from dynestyx.inference.filters import EKFConfig + +with Filter(filter_config=EKFConfig()): + nuts_kernel = NUTS(model) + mcmc = MCMC(nuts_kernel, num_warmup=100, num_samples=100) + mcmc.run(jr.PRNGKey(1), obs_times=obs_times, obs_values=obs_values, + ctrl_times=ctrl_times, ctrl_values=ctrl_values) +posterior = mcmc.get_samples() +``` + +Choose the filter by the rules in [inference-filtering.md](inference-filtering.md). Use a particle filter here for non-Gaussian likelihoods, which gives a particle-marginal Metropolis-Hastings style sampler. + +## Joint state and parameter MCMC + +Wrap the model in a simulator instead of a filter. The latent states stay in the trace, so NUTS samples states and parameters together. This suits linear-Gaussian or mildly nonlinear models where the joint posterior is well behaved. + +```python +from dynestyx import DiscreteTimeSimulator + +with DiscreteTimeSimulator(): + nuts_kernel = NUTS(model) + mcmc = MCMC(nuts_kernel, num_warmup=100, num_samples=100) + mcmc.run(jr.PRNGKey(1), obs_times=obs_times, obs_values=obs_values, + ctrl_times=ctrl_times, ctrl_values=ctrl_values) +posterior = mcmc.get_samples() +``` + +## After sampling + +Read parameter draws by name, as in `posterior["rho"]`. A posterior whose mass covers the known generating value is a clean correctness signal on simulated data. To forecast from the fitted model, take a posterior summary such as the mean and follow the forecasting pattern in [inference-filtering.md](inference-filtering.md). + +Gradient-based and stochastic-gradient kernels are also available through `dynestyx.inference.mcmc_configs`, including `NUTSConfig`, `HMCConfig`, `MALAConfig`, and `SGLDConfig`. Start with NUTS and reach for these only when scale or geometry demands it. diff --git a/agent_skills/implement-with-dynestyx/inference-smoothing.md b/agent_skills/implement-with-dynestyx/inference-smoothing.md new file mode 100644 index 00000000..f425daea --- /dev/null +++ b/agent_skills/implement-with-dynestyx/inference-smoothing.md @@ -0,0 +1,34 @@ +# Smoothing + +Smoothers estimate the latent state at each time using the whole observation window, both past and future. They give better state estimates than a filter when the goal is to reconstruct a trajectory after the fact rather than online. Read [reference.md](reference.md) first. + +## Choosing a smoother + +The smoother families mirror the filters, so pick by the same model shape. + +- Linear Gaussian → `KFSmootherConfig`, the exact Rauch-Tung-Striebel smoother. +- Nonlinear, roughly Gaussian → `EKFSmootherConfig` or `UKFSmootherConfig`. +- Nonlinear and non-Gaussian → `PFSmootherConfig`, the particle smoother with backward sampling. + +Continuous-time variants exist as `ContinuousTimeKFSmootherConfig` and `ContinuousTimeEKFSmootherConfig` for models with an SDE between observations. Import these from `dynestyx.inference.smoothers`. `Smoother` is exported from the top level. + +## Run a smoother + +The pattern matches a filter. Wrap the model in `Smoother`, request the smoothed states, and read them from the trace. + +```python +import jax.random as jr +from numpyro.infer import Predictive +from dynestyx import Smoother +from dynestyx.inference.smoothers import KFSmootherConfig + +with Smoother(smoother_config=KFSmootherConfig(record_smoothed_states_mean=True)): + out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, + exclude_deterministic=False)( + jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) + +mll = out["f_marginal_loglik"] +smoothed_means = out["f_smoothed_states_mean"] # (num_samples, T, state_dim) +``` + +A smoother also returns `f_marginal_loglik`, so it can drive parameter inference exactly as a filter does. Overlaying the smoothed mean on the true simulated states is the correctness signal for a smoothing task. For online estimates or for the pseudo-marginal likelihood alone, use a filter from [inference-filtering.md](inference-filtering.md) instead. diff --git a/agent_skills/implement-with-dynestyx/inference-svi.md b/agent_skills/implement-with-dynestyx/inference-svi.md new file mode 100644 index 00000000..2fd79874 --- /dev/null +++ b/agent_skills/implement-with-dynestyx/inference-svi.md @@ -0,0 +1,50 @@ +# SVI + +SVI fits an approximate posterior by optimizing a variational guide. It trades exactness for speed and scale. Read [reference.md](reference.md) first. The handler around the model decides what is inferred. + +## Filter-conditioned SVI + +Define a zero-argument wrapper that conditions the model under a `Filter`, build an autoguide over it, and optimize the ELBO. The guide then approximates the posterior over the parameters with the states marginalized. + +```python +import jax.random as jr +import optax +from numpyro.infer import SVI, Trace_ELBO, Predictive +from numpyro.infer.autoguide import AutoMultivariateNormal +from dynestyx import Filter + +def filter_conditioned_model(): + with Filter(): # ensemble Kalman filter by default + return model(obs_times=obs_times, obs_values=obs_values, + ctrl_times=ctrl_times, ctrl_values=ctrl_values) + +guide = AutoMultivariateNormal(filter_conditioned_model) +optimizer = optax.adam(learning_rate=1e-3) +svi = SVI(filter_conditioned_model, guide, optimizer, loss=Trace_ELBO()) +svi_result = svi.run(jr.PRNGKey(1), num_steps=1500) + +posterior = Predictive(guide, params=svi_result.params, num_samples=500)(jr.PRNGKey(2)) +``` + +Pick the filter inside the wrapper by the rules in [inference-filtering.md](inference-filtering.md). + +## Joint SVI + +Swap the filter for a simulator in the wrapper to put the latent states in the guide and infer states and parameters jointly. + +```python +from dynestyx import DiscreteTimeSimulator + +def joint_conditioned_model(): + with DiscreteTimeSimulator(): + return model(obs_times=obs_times, obs_values=obs_values, + ctrl_times=ctrl_times, ctrl_values=ctrl_values) + +guide = AutoMultivariateNormal(joint_conditioned_model) +svi = SVI(joint_conditioned_model, guide, optimizer, loss=Trace_ELBO()) +svi_result = svi.run(jr.PRNGKey(3), num_steps=15000) +``` + +## After fitting + +Sample the fitted guide with `Predictive(guide, params=svi_result.params, num_samples=...)` and read parameter draws by name. Recovering the known generating value on simulated data is the correctness signal. Watch the ELBO trace in `svi_result.losses` for convergence, and raise `num_steps` or lower the learning rate when it has not flattened. diff --git a/agent_skills/implement-with-dynestyx/reference.md b/agent_skills/implement-with-dynestyx/reference.md new file mode 100644 index 00000000..8cd4e6b1 --- /dev/null +++ b/agent_skills/implement-with-dynestyx/reference.md @@ -0,0 +1,160 @@ +# dynestyx core reference + +The spine for writing dynestyx code. Read this before any inference file. + +## Contents + +- The model function contract +- State evolution +- Observation models +- Initial condition and diffusions +- Simulate data +- Condition on data and run inference +- Output sites and shapes +- Hierarchical models with plates +- Gotchas + +## The model function contract + +A dynestyx model is an ordinary NumPyro model function. It samples unknown parameters with `numpyro.sample`, builds a `DynamicalModel`, and returns `dsx.sample(name, dynamics, ...)`. The same function is reused for simulation and for every inference method. Inference is chosen by the handler you wrap around it, not by editing the model. + +```python +import jax.numpy as jnp +import numpyro +import numpyro.distributions as dist +import dynestyx as dsx +from dynestyx import DynamicalModel + +def model( + obs_times=None, + obs_values=None, + ctrl_times=None, + ctrl_values=None, + predict_times=None, +): + rho = numpyro.sample("rho", dist.Uniform(-0.5, 0.5)) + A = jnp.array([[0.0, 0.3], [rho, -0.2]]) + + # Anything that depends on a sampled parameter MUST be built inside the + # model, so the dependence is traced. + state_evolution = lambda x, u, t_now, t_next: dist.MultivariateNormal( + A @ x, 0.1**2 * jnp.eye(2) + ) + observation_model = lambda x, u, t: dist.Normal(x[0], 0.1) + + dynamics = DynamicalModel( + initial_condition=dist.MultivariateNormal(jnp.zeros(2), jnp.eye(2)), + state_evolution=state_evolution, + observation_model=observation_model, + ) + return dsx.sample( + "f", + dynamics, + obs_times=obs_times, + obs_values=obs_values, + ctrl_times=ctrl_times, + ctrl_values=ctrl_values, + predict_times=predict_times, + ) +``` + +The name passed to `dsx.sample` becomes the prefix on every output site. With `"f"` you read `f_observations`, `f_states`, `f_times`, `f_marginal_loglik`, `f_filtered_states_mean`, `f_predicted_states`, and so on. + +`dsx.sample` takes `obs_times` and `obs_values` for observations to condition on, `ctrl_times` and `ctrl_values` for known controls, and `predict_times` for times to roll forward and predict. Pass `predict_times` to generate data, pass `obs_times` and `obs_values` to condition. + +## State evolution + +Three ways to specify dynamics, in rising order of structure. + +Plain callable. Discrete time maps `(x, u, t_now, t_next)` to a distribution. Continuous time supplies a drift `(x, u, t)`. + +```python +# discrete time transition +state_evolution = lambda x, u, t_now, t_next: dist.MultivariateNormal(A @ x, Q) + +# continuous time drift, wrapped with a diffusion for an SDE +from dynestyx import ContinuousTimeStateEvolution, ScalarDiffusion +state_evolution = ContinuousTimeStateEvolution( + drift=lambda x, u, t: A @ x, + diffusion=ScalarDiffusion(1.0, bm_dim=2), +) +``` + +Typed classes. `LinearGaussianStateEvolution(A, cov, B=None, bias=None)` for `x_next ~ N(A x + B u + bias, cov)`. `GaussianStateEvolution(F, cov)` for a nonlinear mean `F(x, u, t_now, t_next)`. + +LTI factories. `LTI_discrete(A, Q, H, R, ...)` and `LTI_continuous(A, L, H, R, ...)` build a whole linear-Gaussian `DynamicalModel` in one call, including the observation model and a default standard-normal initial condition. + +A continuous-time model with a diffusion is an SDE and runs under `SDESimulator`. A continuous-time model with no diffusion is an ODE and runs under `ODESimulator`. Both can also be discretized for filtering with `Discretizer`. + +## Observation models + +Plain callable mapping `(x, u, t)` to a distribution, as in `lambda x, u, t: dist.Normal(x[0], sigma)`. Non-Gaussian likelihoods such as Poisson counts or heavy-tailed noise go here. Typed options are `LinearGaussianObservation(H, R, D=None, bias=None)` for `y ~ N(H x + D u + bias, R)`, `GaussianObservation(h, R)` for a nonlinear mean, and `DiracIdentityObservation()` for noiseless direct observation of the state. + +## Initial condition and diffusions + +The initial condition is any NumPyro distribution over the state, commonly `dist.MultivariateNormal(loc, cov)`. + +Diffusions set the SDE noise. `ScalarDiffusion(sigma, bm_dim=d)` gives isotropic noise, `DiagonalDiffusion(vec, bm_dim=d)` gives per-coordinate noise, and `FullDiffusion(matrix, bm_dim=k)` gives a general state-by-Brownian matrix. The coefficient is a constant or a callable `(x, u, t)`. + +## Simulate data + +Generate synthetic data by running the model under a simulator with parameters held fixed. Pass `predict_times` for the times to produce. Use `exclude_deterministic=False` so the output sites are returned. + +```python +import jax.random as jr +from numpyro.infer import Predictive +from dynestyx import DiscreteTimeSimulator + +predictive = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, + exclude_deterministic=False) +with DiscreteTimeSimulator(): + pred = predictive(jr.PRNGKey(0), predict_times=obs_times, + ctrl_times=ctrl_times, ctrl_values=ctrl_values) + +obs_values = pred["f_observations"][0, 0] # (T, obs_dim) +states_true = pred["f_states"][0, 0] # (T, state_dim) +``` + +Pick the simulator to match the dynamics. `DiscreteTimeSimulator` for discrete time, `SDESimulator` for an SDE, `ODESimulator` for an ODE. Each accepts `n_simulations` to draw several trajectories at once. + +## Condition on data and run inference + +Conditioning happens by wrapping the model in `Filter` (or `Smoother`) and supplying `obs_times` and `obs_values`. The filter integrates out the latent states and adds the marginal log-likelihood to the trace as a `numpyro.factor`, which is what makes parameter inference work. + +```python +from dynestyx import Filter +from dynestyx.inference.filters import EKFConfig + +with Filter(filter_config=EKFConfig()): + out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, + exclude_deterministic=False)( + jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) +mll = out["f_marginal_loglik"] +``` + +To profile the likelihood over a parameter, `vmap` the call. The mcmc, svi, smoothing, and filtering files build on this same wrap-then-run shape. + +## Output sites and shapes + +With the `dsx.sample` name `"f"`, common sites are below. Simulator outputs carry leading `(num_samples, n_simulations, ...)` axes. Filter and smoother summaries carry a leading `(num_samples, ...)` axis. + +- `f_observations` shape `(num_samples, n_sim, T, obs_dim)` +- `f_states` shape `(num_samples, n_sim, T, state_dim)` +- `f_times` shape `(num_samples, n_sim, T)` +- `f_marginal_loglik` the conditioned log-likelihood +- `f_filtered_states_mean` shape `(num_samples, T, state_dim)` when recorded +- `f_predicted_states` and `f_predicted_times` for forecasts at `predict_times` + +`flatten_draws` merges the `(num_samples, n_sim, T, D)` axes into `(num_samples * n_sim, T, D)` for plotting. + +## Hierarchical models with plates + +`dsx.plate(name, size)` batches several trajectories or groups. Sample shared or group-level parameters inside the plate, then call `dsx.sample` inside it. Observation arrays gain a leading group axis of `(N, T, obs_dim)`. + +## Gotchas + +- Build anything that depends on a sampled parameter inside the model function, never at module scope, or the dependence is not traced. +- Pass `exclude_deterministic=False` to `Predictive` to get the output sites back. +- Conditioning needs both `obs_times` and `obs_values`. Generating needs `predict_times`. +- `SDESimulator` does not condition on observations. Use a `Filter` to condition a continuous-time stochastic model. +- Run everything through the project uv environment with `uv run`. From 04e7b644dc1131c7b7d16688644ae11aae73c71e Mon Sep 17 00:00:00 2001 From: EddieCunningham Date: Thu, 18 Jun 2026 15:40:08 -0400 Subject: [PATCH 2/2] Agent skill now focuses on grilling model to be implemented --- agent_skills/grill-with-docs/ADR-FORMAT.md | 47 ----- agent_skills/grill-with-docs/ATTRIBUTION.md | 10 -- .../grill-with-docs/CONTEXT-FORMAT.md | 60 ------- agent_skills/grill-with-docs/SKILL.md | 88 ---------- agent_skills/implement-with-dynestyx/SKILL.md | 45 ----- .../inference-filtering.md | 71 -------- .../implement-with-dynestyx/inference-mcmc.md | 44 ----- .../inference-smoothing.md | 34 ---- .../implement-with-dynestyx/inference-svi.md | 50 ------ .../implement-with-dynestyx/reference.md | 160 ------------------ agent_skills/modeling-with-dynestyx/SKILL.md | 88 ++++++++++ 11 files changed, 88 insertions(+), 609 deletions(-) delete mode 100644 agent_skills/grill-with-docs/ADR-FORMAT.md delete mode 100644 agent_skills/grill-with-docs/ATTRIBUTION.md delete mode 100644 agent_skills/grill-with-docs/CONTEXT-FORMAT.md delete mode 100644 agent_skills/grill-with-docs/SKILL.md delete mode 100644 agent_skills/implement-with-dynestyx/SKILL.md delete mode 100644 agent_skills/implement-with-dynestyx/inference-filtering.md delete mode 100644 agent_skills/implement-with-dynestyx/inference-mcmc.md delete mode 100644 agent_skills/implement-with-dynestyx/inference-smoothing.md delete mode 100644 agent_skills/implement-with-dynestyx/inference-svi.md delete mode 100644 agent_skills/implement-with-dynestyx/reference.md create mode 100644 agent_skills/modeling-with-dynestyx/SKILL.md diff --git a/agent_skills/grill-with-docs/ADR-FORMAT.md b/agent_skills/grill-with-docs/ADR-FORMAT.md deleted file mode 100644 index da7e78ec..00000000 --- a/agent_skills/grill-with-docs/ADR-FORMAT.md +++ /dev/null @@ -1,47 +0,0 @@ -# ADR Format - -ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. - -Create the `docs/adr/` directory lazily — only when the first ADR is needed. - -## Template - -```md -# {Short title of the decision} - -{1-3 sentences: what's the context, what did we decide, and why.} -``` - -That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. - -## Optional sections - -Only include these when they add genuine value. Most ADRs won't need them. - -- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited -- **Considered Options** — only when the rejected alternatives are worth remembering -- **Consequences** — only when non-obvious downstream effects need to be called out - -## Numbering - -Scan `docs/adr/` for the highest existing number and increment by one. - -## When to offer an ADR - -All three of these must be true: - -1. **Hard to reverse** — the cost of changing your mind later is meaningful -2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" -3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons - -If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." - -### What qualifies - -- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." -- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." -- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. -- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. -- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. -- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." -- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/agent_skills/grill-with-docs/ATTRIBUTION.md b/agent_skills/grill-with-docs/ATTRIBUTION.md deleted file mode 100644 index 95e588ab..00000000 --- a/agent_skills/grill-with-docs/ATTRIBUTION.md +++ /dev/null @@ -1,10 +0,0 @@ -# Attribution - -This skill was written by Matt Pocock and is vendored here unmodified from his skills repository, which is distributed under the MIT License. - -- Author. Matt Pocock -- Repository. https://github.com/mattpocock/skills -- Skill source. https://github.com/mattpocock/skills/tree/main/skills/engineering/grill-with-docs -- License. MIT - -The vendored files are `SKILL.md`, `CONTEXT-FORMAT.md`, and `ADR-FORMAT.md`. diff --git a/agent_skills/grill-with-docs/CONTEXT-FORMAT.md b/agent_skills/grill-with-docs/CONTEXT-FORMAT.md deleted file mode 100644 index eaf2a185..00000000 --- a/agent_skills/grill-with-docs/CONTEXT-FORMAT.md +++ /dev/null @@ -1,60 +0,0 @@ -# CONTEXT.md Format - -## Structure - -```md -# {Context Name} - -{One or two sentence description of what this context is and why it exists.} - -## Language - -**Order**: -{A one or two sentence description of the term} -_Avoid_: Purchase, transaction - -**Invoice**: -A request for payment sent to a customer after delivery. -_Avoid_: Bill, payment request - -**Customer**: -A person or organization that places orders. -_Avoid_: Client, buyer, account -``` - -## Rules - -- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. -- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. -- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. -- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. - -## Single vs multi-context repos - -**Single context (most repos):** One `CONTEXT.md` at the repo root. - -**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: - -```md -# Context Map - -## Contexts - -- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders -- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments -- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping - -## Relationships - -- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking -- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices -- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` -``` - -The skill infers which structure applies: - -- If `CONTEXT-MAP.md` exists, read it to find contexts -- If only a root `CONTEXT.md` exists, single context -- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved - -When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/agent_skills/grill-with-docs/SKILL.md b/agent_skills/grill-with-docs/SKILL.md deleted file mode 100644 index 5ea0aa91..00000000 --- a/agent_skills/grill-with-docs/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: grill-with-docs -description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. ---- - - - -Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. - -Ask the questions one at a time, waiting for feedback on each question before continuing. - -If a question can be answered by exploring the codebase, explore the codebase instead. - - - - - -## Domain awareness - -During codebase exploration, also look for existing documentation: - -### File structure - -Most repos have a single context: - -``` -/ -├── CONTEXT.md -├── docs/ -│ └── adr/ -│ ├── 0001-event-sourced-orders.md -│ └── 0002-postgres-for-write-model.md -└── src/ -``` - -If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: - -``` -/ -├── CONTEXT-MAP.md -├── docs/ -│ └── adr/ ← system-wide decisions -├── src/ -│ ├── ordering/ -│ │ ├── CONTEXT.md -│ │ └── docs/adr/ ← context-specific decisions -│ └── billing/ -│ ├── CONTEXT.md -│ └── docs/adr/ -``` - -Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. - -## During the session - -### Challenge against the glossary - -When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" - -### Sharpen fuzzy language - -When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." - -### Discuss concrete scenarios - -When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. - -### Cross-reference with code - -When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" - -### Update CONTEXT.md inline - -When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). - -`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. - -### Offer ADRs sparingly - -Only offer to create an ADR when all three are true: - -1. **Hard to reverse** — the cost of changing your mind later is meaningful -2. **Surprising without context** — a future reader will wonder "why did they do it this way?" -3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons - -If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). - - diff --git a/agent_skills/implement-with-dynestyx/SKILL.md b/agent_skills/implement-with-dynestyx/SKILL.md deleted file mode 100644 index a20a8841..00000000 --- a/agent_skills/implement-with-dynestyx/SKILL.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: implement-with-dynestyx -description: Re-implements an existing piece of work, usually a research paper, as a minimal and tested dynestyx notebook or sub-repo. Use when the user wants to reproduce or port a dynamical-systems or state-space paper into dynestyx, points at a paper PDF or an arXiv or biorxiv link and asks for a dynestyx version, or asks to build a state-space, filtering, particle-filter, smoothing, pseudo-marginal MCMC, or SVI model in dynestyx from a described model. Use this even when the user does not name the skill but describes a dynamical-systems modeling task to carry out in dynestyx. ---- - -# Implement with dynestyx - -`dynestyx` is a NumPyro extension for Bayesian inference in dynamical systems. A model is a NumPyro model function that builds a `DynamicalModel` and returns `dsx.sample(...)`. Inference is selected by wrapping that same model in a handler (`Filter`, `Smoother`, or a simulator) and running it through NumPyro's `Predictive`, `MCMC`, or `SVI`. The same model serves every inference method. - -## Workflow - -Copy this checklist into your response and tick items off as you go. - -``` -- [ ] 1. Acquire the source -- [ ] 2. Extract the model spec -- [ ] 3. Grill the user on requirements -- [ ] 4. Map the spec to dynestyx -- [ ] 5. Build, validate, deliver -``` - -### 1. Acquire the source - -If the input is a PDF, first ask the user whether they can provide the TeX source. TeX gives exact equations, while a PDF forces error-prone extraction of math. If the user cannot provide it, read the PDF and find the source online, and fall back to the PDF when no source exists. If the paper ships code, pull that codebase into a scratch location and keep it as a reference for the dynamics and the data, without letting its style drive yours. - -### 2. Extract the model spec - -Distill a precise spec that the later phases can implement directly. A complete spec states the latent state and its dimension, the dynamics and whether they are discrete or continuous in time and deterministic or stochastic, the observation model, the parameters and which of them are unknown, the priors on the unknowns, and the inference target of states or parameters or both. The discrete-versus-continuous and linear-Gaussian-versus-nonlinear choices decide most of the downstream API, so settle them here. - -### 3. Grill the user on requirements - -Invoke the `grill-with-docs` skill to settle the open requirements with the user. Cover the deliverable shape of a single notebook versus a sub-repo and where it lands, whether to use simulated data or the paper's real data, which model hypotheses from the paper to include, and the concrete success signal that will count as done. - -### 4. Map the spec to dynestyx - -Read [reference.md](reference.md), then exactly one inference file picked from the spec. Translate the spec into idiomatic dynestyx using only those files. Avoid reading the wider `docs/` tree. - -- Online state estimates or the marginal likelihood, via Kalman-family, particle, or HMM filters → [inference-filtering.md](inference-filtering.md) -- State estimates that use the full observation window → [inference-smoothing.md](inference-smoothing.md) -- Posterior over unknown parameters, pseudo-marginal or joint MCMC → [inference-mcmc.md](inference-mcmc.md) -- Fast approximate posterior for larger problems → [inference-svi.md](inference-svi.md) - -### 5. Build, validate, deliver - -Write the deliverable and run it end to end. Show one correctness signal, such as recovering known generating parameters on simulated data or reproducing the paper's qualitative result. Each inference file names the natural signal for its method. Do not hand over code that has not been executed. diff --git a/agent_skills/implement-with-dynestyx/inference-filtering.md b/agent_skills/implement-with-dynestyx/inference-filtering.md deleted file mode 100644 index 94e806fc..00000000 --- a/agent_skills/implement-with-dynestyx/inference-filtering.md +++ /dev/null @@ -1,71 +0,0 @@ -# Filtering - -Filters compute the filtering distribution over the latent state given past observations and return the marginal log-likelihood `f_marginal_loglik`. Read [reference.md](reference.md) first. - -## Choosing a filter - -Match the filter to the model. - -- Linear dynamics with Gaussian noise → `KFConfig`, the exact Kalman filter. -- Nonlinear dynamics, roughly Gaussian → `EKFConfig` for a Jacobian linearization or `UKFConfig` for a derivative-free sigma-point version. -- Nonlinear dynamics, higher dimension, ensemble approach → `EnKFConfig`. A bare `Filter()` defaults to this. -- Nonlinear and non-Gaussian, arbitrary likelihoods, tracking → `PFConfig`, the bootstrap particle filter. The right default whenever the observation model is not Gaussian, which is the common reason a Kalman variant fails. -- Discrete latent regimes → `HMMConfig`. - -Every Gaussian filter and the particle filter has a continuous-time variant for models with an SDE between observations, named `ContinuousTimeKFConfig`, `ContinuousTimeEKFConfig`, `ContinuousTimeUKFConfig`, `ContinuousTimeEnKFConfig`, and `ContinuousTimeDPFConfig`. Use these when the dynamics are continuous in time and you do not want to discretize first. - -Import configs from `dynestyx.inference.filters`. `Filter` is exported from the top level. - -## Likelihood sweeps and recording states - -Wrap the conditioning pattern from [reference.md](reference.md) in a function and `vmap` it to profile the marginal log-likelihood over a parameter. The argmax is a quick parameter estimate and a good first correctness check on simulated data. - -```python -import jax.random as jr -from jax import vmap -from numpyro.infer import Predictive -from dynestyx import Filter -from dynestyx.inference.filters import EKFConfig, PFConfig - -def get_mll(rho, filter_config): - with Filter(filter_config): - out = Predictive(model, params={"rho": rho}, num_samples=1, - exclude_deterministic=False)( - jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) - return out["f_marginal_loglik"] - -rho_grid = jnp.linspace(-0.8, 0.8, 50) -mll_pf = vmap(lambda r: get_mll(r, PFConfig(n_particles=1000)))(rho_grid) -rho_hat = rho_grid[jnp.argmax(mll_pf)] -``` - -Filtered state estimates are recorded on request and then read from the trace. - -```python -with Filter(filter_config=EKFConfig(record_filtered_states_mean=True)): - out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, - exclude_deterministic=False)( - jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) -filtered_means = out["f_filtered_states_mean"] # (num_samples, T, state_dim) -``` - -## Particle filter notes - -`PFConfig(n_particles=...)` runs the bootstrap particle filter and handles arbitrary nonlinear dynamics and non-Gaussian observation models. Set `n_particles` from the difficulty of the problem, in the thousands for hard tracking. Resampling is configured through `PFResamplingConfig`. For a continuous-time SDE with discrete observations, use `ContinuousTimeDPFConfig` instead. - -## Forecasting - -To roll a fitted model forward, nest a simulator outside the filter and pass `predict_times`. The filter conditions on the training window and the simulator predicts beyond it. - -```python -from dynestyx import DiscreteTimeSimulator - -with DiscreteTimeSimulator(n_simulations=20): - with Filter(filter_config=EKFConfig(record_filtered_states_mean=True)): - out = Predictive(model, params={"rho": rho_hat}, num_samples=1, - exclude_deterministic=False)( - jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values, - ctrl_times=ctrl_times_full, ctrl_values=ctrl_values_full, - predict_times=future_times) -pred_states = out["f_predicted_states"] # (num_samples, n_sim, T_pred, state_dim) -``` diff --git a/agent_skills/implement-with-dynestyx/inference-mcmc.md b/agent_skills/implement-with-dynestyx/inference-mcmc.md deleted file mode 100644 index 2617ce5b..00000000 --- a/agent_skills/implement-with-dynestyx/inference-mcmc.md +++ /dev/null @@ -1,44 +0,0 @@ -# MCMC - -MCMC draws a posterior over the unknown parameters. Read [reference.md](reference.md) first. There are two modes, and the only difference is the handler you wrap around the model. - -## Pseudo-marginal MCMC - -Wrap the model in a `Filter`. The filter integrates out the latent states and contributes the marginal log-likelihood, so NUTS explores the parameters with the states marginalized. This is the method for nonlinear or non-Gaussian models where the states are nuisance variables. - -```python -import jax.random as jr -from numpyro.infer import MCMC, NUTS -from dynestyx import Filter -from dynestyx.inference.filters import EKFConfig - -with Filter(filter_config=EKFConfig()): - nuts_kernel = NUTS(model) - mcmc = MCMC(nuts_kernel, num_warmup=100, num_samples=100) - mcmc.run(jr.PRNGKey(1), obs_times=obs_times, obs_values=obs_values, - ctrl_times=ctrl_times, ctrl_values=ctrl_values) -posterior = mcmc.get_samples() -``` - -Choose the filter by the rules in [inference-filtering.md](inference-filtering.md). Use a particle filter here for non-Gaussian likelihoods, which gives a particle-marginal Metropolis-Hastings style sampler. - -## Joint state and parameter MCMC - -Wrap the model in a simulator instead of a filter. The latent states stay in the trace, so NUTS samples states and parameters together. This suits linear-Gaussian or mildly nonlinear models where the joint posterior is well behaved. - -```python -from dynestyx import DiscreteTimeSimulator - -with DiscreteTimeSimulator(): - nuts_kernel = NUTS(model) - mcmc = MCMC(nuts_kernel, num_warmup=100, num_samples=100) - mcmc.run(jr.PRNGKey(1), obs_times=obs_times, obs_values=obs_values, - ctrl_times=ctrl_times, ctrl_values=ctrl_values) -posterior = mcmc.get_samples() -``` - -## After sampling - -Read parameter draws by name, as in `posterior["rho"]`. A posterior whose mass covers the known generating value is a clean correctness signal on simulated data. To forecast from the fitted model, take a posterior summary such as the mean and follow the forecasting pattern in [inference-filtering.md](inference-filtering.md). - -Gradient-based and stochastic-gradient kernels are also available through `dynestyx.inference.mcmc_configs`, including `NUTSConfig`, `HMCConfig`, `MALAConfig`, and `SGLDConfig`. Start with NUTS and reach for these only when scale or geometry demands it. diff --git a/agent_skills/implement-with-dynestyx/inference-smoothing.md b/agent_skills/implement-with-dynestyx/inference-smoothing.md deleted file mode 100644 index f425daea..00000000 --- a/agent_skills/implement-with-dynestyx/inference-smoothing.md +++ /dev/null @@ -1,34 +0,0 @@ -# Smoothing - -Smoothers estimate the latent state at each time using the whole observation window, both past and future. They give better state estimates than a filter when the goal is to reconstruct a trajectory after the fact rather than online. Read [reference.md](reference.md) first. - -## Choosing a smoother - -The smoother families mirror the filters, so pick by the same model shape. - -- Linear Gaussian → `KFSmootherConfig`, the exact Rauch-Tung-Striebel smoother. -- Nonlinear, roughly Gaussian → `EKFSmootherConfig` or `UKFSmootherConfig`. -- Nonlinear and non-Gaussian → `PFSmootherConfig`, the particle smoother with backward sampling. - -Continuous-time variants exist as `ContinuousTimeKFSmootherConfig` and `ContinuousTimeEKFSmootherConfig` for models with an SDE between observations. Import these from `dynestyx.inference.smoothers`. `Smoother` is exported from the top level. - -## Run a smoother - -The pattern matches a filter. Wrap the model in `Smoother`, request the smoothed states, and read them from the trace. - -```python -import jax.random as jr -from numpyro.infer import Predictive -from dynestyx import Smoother -from dynestyx.inference.smoothers import KFSmootherConfig - -with Smoother(smoother_config=KFSmootherConfig(record_smoothed_states_mean=True)): - out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, - exclude_deterministic=False)( - jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) - -mll = out["f_marginal_loglik"] -smoothed_means = out["f_smoothed_states_mean"] # (num_samples, T, state_dim) -``` - -A smoother also returns `f_marginal_loglik`, so it can drive parameter inference exactly as a filter does. Overlaying the smoothed mean on the true simulated states is the correctness signal for a smoothing task. For online estimates or for the pseudo-marginal likelihood alone, use a filter from [inference-filtering.md](inference-filtering.md) instead. diff --git a/agent_skills/implement-with-dynestyx/inference-svi.md b/agent_skills/implement-with-dynestyx/inference-svi.md deleted file mode 100644 index 2fd79874..00000000 --- a/agent_skills/implement-with-dynestyx/inference-svi.md +++ /dev/null @@ -1,50 +0,0 @@ -# SVI - -SVI fits an approximate posterior by optimizing a variational guide. It trades exactness for speed and scale. Read [reference.md](reference.md) first. The handler around the model decides what is inferred. - -## Filter-conditioned SVI - -Define a zero-argument wrapper that conditions the model under a `Filter`, build an autoguide over it, and optimize the ELBO. The guide then approximates the posterior over the parameters with the states marginalized. - -```python -import jax.random as jr -import optax -from numpyro.infer import SVI, Trace_ELBO, Predictive -from numpyro.infer.autoguide import AutoMultivariateNormal -from dynestyx import Filter - -def filter_conditioned_model(): - with Filter(): # ensemble Kalman filter by default - return model(obs_times=obs_times, obs_values=obs_values, - ctrl_times=ctrl_times, ctrl_values=ctrl_values) - -guide = AutoMultivariateNormal(filter_conditioned_model) -optimizer = optax.adam(learning_rate=1e-3) -svi = SVI(filter_conditioned_model, guide, optimizer, loss=Trace_ELBO()) -svi_result = svi.run(jr.PRNGKey(1), num_steps=1500) - -posterior = Predictive(guide, params=svi_result.params, num_samples=500)(jr.PRNGKey(2)) -``` - -Pick the filter inside the wrapper by the rules in [inference-filtering.md](inference-filtering.md). - -## Joint SVI - -Swap the filter for a simulator in the wrapper to put the latent states in the guide and infer states and parameters jointly. - -```python -from dynestyx import DiscreteTimeSimulator - -def joint_conditioned_model(): - with DiscreteTimeSimulator(): - return model(obs_times=obs_times, obs_values=obs_values, - ctrl_times=ctrl_times, ctrl_values=ctrl_values) - -guide = AutoMultivariateNormal(joint_conditioned_model) -svi = SVI(joint_conditioned_model, guide, optimizer, loss=Trace_ELBO()) -svi_result = svi.run(jr.PRNGKey(3), num_steps=15000) -``` - -## After fitting - -Sample the fitted guide with `Predictive(guide, params=svi_result.params, num_samples=...)` and read parameter draws by name. Recovering the known generating value on simulated data is the correctness signal. Watch the ELBO trace in `svi_result.losses` for convergence, and raise `num_steps` or lower the learning rate when it has not flattened. diff --git a/agent_skills/implement-with-dynestyx/reference.md b/agent_skills/implement-with-dynestyx/reference.md deleted file mode 100644 index 8cd4e6b1..00000000 --- a/agent_skills/implement-with-dynestyx/reference.md +++ /dev/null @@ -1,160 +0,0 @@ -# dynestyx core reference - -The spine for writing dynestyx code. Read this before any inference file. - -## Contents - -- The model function contract -- State evolution -- Observation models -- Initial condition and diffusions -- Simulate data -- Condition on data and run inference -- Output sites and shapes -- Hierarchical models with plates -- Gotchas - -## The model function contract - -A dynestyx model is an ordinary NumPyro model function. It samples unknown parameters with `numpyro.sample`, builds a `DynamicalModel`, and returns `dsx.sample(name, dynamics, ...)`. The same function is reused for simulation and for every inference method. Inference is chosen by the handler you wrap around it, not by editing the model. - -```python -import jax.numpy as jnp -import numpyro -import numpyro.distributions as dist -import dynestyx as dsx -from dynestyx import DynamicalModel - -def model( - obs_times=None, - obs_values=None, - ctrl_times=None, - ctrl_values=None, - predict_times=None, -): - rho = numpyro.sample("rho", dist.Uniform(-0.5, 0.5)) - A = jnp.array([[0.0, 0.3], [rho, -0.2]]) - - # Anything that depends on a sampled parameter MUST be built inside the - # model, so the dependence is traced. - state_evolution = lambda x, u, t_now, t_next: dist.MultivariateNormal( - A @ x, 0.1**2 * jnp.eye(2) - ) - observation_model = lambda x, u, t: dist.Normal(x[0], 0.1) - - dynamics = DynamicalModel( - initial_condition=dist.MultivariateNormal(jnp.zeros(2), jnp.eye(2)), - state_evolution=state_evolution, - observation_model=observation_model, - ) - return dsx.sample( - "f", - dynamics, - obs_times=obs_times, - obs_values=obs_values, - ctrl_times=ctrl_times, - ctrl_values=ctrl_values, - predict_times=predict_times, - ) -``` - -The name passed to `dsx.sample` becomes the prefix on every output site. With `"f"` you read `f_observations`, `f_states`, `f_times`, `f_marginal_loglik`, `f_filtered_states_mean`, `f_predicted_states`, and so on. - -`dsx.sample` takes `obs_times` and `obs_values` for observations to condition on, `ctrl_times` and `ctrl_values` for known controls, and `predict_times` for times to roll forward and predict. Pass `predict_times` to generate data, pass `obs_times` and `obs_values` to condition. - -## State evolution - -Three ways to specify dynamics, in rising order of structure. - -Plain callable. Discrete time maps `(x, u, t_now, t_next)` to a distribution. Continuous time supplies a drift `(x, u, t)`. - -```python -# discrete time transition -state_evolution = lambda x, u, t_now, t_next: dist.MultivariateNormal(A @ x, Q) - -# continuous time drift, wrapped with a diffusion for an SDE -from dynestyx import ContinuousTimeStateEvolution, ScalarDiffusion -state_evolution = ContinuousTimeStateEvolution( - drift=lambda x, u, t: A @ x, - diffusion=ScalarDiffusion(1.0, bm_dim=2), -) -``` - -Typed classes. `LinearGaussianStateEvolution(A, cov, B=None, bias=None)` for `x_next ~ N(A x + B u + bias, cov)`. `GaussianStateEvolution(F, cov)` for a nonlinear mean `F(x, u, t_now, t_next)`. - -LTI factories. `LTI_discrete(A, Q, H, R, ...)` and `LTI_continuous(A, L, H, R, ...)` build a whole linear-Gaussian `DynamicalModel` in one call, including the observation model and a default standard-normal initial condition. - -A continuous-time model with a diffusion is an SDE and runs under `SDESimulator`. A continuous-time model with no diffusion is an ODE and runs under `ODESimulator`. Both can also be discretized for filtering with `Discretizer`. - -## Observation models - -Plain callable mapping `(x, u, t)` to a distribution, as in `lambda x, u, t: dist.Normal(x[0], sigma)`. Non-Gaussian likelihoods such as Poisson counts or heavy-tailed noise go here. Typed options are `LinearGaussianObservation(H, R, D=None, bias=None)` for `y ~ N(H x + D u + bias, R)`, `GaussianObservation(h, R)` for a nonlinear mean, and `DiracIdentityObservation()` for noiseless direct observation of the state. - -## Initial condition and diffusions - -The initial condition is any NumPyro distribution over the state, commonly `dist.MultivariateNormal(loc, cov)`. - -Diffusions set the SDE noise. `ScalarDiffusion(sigma, bm_dim=d)` gives isotropic noise, `DiagonalDiffusion(vec, bm_dim=d)` gives per-coordinate noise, and `FullDiffusion(matrix, bm_dim=k)` gives a general state-by-Brownian matrix. The coefficient is a constant or a callable `(x, u, t)`. - -## Simulate data - -Generate synthetic data by running the model under a simulator with parameters held fixed. Pass `predict_times` for the times to produce. Use `exclude_deterministic=False` so the output sites are returned. - -```python -import jax.random as jr -from numpyro.infer import Predictive -from dynestyx import DiscreteTimeSimulator - -predictive = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, - exclude_deterministic=False) -with DiscreteTimeSimulator(): - pred = predictive(jr.PRNGKey(0), predict_times=obs_times, - ctrl_times=ctrl_times, ctrl_values=ctrl_values) - -obs_values = pred["f_observations"][0, 0] # (T, obs_dim) -states_true = pred["f_states"][0, 0] # (T, state_dim) -``` - -Pick the simulator to match the dynamics. `DiscreteTimeSimulator` for discrete time, `SDESimulator` for an SDE, `ODESimulator` for an ODE. Each accepts `n_simulations` to draw several trajectories at once. - -## Condition on data and run inference - -Conditioning happens by wrapping the model in `Filter` (or `Smoother`) and supplying `obs_times` and `obs_values`. The filter integrates out the latent states and adds the marginal log-likelihood to the trace as a `numpyro.factor`, which is what makes parameter inference work. - -```python -from dynestyx import Filter -from dynestyx.inference.filters import EKFConfig - -with Filter(filter_config=EKFConfig()): - out = Predictive(model, params={"rho": jnp.array(0.3)}, num_samples=1, - exclude_deterministic=False)( - jr.PRNGKey(0), obs_times=obs_times, obs_values=obs_values) -mll = out["f_marginal_loglik"] -``` - -To profile the likelihood over a parameter, `vmap` the call. The mcmc, svi, smoothing, and filtering files build on this same wrap-then-run shape. - -## Output sites and shapes - -With the `dsx.sample` name `"f"`, common sites are below. Simulator outputs carry leading `(num_samples, n_simulations, ...)` axes. Filter and smoother summaries carry a leading `(num_samples, ...)` axis. - -- `f_observations` shape `(num_samples, n_sim, T, obs_dim)` -- `f_states` shape `(num_samples, n_sim, T, state_dim)` -- `f_times` shape `(num_samples, n_sim, T)` -- `f_marginal_loglik` the conditioned log-likelihood -- `f_filtered_states_mean` shape `(num_samples, T, state_dim)` when recorded -- `f_predicted_states` and `f_predicted_times` for forecasts at `predict_times` - -`flatten_draws` merges the `(num_samples, n_sim, T, D)` axes into `(num_samples * n_sim, T, D)` for plotting. - -## Hierarchical models with plates - -`dsx.plate(name, size)` batches several trajectories or groups. Sample shared or group-level parameters inside the plate, then call `dsx.sample` inside it. Observation arrays gain a leading group axis of `(N, T, obs_dim)`. - -## Gotchas - -- Build anything that depends on a sampled parameter inside the model function, never at module scope, or the dependence is not traced. -- Pass `exclude_deterministic=False` to `Predictive` to get the output sites back. -- Conditioning needs both `obs_times` and `obs_values`. Generating needs `predict_times`. -- `SDESimulator` does not condition on observations. Use a `Filter` to condition a continuous-time stochastic model. -- Run everything through the project uv environment with `uv run`. diff --git a/agent_skills/modeling-with-dynestyx/SKILL.md b/agent_skills/modeling-with-dynestyx/SKILL.md new file mode 100644 index 00000000..0dc19b26 --- /dev/null +++ b/agent_skills/modeling-with-dynestyx/SKILL.md @@ -0,0 +1,88 @@ +--- +name: modeling-with-dynestyx +description: Grills the user about a system they describe, then builds a dynestyx probabilistic dynamical model from it. Delivers a folder with a LaTeX PDF that states the problem setting, the exact mathematical definition of the model, the interpretation of each component, and a minimal numpyro-style dynestyx model function. Use when the user wants to model a dynamical or state-space system in dynestyx, points to a paper or codebase describing such a system, or asks to be grilled on a dynestyx model, even if they do not name this skill. +--- + +# Workflow + +The user describes a system, or points to a paper or codebase that describes one, that can be cast as a probabilistic dynamical model. The deliverable folder holds the compiled PDF and a minimal model function at its top level. Keep the LaTeX source and every file the compilation produces in a subfolder of the deliverable, so the build clutter stays out of the way. The PDF is the shared document that you and the user improve together until the model is correct. Work in three stages. + +Sourcing the real data the model runs on is a hard requirement that the build depends on. Read Source the real data below before you reach the build. + +First read the dynestyx documentation so you know which modeling choices the library forces a user to make. Start with docs/index.md and docs/tutorials.md, then docs/api_reference/. + +## Stage 1. Draft the PDF + +From the user's description or the source they point to, write your best first attempt at the model and compile it to a PDF right away, before resolving every detail and before writing any model code. Do not wait for a complete picture. Where information is missing or you had to assume something, write the gap into the PDF as a marked open question. + +Put every equation in the PDF, never in the chat. LaTeX does not render in the chat, and the math is the part the user most needs to read. Lay the PDF out as described under Document structure below. + +## Stage 2. Grill and refine the PDF + +Interview the user to close the open questions and correct the model. Run the interview as ordinary chat and anchor every question to a place in the PDF. Do not use any structured question or multiple choice tool, and do not restate equations in the chat. Point the user at the relevant part of the PDF instead. + +Resolve each of these. + +- The problem setting and the question the model answers. +- Every variable in the model, captured in the variables table that Document structure below specifies. +- The initial distribution of the latent state. +- The state evolution. Settle whether time is discrete or continuous, whether the dynamics are deterministic or carry process noise, and whether external controls enter. +- The observation model. Settle which variables are observed, the form of the observation noise, and whether observations arrive at irregular times. +- How to verify the model. The shape check in Stage 3 is not enough on its own. Settle on a real verification and grill the user on which of two routes fits. A prior predictive check draws from the model, reduces the draws to summaries, and asks whether each data summary sits within the spread of the simulated ones. Choose summaries the data can speak to, such as the spread of one-step displacements or the count of observations per unit time. A figure recreation reproduces a specific figure from the paper, such as a distribution it plots or a trajectory it shows. Settle the route, the dataset or figure it targets, and what counts as agreement. + +After each round, revise the LaTeX, recompile, and show the PDF again. Repeat until the user confirms it is correct. A wrong PDF propagates into wrong code, so do not begin Stage 3 until the user approves the PDF. + +Only the user can declare the grilling finished. After each round, ask directly whether each open question is settled and treat it as closed only when the user says so. When you believe the model is complete, say so and ask the user to confirm, then hold until the user gives that confirmation in plain words. Silence or the absence of objection does not count as approval. + +## Source the real data + +The model and its verification run on real data, and sourcing that data is a hard requirement. Before you build the model you must find and obtain the actual datasets, maps, coordinates, and other fixed inputs it needs. A synthetic or stand-in substitute is a last resort. Use one only after you have genuinely tried and failed to obtain the real data, and when you do, tell the user plainly that the asset is a substitute. + +Never assume that data is unavailable. Read the paper's data availability statement and follow every archive it names, such as a Zenodo or Dryad deposit or a code repository. A code archive often ships code alone, while the data sits in a separate archive under its own identifier, so a missing file in one place does not mean the data is gone. Where the paper builds an asset from a public source, such as a government GIS portal or an open data service, rebuild it from that source. Keep the data the model is fit to, the observations, separate from the complementary datasets that only set parameter values, and source each from its own origin. When the first route fails, try the cited source studies, the corresponding author's repositories, and public portals before you settle for a substitute. + +## Stage 3. Build and verify the model + +After the user approves the PDF, add the model to the folder. It is a minimal numpyro-style python function that holds only the dynestyx model definition and follows the model the PDF describes. Build it on the real data you sourced, not on placeholders. Match the dynestyx API you read in the docs. + +Expose the model through a single builder that returns a dynestyx model object, so the definition lives in one place. Every piece of downstream code builds the model through that builder and drives it with the dynestyx simulators and inference functions. The verification, any figure the model reproduces, and any later inference all take this path. Downstream code does not rebuild the model's internal distribution classes by hand, since that bypasses the library and duplicates the definition. Give the state evolution and the observation model their proper dynestyx base classes so the type-based machinery, such as the auto-selecting simulator, routes the model on its own. + +Do not stop at writing the function. Verify that it runs and generates reasonable data before you hand it over. Verification has two parts. First, draw a forward simulation and check that the sampled states and observations carry the shapes the PDF implies. Second, run the verification the PDF specifies, for a prior predictive check by comparing each data summary against the spread of many simulated trajectories, for a figure recreation by generating the figure's quantity from the model and placing it beside the paper's version. Report the result and flag any disagreement. Keep both parts in the folder as a small script or test so the user can rerun them, and run everything through the project uv environment. When the model fails to build or sample, fix it and verify again. When the verification disagrees with the paper or the data, surface it to the user and settle it before you report the model as done. + +## Document structure + +The PDF follows a fixed section order. + +1. Problem setting. A crisp statement of the system, the question the model answers, and the time index. Keep it short. +2. Variables. A complete account of every quantity in the model, laid out as described below. +3. The model. The initial distribution of the latent state, the state evolution, and the observation model, each written as a labelled equation. +4. Verification. The verification the model will pass and its target. For a prior predictive check, the summaries compared against the available data, each with the value the data takes and its source. For a figure recreation, the paper figure to reproduce, its locator, and the quantity it shows. + +Begin the variables section by writing the latent state vector as a display equation, so the state space the model evolves is defined inside this section rather than left to prose elsewhere. Then give one table that accounts for every variable, with these columns. + +- Symbol. The notation used in the equations. +- Meaning. A short interpretation in the problem. +- Dimension and units. The size of the variable and its physical units. +- Role. One of latent state, latent increment, observed data, fixed input, or hyperparameter. +- Value. The concrete value for every fixed scalar, and a dash for anything that has no single value. +- Source. The exact location in the paper or supplement where the variable is defined or its value is fixed, given as an equation, section, table, or figure number, with a page when that helps the reader find it. + +A latent variable points to the equation that defines it. A hyperparameter or other constant points to the equation, table, or passage that fixes its value, and where the paper attributes that value to a complementary dataset or to earlier work, name that origin next to the in-paper locator and carry the original citation through. Observed data point to the passage that describes the dataset, with the archive identifier or repository link that the paper provides. Do not put a code filename or an internal artifact in this column when the paper or supplement states the same fact. When an entry runs longer than a short phrase, keep the locator in the cell and add a short sourcing subsection after the table for the detail. + +Order the rows by role, with the latent state components first and the hyperparameters last. + +The verification section records the route chosen with the user. For a prior predictive check, give one table that lists each summary, with these columns. + +- Summary. The name of the quantity computed from a trajectory or its observations. +- Definition. How the summary is computed from the sampled states and observations. The same computation runs on the available data. +- Data value. The value the summary takes on the available data, as a number or a range. +- Source. The dataset that provides the comparison, given with the archive identifier or repository link, or the paper or supplement passage when the value comes from there. + +The check draws many trajectories, computes each summary on the draws and on the data, and asks whether the data summary sits within the spread of the simulated summaries. Choose summaries the data can speak to. Where no data constrains a summary, give a plausible range from the paper or the user's domain knowledge and name its source in the Data value cell. + +For a figure recreation, name the figure and its locator in the paper, describe the quantity it shows, and state how the model reproduces it. The reproduction stands next to the original so the reader can compare them. + +## Writing style + +Write the PDF prose in the voice of a tenured machine learning professor. Keep it direct, technical, and clean of the usual tells of machine generation. Avoid semicolons, colons, and em-dashes. Avoid the construction that denies one claim to assert another, such as saying that something is not X but instead Y. Avoid trailing participial phrases of the form X does Y, enabling Z, and break them into separate sentences with explicit subjects and finite verbs. Avoid lists of negations of the form no X, no Y, no Z. Describe the model on its own terms. Do not reference an earlier draft of the writeup or justify the current text by contrast with what it replaced. + +