Bayesian-ACh is a falsifiable computational benchmark for testing what hippocampal acetylcholine (ACh) encodes during predictive learning.
The project starts from the state-transition prediction-error hypothesis of de Cothi, Shipley, and Barry (2026) and asks a sharper estimation-theoretic question:
Does ACh encode a raw sensory mismatch, a rational parameter update, a state-belief correction, a sensor fault, retrieval of a latent context, or evidence that the world model itself should change—and can that event signal still be identified after realistic release and measurement dynamics?
Version 0.5 implements six computational layers:
- conjugate Dirichlet learning for one transition model;
- exact HMM filtering over already learned transition contexts;
- exact categorical Bayesian online change-point detection for fully observed regimes;
- exact joint multisensory filtering over latent state, context, and binary sensor health;
- a calibration-separated ACh measurement model with phasic and tonic release, indicator convolution, nuisance regressors, subject effects, session baselines, and held-out candidate comparison;
- causal closed-loop triggering with explicit transport delay, randomized latency, local eligibility traces, yoked sham controls, and held-out recovery of the causal stimulation window.
This repository is a computational hypothesis-testing project. It does not claim that any candidate has already been established as the biological ACh signal.
For a categorical transition row,
[ \boldsymbol\theta\sim\operatorname{Dir}(\boldsymbol\alpha), \qquad \widehat{\boldsymbol p}=\frac{\boldsymbol\alpha}{\alpha_0}, ]
and observed next state (j), the raw innovation is
[ \boldsymbol\nu=\boldsymbol e_j-\widehat{\boldsymbol p}. ]
The exact posterior-mean change is
\frac{1}{\alpha_0+1}\boldsymbol\nu. ]
The same prediction and observation can therefore produce the same mismatch and surprise but a very different rational update when confidence differs.
For known context (m_t), the exact switching filter computes
[ q_t^-(m)=\sum_{m'}q_{t-1}(m')\Pi_{m'm}, \qquad q_t(m)\propto q_t^-(m) p(x_{t+1}\mid x_t,u_t,m). ]
This can retrieve an already learned transition model without changing any
transition parameter. SwitchingContextFilter.observe(...) performs inference
only unless an external context label is explicitly supplied through
learn_context.
DirichletBOCPD maintains the full run-length posterior
[ p(r_t\mid x_{0:t},u_{0:t-1}) ]
and complete Dirichlet sufficient statistics for each run-length hypothesis. It therefore separates retrieval of a stored context from evidence that a new piecewise-stationary regime began.
The multisensory filter retains
p(m_t=m,x_t=x,\boldsymbol h_t=\boldsymbol h \mid y_{0:t},u_{0:t-1}), ]
where (\boldsymbol h_t) contains one binary health state per sensor. A conflicting visual observation can update state, context, or visual-health belief instead of automatically forcing transition learning.
Version 0.4 models
b_j + (ha_s c_k)_t + \boldsymbol\beta^\top\boldsymbol q_t + (hu)_t, ]
where
- (c_{k,t}) is one candidate Bayesian event train;
- (a_s) is a partially pooled subject-specific signal coefficient;
- (u_t=\rho u_{t-1}+\epsilon_t) is latent tonic release;
- (h) is a causal difference-of-exponentials indicator response;
- (\boldsymbol q_t) contains movement and arousal nuisance regressors;
- (b_j) is a baseline-only session offset.
The sensor and tonic timescales are inferred from a known calibration input in training sessions only. Candidate coefficients are fitted on training task samples, and the final comparison uses held-out task samples only.
For effective event-to-perturbation lag (\ell_i), the closed-loop model tests
\beta_0+\beta_E A_i E(\ell_i;\theta)+\epsilon_i. ]
The null, latency-independent, and latency-dependent models are compared on held-out sessions. Eligibility gating is claimed only when a causal timing interaction clears a preregistered log-evidence threshold over the selected simpler model. Event, command, and effective timestamps are separate, and the command-to-effective delay is treated as independently calibrated rather than silently absorbed into the inferred trace.
python -m pip install -e '.[dev]'Python 3.10 or newer is supported.
bayesian-ach dissociate --output results/dissociation --seed 7
bayesian-ach benchmark --output results/benchmark --seed 7bayesian-ach regime-benchmark \
--output results/regime-recovery \
--seed 7bayesian-ach observation-benchmark \
--output results/observation-attribution \
--seed 7The default observation benchmark recovers all 108 sequences: 36 visual sensor faults, 36 known-context switches, and 36 specified structural changes.
bayesian-ach measurement-benchmark \
--output results/measurement-recovery \
--seed 7The measurement benchmark asks whether the generating computational event train can be recovered after phasic and tonic release, indicator smoothing, nuisance confounding, subject variation, session offsets, and held-out sessions.
It writes:
measurement_generators.csv: one recovery summary per generating signal;measurement_fits.csv: held-out scores for every generator/candidate pair;measurement_kernel_posterior.csv: calibration-only posterior over rise, decay, and tonic-persistence hypotheses;measurement_samples.csv: candidate, nuisance, split, and synthetic trace values;summary.json: acceptance metrics and split provenance.
For the default seed-7 benchmark:
- all 7/7 generating signals are recovered;
- the median held-out evidence margin is 771.532 log units;
- the minimum evidence margin is 421.096 log units;
- the calibration MAP is exactly ((\tau_r,\tau_d,\rho)=(0.4,1.6,0.97));
- median nuisance-coefficient MAE is 0.00749;
- median subject-signal correlation is 0.99769;
- maximum absolute correlation between sensor-convolved candidates is 0.88880.
These are controlled synthetic model-recovery results, not biological evidence.
bayesian-ach closed-loop-benchmark \
--output results/closed-loop-recovery \
--seed 7The default benchmark compares five causal generators: no effect, an untimed main effect, exponential eligibility, rise-and-decay eligibility, and a finite boxcar window. It uses balanced randomized latencies, uncertainty gating, refractory suppression, missed and background commands, explicit actuation delay, and yoked active/sham command times.
For the default seed-7 benchmark:
- all 5/5 causal generators are recovered;
- 2,236 active/sham pairs are accepted, including 29 false-positive or randomized background events;
- the minimum conservative decision margin is 3.474 log units beyond the preregistered claim boundary;
- the median decision margin is 376.937 log units;
- active and sham command timestamps agree exactly;
- a 30-seed stress check recovers all 150 generator instances.
The benchmark writes generator summaries, all candidate fits, pair-level outcomes, opportunity-level trigger decisions, and a provenance-rich JSON summary. These remain controlled synthetic recovery results.
import numpy as np
from bayesian_ach import (
MeasurementDataset,
MeasurementFitConfig,
fit_measurement_models,
)
n = 400
sessions = np.repeat(np.arange(4), n // 4)
subjects = np.repeat([0, 0, 1, 1], n // 4)
train = np.isin(sessions, [0, 2])
calibration = np.tile(np.r_[np.ones(40, dtype=bool), np.zeros(60, dtype=bool)], 4)
task = ~calibration
baseline = np.tile(np.r_[np.ones(8, dtype=bool), np.zeros(92, dtype=bool)], 4)
rng = np.random.default_rng(7)
dataset = MeasurementDataset(
observed=rng.normal(size=n),
calibration_event=np.zeros(n),
candidate_events=rng.normal(size=(n, 2)),
nuisance=rng.normal(size=(n, 1)),
subject_ids=subjects,
session_ids=sessions,
train_mask=train,
calibration_mask=calibration,
task_mask=task,
baseline_mask=baseline,
candidate_names=("surprise", "context_information_gain"),
nuisance_names=("movement",),
)
result = fit_measurement_models(dataset, MeasurementFitConfig(dt=0.2))
print(result.winner.candidate)Real analyses should use a meaningful exogenous calibration event and inspect whether the discrete timescale posterior is concentrated and stable.
- Stage 1 — complete: exact transition learning, matched-confidence dissociation, and six-way scalar model recovery.
- Stage 2 — complete: exact context filtering, supervised parameter learning, full categorical BOCPD, and known-switch-versus-reset recovery.
- Stage 3 — complete: exact partial-observation filtering over state, context, and sensor health, with three-way sensor/world attribution.
- Stage 4 — complete: calibration-only sensor and tonic-timescale inference, phasic/tonic forward measurement modeling, nuisance and subject separation, and seven-way held-out event-signal recovery.
- Stage 5 — complete: causal online triggering, independently calibrated delay, randomized timing, yoked active/sham perturbation, eligibility-family recovery, and falsification against null and latency-independent effects.
- Stage 6 — next: replay as smoothing-based revision rather than unconstrained internally generated prediction error.
See docs/closed_loop.md for causal triggering and
eligibility-window recovery, docs/measurement_model.md
for the ACh measurement derivation,
docs/partial_observation.md for multisensory
inference, docs/switching_model.md for context and
change-point inference, and docs/model.md for the original
transition signals.
src/bayesian_ach/ exact models, measurement and closed-loop benchmarks, CLI
tests/ unit, exhaustive-enumeration, leakage, and recovery tests
docs/ derivations, experiment design, data contract, roadmap
examples/ reproducible plotting examples
data/ external-data instructions; no data vendored
results/ generated evidence; only documentation is tracked
ruff check .
mypy src/bayesian_ach
pytest --cov=bayesian_ach --cov-report=term-missing
python -m buildGitHub Actions runs linting, typing, build checks, the complete test suite on Python 3.10–3.13, and smoke tests for all benchmark commands.
Please cite both the motivating Perspective and this software. Repository
citation metadata are provided in CITATION.cff.
MIT. See LICENSE.