Spec and steps#

The Spec is the one object you build to describe a recording: an acquisition, a seed, an ordered list of steps, and output options. It validates the whole pipeline on construction.

Spec#

pydantic model minisim.Spec[source]#

Bases: _Base

A complete, reproducible recording specification.

acquisition + steps (the ordered pipe) + seed + output fully determine a recording. The cross-field validators below catch genuinely invalid configs (raise) and flag unusual-but-legal ones (SpecWarning).

Fields:
  • acquisition (minisim.spec.Acquisition)

  • output (minisim.spec.Output)

  • seed (int)

  • steps (list[minisim.spec.PlaceNeurons | minisim.spec.CellActivity | minisim.spec.CellOptics | minisim.spec.Composite | minisim.spec.Neuropil | minisim.spec.Vasculature | minisim.spec.Bleaching | minisim.spec.BrainMotion | minisim.spec.IlluminationProfile | minisim.spec.Vignette | minisim.spec.Leakage | minisim.spec.Sensor])

field acquisition: Acquisition [Optional]#
field seed: int = 42#

RNG seed for full reproducibility.

field steps: list[AnyStep] [Required]#
field output: Output [Optional]#

Acquisition and physical models#

The acquisition owns all unit conversions between the physical world (µm, seconds) and the sampled world (pixels, frames), and composes the three physical models below.

pydantic model minisim.Acquisition[source]#

Bases: _Base

The physical acquisition: optics, image sensor, tissue, and sampling.

Owns all unit conversions between the physical world (µm, seconds) and the sampled world (pixels, frames). Pixel size is the joint optics×sensor quantity image_sensor.pixel_pitch_um / optics.magnification; FOV is then derived from the sensor’s pixel count - any two of {FOV, pixel size, pixel count} fix the third.

Fields:
  • duration_s (float)

  • focal_depth_in_tissue_um (float | Literal['auto'])

  • fps (float)

  • front_working_distance_um (float | None)

  • image_sensor (minisim.spec.ImageSensor)

  • optics (minisim.spec.Optics)

  • tissue (minisim.spec.Tissue)

field optics: Optics [Optional]#
field image_sensor: ImageSensor [Optional]#
field tissue: Tissue [Optional]#
field fps: float = 20.0#

Frame rate, frames per second.

Constraints:
  • gt = 0

field duration_s: float = 150.0#

Recording duration, seconds.

Constraints:
  • gt = 0

field focal_depth_in_tissue_um: float | Literal['auto'] = 'auto'#

Depth of the focal plane below the tissue surface, µm (0 = surface), in the same coordinate as each cell’s depth z. Cells above or below it defocus; ‘auto’ resolves to the median realized cell depth at the optics step.

field front_working_distance_um: float | None = None#

Front working distance (lens front → focal point), µm - Miniscope V4 ≈ 700. Informational only: it does NOT affect the simulation (the optics math uses focal_depth_in_tissue_um), but it’s a physically relevant number for surgery/implant planning, so it’s recorded here.

pydantic model minisim.Optics[source]#

Bases: _Base

Objective optics - the measurable lens properties of a 1-photon scope.

Layer-2 phenomenological quantities (diffraction sigma, defocus blur) are derived from these fields by the helper methods below. Pixel size is a joint optics×sensor quantity (sensor pitch / magnification) and so lives on Acquisition, not here.

Typical 1-photon miniscope ranges: NA 0.3–0.6, magnification ~5–10×, GCaMP emission ~510–540 nm.

Fields:
  • depth_of_field_um (float | Literal['auto'])

  • emission_nm (float)

  • field_curvature_radius_um (float | None)

  • magnification (float)

  • na (float)

field na: float = 0.45#

Numerical aperture of the GRIN objective.

Constraints:
  • gt = 0

field magnification: float = 8.0#

Optical magnification (sensor side / object side).

Constraints:
  • gt = 0

field emission_nm: float = 525.0#

Fluorophore emission wavelength, nm (GCaMP ≈ 525).

Constraints:
  • gt = 0

field depth_of_field_um: float | Literal['auto'] = 'auto'#

±in-focus half-depth around the focal plane, µm. ‘auto’ (default) derives it from NA as ≈ n·λ/NA² (the diffraction depth of field - the physical behavior, since DOF is set by the optics, not chosen); a number overrides it.

field field_curvature_radius_um: float | None = None#

Petzval field-curvature radius, µm (typical miniscope ≈ 2000–3000). Off-axis cells focus shallower by the spherical sagitta; None = ideal flat field. A miniscope has no room for a field flattener, so this is usually finite.

pydantic model minisim.ImageSensor[source]#

