v0.3.7: Source Bookkeeping, Field Handoff & Probe Readout¶
Version: 0.3.7
Difficulty: Intermediate
Duration: 20–30 minutes
Scope: Computational scaffold, simulated proxy fields, tutorial-scale learning
Overview¶
This tutorial documents the jaxfne source bookkeeping API: how neural sources are declared (implicitly via emitter + probes), how they flow to fields (convolution-based proxies), and how metadata gates physical amplitude statuss.
The core concepts:
- Source Declaration (Implicit): Emitter type (
izhikevich, preset) + probe modes determine which fields are computed - Field Handoff (Convolution-based): Sources spread spatially via fixed convolution kernels (not PDE-solved)
- Probe Readout (Configurable): Multiple readout modes (
source,LFP-proxy,CSD-proxy) extract different field perspectives - Scope Clarity (Metadata): Manifest keys (
amplitude_status=False) prevent misinterpretation
This is not a biophysical validation tutorial. It is a computational scaffold for understanding how neural sources map to observable fields in the jaxfne framework.
Interactive 3D Column Visualization¶
The visualization below is a standalone Plotly HTML showing a laminar cortical column with:
- 3D neuron scatter: Colored by firing rate, positioned by layer and depth
- Hover metadata: Neuron ID, layer, cell type, depth, source index, mean firing rate
- Readout panels: Source summary, population firing rate, LFP-proxy, laminar voltage profile
- Equation annotations: Source bookkeeping (S), field handoff (Y = P·S), probe readout (R = Q·Y)
- Interactive controls: Pan, zoom, rotate (mouse + keyboard)
View the Interactive Column¶
Understanding the Visualization¶
Neuron Colors (Firing Rate)¶
Hover over any neuron to see: - Neuron ID: Index in the population - Layer: L2/3, L4, L5, or L6 - Cell type: E (excitatory), PV (parvalbumin-positive), SST, or VIP - Depth: Distance from surface (µm) - Source index: Mapping to source current in simulation - Rate: Mean firing rate (Hz)
Color scale (Viridis): Blue = low rate, Yellow = high rate
The Equations¶
Three core relationships are annotated in the visualization:
$\(S(t) \in \mathbb{R}^{T \times N}\)$ Source bookkeeping: Time-series of neural source currents. T = timepoints, N = neurons.
$\(Y(t) = P \cdot S(t)\)$ Field handoff: Spatial convolution (P is the convolution kernel) maps point sources to field.
$\(R_k(t) = Q_k \cdot Y(t)\)$
Probe readout: Different probes (k = source, LFP-proxy, CSD-proxy) extract different aspects of the field via operators Q_k.
Configuration API & Source Declaration¶
The Implicit Contract¶
Sources are not explicitly declared. Instead, they are inferred from two decisions:
- Emitter type & preset: Determines what sources are available (e.g., Izhikevich → intrinsic currents + synaptic input)
- Probe modes: Determines which sources are computed and returned
import jaxfne as jtfne
cfg = (jtfne.Configuration()
.runtime(seed=42, dtype='float32', duration_ms=1000, dt_ms=0.1)
.column(name='tutorial_column', layers=['L2/3', 'L4', 'L5', 'L6'], n=48)
.cell_types({'E': 0.70, 'PV': 0.15, 'SST': 0.10, 'VIP': 0.05})
.connectivity(kind='laminar_signed_metadata', recurrent=True)
.set_emitter('izhikevich', 'cortical_eig')
.probes(['spikes', 'V_m', 'source', 'LFP-proxy', 'CSD-proxy'], n_contacts=16))
model = jtfne.construct(cfg)
signals = jtfne.simulate(model, duration_ms=1000, dt_ms=0.1, seed=42)
Key observation: There is no .declare_source() method. The flow is:
- Emitter defines neuronal dynamics and available source types
- Probes select which sources to extract and how to compute them
- Signals object returns the requested readouts
Signals API Contract¶
After jtfne.simulate(), the returned signals object has:
signals.spikes # np.ndarray, shape (T, N), boolean spike indicator
signals.V_m # np.ndarray, shape (T, N), membrane voltage
signals.sources # np.ndarray, shape (T, N) or (T, S), source currents
signals.time_ms # np.ndarray, shape (T,), time axis in milliseconds
signals.metadata # dict, scope/readout metadata (see below)
Metadata Keys (Scope Clarity)¶
signals.metadata = {
"scope_status": "computational_scaffold",
"readout_status": "simulated_proxy",
"field_mode": "proxy_convolution_no_pde",
"amplitude_status": False,
"duration_ms": 1000.0,
"dt_ms": 0.1,
"dtype": "float32",
"seed": 42,
}
Critical key: amplitude_status=False gates statements about real-world amplitude. This is a computational scaffold, not a calibrated model.
Probe Modes & Field Computation¶
Available Probe Modes¶
| Mode | Shape | Description | Biophysical? |
|---|---|---|---|
spikes |
(T, N) | Spike detection (boolean) | Simplified |
V_m |
(T, N) | Membrane voltage | Phenomenological |
source |
(T, N) | Raw source currents from emitter | Phenomenological |
LFP-proxy |
(T, C) | Local-field-potential proxy via convolution | Proxy only |
CSD-proxy |
(T, C) | Current-source-density proxy (spatial derivative) | Proxy only |
How Proxy Fields are Computed¶
Not PDE-solved. Instead:
- Extract source currents from neurons: \(S(t)\)
- Apply fixed spatial convolution kernel (e.g., Gaussian): \(Y(t) = P \cdot S(t)\)
- Optionally compute spatial derivatives (for CSD)
This is fast (no solver loop) but approximate (proxy-scoped for tutorial data).
Source-to-Field Mechanism¶
The Handoff Flow¶
Neuron Emitter (Izhikevich)
↓
Intrinsic Currents (I_intrinsic)
+ Synaptic Input (I_syn = W @ s)
= Source Signal S(t)
↓
Convolution Kernel P
↓
Field Y(t) = P * S(t)
↓
Probe Operators (Q_LFP, Q_CSD, etc.)
↓
Readout R(t) = Q @ Y(t)
Spatial Representation¶
- Source coordinates: Point sources at each neuron location
- Field coordinates: Spatially expanded via convolution (spatial units arbitrary, normalized)
- Kernel type: Gaussian (default); width parameterized by distance in µm
- Boundary handling: Zero-padding (no boundary currents)
Important: Spatial units are arbitrary normalized units, not physical micrometers. The visualization uses µm for layer depth (anatomical reference), but field amplitudes are not calibrated.
Manifest & Metadata Validation¶
Run Manifest Template¶
import json
RUN_METADATA = {
"scope_status": "computational_scaffold",
"readout_status": "simulated_proxy",
"field_mode": "proxy_convolution_no_pde",
"amplitude_status": False,
"duration_ms": 1000.0,
"dt_ms": 0.1,
"dtype": "float32",
"seed": 42,
"n_neurons": 48,
"layers": ["L2/3", "L4", "L5", "L6"],
"mean_population_rate_hz": 2.5,
"voltage_range_mv_like": [-86.0, 30.0],
"all_outputs_finite": True,
"equations": {
"source_bookkeeping": "S(t) ∈ ℝ^{T×N}",
"field_handoff": "Y(t) = P·S(t)",
"probe_readout": "R_k(t) = Q_k·Y(t)",
},
}
# Validate JSON safety (no NaN/Inf)
json.dumps(RUN_METADATA, allow_nan=False)
JSON Safety¶
All manifest outputs must serialize with allow_nan=False:
# OK
json.dumps(manifest, allow_nan=False)
# Will fail if NaN/Inf present
manifest["rate"] = float('nan')
json.dumps(manifest, allow_nan=False) # ← JSONDecodeError
Statement Gates & Interpretation¶
The Gate: amplitude_status¶
This boolean key prevents misinterpretation:
if not metadata["amplitude_status"]:
# BLOCKED: Stating real-world amplitude
# "The LFP-proxy amplitude is 50 µV"
# ALLOWED: Relative or tutorial statements
# "LFP-proxy increases during high firing rate"
# "Layer 5 sources dominate the field"
v0.3.7 Scope¶
- No biophysical compartments (soma, dendrite, axon)
- No temperature sensitivity, frequency-dependent effects
- No subject-specific anatomy
- No experimental validation
- Kernels are fixed defaults (not tunable in v0.3.7)
Reserved Work (v0.3.8+)¶
- Custom convolution kernels via
.field_kernel()method - PDE-based field solvers (optional)
- Calibration to real neural recordings
- Frequency-response properties
Summary & Next Steps¶
What You've Learned¶
- Sources are implicit: Emitter + probes determine field computation
- Fields are proxies: Convolution-based, fast, approximate
- Readouts are multi-modal: Different operators extract different field perspectives
- Metadata gates statements:
amplitude_status=Falseprevents misinterpretation
How to Use This in Your Work¶
# Step 1: Configure a column
cfg = jtfne.Configuration().set_emitter(...).probes([...])
# Step 2: Simulate
model = jtfne.construct(cfg)
signals = jtfne.simulate(model, ...)
# Step 3: Check scope before interpreting
assert not signals.metadata["amplitude_status"]
# Step 4: Use relative comparisons, not absolute statements
layer5_rate = signals.spikes[layer5_idx].mean()
layer23_rate = signals.spikes[layer23_idx].mean()
print(f"L5 rate is {layer5_rate / layer23_rate:.1f}x L2/3 rate") # ✓ OK
# Step 5: Document scope in your output
json.dump(signals.metadata, fp, allow_nan=False)
References¶
- v0.3.6 Tutorial: Configuration API & E/I Networks
- API Reference: API Overview
- Guides: Probe Operators | Tensor-Field Workflows
- Interactive Visualization: The 3D source/field/probe column is embedded above via Plotly (interactive HTML)
- GitHub: jaxfne Issues
End of v0.3.7 Tutorial
Feedback? Open an issue: jaxfne/issues