Use minisim in your test suite#
If you maintain a calcium-imaging analysis pipeline (minian, CaImAn, suite2p,
…), minisim can supply small, reproducible recordings whose answer you already
know, so a test can assert that your pipeline recovers them. The
minisim.testing module gives you a one-call fixture and a one-call
scorecard built for exactly this.
A recording in one call#
make_recording() returns a small, fast, deterministic
Recording: the same arguments (and seed) always produce
the same movie and the same ground truth.
from minisim.testing import make_recording
rec = make_recording(n_cells=6, n_px=128, duration_s=2.0, seed=0)
movie = rec.observed # (frame, height, width) sensor counts
truth = rec.ground_truth # exact cells, traces, spikes
The defaults are tuned for CI (a 128 px field at 1 µm/px, six cells at 50 µm
depth, two seconds at 20 fps). Shrink n_px / duration_s for an even faster
fixture, raise n_cells for a denser one, pass motion=True to exercise motion
correction, or hand in your own activity= / sensor= / extra_steps=.
Scoring your pipeline in one call#
Wrap your pipeline’s output in an Estimate and pass
it to score(). It applies the conventions the
benchmarking guide spells out (match against A_observed,
score recall over the detectable cells, nan-safe trace median, treat the motion
estimate as a correction) and returns a Report.
from minisim.testing import Estimate, score
A_est, C_est, S_est = run_my_pipeline(rec.observed)
report = score(Estimate(A=A_est, C=C_est, S=S_est), rec.ground_truth)
assert report.recall > 0.8, report.summary()
The footprints are the only required field of Estimate; leave the traces /
activity / shifts out and the matching scores come back as nan / None. Each
field takes either the terse CNMF symbol your pipeline already emits or a
spelled-out alias - Estimate(A=A, C=C, S=S) and
Estimate(footprints=A, traces=C, activity=S) are the same thing (the deconvolved
S is a non-negative activity rate, not a spike train). Arrays may be numpy or
xarray (minian’s CNMF returns xr.DataArray); both are accepted.
The Report carries recall, precision, f1, mean_overlap, trace_corr
(median Pearson r), activity_corr, activity_variance_explained,
activity_scale, shift_rmse, and the footprint_shift it absorbed.
Important
recall is over the detectable cells, not every planted cell. By default
score drops cells too dim to clear the detection floor before computing the
denominator, so recall = 1.0 can mean “recovered every detectable cell” while
some planted cells were undetectable. The Report makes this explicit:
n_requested (cells planted), n_detectable (cells above the floor), and
n_true (the denominator recall actually used). Read report.summary(), or
pass restrict_to_detectable=False to score against every planted cell instead.
The detection threshold itself is provisional and may change before 1.0 - see the
reproducibility & stability contract.
When you need more than the common case, the underlying primitives
(hungarian_match(), trace_pearson(),
activity_similarity(), shift_rmse()) stay
fully available; score is just the 90%-path on top of them.
A pytest fixture#
Drop a fixture in your conftest.py. Build the recording once per test (or per
session, since a Recording is immutable and safe to share):
# conftest.py
import pytest
from minisim.testing import make_recording
@pytest.fixture(scope="session")
def sim_recording():
return make_recording(n_cells=8, duration_s=3.0, seed=0)
# test_recovery.py
from minisim.testing import Estimate, score
def test_pipeline_recovers_cells(sim_recording):
A, C, S = run_my_pipeline(sim_recording.observed)
report = score(Estimate(A=A, C=C, S=S), sim_recording.ground_truth)
assert report.recall > 0.8, report.summary()
Use plain make_recording / simulate() in tests. The on-disk
simulate_cached() is meant for parameter sweeps, not CI: in a
fresh CI environment its cache is cold (no speedup, just disk writes).
Add minisim as a test-only dependency#
minisim never imports an analysis pipeline, so the dependency is strictly
one-directional and adding minisim to your test extra cannot create an import
cycle. In your pyproject.toml:
[project.optional-dependencies]
test = ["minisim", "pytest"]
minisim is then present when your tests run, but is not a runtime dependency of your package.