Bases: _Base

Physical and noise properties of the bare image sensor (the detector).

Named image sensor, not camera, on purpose: a camera bundles optics on top of a sensor, whereas this is only the photosensitive array and its readout chain. The optics live separately on Optics. Together with the exposure scale on the sensor step, the fields here fully specify the photons→counts conversion.

Typical CMOS miniscope sensors: ~2–6 µm pixel pitch, QE 0.6–0.9, read noise 1–5 e⁻ RMS, 8–12-bit ADC.

Fields:
  • bit_depth (int)

  • gain_adu_per_e (float)

  • n_px_height (int)

  • n_px_width (int)

  • pixel_pitch_um (float)

  • quantum_efficiency (float)

  • read_noise_e (float)

field n_px_height: int = 256#

Sensor height, pixels.

Constraints:
  • gt = 0

field n_px_width: int = 256#

Sensor width, pixels.

Constraints:
  • gt = 0

field pixel_pitch_um: float = 3.0#

Physical sensor pixel pitch, µm.

Constraints:
  • gt = 0

field quantum_efficiency: float = 0.7#

Photon → electron conversion efficiency.

Constraints:
  • gt = 0

  • le = 1

field read_noise_e: float = 2.0#

Read noise, electrons RMS.

Constraints:
  • ge = 0

field gain_adu_per_e: float = 1.0#

Camera gain, ADU per electron.

Constraints:
  • gt = 0

field bit_depth: int = 8#

ADC bit depth; counts clipped to [0, 2^bit_depth − 1].

Constraints:
  • gt = 0

pydantic model minisim.Tissue[source]#

Bases: _Base

Light-scattering properties of the imaged tissue, as a function of depth.

The fields parametrize Layer-2 helpers (attenuation, scatter_sigma). Scattering has two separable consequences on a cell’s image, modelled by two knobs: it dims the sharp signal (light scattered out of the collection cone is lost → attenuation(), the scatter_mfp_* fields) and it blurs the footprint (forward-scattered light, g 0.88, is recollected as a growing halo → scatter_sigma_um(), scatter_blur_per_um).

Round-trip scattering, asymmetric. The signal makes two passes through tissue, but they attenuate very differently:

  • Excitation in (≈470 nm) is delivered by widefield illumination, so it reaches a cell as a diffuse fluence, not a ballistic beam. Diffuse light penetrates far (transport length ≈ 800 µm) and its fluence actually peaks a few hundred µm deep before falling (Ma et al. 2020, Neurophotonics 7:031208), so over the depths a 1-photon scope images, excitation barely dims cells - a long effective MFP (scatter_mfp_excitation_um).

  • Emission out (≈525 nm) is the image-forming sharp signal, which decays at roughly the scattering MFP (scatter_mfp_emission_um). This leg dominates the depth-dimming.

Modelling excitation as ballistic (a short, symmetric leg) double-counts the loss and makes deep cells unrealistically dim; the asymmetric split above is both more honest and what keeps the round trip from over-attenuating.

Literature anchors (mouse cortex / gray matter). The ballistic scattering mean free path at blue/green is ≈ 40–50 µm (μ_s ≈ 200 cm⁻¹, g ≈ 0.86–0.89): ≈ 47 µm at 473 nm (Al-Juboori et al. 2013, PLoS ONE 8:e67626) and ≈ 38 µm at 515 nm (Azimipour et al. 2014, Biomed. Opt. Express). That ballistic length is what sets the blur rate. The light an objective actually collects decays more slowly, because the strong forward scattering (g ≈ 0.88) is largely recollected - so the emission leg uses the high end of the scattering-MFP literature (~100 µm), and the diffuse excitation leg is longer still; their round trip gives an effective ≈ 85 µm (see scatter_mfp_um).

Fields:
  • scatter_blur_per_um (float)

  • scatter_mfp_emission_um (float)

  • scatter_mfp_excitation_um (float)

field scatter_mfp_excitation_um: float = 600.0#

Effective attenuation MFP for the excitation leg (≈470 nm, in) - long, diffuse fluence, µm.

Constraints:
  • gt = 0

field scatter_mfp_emission_um: float = 100.0#

Effective attenuation MFP for the emission leg (≈525 nm, out) - the image-forming scattering MFP, µm.

Constraints:
  • gt = 0

field scatter_blur_per_um: float = 0.05#

Linear broadening of the footprint per µm of depth (µm sigma per µm depth).

Constraints:
  • ge = 0

Output#

pydantic model minisim.Output[source]#

Bases: _Base

Final-array formatting - formatting only, never rescaling (honest radiometry).

