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.