Skip to content

Source and Field Equations

Purpose

This document specifies source bookkeeping modes, field metadata contracts, forbidden patterns, and the mappings between jaxfne core equations and runtime manifest/report fields.

It complements the Mathematical Glossary Flow by grounding source and field equations in implementation detail: which manifest field controls which equation, what modes are allowed, what combinations are forbidden, and how to interpret statement boundaries in code.


Source Bookkeeping and Calibration

Definition: Source Density

The source density \(q(x,t)\) is the current per unit volume (or area) at position \(x\) and time \(t\):

\[q(x,t) = P_s[z(t), I(t), \chi(x)]\]

Physically, \(q\) is the transmembrane current density that becomes the boundary condition for the field equation.

Source Modes (Mutually Exclusive)

A jaxfne model declares exactly one source mode per simulation run. All others are inactive.

Mode Equation Status Implementation
total_membrane_current \(q = I_\mathrm{mem}(z, t)\) Reserved (reserved) Not in v0.2.24–v0.2.27
decomposed_cap_ion_syn \(q = I_\mathrm{cap} + I_\mathrm{ion} + I_\mathrm{syn}\) Reserved (reserved) Not in v0.2.24–v0.2.27
proxy_no_field_solve \(q = \text{declared proxy} \approx I_\mathrm{native}\) Active (current default) jaxfne.fields.project_laminar_sources()
(none declared) Signals.sources = None Allowed (no source) Field=None, no readouts

Rule: Declare source_projection_mode and source_calibration_status in Manifest. If both are None, Signals.sources remains None.

Current Default: proxy_no_field_solve

In v0.2.24–v0.2.27, the active mode is:

source_projection_mode = "proxy_no_field_solve"
source_calibration_status = "uncalibrated_izhikevich_model_current"

What this means: - \(q\) is computed as: emitter model current (Izhikevich \(I_k\)) + spike impulse proxy (20× gain) - \(q\) lacks validation against empirical synaptic or ionic current (calibration status: uncalibrated) - \(q\) is spatial proxy: neuron position (in laminar_source_geometry) → spatial contact coupling - Field computation is deferred (field_solver_status = "linear_solver") - CSD is derived from source via differentiation: \(\mathrm{CSD} \propto \nabla \cdot q\) (proxy)

Code example:

cfg = jtfne.configuration()
    .emitter(family="izhikevich", preset="cortical_eig")
    .field(
        domain="laminar_column",
        conductivity="proxy",
        boundary="mean_zero_neumann",  # metadata-only
        gauge="mean_zero",  # metadata-only
    )

# In the Manifest:
manifest = model.manifest(signals, readouts)
print(manifest["source_calibration_status"])  # → "uncalibrated_izhikevich_model_current"
print(manifest["source_projection_mode"])  # → "proxy_no_field_solve"
print(manifest["field_solver_status"])  # → "linear_solver"


Forbidden Pattern: Synaptic Double-Counting

Critical rule: A source declaration must count synaptic current exactly once.

The Pattern (FORBIDDEN)

FORBIDDEN:
q(x,t) = χ(x) · (I_cap(t) + I_ion(t) + I_syn(t)) + q_syn_extra(t)
         ↑ Single source (membrane current)         ↑ Extra synaptic term

Result: I_syn is counted twice (once in total membrane, once in q_syn_extra)

Why it's forbidden: - Current conservation: ∇·q must balance all currents once, not twice - Gauge invariance breaks: mean-zero gauge cannot hold if synaptic current is duplicated - Probe readout corruption: CSD, LFP, and EMM metrics become nonphysical

The Pattern (ALLOWED)

Option A: Single membrane-current source

ALLOWED:
q(x,t) = χ(x) · (I_cap(t) + I_ion(t) + I_syn(t))
         Single transmembrane source, all components included once

Option B: Decomposed sources (reserved source mode)

ALLOWED (declared reserved):
q_cap(x,t) = χ(x) · I_cap(t)
q_ion(x,t) = χ(x) · I_ion(t)
q_syn(x,t) = χ(x) · I_syn(t)
→ Keep separate in Signals, sum only at field boundary
→ Requires explicit source_decomposition contract

Option C: Proxy current (current default v0.2.24)

ALLOWED (active):
q_proxy(x,t) = χ(x) · (I_Iz(t) + I_spike_impulse(t))
              ↑ Izhikevich model current (preserved as-is)
              ↑ Spike impulse proxy (20× gain, derived)
→ Single proxy source, no decomposition
→ source_calibration_status: "uncalibrated_izhikevich_model_current"

How to Audit Your Code

Before releasing a model:

# Check manifest:
manifest = model.manifest(signals, readouts)
source_cal = manifest.get("source_calibration_status")
source_mode = manifest.get("source_projection_mode")
source_decomp = manifest.get("source_decomposition", "unknown")

# Assertion:
assert source_cal in [
    "uncalibrated_izhikevich_model_current",
    "uncalibrated_hh_model_current",
    "uncalibrated_jaxley_voltage_proxy",
    None,  # no source declared
], f"Unexpected source_calibration_status: {source_cal}"