Fields:
  • save_intermediates (bool)

  • store_dtype (Literal['float32', 'float64'])

field save_intermediates: bool = False#

Retain a snapshot after every step (test oracle + teaching visuals). Default False to keep memory flat for the programmatic and sweep paths; the two notebook presets opt in explicitly. When False, only observed + ground_truth are kept and stage() raises.

field store_dtype: Literal['float32', 'float64'] = 'float32'#

Float container for the integer-valued sensor counts.

Steps#

Spec.steps is the forward chain. Each step below is a StepSpec (the base class lives in minisim.spec); AnyStep is the discriminated union of them and is the type you annotate a step list with. Each kind may appear at most once in a spec.

pydantic model minisim.spec.StepSpec[source]#

Bases: _Base

Base class for a single pipeline step’s configuration.

A concrete step spec carries its physical parameters and a literal kind discriminator, and declares its domain (a class attribute). build() turns the spec into the executable step that mutates a Scene, resolving kind through the minisim.steps.STEP_FOR_KIND table.

requires declares the step kinds whose output this step consumes through the shared Scene (e.g. composite reads the footprints place_neurons makes and the traces cell_activity makes). It is about presence-order, not completeness: a present requirement is placed before this step by the canonical ordering (_PIPELINE_ORDER), but it may be absent entirely. Partial pipelines are first-class - a spec of [place_neurons, cell_activity, composite] with no sensor is valid, so targeted test data for a downstream calcium pipeline can exercise just a few stages.

Fields:
field kind: str [Required]#

The forward chain#

pydantic model minisim.PlaceNeurons[source]#

Bases: StepSpec, NeuronPopulation

Place generic neurons in a 3-D µm volume, soma-only or with dendrites.

‘Place’ is the verb - this positions neurons in space (anchored at the cell body); it is unrelated to hippocampal place cells. v1 models one generic excitable cell type (an irregular soma blob) with two GCaMP targeting variants via morphology: "soma" (soma-targeted, body only) or "cytosolic" (standard GCaMP, the soma plus a few tapering proximal dendrites). There is no further cell-type distinction and no spatial/behavioral tuning. Footprints are 2-D masks carrying a scalar depth z; out-of-focus neurons that become background emerge for free downstream from z + optics.

The step is a single NeuronPopulation: its inherited fields (morphology, soma_radius_um, depth_range_um, …) describe that one group. To place several distinct groups together - a thin soma-targeted layer over a deep cytosolic volume, say - set populations to a list of NeuronPopulation instead; the step then samples each in turn (its own step-level population fields are ignored, and mixing the two raises).

Fields:
  • kind (Literal['place_neurons'])

  • populations (list[NeuronPopulation] | None)

field kind: Literal['place_neurons'] = 'place_neurons'#
field populations: list[NeuronPopulation] | None = None#

Distinct neuron populations to place together (e.g. a thin layer + a deep volume). None (default) = the step is a single population described by its own fields; a list = sample each entry in turn and ignore the step-level population fields.

A PlaceNeurons step is itself a single NeuronPopulation (its fields describe one group of cells). To place several distinct groups at once - a thin soma-targeted layer over a deep cytosolic volume, say - set populations to a list of NeuronPopulation instead. A population can be density-sampled (density_per_mm3 over a depth_range_um) or placed at exact positions_um centers (z, y, x); the two kinds can be mixed in one spec.

pydantic model minisim.NeuronPopulation[source]#

Bases: _Base

One homogeneous group of neurons to place: a morphology + a 3-D distribution.

A population is the unit place_neurons actually samples - one cell shape (soma-only or cytosolic) at one soma size, scattered at one volumetric density across one depth range. A single population is the common case; list several on PlaceNeurons.populations to build layered anatomy - e.g. a thin soma-targeted band over a deep cytosolic volume. The cell count is derived volumetrically from density_per_mm3 and the depth thickness (see sample_neurons()); brightness is biology and is drawn later in cell_activity, never here.

Fields:
  • dendrite_length_um (float)

  • dendrite_width_um (float)

  • density_per_mm3 (float)

  • depth_range_um (tuple[float, float])

  • irregularity (float)

  • min_distance_um (float)

  • morphology (Literal['soma', 'cytosolic'])

  • positions_um (list[tuple[float, float, float]] | None)

  • soma_radius_um (float)

field density_per_mm3: float = 25000.0#

Cell volumetric density (cells/mm³); count = density × FOV area × depth thickness, the thickness floored at one soma diameter so a thin or planar layer still yields cells.

Constraints:
  • gt = 0

field soma_radius_um: float = 7.0#

