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#
.. py:pydantic_model:: Spec :module: minisim :canonical: minisim.spec.Spec
Bases: :py:class:~minisim.spec._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:
- :py:obj:acquisition (minisim.spec.Acquisition) <minisim.spec.Spec.acquisition>
- :py:obj:output (minisim.spec.Output) <minisim.spec.Spec.output>
- :py:obj:seed (int) <minisim.spec.Spec.seed>
- :py:obj: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]) <minisim.spec.Spec.steps>
.. py:pydantic_field:: Spec.acquisition :module: minisim :type: Acquisition :optional:
.. py:pydantic_field:: Spec.seed :module: minisim :type: int :value: 42
RNG seed for full reproducibility.
.. py:pydantic_field:: Spec.steps :module: minisim :type: list[AnyStep] :required:
.. py:pydantic_field:: Spec.output :module: minisim :type: 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.
.. py:pydantic_model:: Acquisition :module: minisim :canonical: minisim.spec.Acquisition
Bases: :py:class:~minisim.spec._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:
- :py:obj:duration_s (float) <minisim.spec.Acquisition.duration_s>
- :py:obj:focal_depth_in_tissue_um (float | Literal['auto']) <minisim.spec.Acquisition.focal_depth_in_tissue_um>
- :py:obj:fps (float) <minisim.spec.Acquisition.fps>
- :py:obj:front_working_distance_um (float | None) <minisim.spec.Acquisition.front_working_distance_um>
- :py:obj:image_sensor (minisim.spec.ImageSensor) <minisim.spec.Acquisition.image_sensor>
- :py:obj:optics (minisim.spec.Optics) <minisim.spec.Acquisition.optics>
- :py:obj:tissue (minisim.spec.Tissue) <minisim.spec.Acquisition.tissue>
.. py:pydantic_field:: Acquisition.optics :module: minisim :type: Optics :optional:
.. py:pydantic_field:: Acquisition.image_sensor :module: minisim :type: ImageSensor :optional:
.. py:pydantic_field:: Acquisition.tissue :module: minisim :type: Tissue :optional:
.. py:pydantic_field:: Acquisition.fps :module: minisim :type: float :value: 20.0
Frame rate, frames per second.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Acquisition.duration_s :module: minisim :type: float :value: 150.0
Recording duration, seconds.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Acquisition.focal_depth_in_tissue_um :module: minisim :type: float | Literal[‘auto’] :value: ‘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.
.. py:pydantic_field:: Acquisition.front_working_distance_um :module: minisim :type: float | None :value: 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.
.. py:pydantic_model:: Optics :module: minisim :canonical: minisim.spec.Optics
Bases: :py:class:~minisim.spec._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:
- :py:obj:depth_of_field_um (float | Literal['auto']) <minisim.spec.Optics.depth_of_field_um>
- :py:obj:emission_nm (float) <minisim.spec.Optics.emission_nm>
- :py:obj:field_curvature_radius_um (float | None) <minisim.spec.Optics.field_curvature_radius_um>
- :py:obj:magnification (float) <minisim.spec.Optics.magnification>
- :py:obj:na (float) <minisim.spec.Optics.na>
.. py:pydantic_field:: Optics.na :module: minisim :type: float :value: 0.45
Numerical aperture of the GRIN objective.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Optics.magnification :module: minisim :type: float :value: 8.0
Optical magnification (sensor side / object side).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Optics.emission_nm :module: minisim :type: float :value: 525.0
Fluorophore emission wavelength, nm (GCaMP ≈ 525).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Optics.depth_of_field_um :module: minisim :type: float | Literal[‘auto’] :value: ‘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.
.. py:pydantic_field:: Optics.field_curvature_radius_um :module: minisim :type: float | None :value: 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.
.. py:pydantic_model:: ImageSensor :module: minisim :canonical: minisim.spec.ImageSensor
Bases: :py:class:~minisim.spec._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:
- :py:obj:bit_depth (int) <minisim.spec.ImageSensor.bit_depth>
- :py:obj:gain_adu_per_e (float) <minisim.spec.ImageSensor.gain_adu_per_e>
- :py:obj:n_px_height (int) <minisim.spec.ImageSensor.n_px_height>
- :py:obj:n_px_width (int) <minisim.spec.ImageSensor.n_px_width>
- :py:obj:pixel_pitch_um (float) <minisim.spec.ImageSensor.pixel_pitch_um>
- :py:obj:quantum_efficiency (float) <minisim.spec.ImageSensor.quantum_efficiency>
- :py:obj:read_noise_e (float) <minisim.spec.ImageSensor.read_noise_e>
.. py:pydantic_field:: ImageSensor.n_px_height :module: minisim :type: int :value: 256
Sensor height, pixels.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: ImageSensor.n_px_width :module: minisim :type: int :value: 256
Sensor width, pixels.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: ImageSensor.pixel_pitch_um :module: minisim :type: float :value: 3.0
Physical sensor pixel pitch, µm.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: ImageSensor.quantum_efficiency :module: minisim :type: float :value: 0.7
Photon → electron conversion efficiency.
:Constraints:
- **gt** = 0
- **le** = 1
.. py:pydantic_field:: ImageSensor.read_noise_e :module: minisim :type: float :value: 2.0
Read noise, electrons RMS.
:Constraints:
- **ge** = 0
.. py:pydantic_field:: ImageSensor.gain_adu_per_e :module: minisim :type: float :value: 1.0
Camera gain, ADU per electron.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: ImageSensor.bit_depth :module: minisim :type: int :value: 8
ADC bit depth; counts clipped to [0, 2^bit_depth − 1].
:Constraints:
- **gt** = 0
.. py:pydantic_model:: Tissue :module: minisim :canonical: minisim.spec.Tissue
Bases: :py:class:~minisim.spec._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 → :meth:attenuation, the scatter_mfp_* fields) and it blurs
the footprint (forward-scattered light, g ≈ 0.88, is recollected as a
growing halo → :meth: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 :attr:scatter_mfp_um).
:Fields:
- :py:obj:scatter_blur_per_um (float) <minisim.spec.Tissue.scatter_blur_per_um>
- :py:obj:scatter_mfp_emission_um (float) <minisim.spec.Tissue.scatter_mfp_emission_um>
- :py:obj:scatter_mfp_excitation_um (float) <minisim.spec.Tissue.scatter_mfp_excitation_um>
.. py:pydantic_field:: Tissue.scatter_mfp_excitation_um :module: minisim :type: float :value: 600.0
Effective attenuation MFP for the excitation leg (≈470 nm, in) - long, diffuse fluence, µm.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Tissue.scatter_mfp_emission_um :module: minisim :type: float :value: 100.0
Effective attenuation MFP for the emission leg (≈525 nm, out) - the image-forming scattering MFP, µm.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Tissue.scatter_blur_per_um :module: minisim :type: float :value: 0.05
Linear broadening of the footprint per µm of depth (µm sigma per µm depth).
:Constraints:
- **ge** = 0
Output#
.. py:pydantic_model:: Output :module: minisim :canonical: minisim.spec.Output
Bases: :py:class:~minisim.spec._Base
Final-array formatting - formatting only, never rescaling (honest radiometry).
:Fields:
- :py:obj:save_intermediates (bool) <minisim.spec.Output.save_intermediates>
- :py:obj:store_dtype (Literal['float32', 'float64']) <minisim.spec.Output.store_dtype>
.. py:pydantic_field:: Output.save_intermediates :module: minisim :type: bool :value: 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.
.. py:pydantic_field:: Output.store_dtype :module: minisim :type: Literal[‘float32’, ‘float64’] :value: ‘float32’
Float container for the integer-valued sensor counts.
Steps#
Spec.steps is the forward chain. Each step below is a
StepSpec; AnyStep is the discriminated union of them.
Each kind may appear at most once in a spec.
.. py:pydantic_model:: StepSpec :module: minisim :canonical: minisim.spec.StepSpec
Bases: :py:class:~minisim.spec._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 :data: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 (:data:_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:
- :py:obj:kind (str) <minisim.spec.StepSpec.kind>
.. py:pydantic_field:: StepSpec.kind :module: minisim :type: str :required:
The forward chain#
.. py:pydantic_model:: PlaceNeurons :module: minisim :canonical: minisim.spec.PlaceNeurons
Bases: :py:class:~minisim.spec.StepSpec, :py:class:~minisim.spec.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 :class: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 :attr: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:
- :py:obj:kind (Literal['place_neurons']) <minisim.spec.PlaceNeurons.kind>
- :py:obj:populations (list[NeuronPopulation] | None) <minisim.spec.PlaceNeurons.populations>
.. py:pydantic_field:: PlaceNeurons.kind :module: minisim :type: Literal[‘place_neurons’] :value: ‘place_neurons’
.. py:pydantic_field:: PlaceNeurons.populations :module: minisim :type: list[NeuronPopulation] | None :value: 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.
.. py:pydantic_model:: NeuronPopulation :module: minisim :canonical: minisim.spec.NeuronPopulation
Bases: :py:class:~minisim.spec._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
:attr: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
:func:~minisim.steps.cell.sample_neurons); brightness is biology and is drawn
later in cell_activity, never here.
:Fields:
- :py:obj:dendrite_length_um (float) <minisim.spec.NeuronPopulation.dendrite_length_um>
- :py:obj:dendrite_width_um (float) <minisim.spec.NeuronPopulation.dendrite_width_um>
- :py:obj:density_per_mm3 (float) <minisim.spec.NeuronPopulation.density_per_mm3>
- :py:obj:depth_range_um (tuple[float, float]) <minisim.spec.NeuronPopulation.depth_range_um>
- :py:obj:irregularity (float) <minisim.spec.NeuronPopulation.irregularity>
- :py:obj:min_distance_um (float) <minisim.spec.NeuronPopulation.min_distance_um>
- :py:obj:morphology (Literal['soma', 'cytosolic']) <minisim.spec.NeuronPopulation.morphology>
- :py:obj:n_dendrites (int) <minisim.spec.NeuronPopulation.n_dendrites>
- :py:obj:positions_um (list[tuple[float, float, float]] | None) <minisim.spec.NeuronPopulation.positions_um>
- :py:obj:soma_radius_um (float) <minisim.spec.NeuronPopulation.soma_radius_um>
.. py:pydantic_field:: NeuronPopulation.density_per_mm3 :module: minisim :type: float :value: 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
.. py:pydantic_field:: NeuronPopulation.soma_radius_um :module: minisim :type: float :value: 7.0
Soma radius, µm (typical cortical neuron ≈ 5–10).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: NeuronPopulation.irregularity :module: minisim :type: float :value: 0.3
0 = smooth disk; higher = lumpier soma (low-pass-noise threshold).
:Constraints:
- **ge** = 0
- **le** = 1
.. py:pydantic_field:: NeuronPopulation.morphology :module: minisim :type: Literal[‘soma’, ‘cytosolic’] :value: ‘soma’
GCaMP targeting variant: 'soma' = soma-targeted (lumpy disk only); 'cytosolic' = standard GCaMP (soma + tapering proximal dendrites).
.. py:pydantic_field:: NeuronPopulation.n_dendrites :module: minisim :type: int :value: 4
Proximal dendrites grown per cell when morphology='cytosolic' (ignored for 'soma').
:Constraints:
- **ge** = 0
.. py:pydantic_field:: NeuronPopulation.dendrite_length_um :module: minisim :type: float :value: 45.0
Proximal-dendrite length, µm (cytosolic only).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: NeuronPopulation.dendrite_width_um :module: minisim :type: float :value: 3.0
Proximal-dendrite base width (diameter), µm; tapers to a ~1 px thread at the tip (cytosolic only).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: NeuronPopulation.depth_range_um :module: minisim :type: tuple[float, float] :value: (0.0, 200.0)
(min, max) depth into tissue, µm.
.. py:pydantic_field:: NeuronPopulation.min_distance_um :module: minisim :type: float :value: 0.0
3-D center-to-center minimum (Poisson-disk if > 0).
:Constraints:
- **ge** = 0
.. py:pydantic_field:: NeuronPopulation.positions_um :module: minisim :type: list[tuple[float, float, float]] | None :value: None
Explicit soma centers as (z, y, x) µm tuples - depth, row, column - in the tissue frame (origin = canvas top-left, the same coordinates the sampled centers and ground-truth positions use; note the depth-first order, matching Cell.center_um rather than x,y,z). 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.
.. py:pydantic_model:: CellActivity :module: minisim :canonical: minisim.spec.CellActivity
Bases: :py:class:~minisim.spec.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:
- :py:obj:active_rate_hz (float) <minisim.spec.CellActivity.active_rate_hz>
- :py:obj:brightness_cv (float) <minisim.spec.CellActivity.brightness_cv>
- :py:obj:f0 (float) <minisim.spec.CellActivity.f0>
- :py:obj:kind (Literal['cell_activity']) <minisim.spec.CellActivity.kind>
- :py:obj:p_active_to_quiescent (float) <minisim.spec.CellActivity.p_active_to_quiescent>
- :py:obj:p_quiescent_to_active (float) <minisim.spec.CellActivity.p_quiescent_to_active>
- :py:obj:quiescent_rate_hz (float) <minisim.spec.CellActivity.quiescent_rate_hz>
- :py:obj:spike_sim_hz (float) <minisim.spec.CellActivity.spike_sim_hz>
- :py:obj:tau_decay_s (float) <minisim.spec.CellActivity.tau_decay_s>
- :py:obj:tau_rise_s (float) <minisim.spec.CellActivity.tau_rise_s>
- :py:obj:trace_noise (float) <minisim.spec.CellActivity.trace_noise>
.. py:pydantic_field:: CellActivity.kind :module: minisim :type: Literal[‘cell_activity’] :value: ‘cell_activity’
.. py:pydantic_field:: CellActivity.spike_sim_hz :module: minisim :type: float :value: 300.0
High-res spike-simulation rate, Hz (~300 = a ~3 ms refractory); binned to the frame rate.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.p_quiescent_to_active :module: minisim :type: float :value: 0.005
Per-frame quiescent→active transition prob.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.p_active_to_quiescent :module: minisim :type: float :value: 0.3
Per-frame active→quiescent transition prob.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.active_rate_hz :module: minisim :type: float :value: 150.0
Instantaneous firing rate while active, Hz (the in-burst rate).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.quiescent_rate_hz :module: minisim :type: float :value: 0.6
Instantaneous firing rate while quiescent, Hz (the intrinsic background).
:Constraints:
- **ge** = 0
.. py:pydantic_field:: CellActivity.tau_rise_s :module: minisim :type: float :value: 0.05
Calcium rise time constant, s.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.tau_decay_s :module: minisim :type: float :value: 0.5
Calcium decay time constant, s.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: CellActivity.brightness_cv :module: minisim :type: float :value: 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
.. py:pydantic_field:: CellActivity.f0 :module: minisim :type: float :value: 1.0
Baseline fluorescence.
:Constraints:
- **ge** = 0
.. py:pydantic_field:: CellActivity.trace_noise :module: minisim :type: float :value: 0.0
Non-physical additive trace noise (default 0). An advanced override only; real noise enters at sensor/neuropil, not here.
:Constraints:
- **ge** = 0
.. py:pydantic_model:: CellOptics :module: minisim :canonical: minisim.spec.CellOptics
Bases: :py:class:~minisim.spec.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:
- :py:obj:kind (Literal['optics']) <minisim.spec.CellOptics.kind>
.. py:pydantic_field:: CellOptics.kind :module: minisim :type: Literal[‘optics’] :value: ‘optics’
.. py:pydantic_model:: Composite :module: minisim :canonical: minisim.spec.Composite
Bases: :py:class:~minisim.spec.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 CNMF target in ground truth.
:Fields:
- :py:obj:kind (Literal['composite']) <minisim.spec.Composite.kind>
.. py:pydantic_field:: Composite.kind :module: minisim :type: Literal[‘composite’] :value: ‘composite’
.. py:pydantic_model:: Neuropil :module: minisim :canonical: minisim.spec.Neuropil
Bases: :py:class:~minisim.spec.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:
- :py:obj:amplitude (float) <minisim.spec.Neuropil.amplitude>
- :py:obj:kind (Literal['neuropil']) <minisim.spec.Neuropil.kind>
- :py:obj:n_components (int) <minisim.spec.Neuropil.n_components>
- :py:obj:population_coupling (float) <minisim.spec.Neuropil.population_coupling>
- :py:obj:population_tau_s (float) <minisim.spec.Neuropil.population_tau_s>
- :py:obj:spatial_sigma_um (float) <minisim.spec.Neuropil.spatial_sigma_um>
- :py:obj:temporal_tau_s (float) <minisim.spec.Neuropil.temporal_tau_s>
.. py:pydantic_field:: Neuropil.kind :module: minisim :type: Literal[‘neuropil’] :value: ‘neuropil’
.. py:pydantic_field:: Neuropil.spatial_sigma_um :module: minisim :type: float :value: 40.0
Spatial smoothness of the mesh, µm.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Neuropil.temporal_tau_s :module: minisim :type: float :value: 10.0
OU correlation time of the independent slow-drift leg, s (slow).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Neuropil.population_tau_s :module: minisim :type: float :value: 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
.. py:pydantic_field:: Neuropil.amplitude :module: minisim :type: float :value: 0.5
Background amplitude relative to cell signal.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Neuropil.n_components :module: minisim :type: int :value: 3
Number of independent diffuse components.
:Constraints:
- **ge** = 1
.. py:pydantic_field:: Neuropil.population_coupling :module: minisim :type: float :value: 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
.. py:pydantic_model:: Vasculature :module: minisim :canonical: minisim.spec.Vasculature
Bases: :py:class:~minisim.spec.StepSpec
Dark absorbing mask × (slow dilation + cardiac). Placeholder no-op for v1.
:Fields:
- :py:obj:enabled (bool) <minisim.spec.Vasculature.enabled>
- :py:obj:kind (Literal['vasculature']) <minisim.spec.Vasculature.kind>
.. py:pydantic_field:: Vasculature.kind :module: minisim :type: Literal[‘vasculature’] :value: ‘vasculature’
.. py:pydantic_field:: Vasculature.enabled :module: minisim :type: bool :value: False
Placeholder; multiplicative absorption lands in v1.1.
.. py:pydantic_model:: Bleaching :module: minisim :canonical: minisim.spec.Bleaching
Bases: :py:class:~minisim.spec.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
:class:~minisim.steps.tissue.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:
- :py:obj:bleach_susceptibility (float) <minisim.spec.Bleaching.bleach_susceptibility>
- :py:obj:excitation_intensity (float) <minisim.spec.Bleaching.excitation_intensity>
- :py:obj:kind (Literal['bleaching']) <minisim.spec.Bleaching.kind>
- :py:obj:turnover_tau_s (float) <minisim.spec.Bleaching.turnover_tau_s>
.. py:pydantic_field:: Bleaching.kind :module: minisim :type: Literal[‘bleaching’] :value: ‘bleaching’
.. py:pydantic_field:: Bleaching.bleach_susceptibility :module: minisim :type: float :value: 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
.. py:pydantic_field:: Bleaching.turnover_tau_s :module: minisim :type: float :value: 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
.. py:pydantic_field:: Bleaching.excitation_intensity :module: minisim :type: float :value: 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
.. py:pydantic_model:: BrainMotion :module: minisim :canonical: minisim.spec.BrainMotion
Bases: :py:class:~minisim.spec.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 dominantlocomotion_axisby an always-on locomotion rhythm atlocomotion_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_umsets the typical excursion andmax_shift_umis the hard safety clamp (and the margin size). This is the realistic model the teaching notebook uses."walk": a bounded random walk (walk_step_umper frame, clamped to themax_shift_umdisk). Cheap and rhythm-free; kept for simple tests/fixtures.an explicit
trajectory_umoverrides both, regardless ofmodel.
Axial focus-drift motion is a deferred placeholder.
:Fields:
- :py:obj:damping_ratio (float) <minisim.spec.BrainMotion.damping_ratio>
- :py:obj:kind (Literal['brain_motion']) <minisim.spec.BrainMotion.kind>
- :py:obj:locomotion_axis (Literal['y', 'x']) <minisim.spec.BrainMotion.locomotion_axis>
- :py:obj:locomotion_fraction (float) <minisim.spec.BrainMotion.locomotion_fraction>
- :py:obj:locomotion_freq_hz (float) <minisim.spec.BrainMotion.locomotion_freq_hz>
- :py:obj:max_shift_um (float) <minisim.spec.BrainMotion.max_shift_um>
- :py:obj:model (Literal['physical', 'walk']) <minisim.spec.BrainMotion.model>
- :py:obj:motion_amplitude_um (float) <minisim.spec.BrainMotion.motion_amplitude_um>
- :py:obj:resonance_freq_hz (float) <minisim.spec.BrainMotion.resonance_freq_hz>
- :py:obj:trajectory_um (list[tuple[float, float]] | None) <minisim.spec.BrainMotion.trajectory_um>
- :py:obj:walk_step_um (float) <minisim.spec.BrainMotion.walk_step_um>
.. py:pydantic_field:: BrainMotion.kind :module: minisim :type: Literal[‘brain_motion’] :value: ‘brain_motion’
.. py:pydantic_field:: BrainMotion.model :module: minisim :type: Literal[‘physical’, ‘walk’] :value: ‘physical’
Trajectory generator: 'physical' (driven damped oscillator) or 'walk' (bounded random walk). An explicit trajectory_um overrides both.
.. py:pydantic_field:: BrainMotion.trajectory_um :module: minisim :type: list[tuple[float, float]] | None :value: None
Explicit per-frame (dy, dx) in µm; overrides model.
.. py:pydantic_field:: BrainMotion.max_shift_um :module: minisim :type: float :value: 15.0
Hard safety clamp on cumulative shift magnitude, µm (also sizes the tissue margin).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: BrainMotion.locomotion_freq_hz :module: minisim :type: float :value: 7.0
Locomotion (stride) drive frequency, Hz; mice/rats run at ~6-8 Hz.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: BrainMotion.motion_amplitude_um :module: minisim :type: float :value: 10.0
Extreme excursion (99th-percentile displacement radius), µm; most frames move less.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: BrainMotion.locomotion_axis :module: minisim :type: Literal[‘y’, ‘x’] :value: ‘y’
Dominant motion axis the locomotion rhythm drives (y = height; the cross axis gets noise only).
.. py:pydantic_field:: BrainMotion.resonance_freq_hz :module: minisim :type: float :value: 6.0
Natural frequency of the brain-on-skull oscillator, Hz.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: BrainMotion.damping_ratio :module: minisim :type: float :value: 0.5
Damping ratio ζ of the oscillator (<1 under-damped, sloshy; ≥1 over-damped).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: BrainMotion.locomotion_fraction :module: minisim :type: float :value: 0.25
Share of motion amplitude carried by the locomotion rhythm vs broadband sloshing noise (noise-dominated by default).
:Constraints:
- **ge** = 0
- **le** = 1
.. py:pydantic_field:: BrainMotion.walk_step_um :module: minisim :type: float :value: 0.3
Random-walk step size, µm/frame (model='walk').
:Constraints:
- **ge** = 0
.. py:pydantic_model:: IlluminationProfile :module: minisim :canonical: minisim.spec.IlluminationProfile
Bases: :py:class:~minisim.spec.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:
- :py:obj:center_offset_um (tuple[float, float]) <minisim.spec.IlluminationProfile.center_offset_um>
- :py:obj:exponent (float) <minisim.spec.IlluminationProfile.exponent>
- :py:obj:falloff (float) <minisim.spec.IlluminationProfile.falloff>
- :py:obj:kind (Literal['illumination_profile']) <minisim.spec.IlluminationProfile.kind>
.. py:pydantic_field:: IlluminationProfile.kind :module: minisim :type: Literal[‘illumination_profile’] :value: ‘illumination_profile’
.. py:pydantic_field:: IlluminationProfile.falloff :module: minisim :type: float :value: 0.7
Edge excitation relative to center (1 = uniform).
:Constraints:
- **ge** = 0
- **le** = 1
.. py:pydantic_field:: IlluminationProfile.exponent :module: minisim :type: float :value: 2.0
Radial falloff exponent (gentle/broad by default).
:Constraints:
- **gt** = 0
.. py:pydantic_field:: IlluminationProfile.center_offset_um :module: minisim :type: tuple[float, float] :value: (0.0, 0.0)
(dy, dx) offset of the bright center from FOV center, µm.
.. py:pydantic_model:: Vignette :module: minisim :canonical: minisim.spec.Vignette
Bases: :py:class:~minisim.spec.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:
- :py:obj:center_offset_um (tuple[float, float]) <minisim.spec.Vignette.center_offset_um>
- :py:obj:exponent (float) <minisim.spec.Vignette.exponent>
- :py:obj:falloff (float) <minisim.spec.Vignette.falloff>
- :py:obj:kind (Literal['vignette']) <minisim.spec.Vignette.kind>
.. py:pydantic_field:: Vignette.kind :module: minisim :type: Literal[‘vignette’] :value: ‘vignette’
.. py:pydantic_field:: Vignette.falloff :module: minisim :type: float :value: 0.5
Corner brightness relative to center (1 = none).
:Constraints:
- **ge** = 0
- **le** = 1
.. py:pydantic_field:: Vignette.exponent :module: minisim :type: float :value: 2.0
Radial falloff exponent.
:Constraints:
- **gt** = 0
.. py:pydantic_field:: Vignette.center_offset_um :module: minisim :type: tuple[float, float] :value: (0.0, 0.0)
(dy, dx) offset of the bright center from FOV center, µm.
.. py:pydantic_model:: Leakage :module: minisim :canonical: minisim.spec.Leakage
Bases: :py:class:~minisim.spec.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:
- :py:obj:kind (Literal['leakage']) <minisim.spec.Leakage.kind>
- :py:obj:level (float) <minisim.spec.Leakage.level>
- :py:obj:profile (Literal['uniform', 'gaussian']) <minisim.spec.Leakage.profile>
- :py:obj:sigma_um (float | None) <minisim.spec.Leakage.sigma_um>
.. py:pydantic_field:: Leakage.kind :module: minisim :type: Literal[‘leakage’] :value: ‘leakage’
.. py:pydantic_field:: Leakage.profile :module: minisim :type: Literal[‘uniform’, ‘gaussian’] :value: ‘gaussian’
Spatial baseline shape.
.. py:pydantic_field:: Leakage.level :module: minisim :type: float :value: 0.1
Additive baseline level.
:Constraints:
- **ge** = 0
.. py:pydantic_field:: Leakage.sigma_um :module: minisim :type: float | None :value: None
Spatial sigma for the gaussian profile, µm; None defaults to a quarter of the smaller FOV dimension. Ignored by the uniform profile.
.. py:pydantic_model:: Sensor :module: minisim :canonical: minisim.spec.Sensor
Bases: :py:class:~minisim.spec.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:
- :py:obj:kind (Literal['sensor']) <minisim.spec.Sensor.kind>
- :py:obj:photons_per_unit (float) <minisim.spec.Sensor.photons_per_unit>
.. py:pydantic_field:: Sensor.kind :module: minisim :type: Literal[‘sensor’] :value: ‘sensor’
.. py:pydantic_field:: Sensor.photons_per_unit :module: minisim :type: float :value: 100.0
Photons per fluorescence intensity unit (exposure/flux scale); sets the shot-noise regime. A scene/illumination property, not sensor hardware.
:Constraints:
- **gt** = 0
Warnings#
.. py:class:: SpecWarning :module: minisim :canonical: minisim.spec.SpecWarning
Bases: :py:class: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.