# Check: if source_decomposition is "decomposed_cap_ion_syn", 
# ensure Signals.sources has shape [n_time, n_neurons, 3] (cap, ion, syn channels)
if source_decomp == "decomposed_cap_ion_syn":
    assert signals.sources.shape[-1] == 3, "Expected 3 source channels (cap, ion, syn)"

Field Metadata and Statement Boundaries

Field Solver Status

The field_solver_status field in Manifest declares whether the field PDE is solved or proxy-only.

Status Solver φ_e Current CSD Statement
linear_solver None Proxy Proxy Proxy Computational scaffold; no physical conductivity statement
specified_reserved_solver Reserved To be solved To be solved Solved Reserved v0.2.27+; reserved status yet

Current default (v0.2.24–v0.2.27):

field_solver_status = "linear_solver"

What it means: - The field equation \(\nabla \cdot (-\sigma_e \nabla \phi_e) = q\) is declared for reference; forward-field computation uses laminar-proxy approximation - \(\phi_e\), \(\mathbf{J}_e\), and \(\mathrm{CSD}\) are computed from \(q\) using laminar-proxy kernels (proxy approximation, not full PDE solve, no conductivity calibration) - Boundary conditions and gauge are metadata-only (informational, separate from proxy computation) - CSD sign convention is declared: positive = extracellular source (current flowing outward)

Boundary Conditions and Gauge (Metadata-Only in v0.2.24)

cfg = jtfne.configuration()
    .field(
        domain="laminar_column",
        conductivity="proxy",
        boundary="mean_zero_neumann",   Metadata field (v0.2.24)
        gauge="mean_zero",               Metadata field (v0.2.24)
    )

In v0.2.24–v0.2.27, these are stored in Manifest but do not affect simulation:

boundary_condition: Specifies Neumann (zero-flux) condition (reserved solver regime)
gauge: Specifies mean-zero constraint (reserved solver regime)

In v0.2.27+ (future), when a field solver is added: - boundary_condition will be enforced during PDE solve - gauge will be applied to enforce \(\int \phi_e \, dx = 0\) (mean-zero potential)

Code example (current):

manifest = model.manifest(signals, readouts)
print(manifest["field_solver_status"])  # → "linear_solver"
print(manifest["boundary_condition"])  # → "mean_zero_neumann"
print(manifest["gauge"])  # → "mean_zero"

# These fields are informational only in v0.2.24.
# They document intended reserved behavior.

CSD Sign Convention

The current jaxfne convention:

\[\mathrm{CSD}(x,t) = \nabla \cdot \mathbf{J}_e(x,t) = -\nabla \cdot (\sigma_e \nabla \phi_e)\]

Sign convention in jaxfne: - Positive CSD = extracellular current diverging (flowing outward) - Negative CSD = extracellular current converging (flowing inward) - Interpretation: Positive CSD suggests membrane sink (inward membrane current); negative CSD suggests source (outward membrane current)

Declared in Manifest:

manifest = model.manifest(signals, readouts)
print(manifest.get("csd_sign_convention"))  # → "positive_equals_extracellular_source"

Validation: Always verify CSD sign convention when comparing to external data or evidences. Different fields/literature use opposite conventions.


Calibration Labels and Constraints

Source Calibration Status

The source_calibration_status field documents the empirical grounding of the source model.

Status Meaning Biological Statement Allowed?
uncalibrated_izhikevich_model_current Izhikevich model current, lacks empirical validation None; computational scaffold ✓ v0.2.24+ default
uncalibrated_hh_model_current Hodgkin-Huxley model current, lacks empirical validation None; computational scaffold ✓ Reserved
uncalibrated_jaxley_voltage_proxy Voltage trace proxy from external emitter, no empirical validation None; computational scaffold ✓ v0.2.22+ bridge
calibrated_* Validated against empirical current/field data Conditional; requires methods section & receipt ✗ v0.2.24–v0.2.26; reserved

Current constraint:

amplitude_status = False

This immutable field means: - Status note that readout values are in physical units (pA, mV, μA/mm³) - Voltage and current are computational proxies - CSD and LFP are readout proxies (derived from proxy source + proxy field) - Biological interpretation requires separate calibration and validation


Mapping Equations to Implementation

From Emitter Dynamics to Source Projection

Equation chain:

\[z(t) \xrightarrow{\text{Emitter}} \text{state} \xrightarrow{\text{Native current}} I(t) \xrightarrow{\text{Source projection}} q(x,t) \xrightarrow{\text{Readout}} \mathrm{CSD}(x,t)\]

Implementation mapping:

Equation Code Location Manifest Field Signals Field
\(\frac{dz}{dt} = F_\theta(z, u, t)\) jaxfne.emitters.simulate_eig_izhikevich() emitter_family, emitter_preset
\(I(t) = I_\mathrm{Iz}(z, \theta)\) jaxfne.emitters.simulate_eig_izhikevich() internal
\(q(x,t) = P_s[I(t), \chi(x)]\) jaxfne.fields.project_laminar_sources() source_projection_mode, source_calibration_status Signals.sources
\(\mathrm{CSD} = \nabla \cdot q\) jaxfne.fields.project_laminar_sources() field_solver_status Signals.field.csd