Soma radius, µm (typical cortical neuron ≈ 5–10).

Constraints:
  • gt = 0

field irregularity: float = 0.3#

0 = smooth disk; higher = lumpier soma (low-pass-noise threshold).

Constraints:
  • ge = 0

  • le = 1

field morphology: Literal['soma', 'cytosolic'] = 'soma'#

GCaMP targeting variant: ‘soma’ = soma-targeted (lumpy disk only); ‘cytosolic’ = standard GCaMP (soma + branched proximal dendrites).

field dendrite_length_um: float = 24.0#

Nominal proximal-dendrite reach, µm (cytosolic only); kept proximal so the arbor shapes the blurred footprint without blowing up the sparse-patch bounding box. The per-cell count is drawn randomly (not a spec input, so cells differ), and each dendrite’s length jitters around this value and may branch.

Constraints:
  • gt = 0

field dendrite_width_um: float = 3.0#

Proximal-dendrite base width (diameter), µm; tapers to a ~1 px thread at the tip (cytosolic only).

Constraints:
  • gt = 0

field depth_range_um: tuple[float, float] = (0.0, 200.0)#

(min, max) depth into tissue, µm.

field min_distance_um: float = 0.0#

3-D center-to-center minimum (Poisson-disk if > 0).

Constraints:
  • ge = 0

field positions_um: list[tuple[float, float, float]] | None = None#

Explicit soma centers as (z, y, x) µm tuples - depth first, matching Cell.center_um (not x,y,z). z is depth below the tissue surface (0 = surface); y, x are lateral in the optical-center frame: the optical axis is (0, 0), +y down and +x right (image convention), so a cell at (z, 0, 0) sits dead-center regardless of the motion margin, and these are the same coordinates GroundTruth.centers_um reports back. When given, these exact positions are placed instead of density-sampling, so the distribution fields (density_per_mm3, depth_range_um, min_distance_um) are ignored; the shape fields (soma_radius_um, irregularity, morphology, dendrites) still apply to each placed cell.

pydantic model minisim.CellActivity[source]#

Bases: StepSpec

Calcium activity: 2-state Markov gate → Poisson spikes → double-exp kernel.

Modeled on the CaLab web simulator: spikes are generated on a high-resolution grid (spike_sim_hz, ~300 Hz), convolved with the double-exponential kernel k(t) = exp(-t/τ_d) exp(-t/τ_r) at that rate, then bin-averaged down to the camera frame rate (exposure integration). One spike per fine bin respects the ~3 ms refractory period. The ground-truth S is the per-frame spike count (the fine train is binned away - nothing recovers spikes faster than the frame rate). Indicator saturation and per-cell τ jitter are deferred to v1.1.

Amplitude is biology and lives here as a single per-cell gain: brightness_cv is the cell-to-cell spread of an overall expression/response gain that scales each cell’s whole trace (baseline and transients together). The emitted trace is the clean ground truth C; measurement noise is deliberately not added here. Photon shot noise and read noise enter at the sensor, background fluctuations at neuropil - so any SNR is an emergent property of the physical chain, computable downstream, never an input.

Fields:
  • active_rate_hz (float)

  • brightness_cv (float)

  • f0 (float)

  • kind (Literal['cell_activity'])

  • p_active_to_quiescent (float)

  • p_quiescent_to_active (float)

  • quiescent_rate_hz (float)

  • spike_sim_hz (float)

  • tau_decay_s (float)

  • tau_rise_s (float)

  • trace_noise (float)

field kind: Literal['cell_activity'] = 'cell_activity'#
field spike_sim_hz: float = 300.0#

High-res spike-simulation rate, Hz (~300 = a ~3 ms refractory); binned to the frame rate.

Constraints:
  • gt = 0

field p_quiescent_to_active: float = 0.005#

Per-frame quiescent→active transition prob.

Constraints:
  • gt = 0

field p_active_to_quiescent: float = 0.3#

Per-frame active→quiescent transition prob.

Constraints:
  • gt = 0

field active_rate_hz: float = 150.0#

Instantaneous firing rate while active, Hz (the in-burst rate).

Constraints:
  • gt = 0

field quiescent_rate_hz: float = 0.6#

Instantaneous firing rate while quiescent, Hz (the intrinsic background).

Constraints:
  • ge = 0

field tau_rise_s: float = 0.05#

Calcium rise time constant, s.

Constraints:
  • gt = 0

field tau_decay_s: float = 0.5#

Calcium decay time constant, s.

Constraints:
  • gt = 0

field brightness_cv: float = 0.3#

Cell-to-cell brightness spread: lognormal CV (mean 1) of the per-cell expression/response gain that scales the whole trace. 0 = every cell equally bright.

