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 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: - :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.