Manifest Fields (Complete List)

Source declaration:

manifest["source_calibration_status"]    # E.g. "uncalibrated_izhikevich_model_current"
manifest["source_projection_mode"]       # E.g. "proxy_no_field_solve"
manifest["source_decomposition"]         # E.g. "proxy_voltage_trace_not_current" (if applicable)
manifest["source_model"]                 # Struct: {"izhikevich_model_current_plus_spike_impulse_proxy": {...}}

Field declaration:

manifest["field_solver_status"]          # E.g. "linear_solver"
manifest["field_scope_level"]            # E.g. "proxy_readout"
manifest["boundary_condition"]           # E.g. "mean_zero_neumann"
manifest["gauge"]                        # E.g. "mean_zero"
manifest["conductivity_status"]          # E.g. "proxy" (not "calibrated_physical")

Validation gates (immutable):

manifest["amplitude_status"]  # Always False in v0.2.24
manifest["scope_status"]                      # Always "computational_scaffold" in v0.2.24
manifest["run_status"]                        # Always "tutorial_scaffold" in v0.2.24

Readout Report Fields

The ProbeReport returned by compute_readout() includes per-operator metadata:

readouts = model.compute_readout(signals, [
    jtfne.readout_spec("spikes", "spike_rate_hz"),
    jtfne.readout_spec("source", "source_abs_mean"),
    jtfne.readout_spec("csd", "csd_abs_mean"),
])

# Each ReadoutResult includes:
# - result.name (e.g., "spikes")
# - result.metric (e.g., "spike_rate_hz")
# - result.value (computed value)
# - result.status (e.g., "computed", "placeholder")
# - result.operator_status (if applicable)

Minimal Code Example: Tracing Equations

Example 1: Izhikevich → Source → CSD (Current Default)

import jaxfne as jtfne

# Configuration declares source and field modes
cfg = (
    jtfne.configuration()
    .network(name="V1_proxy", kind="cortical_column", n=100)
    .emitter(family="izhikevich", preset="cortical_eig")
    .field(
        domain="laminar_column",
        conductivity="proxy",
        boundary="mean_zero_neumann",
        gauge="mean_zero",
    )
    .probe(name="laminar_probe", modes=["spikes", "source", "CSD"])
)

model = jtfne.construct(cfg)

sim = jtfne.simulation(
    duration_ms=100.0,
    dt_ms=0.1,
    record_sources=True,
    record_fields=True,
)

# Simulate
signals = model.simulate(sim)

# Check source declaration
manifest = model.manifest(signals, [])
print(f"Source calibration: {manifest['source_calibration_status']}")
# → "uncalibrated_izhikevich_model_current"

print(f"Field solver: {manifest['field_solver_status']}")
# → "linear_solver"

print(f"CSD sign convention: {manifest['csd_sign_convention']}")
# → "positive_equals_extracellular_source"

# Signals.sources [T, N] = Izhikevich model current + spike impulse
# Signals.field.csd [T, N] = ∇·q (proxy)

Example 2: Jaxley Voltage Proxy → Source → LFP

import jaxfne as jtfne

# External voltage trace (e.g., from Jaxley simulation)
voltage_trace = jnp.ones((1000, 100))  # [time, neurons]

# Convert to jaxfne Signals via bridge
from jaxfne.bridges import jaxley_trace_to_signals, JaxleyTraceSpec

spec = JaxleyTraceSpec(
    layout="time_by_unit",
    dt_ms=0.025,
    spike_threshold=0.0,
)

signals = jaxley_trace_to_signals(
    voltage_trace,
    spec=spec,
    source=None,  # Use voltage proxy
)

# Check source declaration (from bridge)
print(f"Source calibration: {signals.metadata.get('source_calibration_status')}")
# → "uncalibrated_jaxley_voltage_proxy"

print(f"Physical amplitude allowed: {signals.metadata.get('amplitude_status')}")
# → False

# Signals.sources [T, N] = voltage proxy (no field computation)
# Signals.field = None (no field in bridge; computed downstream if needed)

Audit Checklist

Before releasing a model, verify:

  • [ ] Source calibration status is declared and one of: uncalibrated_izhikevich_model_current, uncalibrated_hh_model_current, uncalibrated_jaxley_voltage_proxy, or None
  • [ ] Source projection mode is declared (if source_calibration_status is not None)
  • [ ] Field solver status is declared and is either "linear_solver" or a reserved solver name
  • [ ] Boundary condition and gauge are documented (metadata-only in v0.2.24)
  • [ ] CSD sign convention is documented: positive = extracellular source (current flowing outward)
  • [ ] amplitude_status is False
  • [ ] No forbidden synaptic double-counting pattern in source computation
  • [ ] Manifest JSON is NaN/Inf-free and JSON-safe

See Also