Constraints:
  • ge = 0

field f0: float = 1.0#

Baseline fluorescence.

Constraints:
  • ge = 0

field trace_noise: float = 0.0#

Non-physical additive trace noise (default 0). An advanced override only; real noise enters at sensor/neuropil, not here.

Constraints:
  • ge = 0

pydantic model minisim.CellOptics[source]#

Bases: StepSpec

Per-cell diffraction + defocus |z z_f| + scatter(z) blur & attenuation.

No tunable fields: blur and attenuation are fully determined by each cell’s z plus the physical Optics/Tissue constants on Acquisition. Writes the observed (degraded) footprint alongside the planted (sharp) one, sets the geometric in_focus flag, and stores the per-cell optical_brightness peak scalar. detectable is not set here - it is a whole-pipeline flag (optics × illumination vs the sensor noise floor) assembled in finalize().

Fields:
  • kind (Literal['optics'])

field kind: Literal['optics'] = 'optics'#
pydantic model minisim.Composite[source]#

Bases: StepSpec

Composite Σ_i degraded_footprint_i × trace_i into the movie.

The built step’s snapshot name is "cells_only". The planted (sharp) A/C remain the ideal, optics-free target in ground truth.

Fields:
  • kind (Literal['composite'])

field kind: Literal['composite'] = 'composite'#
pydantic model minisim.Neuropil[source]#

Bases: StepSpec

Additive diffuse background from the dendritic/axonal felt around the cells.

A smooth spatial field (mesh-density variation on spatial_sigma_um) modulated by a temporal envelope that is biologically driven: the haze is the aggregate calcium of the surrounding neural processes, so its time course is the local population activity, lagged and smoothed by the felt’s integration (population_tau_s, short). Each component’s envelope mixes that population driver with an independent slow drift (the unmodeled out-of-FOV/out-of-plane tissue, temporal_tau_s, slow) at population_coupling. This is the modeled diffuse mesh only - out-of-focus somata are a separate background that emerges for free from place_neurons + optics.

Fields:
  • amplitude (float)

  • kind (Literal['neuropil'])

  • n_components (int)

  • population_coupling (float)

  • population_tau_s (float)

  • spatial_sigma_um (float)

  • temporal_tau_s (float)

field kind: Literal['neuropil'] = 'neuropil'#
field spatial_sigma_um: float = 40.0#

Spatial smoothness of the mesh, µm.

Constraints:
  • gt = 0

field temporal_tau_s: float = 10.0#

OU correlation time of the independent slow-drift leg, s (slow).

Constraints:
  • gt = 0

field population_tau_s: float = 1.5#

Low-pass time constant of the population-coupled leg, s: the felt’s integration/lag, short relative to the drift.

Constraints:
  • gt = 0

field amplitude: float = 0.5#

Background amplitude relative to cell signal.

Constraints:
  • gt = 0

field n_components: int = 3#

Number of independent diffuse components.

Constraints:
  • ge = 1

field population_coupling: float = 0.7#

Fraction of the temporal envelope driven by local population activity vs independent slow drift (0=pure drift, 1=pure population).

Constraints:
  • ge = 0

  • le = 1

pydantic model minisim.Vasculature[source]#

Bases: StepSpec

Dark, spatially-static absorbing vessels - a multiplicative shadow on the movie.

Grows the layers of branching vessel trees, composites their Beer-Lambert transmission masks multiplicatively, and multiplies the result into the brain-frame movie (movie *= M). Because it is a tissue-domain effect applied before brain_motion, the vessel pattern is fixed in the brain frame and rides the motion crop exactly with the cells - the high-contrast, temporally static landmark that motion-correction leans on (unlike the cells, whose brightness flickers with activity). It is also a tunable confound: a vessel crossing a soma corrupts its footprint and trace, so dialing vasculature up stress-tests footprint/dynamics extraction. That occlusion is scored, not hidden: each cell’s footprint-weighted vessel burden is recorded as GroundTruth.vessel_overlap_fraction, and a vessel over a soma dims its peak in the detectable test - but the footprints (A_observed) stay vessel-free, the single-cell optical truth the confound is measured against.

v1 simplification: a single multiplicative attenuation of the already composited movie. Excitation and emission absorption are lumped into one static transmission, and light generated in front of the vessel is not exempted from the shadow (real out-of-plane / in-front fluorescence fills it in); the opacity floor stands in for that fill rather than modeling it. Good enough for a landmark and an occlusion confound; not a radiative-transfer model.

Off by default: enabled=False and an empty layers both make the step a no-op, so it must be explicitly turned on with at least one VesselLayer. The static absorbing mask is recorded to GroundTruth.vasculature_mask (cropped to the FOV) so the confound is scoreable. Temporal pulsation (cardiac / vasomotion) is deliberately deferred - the vessels are static here; only the brightness, not the position, would ever breathe, so the landmark property is unaffected either way.

Fields:
  • enabled (bool)

  • kind (Literal['vasculature'])

  • layers (list[VesselLayer])

field kind: Literal['vasculature'] = 'vasculature'#
field enabled: bool = False#

Master switch; with no layers the step is a no-op regardless.

field layers: list[VesselLayer] [Optional]#

Vessel layers to grow, each at its own depth/caliber. Empty = no vessels.

pydantic model minisim.VesselLayer[source]#

Bases: _Base

One depth’s worth of randomly-grown blood vessels - a dark absorbing tree.

A vessel absorbs both the excitation going in and the emission coming out (haemoglobin), so a layer is a shadow cast on the tissue behind it, not a light source. Each layer grows n_roots branching trees (see grow_vessel_tree()) entering from the field edges, tapering from root_radius_um trunks down to min_radius_um capillaries by Murray’s law, then rasterizes them to a Beer-Lambert transmission mask and blurs it by the defocus + scatter at this layer’s depth_um (reusing the optics model): a vessel near the focal plane is a crisp dark thread, one far from focus a broad soft shadow. List several layers on Vasculature to stack scales and depths (e.g. a shallow large-caliber cortical layer above the cells, plus a deeper fine-capillary bed).

All lengths are µm; depth_um shares the cell-z / focal-plane coordinate (0 = surface), so a layer can sit above the cells (shallower) or, less commonly, below them.

Fields:
  • absorption_per_um (float)

  • branch_angle_deg (float)

  • branch_area_main (float)

  • branch_prob (float)

  • depth_um (float)

  • min_radius_um (float)

  • n_roots (int)

  • opacity (float)

  • root_radius_um (float)

  • step_per_radius (float)

  • tortuosity_deg (float)

field depth_um: float = 30.0#

Depth of this vessel layer below the surface, µm (same coord as cell z / focal plane).

Constraints:
  • ge = 0

field n_roots: int = 4#

Number of vessel trees entering this layer from the field edges.

Constraints:
  • ge = 1

field root_radius_um: float = 22.0#

Trunk (thickest) vessel radius, µm.

Constraints:
  • gt = 0

field min_radius_um: float = 2.0#

Capillary floor: a branch terminates once Murray-law tapering drops below this, µm.

Constraints:
  • gt = 0

field opacity: float = 0.85#

Peak absorption of a thick trunk in (0, 1]; transmission floors at 1 − opacity (real vessels are never fully black).

Constraints:
  • gt = 0

  • le = 1

field absorption_per_um: float = 0.04#

Beer-Lambert absorption per µm of blood path length: sets the capillary-vs-trunk contrast.

Constraints:
  • gt = 0

field branch_prob: float = 0.2#

Per-step bifurcation probability (a side branch peels off, the main vessel continues).

Constraints:
  • ge = 0

  • lt = 1

field tortuosity_deg: float = 8.0#

Std of the per-step heading perturbation, degrees (0 = ruler-straight vessels).

Constraints:
  • ge = 0

field branch_angle_deg: float = 30.0#

Base heading deviation at a bifurcation, degrees; the thinner child turns more.

Constraints:
  • ge = 0

field branch_area_main: float = 0.7#

Main child’s share of the conserved Murray cube-sum at a bifurcation (0.5 = symmetric, →1 = a thin twig off a barely-thinned trunk).

Constraints:
  • gt = 0.5

  • lt = 1.0

field step_per_radius: float = 1.5#

Growth step length as a multiple of the current radius.

Constraints:
  • gt = 0

pydantic model minisim.Bleaching[source]#

Bases: StepSpec

Per-cell, activity-driven photobleaching, opposed by protein turnover.

Photobleaching is a per-photon hazard, so each cell loses intact fluorophore in proportion to how much it emits (its calcium activity × excitation intensity), while turnover replenishes it toward full expression. The realized envelope is a cell-domain effect computed before composite (see BleachingStep), not a global movie multiply: busy or brightly-lit cells fade faster and to a lower floor, and with the light off the pool recovers, so the same model spans single recordings and repeated sessions. Defaults are calibrated to measured CA1 GCaMP6f bleaching curves across a wide range of excitation powers (bleaching linear in excitation; effective recovery ≈5.5 h, so darkness restores the pool within a couple of days).

Fields:
  • bleach_susceptibility (float)

  • excitation_intensity (float)

  • kind (Literal['bleaching'])

  • turnover_tau_s (float)

field kind: Literal['bleaching'] = 'bleaching'#
field bleach_susceptibility: float = 6.3e-06#

Bleach rate per second at unit excitation and baseline emission (the per-photon hazard); 0 disables bleaching. Calibrated to CA1 GCaMP6f.

Constraints:
  • ge = 0

field turnover_tau_s: float = 20000.0#

Effective fluorophore-recovery time constant, s (≈5.5 h, from the measured replenish rate). Restores the intact pool toward 1, opposing bleaching.

Constraints:
  • gt = 0

field excitation_intensity: float = 1.0#

Excitation level, dimensionless (1 = a typical continuous miniscope level). Deliberately unitless - absolute irradiance depends on the rig, depth, and optics. Scales the bleach rate linearly: the brighter-but-faster-fading trade-off.

Constraints:
  • ge = 0

pydantic model minisim.BrainMotion[source]#

Bases: StepSpec

Rigid x,y translation of the whole tissue frame - the tissue→sensor boundary.

The built step shifts the brain-frame canvas per frame and crops the sensor FOV from its center; it therefore requires a scene whose tissue canvas carries a margin ≥ the maximum shift (Scene.zeros(acq, margin_px=…), sized automatically by simulate()), so real off-FOV tissue moves into view instead of a fabricated fill. Ground truth records the per-frame (dy, dx) displacement in pixels.

Three sources of the trajectory, selected by model:

  • "physical" (default): a 2-D damped harmonic oscillator. The brain is a damped mass elastically tethered to the (rigid) skull, driven on the dominant locomotion_axis by an always-on locomotion rhythm at locomotion_freq_hz (mice/rats run at ~6-8 Hz) and on both axes by broadband sloshing noise. The restoring force bounds the motion physically; motion_amplitude_um sets the typical excursion and max_shift_um is the hard safety clamp (and the margin size). This is the realistic model the teaching notebook uses.

  • "walk": a bounded random walk (walk_step_um per frame, clamped to the max_shift_um disk). Cheap and rhythm-free; kept for simple tests/fixtures.

  • an explicit trajectory_um overrides both, regardless of model.

Axial focus-drift motion is a deferred placeholder.

Fields:
  • damping_ratio (float)

  • kind (Literal['brain_motion'])

  • locomotion_axis (Literal['y', 'x'])

  • locomotion_fraction (float)

  • locomotion_freq_hz (float)

  • max_shift_um (float)

  • model (Literal['physical', 'walk'])

  • motion_amplitude_um (float)

  • resonance_freq_hz (float)

  • trajectory_um (list[tuple[float, float]] | None)

  • walk_step_um (float)

field kind: Literal['brain_motion'] = 'brain_motion'#
field model: Literal['physical', 'walk'] = 'physical'#

Trajectory generator: ‘physical’ (driven damped oscillator) or ‘walk’ (bounded random walk). An explicit trajectory_um overrides both.

field trajectory_um: list[tuple[float, float]] | None = None#

Explicit per-frame (dy, dx) in µm; overrides model.

field max_shift_um: float = 15.0#

Hard safety clamp on cumulative shift magnitude, µm (also sizes the tissue margin).

Constraints:
  • gt = 0

field locomotion_freq_hz: float = 7.0#

Locomotion (stride) drive frequency, Hz; mice/rats run at ~6-8 Hz.

Constraints:
  • gt = 0

field motion_amplitude_um: float = 10.0#

Extreme excursion (99th-percentile displacement radius), µm; most frames move less.

Constraints:
  • gt = 0

field locomotion_axis: Literal['y', 'x'] = 'y'#

Dominant motion axis the locomotion rhythm drives (y = height; the cross axis gets noise only).

field resonance_freq_hz: float = 6.0#

Natural frequency of the brain-on-skull oscillator, Hz.

Constraints:
  • gt = 0

field damping_ratio: float = 0.5#

Damping ratio ζ of the oscillator (<1 under-damped, sloshy; ≥1 over-damped).

Constraints:
  • gt = 0

field locomotion_fraction: float = 0.25#

Share of motion amplitude carried by the locomotion rhythm vs broadband sloshing noise (noise-dominated by default).

Constraints:
  • ge = 0

  • le = 1

field walk_step_um: float = 0.3#

Random-walk step size, µm/frame (model=’walk’).

Constraints:
  • ge = 0

pydantic model minisim.IlluminationProfile[source]#

Bases: StepSpec

Static excitation-illumination falloff - the LED lights the FOV unevenly.

A single excitation LED illuminates the tissue brightest at the center and dimmer toward the edges, so peripheral cells fluoresce less to begin with. Modeled as a multiplicative radial falloff (1 at the bright center, dropping to falloff at the farthest corner, exponent shaping the rolloff) - fixed to the scope, so it does not move with the brain. Typically a gentle, broad rolloff (vs the sharper emission Vignette). Being on the excitation side, this falloff also drives photobleaching faster at the bright center: that coupling is wired in Bleaching (which evaluates this field at each cell’s rest position), the one way it differs from the collection-side vignette.

Fields:
  • center_offset_um (tuple[float, float])

  • exponent (float)

  • falloff (float)

  • kind (Literal['illumination_profile'])

field kind: Literal['illumination_profile'] = 'illumination_profile'#
field falloff: float = 0.7#

Edge excitation relative to center (1 = uniform).

Constraints:
  • ge = 0

  • le = 1

field exponent: float = 2.0#

Radial falloff exponent (gentle/broad by default).

Constraints:
  • gt = 0

field center_offset_um: tuple[float, float] = (0.0, 0.0)#

(dy, dx) offset of the bright center from FOV center, µm.

pydantic model minisim.Vignette[source]#

Bases: StepSpec

Static radial vignette on the emission / return path (collection light loss).

The physical return path trims light rays toward the field edges (aperture and relay clipping, compounded by poorer off-axis optical performance), so corners read dimmer regardless of how brightly the tissue was lit. Same multiplicative radial-falloff shape as the IlluminationProfile but on the collection side - so it does not drive bleaching - and typically a sharper edge rolloff. Also fixed to the scope (does not move with the brain). Off-axis blur is a separate concern, deferred to a future optical-aberration step.

Fields:
  • center_offset_um (tuple[float, float])

  • exponent (float)

  • falloff (float)

  • kind (Literal['vignette'])

field kind: Literal['vignette'] = 'vignette'#
field falloff: float = 0.5#

Corner brightness relative to center (1 = none).

Constraints:
  • ge = 0

  • le = 1

field exponent: float = 2.0#

Radial falloff exponent.

Constraints:
  • gt = 0

field center_offset_um: tuple[float, float] = (0.0, 0.0)#

(dy, dx) offset of the bright center from FOV center, µm.

pydantic model minisim.Leakage[source]#

Bases: StepSpec

Static additive baseline (stray excitation light on the detector).

One additive contributor to the smooth, low-frequency background that minian’s ‘glow removal’ estimates and subtracts - not its sole target: that removal also strips the multiplicative illumination falloff and vignette (see IlluminationProfile / Vignette), since all three are smooth and static while the cells are sharp and moving.

Fields:
  • kind (Literal['leakage'])

  • level (float)

  • profile (Literal['uniform', 'gaussian'])

  • sigma_um (float | None)

field kind: Literal['leakage'] = 'leakage'#
field profile: Literal['uniform', 'gaussian'] = 'gaussian'#

Spatial baseline shape.

field level: float = 0.1#

Additive baseline level.

Constraints:
  • ge = 0

field sigma_um: float | None = None#

Spatial sigma for the gaussian profile, µm; None defaults to a quarter of the smaller FOV dimension. Ignored by the uniform profile.

pydantic model minisim.Sensor[source]#

Bases: StepSpec

Photons → e⁻ → Poisson shot + read noise → ×gain → quantize → clip.

The only step that produces integer-valued counts. The sensor hardware (QE, read noise, gain, bit depth, pixel pitch) lives on Acquisition.image_sensor and is read from there. The single field below is the exposure/flux scale - a scene property, not sensor hardware - which is why it stays on the step rather than the image-sensor spec.

Fields:
  • kind (Literal['sensor'])

  • photons_per_unit (float)

field kind: Literal['sensor'] = 'sensor'#
field photons_per_unit: float = 100.0#

Photons per fluorescence intensity unit (exposure/flux scale); sets the shot-noise regime, and scales every light source in the frame (cells, neuropil, vasculature, leakage). A scene/illumination property, not sensor hardware (it lumps excitation power x integration time x collection efficiency), which is why it stays on the step rather than the image-sensor spec. Raise it (or the sensor gain) for a brighter recording; raising it also lifts the shot-noise-limited SNR.

Constraints:
  • gt = 0

Warnings#

class minisim.SpecWarning[source]#

Bases: UserWarning

Advisory warning for unusual-but-legal spec configurations.

The simulator distinguishes invalid configs (which raise) from unusual ones (which warn but still run) - e.g. a focal plane outside the cell depth range, or motion larger than the configured FOV margin.