Skip to content

Reception

Pulse-echo RF simulator. See the Reception user guide for the method taxonomy, PSF, and phantom recipes.

Reception

The single pulse-echo class. Its method selector chooses how the two-way SIR is evaluated — all methods give the same RF, they trade speed only:

  • "spectral" (default) — H_TX·H_RX from the closed-form SIR spectrum, in-band only.
  • "temporal" (= "sdi") / "fst" / "auto" — sampled two-way SIR convolution (delegated to the conventional ReceptionConventional backend; the string names its SIR-sampling kernel).
  • ReceptionPaired — exact but slow pedagogic reference, a separate class (warns on construction).

Reception

Reception(
    tx,
    rx,
    *,
    c=1540.0,
    rho=1.0,
    fs=100000000.0,
    alpha0=None,
    freq_power=1.0,
    excitation=None,
    method="spectral",
    n_depth_bins="auto",
    verbose=True,
)

Bases: ReceptionBase

Compute pulse-echo RF from point scatterers (Jensen's SIR scattering model).

p_pe = v_pe ⊛ h_tx ⊛ h_rx under the Tupholme-Stepanishen far-field trapezoidal rectangular-patch SIR. All methods evaluate the same RF equation and agree to correlation ~1.0 with each other and with Field II; method trades speed only:

  • "spectral" (default) — multiply the closed-form one-way SIR spectra H_TX·H_RX on the in-band bins only. No forward FFT, cost linear in patches, exact.
  • "fst" / "sdi" / "auto" — sample h_tx/h_rx and convolve, delegated to ReceptionConventional. The string names its SIR-sampling kernel: "fst" fully samples each trapezoid, "sdi" places sparse corner deltas, "auto" lets the delegate choose per grid.

The paired SDI form (O(M²), pedagogic) is the separate class ReceptionPaired.

Parameters:

Name Type Description Default
tx TransducerBase

Transmit transducer (with delays, apodization, optional impulse_response and excitation).

required
rx TransducerBase

Receive transducer (with apodization, optional impulse_response). Can be the same object as tx for monostatic pulse-echo.

required
c float

Speed of sound (m/s).

1540.0
rho float

Medium density (kg/m^3).

1.0
fs float

Sampling frequency (Hz).

100e6
alpha0 float or None

Attenuation in dB/(MHz^y·cm). None = no attenuation.

None
freq_power float

Attenuation power-law exponent.

1.0
excitation ndarray or None

TX excitation pulse (L,). If None, uses tx.excitation or delta.

None
method str

Pulse-echo formulation: "spectral" (default) / "fst" / "sdi" / "auto". All produce the same RF; they trade speed only. "fst"/"sdi"/"auto" delegate to ReceptionConventional.

"spectral"
n_depth_bins auto or int

Spectral speed knob. Scatterers are grouped into this many depth bins so each bin uses a short time window — a small nfft and hence few in-band frequency bins, the spectral form's dominant cost factor — with the per-bin results added back on a shared sample lattice (big speedup at high scatterer counts). "auto" sizes it from the arrival-time spread; 1 disables binning. Applies to the summed RF only.

"auto"
verbose bool

Print diagnostic information during simulation.

True

pulse_echo_rf

pulse_echo_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    per_scatterer=False,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF from point scatterers.

The core reception primitive: amplitude-weighted superposition of each scatterer's pulse-echo response v_pe ⊛ h_tx ⊛ h_rx; the single physical ∂³ stays on the band-limited excitation / impulse-response chain (e ⊛ h_e ⊛ h_r). Field II uses the same convention, so this equals Field II calc_scat (≡ calc_hhp for a unit point, corr 1.0000).

per_scatterer=True keeps each scatterer separate (PSF); False sums them. coords["t0"] is beam-axis referenced (TX bulk delays.max() subtracted) so downstream beamforming needs no per-line correction.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
per_scatterer bool

If False, sum over scatterers → (Erx, Nt). If True → (N_scat, Erx, Nt) (PSF per point).

False
downsampling int or None

Anti-aliased time decimation factor.

None
out_path str or Path or None

Checkpoint folder (an RFDataset): the acquisition is written to disk as it progresses and a re-run resumes instead of starting over — use with checkpoint_chunks for hours-long phantoms.

None
checkpoint_chunks int

Scatterer chunks checkpointed separately (requires out_path); the RF is linear in the scatterers, so a crash costs at most one chunk. Incompatible with per_scatterer=True.

1

Returns:

Name Type Description
rf (Erx, Nt) or (N_scat, Erx, Nt) numpy.ndarray

Pulse-echo RF per receive element (channels before time).

coords dict

Keys "t0" and "dt" (seconds).

Raises:

Type Description
ValueError

If out_path is combined with per_scatterer=True.

sequence_rf

sequence_rf(
    scatterer_positions_mm,
    amplitudes,
    tx_events,
    *,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF for a sequence of TX events (emission basis: PW/DW/...).

Each event sets the TX delays/apodization, then pulse_echo_rf is run (summed over scatterers). Useful as the emission basis for matrix imaging.

MOVING SCATTERERS. The medium may move between emissions: give scatterer_positions_mm a (N_events, N_scat, 3) stack, one cloud per event, and each emission sees its own cloud. Blood travelling at v covers v/PRF between emissions, and the resulting round-trip path change (twice the axial step) is the inter-emission phase a Doppler estimator reads. eSDIva has no slow-time clock of its own, so the trajectory is yours to write — one line for constant flow::

t = np.arange(n_events) / prf                       # slow-time, s
pos = pos0[None] + v_mm_s[None] * t[:, None, None]  # (N_ev, N, 3)

and any pulsatile, cardiac or decorrelating trajectory is the same call. amplitudes may vary per event too, as (N_events, N_scat): the scatterer count is fixed across the sequence, so a zero amplitude is how a scatterer that has left the region of interest — or a contrast bubble that has been destroyed — is retired.

The scatterers are FROZEN during each emission. Motion is resolved only at the event rate, so the RF carries the phase shift BETWEEN emissions, not an intra-pulse Doppler frequency shift within one echo; velocities of clinical interest move a small fraction of a wavelength during a single pulse, which is why this is the standard flow model.

With out_path, the sequence is CHECKPOINTED: each event's RF is written to disk (one compressed file per event + a contents file) the moment it finishes, and re-running the same call on the same folder skips the completed events and resumes from the first missing one. The contents file fingerprints the full simulation (probe, medium, excitation, scatterers, events); re-running with anything changed raises instead of silently mixing incompatible data.

checkpoint_chunks bounds the work lost to a crash WITHIN one event: the RF is linear in the scatterers, so the cloud is split into chunks that are simulated and checkpointed one file at a time, then summed. Every chunk carries four zero-amplitude "grid sentinel" points (just nearer than the nearest scatterer and just farther than the farthest, per aperture) so all chunks of an event share one time grid and their sum is sample-exact.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_events, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 2-D array holds the medium still for the whole sequence; a 3-D array gives every emission its own cloud (moving scatterers), and its first axis must match tx_events.

required
amplitudes (N_scat,) or (N_events, N_scat) numpy.ndarray

Scattering amplitude of each scatterer, either fixed for the sequence or per emission.

required
tx_events list of dict

Each dict has "delays" and/or "apodization" (E,) arrays.

required
downsampling int or None

Anti-aliased time decimation factor applied per event.

None
out_path str or Path or None

Checkpoint folder (created if missing). Events already completed there are skipped; each new event is written atomically on completion, so a crash costs at most the event in flight.

None
checkpoint_chunks int

Number of scatterer chunks checkpointed per TX event (requires out_path). With N chunks a crash costs at most 1/N of an event; pick it so one chunk takes ~10–15 minutes.

1

Returns:

Name Type Description
rf (N_events, Erx, Nt) numpy.ndarray

Per-event, per-receive-element RF (channels before time).

coords dict

"t0"/"dt" of the first event, plus "t0_per_event" — an (N_events,) array of each event's beam-axis time origin. Events with differing focus have differing t0 (the time grid depends on that event's delays, and on where its scatterers are); beamform each event with its own origin. dt is shared (one sampling rate); traces are zero-padded at the END to the common Nt, so only the origin differs.

Raises:

Type Description
ValueError

If checkpoint_chunks > 1 without out_path, if a per-event positions/amplitudes array does not carry one entry per TX event, or if tx and rx are the same object while events set delays/apodization (RX weights are per receive channel, so the event's TX weights would corrupt the receive traces — pass rx=tx.copy()).

TypeError

If checkpoint_chunks > 1 with a grid-dict scatterer input, or a grid dict is combined with per-event amplitudes.

synthetic_aperture_rf

synthetic_aperture_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    tx_groups="element",
    decimation=10,
    out_path=None,
    countdown=True,
    checkpoint_chunks=1,
)

Synthetic-aperture RF: each TX element/group fires alone, all RX record.

Canonical diverging-wave emission basis for matrix imaging: every group fires FLAT (zero delay, unit apodization) — this OVERRIDES any TX delays/ apodization set on the transducer. tx_groups="element" is full FMC (= Field II calc_scat_all); int N uses N-element sub-apertures; a list[list[int]] gives custom groups.

Each group is one TX event of sequence_rf, so it shares that method's checkpointing: with out_path every group (or scatterer chunk, with checkpoint_chunks) lands on disk the moment it finishes, a re-run resumes at the first missing file, and a changed setup refuses. Output Ntx_grp·Erx·Nt can be huge, hence the anti-aliased decimation (default 10×); a 10 s countdown precedes an in-RAM run so it can be aborted (a checkpointed run is always interruptible, so it starts straight away).

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_groups, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 3-D array gives each transmit group its own cloud: an FMC acquisition fires its groups one after another, so a medium that moves meanwhile is seen at a different position by each group — the motion artefact of a slow full-matrix capture. Its first axis must match the number of groups.

required
amplitudes (N_scat,) or (N_groups, N_scat) numpy.ndarray or None

Scattering coefficient at each position, fixed or per transmit group. None defaults to ones.

None
tx_groups str or int or list[list[int]]

Transmit grouping: "element" fires each element alone (full FMC), int N fires N-element sub-apertures, list[list[int]] fires custom element groups.

"element"
decimation int

Anti-aliased time decimation factor.

10
out_path str or Path or None

Checkpoint folder (an RFDataset, created if missing): one compressed file per group, crash-safe and resumable.

None
countdown bool

Print a 10 s abortable countdown before an in-RAM computation (skipped if stdout is not a TTY or out_path is given).

True
checkpoint_chunks int

Scatterer chunks checkpointed per group (requires out_path); a crash then costs at most one chunk of one group.

1

Returns:

Name Type Description
rf (Ntx_grp, Erx, Nt) numpy.ndarray

Per-group, per-receive-element RF.

coords dict

"t0"/"dt" of the decimated time axis (plus "t0_per_event", identical for all groups — they all fire flat).

scan_focusline

scan_focusline(
    focus_mm,
    scatterer_positions_mm,
    amplitudes=None,
    *,
    FoverD=None,
    apodization_type=None,
    rx_FoverD=None,
    rx_apodization_type=None,
    output="envelope",
    downsampling=None,
)

One conventional focused scan line (Field II calc_scat match).

Recomputes the TX and RX focus + apodization FROM focus_mm (reusing each transducer's compute_delays / compute_apodization), then beamforms ON RECEIVE INSIDE THE SIR KERNEL via _focused_sum_rf (focused_sum=True): every RX patch carries its focusing delay + apodization and the kernel sums them into one line — exactly Field II calc_scat's internal receive beamforming. This replaces the old per-element-RF + external DAS_focused_scanline path, so it is ~E_rx× cheaper (one FFT pair, no per-channel loop) and applies the receive focus at corner-time resolution (no sample-interpolation loss). Loop focus_mm externally to build a B-mode, e.g. img[i] = sim.scan_focusline(...)[0].

By default the RX aperture mirrors the TX focus + apodization (the usual focused-imaging case, and what Field II's sesr.m does: identical xdc_apodization on emit and receive). Pass rx_FoverD / rx_apodization_type to give the receive aperture a different profile.

Parameters:

Name Type Description Default
focus_mm (3,) array_like

Focal point [x, y, z] in mm (the per-line looped variable).

required
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
FoverD float or None

TX F-number for the active aperture. Used only when apodization_type is set (defaults to 2.0 there if None).

None
apodization_type str or None

TX apodization: "hanning" / "rect" (F/D=1) / "gaussian" / custom. None → uniform full aperture (no taper).

None
rx_FoverD float or None

RX F-number. None → mirror FoverD (identical TX/RX aperture).

None
rx_apodization_type str or None

RX apodization type. None → mirror apodization_type.

None
output (envelope, rf)

Return the Hilbert envelope (default) or the raw beamformed RF line.

"envelope"
downsampling int or None

Anti-aliased time decimation factor.

None

Returns:

Name Type Description
out (Nt,) numpy.ndarray

Envelope (or RF line) of the beamformed scan line at lateral focus[0].

coords dict

"t0"/"dt" of the line's time axis (beam-axis referenced).

show

show(
    scatterer_positions_mm=None,
    amplitudes=None,
    *,
    TX_color="Delays",
    RX_color="Delays",
    TX_show_edges=False,
    RX_show_edges=False,
    TX_kwargs=None,
    RX_kwargs=None,
    window_size=(900, 700),
    notebook=False,
    jupyter_backend=None,
    legend=True,
    scale=1.0,
    save_path=None,
    off_screen=None,
    return_plotter=False,
    db_scale=True,
    **kwargs,
)

Interactive 3-D preview of the pulse-echo setup (TX, RX, scatterers).

Renders the transmit and receive apertures (their patch meshes, in mm) and the scatterer cloud, each point coloured by its scattering amplitude and faded in proportion to it — a quick visual check that positions, units and aperture poses are what the simulation will see. When TX and RX are the same object the aperture is drawn once.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) array-like

Scatterer positions in mm. None draws only the apertures. A moving cloud (N_events, N_scat, 3) (as sequence_rf takes) is drawn at its FIRST emission — there is no single pose for a sequence.

None
amplitudes (N_scat,) array-like

Scattering amplitude per point. None defaults to ones (all points fully opaque). Per-event (N_events, N_scat) amplitudes are likewise shown for the first emission.

None
TX_color str or tuple

Colour of the transmit aperture. Any PyVista colour (name, hex string or RGB tuple) paints the mesh uniformly. The special strings "Delays" / "Apodization" instead colour each patch by that beamforming quantity with a colour bar, as transducer.show() does. When TX and RX show the SAME quantity, one shared colour bar (right side, common colour range) serves both; different quantities get the TX bar on the left and the RX bar on the right.

"Delays"
RX_color str or tuple

Same as TX_color, for the receive aperture.

"Delays"
TX_show_edges bool

Draw the patch edges of the transmit aperture mesh.

False
RX_show_edges bool

Draw the patch edges of the receive aperture mesh.

False
TX_kwargs dict

Extra keyword arguments for the transmit aperture's add_transducer_mesh call (e.g. {"cmap": "viridis", "opacity": 0.5}); they override the defaults above.

None
RX_kwargs dict

Same as TX_kwargs, for the receive aperture.

None
window_size (int, int)

Pixel dimensions of the render window.

(900, 700)
notebook bool

Enable Jupyter notebook rendering.

False
jupyter_backend str

Backend string passed to PyVista ('static', 'trame' …).

None
legend bool

Draw the TX/RX/Scatterers legend. Note the colour bars belong to the "Delays"/"Apodization" aperture colouring — pass plain colours (e.g. TX_color="lightsteelblue") to remove them.

True
scale float

Global resolution multiplier. Enlarges the render window, every font (grid, aperture tags, colour bars) and the saved screenshot together, so a higher value yields a larger, sharper image without changing the framing (e.g. scale=3 for print figures).

1.0
save_path str or Path

Screenshot file path (e.g. "setup.png"). When given, the scene is rendered off-screen and saved instead of opening a window.

None
off_screen bool

Render without opening a window. None (default) renders off-screen only when save_path is given. Set True when return_plotter is used to screenshot in a headless/batch run — otherwise the returned plotter has nothing rendered and plotter.screenshot() raises "Nothing to screenshot".

None
return_plotter bool

Return the PyVista Plotter object instead of showing or saving the scene. Useful for further customisation (e.g. camera position, extra meshes) before calling plotter.show() or plotter.screenshot(). The axis grid is then NOT drawn, so the caller can apply their own plotter.show_grid(...) settings.

False
db_scale bool

When True, the scatterer amplitudes are converted to dB scale for visualization.

True
**kwargs

Forwarded to the scatterer add_mesh call (e.g. point_size).

{}

Returns:

Type Description
Plotter or None

The plotter (scene assembled, grid and rendering left to the caller) when return_plotter=True; otherwise None.

set

set(name, value)

Update a parameter at runtime, then invalidate the conventional delegate.

Extends ReceptionBase.set with validation of the "method" selector and by dropping the cached ReceptionConventional (rebuilt on the next conventional call, picking up the new tx/rx/medium/excitation state).

Parameters:

Name Type Description Default
name str

Parameter name (a key of _SETTABLE, "tx", or "rx").

required
value object

New value; for "method" it must be a valid selector.

required

ReceptionConventional (backend)

The conventional Tupholme-Stepanishen sampled-convolution backend that Reception delegates to for method="fst"/"sdi"/"auto". Same API; normally reached through Reception, documented here for reference.

ReceptionConventional

ReceptionConventional(
    tx,
    rx,
    *,
    c=1540.0,
    rho=1.0,
    fs=100000000.0,
    alpha0=None,
    freq_power=1.0,
    excitation=None,
    method="auto",
    n_depth_bins="auto",
    verbose=True,
)

Bases: ReceptionBase

Pulse-echo RF by direct two-way SIR convolution (Jensen's model).

Computes the received echo from point scatterers using the standard SIR formulation (model and citations in the module docstring above): the one-way SIRs h_tx and h_rx are built without differentiation, and the recorded RF is the amplitude-weighted sum over scatterers

rf(t) = (ρ₀/2c₀²) Σ_p σ_p · (exc ⊛ ir_tx ⊛ ir_rx ⊛ h_pe)|_{r_p}

where the sum runs over scatterers p at positions r_p with scattering amplitudes σ_p (the amplitudes argument), and h_pe = h_tx(r₁→r_p) ⊛_t h_rx(r_p→r₁) is the plain two-way SIR for that scatterer. The three physical excitation derivatives are carried by the band-limited exc/IR chain, so no extra explicit ∂/∂t is applied (Field II's calc_scat ≡ calc_hhp convention). Public API via pulse_echo_rf (core) plus sequence_rf / synthetic_aperture_rf / scan_focusline from ReceptionBase.

Parameters:

Name Type Description Default
tx TransducerBase

Transmit transducer (with delays, apodization, optional impulse_response and excitation).

required
rx TransducerBase

Receive transducer (with apodization, optional impulse_response). Can be the same object as tx for monostatic pulse-echo.

required
c float

Speed of sound (m/s).

1540.0
rho float

Medium density (kg/m^3).

1.0
fs float

Sampling frequency (Hz).

100e6
alpha0 float or None

Attenuation in dB/(MHz^y·cm). None = no attenuation.

None
freq_power float

Attenuation power-law exponent.

1.0
excitation ndarray or None

TX excitation pulse (L,). If None, uses tx.excitation or delta.

None
method str

SIR computation method: "FST", "sdi", or "auto".

"auto"
n_depth_bins auto or int

Pulse-echo speed knob (result is unchanged). Scatterers at different depths echo at different times, so one FFT over all of them must span the whole depth range; grouping them into depth bins lets each bin use an FFT only as long as its own arrival window — a big speedup at high scatterer counts. Bins recombine sample-exactly on a shared time axis. "auto" picks the count from the scatterer number and depth spread (~100 scatterers per bin); an int forces it; 1 disables binning.

"auto"
verbose bool

Print diagnostic information during simulation.

True

pulse_echo_rf

pulse_echo_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    per_scatterer=False,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF from point scatterers.

The core reception primitive. The recorded pulse-echo signal is the amplitude-weighted superposition of each scatterer's pulse-echo response::

rf = (ρ₀/2c₀²) Σ_i a_i (exc ⊛ ir_tx ⊛ ir_rx ⊛ h_pe)|_{r_i}

The three physical excitation derivatives are carried by the band-limited exc ⊛ ir_tx ⊛ ir_rx chain, so NO extra explicit ∂/∂t is applied. Field II uses the same convention, so this equals its calc_scat (≡ calc_hhp for a unit point, corr 1.0000).

per_scatterer=True keeps each scatterer separate — the point-spread function (PSF) use; False sums them (the recorded echo).

coords["t0"] is referenced to the beam axis (the TX focusing bulk tx.delays.max() is subtracted), so downstream beamforming needs no per-line bulk correction.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
per_scatterer bool

If False, sum over scatterers → (Erx, Nt). If True, keep each scatterer separate → (N_scat, Erx, Nt) (PSF per point).

False
downsampling int or None

Anti-aliased time decimation factor.

None
out_path str or Path or None

Checkpoint folder (an RFDataset): the acquisition is written to disk as it progresses and a re-run resumes instead of starting over — use with checkpoint_chunks for hours-long phantoms.

None
checkpoint_chunks int

Scatterer chunks checkpointed separately (requires out_path); the RF is linear in the scatterers, so a crash costs at most one chunk. Incompatible with per_scatterer=True.

1

Returns:

Name Type Description
rf (Erx, Nt) or (N_scat, Erx, Nt) numpy.ndarray

Pulse-echo RF per receive element (channels before time).

coords dict

Keys "t0" and "dt" (seconds).

Raises:

Type Description
ValueError

If out_path is combined with per_scatterer=True.

sequence_rf

sequence_rf(
    scatterer_positions_mm,
    amplitudes,
    tx_events,
    *,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF for a sequence of TX events (emission basis: PW/DW/...).

Each event sets the TX delays/apodization, then pulse_echo_rf is run (summed over scatterers). Useful as the emission basis for matrix imaging.

MOVING SCATTERERS. The medium may move between emissions: give scatterer_positions_mm a (N_events, N_scat, 3) stack, one cloud per event, and each emission sees its own cloud. Blood travelling at v covers v/PRF between emissions, and the resulting round-trip path change (twice the axial step) is the inter-emission phase a Doppler estimator reads. eSDIva has no slow-time clock of its own, so the trajectory is yours to write — one line for constant flow::

t = np.arange(n_events) / prf                       # slow-time, s
pos = pos0[None] + v_mm_s[None] * t[:, None, None]  # (N_ev, N, 3)

and any pulsatile, cardiac or decorrelating trajectory is the same call. amplitudes may vary per event too, as (N_events, N_scat): the scatterer count is fixed across the sequence, so a zero amplitude is how a scatterer that has left the region of interest — or a contrast bubble that has been destroyed — is retired.

The scatterers are FROZEN during each emission. Motion is resolved only at the event rate, so the RF carries the phase shift BETWEEN emissions, not an intra-pulse Doppler frequency shift within one echo; velocities of clinical interest move a small fraction of a wavelength during a single pulse, which is why this is the standard flow model.

With out_path, the sequence is CHECKPOINTED: each event's RF is written to disk (one compressed file per event + a contents file) the moment it finishes, and re-running the same call on the same folder skips the completed events and resumes from the first missing one. The contents file fingerprints the full simulation (probe, medium, excitation, scatterers, events); re-running with anything changed raises instead of silently mixing incompatible data.

checkpoint_chunks bounds the work lost to a crash WITHIN one event: the RF is linear in the scatterers, so the cloud is split into chunks that are simulated and checkpointed one file at a time, then summed. Every chunk carries four zero-amplitude "grid sentinel" points (just nearer than the nearest scatterer and just farther than the farthest, per aperture) so all chunks of an event share one time grid and their sum is sample-exact.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_events, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 2-D array holds the medium still for the whole sequence; a 3-D array gives every emission its own cloud (moving scatterers), and its first axis must match tx_events.

required
amplitudes (N_scat,) or (N_events, N_scat) numpy.ndarray

Scattering amplitude of each scatterer, either fixed for the sequence or per emission.

required
tx_events list of dict

Each dict has "delays" and/or "apodization" (E,) arrays.

required
downsampling int or None

Anti-aliased time decimation factor applied per event.

None
out_path str or Path or None

Checkpoint folder (created if missing). Events already completed there are skipped; each new event is written atomically on completion, so a crash costs at most the event in flight.

None
checkpoint_chunks int

Number of scatterer chunks checkpointed per TX event (requires out_path). With N chunks a crash costs at most 1/N of an event; pick it so one chunk takes ~10–15 minutes.

1

Returns:

Name Type Description
rf (N_events, Erx, Nt) numpy.ndarray

Per-event, per-receive-element RF (channels before time).

coords dict

"t0"/"dt" of the first event, plus "t0_per_event" — an (N_events,) array of each event's beam-axis time origin. Events with differing focus have differing t0 (the time grid depends on that event's delays, and on where its scatterers are); beamform each event with its own origin. dt is shared (one sampling rate); traces are zero-padded at the END to the common Nt, so only the origin differs.

Raises:

Type Description
ValueError

If checkpoint_chunks > 1 without out_path, if a per-event positions/amplitudes array does not carry one entry per TX event, or if tx and rx are the same object while events set delays/apodization (RX weights are per receive channel, so the event's TX weights would corrupt the receive traces — pass rx=tx.copy()).

TypeError

If checkpoint_chunks > 1 with a grid-dict scatterer input, or a grid dict is combined with per-event amplitudes.

synthetic_aperture_rf

synthetic_aperture_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    tx_groups="element",
    decimation=10,
    out_path=None,
    countdown=True,
    checkpoint_chunks=1,
)

Synthetic-aperture RF: each TX element/group fires alone, all RX record.

Canonical diverging-wave emission basis for matrix imaging: every group fires FLAT (zero delay, unit apodization) — this OVERRIDES any TX delays/ apodization set on the transducer. tx_groups="element" is full FMC (= Field II calc_scat_all); int N uses N-element sub-apertures; a list[list[int]] gives custom groups.

Each group is one TX event of sequence_rf, so it shares that method's checkpointing: with out_path every group (or scatterer chunk, with checkpoint_chunks) lands on disk the moment it finishes, a re-run resumes at the first missing file, and a changed setup refuses. Output Ntx_grp·Erx·Nt can be huge, hence the anti-aliased decimation (default 10×); a 10 s countdown precedes an in-RAM run so it can be aborted (a checkpointed run is always interruptible, so it starts straight away).

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_groups, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 3-D array gives each transmit group its own cloud: an FMC acquisition fires its groups one after another, so a medium that moves meanwhile is seen at a different position by each group — the motion artefact of a slow full-matrix capture. Its first axis must match the number of groups.

required
amplitudes (N_scat,) or (N_groups, N_scat) numpy.ndarray or None

Scattering coefficient at each position, fixed or per transmit group. None defaults to ones.

None
tx_groups str or int or list[list[int]]

Transmit grouping: "element" fires each element alone (full FMC), int N fires N-element sub-apertures, list[list[int]] fires custom element groups.

"element"
decimation int

Anti-aliased time decimation factor.

10
out_path str or Path or None

Checkpoint folder (an RFDataset, created if missing): one compressed file per group, crash-safe and resumable.

None
countdown bool

Print a 10 s abortable countdown before an in-RAM computation (skipped if stdout is not a TTY or out_path is given).

True
checkpoint_chunks int

Scatterer chunks checkpointed per group (requires out_path); a crash then costs at most one chunk of one group.

1

Returns:

Name Type Description
rf (Ntx_grp, Erx, Nt) numpy.ndarray

Per-group, per-receive-element RF.

coords dict

"t0"/"dt" of the decimated time axis (plus "t0_per_event", identical for all groups — they all fire flat).

scan_focusline

scan_focusline(
    focus_mm,
    scatterer_positions_mm,
    amplitudes=None,
    *,
    FoverD=None,
    apodization_type=None,
    rx_FoverD=None,
    rx_apodization_type=None,
    output="envelope",
    downsampling=None,
)

One conventional focused scan line (Field II calc_scat match).

Recomputes the TX and RX focus + apodization FROM focus_mm (reusing each transducer's compute_delays / compute_apodization), then beamforms ON RECEIVE INSIDE THE SIR KERNEL via _focused_sum_rf (focused_sum=True): every RX patch carries its focusing delay + apodization and the kernel sums them into one line — exactly Field II calc_scat's internal receive beamforming. This replaces the old per-element-RF + external DAS_focused_scanline path, so it is ~E_rx× cheaper (one FFT pair, no per-channel loop) and applies the receive focus at corner-time resolution (no sample-interpolation loss). Loop focus_mm externally to build a B-mode, e.g. img[i] = sim.scan_focusline(...)[0].

By default the RX aperture mirrors the TX focus + apodization (the usual focused-imaging case, and what Field II's sesr.m does: identical xdc_apodization on emit and receive). Pass rx_FoverD / rx_apodization_type to give the receive aperture a different profile.

Parameters:

Name Type Description Default
focus_mm (3,) array_like

Focal point [x, y, z] in mm (the per-line looped variable).

required
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
FoverD float or None

TX F-number for the active aperture. Used only when apodization_type is set (defaults to 2.0 there if None).

None
apodization_type str or None

TX apodization: "hanning" / "rect" (F/D=1) / "gaussian" / custom. None → uniform full aperture (no taper).

None
rx_FoverD float or None

RX F-number. None → mirror FoverD (identical TX/RX aperture).

None
rx_apodization_type str or None

RX apodization type. None → mirror apodization_type.

None
output (envelope, rf)

Return the Hilbert envelope (default) or the raw beamformed RF line.

"envelope"
downsampling int or None

Anti-aliased time decimation factor.

None

Returns:

Name Type Description
out (Nt,) numpy.ndarray

Envelope (or RF line) of the beamformed scan line at lateral focus[0].

coords dict

"t0"/"dt" of the line's time axis (beam-axis referenced).

show

show(
    scatterer_positions_mm=None,
    amplitudes=None,
    *,
    TX_color="Delays",
    RX_color="Delays",
    TX_show_edges=False,
    RX_show_edges=False,
    TX_kwargs=None,
    RX_kwargs=None,
    window_size=(900, 700),
    notebook=False,
    jupyter_backend=None,
    legend=True,
    scale=1.0,
    save_path=None,
    off_screen=None,
    return_plotter=False,
    db_scale=True,
    **kwargs,
)

Interactive 3-D preview of the pulse-echo setup (TX, RX, scatterers).

Renders the transmit and receive apertures (their patch meshes, in mm) and the scatterer cloud, each point coloured by its scattering amplitude and faded in proportion to it — a quick visual check that positions, units and aperture poses are what the simulation will see. When TX and RX are the same object the aperture is drawn once.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) array-like

Scatterer positions in mm. None draws only the apertures. A moving cloud (N_events, N_scat, 3) (as sequence_rf takes) is drawn at its FIRST emission — there is no single pose for a sequence.

None
amplitudes (N_scat,) array-like

Scattering amplitude per point. None defaults to ones (all points fully opaque). Per-event (N_events, N_scat) amplitudes are likewise shown for the first emission.

None
TX_color str or tuple

Colour of the transmit aperture. Any PyVista colour (name, hex string or RGB tuple) paints the mesh uniformly. The special strings "Delays" / "Apodization" instead colour each patch by that beamforming quantity with a colour bar, as transducer.show() does. When TX and RX show the SAME quantity, one shared colour bar (right side, common colour range) serves both; different quantities get the TX bar on the left and the RX bar on the right.

"Delays"
RX_color str or tuple

Same as TX_color, for the receive aperture.

"Delays"
TX_show_edges bool

Draw the patch edges of the transmit aperture mesh.

False
RX_show_edges bool

Draw the patch edges of the receive aperture mesh.

False
TX_kwargs dict

Extra keyword arguments for the transmit aperture's add_transducer_mesh call (e.g. {"cmap": "viridis", "opacity": 0.5}); they override the defaults above.

None
RX_kwargs dict

Same as TX_kwargs, for the receive aperture.

None
window_size (int, int)

Pixel dimensions of the render window.

(900, 700)
notebook bool

Enable Jupyter notebook rendering.

False
jupyter_backend str

Backend string passed to PyVista ('static', 'trame' …).

None
legend bool

Draw the TX/RX/Scatterers legend. Note the colour bars belong to the "Delays"/"Apodization" aperture colouring — pass plain colours (e.g. TX_color="lightsteelblue") to remove them.

True
scale float

Global resolution multiplier. Enlarges the render window, every font (grid, aperture tags, colour bars) and the saved screenshot together, so a higher value yields a larger, sharper image without changing the framing (e.g. scale=3 for print figures).

1.0
save_path str or Path

Screenshot file path (e.g. "setup.png"). When given, the scene is rendered off-screen and saved instead of opening a window.

None
off_screen bool

Render without opening a window. None (default) renders off-screen only when save_path is given. Set True when return_plotter is used to screenshot in a headless/batch run — otherwise the returned plotter has nothing rendered and plotter.screenshot() raises "Nothing to screenshot".

None
return_plotter bool

Return the PyVista Plotter object instead of showing or saving the scene. Useful for further customisation (e.g. camera position, extra meshes) before calling plotter.show() or plotter.screenshot(). The axis grid is then NOT drawn, so the caller can apply their own plotter.show_grid(...) settings.

False
db_scale bool

When True, the scatterer amplitudes are converted to dB scale for visualization.

True
**kwargs

Forwarded to the scatterer add_mesh call (e.g. point_size).

{}

Returns:

Type Description
Plotter or None

The plotter (scene assembled, grid and rendering left to the caller) when return_plotter=True; otherwise None.

ReceptionPaired (pedagogic)

The paired SDI form: 16 corner deltas per TX–RX patch pair, exact but O(M²). Same API as Reception; for teaching and cross-checks, and a candidate for future deprecation.

ReceptionPaired

ReceptionPaired(tx, rx, **kwargs)

Bases: Reception

Pulse-echo RF via the paired SDI form (exact, O(M_tx·M_rx), no attenuation).

Same constructor and public methods as Reception; only the RF core differs. Warns on construction because it is far slower than Reception.

Parameters:

Name Type Description Default
tx TransducerBase

Transmit transducer.

required
rx TransducerBase

Receive transducer.

required
**kwargs

Medium and excitation options of Reception (c, rho, fs, excitation, verbose, ...).

{}

tx instance-attribute

tx = tx

excitation instance-attribute

excitation = (
    asarray(excitation, dtype=float32)
    if excitation is not None
    else None
)

_SETTABLE class-attribute instance-attribute

_SETTABLE: dict = {
    "c": (float, "Speed of sound (m/s)"),
    "rho": (float, "Density (kg/m^3)"),
    "fs": (float, "Sampling frequency (Hz)"),
    "alpha0": (
        (float, type(None)),
        "Attenuation dB/(MHz^y cm) or None",
    ),
    "freq_power": (float, "Attenuation exponent"),
    "excitation": (
        (ndarray, type(None)),
        "Excitation pulse or None",
    ),
    "method": (str, "Formulation: spectral/fst/sdi/auto"),
    "n_depth_bins": (
        (int, str),
        "Spectral depth bins: 'auto' or int",
    ),
    "verbose": (bool, "Print diagnostics"),
}

rx instance-attribute

rx = rx

c instance-attribute

c = c

fs instance-attribute

fs = fs

verbose instance-attribute

verbose = verbose

rho instance-attribute

rho = rho

alpha0 instance-attribute

alpha0 = alpha0

freq_power instance-attribute

freq_power = freq_power

n_depth_bins instance-attribute

n_depth_bins = n_depth_bins

_conv instance-attribute

_conv = None

_apply_settable

_apply_settable(name: str, value) -> None

Validate value against _SETTABLE[name] and assign it.

The shared tail of every set(): checks the name is known and the type matches, casts an excitation to float32, and stores the attribute. Subclasses handle their structural keys ("transducer"/"tx"/ "rx") first, then delegate the plain parameters here.

Raises:

Type Description
ValueError

If name is not a recognised parameter.

TypeError

If value has the wrong type for name.

_resolve_excitation

_resolve_excitation()

Effective excitation pulse: self.excitation else tx.excitation.

Returns a float32 array or None. A global pulse is (L,); a per-transmit-element pulse is (L, E) (one column per element) and is preserved as 2-D — only a transducer-stored 1-D pulse is ravelled.

_snap_to_lattice staticmethod

_snap_to_lattice(t0_nat, t0_global, dt)

Align one depth bin's time grid with the shared global time axis.

All depth bins must land on ONE common time axis (origin t0_global, step dt), but a bin's natural start t0_nat generally falls between two of its samples. This rounds t0_nat DOWN to the nearest sample of the shared axis. Time-domain paths build the bin's grid from t0_snap; a spectral path can instead reference its phase to t0_snap (or apply shift as a phase ramp).

Returns:

Name Type Description
n0 int

Integer sample index of the snapped start on the shared axis.

t0_snap float

Snapped start time, t0_global + n0·dt (s).

shift float

Sub-sample remainder t0_nat − t0_snap, in [0, dt) (s).

_require_rigid staticmethod

_require_rigid(*transducers)

Refuse baffle="soft" in a core that models the rigid baffle only.

_group_patches_by_element staticmethod

_group_patches_by_element(n_elements, sub_el_idx, arrays)

Split flat per-patch arrays into one tuple per element.

arrays is the 7-tuple (centers, wx, wy, apod, delays, eu, ev) of per-patch quantities; this returns a list of n_elements such tuples, each holding only the patches belonging to that element (sub_el_idx == e). Used to compute one element's SIR at a time (per-element excitation, per-element attenuation, raw per-channel RF).

pulse_echo_rf

pulse_echo_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    per_scatterer=False,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF from point scatterers.

The core reception primitive: amplitude-weighted superposition of each scatterer's pulse-echo response v_pe ⊛ h_tx ⊛ h_rx; the single physical ∂³ stays on the band-limited excitation / impulse-response chain (e ⊛ h_e ⊛ h_r). Field II uses the same convention, so this equals Field II calc_scat (≡ calc_hhp for a unit point, corr 1.0000).

per_scatterer=True keeps each scatterer separate (PSF); False sums them. coords["t0"] is beam-axis referenced (TX bulk delays.max() subtracted) so downstream beamforming needs no per-line correction.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
per_scatterer bool

If False, sum over scatterers → (Erx, Nt). If True → (N_scat, Erx, Nt) (PSF per point).

False
downsampling int or None

Anti-aliased time decimation factor.

None
out_path str or Path or None

Checkpoint folder (an RFDataset): the acquisition is written to disk as it progresses and a re-run resumes instead of starting over — use with checkpoint_chunks for hours-long phantoms.

None
checkpoint_chunks int

Scatterer chunks checkpointed separately (requires out_path); the RF is linear in the scatterers, so a crash costs at most one chunk. Incompatible with per_scatterer=True.

1

Returns:

Name Type Description
rf (Erx, Nt) or (N_scat, Erx, Nt) numpy.ndarray

Pulse-echo RF per receive element (channels before time).

coords dict

Keys "t0" and "dt" (seconds).

Raises:

Type Description
ValueError

If out_path is combined with per_scatterer=True.

_focused_sum_rf

_focused_sum_rf(points_m, amps, *, downsampling=None)

Receive-beamformed line via in-kernel focused sum.

Backend hook for ReceptionBase.scan_focusline: the pulse-echo RF with focused_sum=True. Returns the single line (Nt,) and its coords. Field II's calc_scat builds its line the same way (focused, apodized, summed on receive).

__call__

__call__(*args, **kwargs)

_reset_time_log

_reset_time_log()

Zero the per-phase wall-clock log at the start of an RF computation.

_timer

_timer(key)

Add the wall-clock time of the enclosed block to self.time_log[key].

_timed_h_sir

_timed_h_sir(*args, **kwargs)

compute_h_sir timed into time_log["sir_s"] (the SIR kernel cost).

_fmt_time_log

_fmt_time_log()

One-line time_grid/sir/fft breakdown of the last RF computation.

_refresh_sub_elem_attributes

_refresh_sub_elem_attributes()

Extract patch arrays from both TX and RX transducers.

set

set(name, value)

Update a parameter at runtime, then invalidate the conventional delegate.

Extends ReceptionBase.set with validation of the "method" selector and by dropping the cached ReceptionConventional (rebuilt on the next conventional call, picking up the new tx/rx/medium/excitation state).

Parameters:

Name Type Description Default
name str

Parameter name (a key of _SETTABLE, "tx", or "rx").

required
value object

New value; for "method" it must be a valid selector.

required

_extract_rx_element_patches

_extract_rx_element_patches()

Pre-extract per-RX-element patch arrays.

_rx_groups

_rx_groups(focused_sum)

Receive patch groups feeding the per-element / per-line SIR.

focused_sum → ONE group holding every RX patch (the kernel sums it into a single beamformed line, Field II calc_scat's internal receive sum); else one group per receive element → raw per-channel RF. Each group is the tuple (centers, wx, wy, apod, delays, eu, ev) of that group's patch arrays.

_oneway_time_grid

_oneway_time_grid(points_m, aperture)

Far-field SIR sample grid for one aperture ("tx" or "rx").

Wraps compute_time_grid with that aperture's patch centres, largest patch dimensions and focusing delays. Returns (time_grid, t0, dt, T): the sampled time axis and its origin t0 (s), step dt (s) and length T (samples), sized to cover the trapezoidal SIR of every scatterer.

_finalize

_finalize(rf, pe_t0, dt, focused_sum, downsampling)

Beamforming t0, coords dict, and optional anti-aliased decimation.

coords["t0"] is the beamforming time reference, not the literal instant of the first RF sample: it is set so that a scatterer's echo peaks at its geometric round-trip time (|p − r_tx| + |p − r_rx|)/c. A delay-and-sum can then read the sample at (t_tx + t_rx − t0)/dt with no further correction — the convention Field II users reach through USTB, and the one MUST gets for free from a zero-phase pulse. Three shifts build it:

  • TX/RX focusing bulk. pe_t0 counts from the first-firing element, so the transmit bulk tx.delays.max() (the last-firing element's delay) is subtracted to put the origin on the beam axis; the RX bulk likewise.
  • Elevation lens transit. A cylindrical lens is a RECESSED surface: its patches lie on an arc whose centre sits at z = −sag, behind the rim plane the beamformer measures depth from. Every patch is therefore farther from an on-axis target than the face plane suggests — at the lens focus, uniformly by one sag R − √(R² − (h/2)²), which is exactly the condition that makes rim and centre equidistant and focuses the beam. The echo thus arrives one lens transit LATE relative to the 2z/c a beamformer assumes, so the sag is SUBTRACTED from t0 (TX once, RX once) to put the echo back at its geometric depth. Flat apertures have zero sag. Measured on a point target at the lens focus: with the term subtracted the target lands within 0.005 mm of truth; adding it instead places the image 2 sags (0.34 mm here) too deep, and omitting it altogether 1 sag too deep.
  • Two-way pulse centre. The band-limited echo peaks half a pulse after the geometric arrival (see _pulse_center_lag_s); subtracting that lag here is what makes the naive delay-and-sum land on the peak. The value stays in coords["pulse_center_lag_s"] as provenance — it is already applied, so a beamformer must NOT add it again.

_pulse_center_lag_s

_pulse_center_lag_s() -> float

Envelope-centre lag of the two-way pulse (seconds), removed from t0.

The received waveform is the electric drive convolved with the transmit and receive element impulse responses, e ⊛ h_tx ⊛ h_rx. Three bursts of lengths L_e, L_tx, L_rx convolve to N = L_e + L_tx + L_rx − 2 samples; each factor has a symmetric envelope, so the two-way envelope peaks at the centre of that support, (N − 1)/2 samples after the geometric arrival. A delay-and-sum reads samples at the geometric round-trip time, so it would land this lag BEFORE the echo peak — an axial bias of c·lag/2 (0.5 mm for a 5 MHz two-cycle pulse model). _finalize subtracts it from coords["t0"] so the naive lookup is already correct. It depends only on the pulse model and fs — never on the phantom.

_accumulate_depth_bins

_accumulate_depth_bins(points_m, n_bins, per_bin_fn)

Compute the RF one depth bin at a time and sum onto one shared time axis.

Scatterers are sorted by distance to the transmit aperture centroid (a proxy for their echo arrival time) and split into n_bins groups of equal size. per_bin_fn(idx) computes one group's RF on a time grid just long enough for that group's arrivals, and returns (rf_bin, n0) where n0 is the integer sample index at which that grid starts on the shared global time axis (see _snap_to_lattice). Because every bin's samples land exactly on that shared axis, recombining is a plain addition into rf[:, n0:n0+len] — no interpolation or resampling anywhere.

_auto_depth_bins

_auto_depth_bins(points_m, n_out)

Choose how many depth bins to split the scatterers into (1 = no binning).

Why bin at all: each scatterer's echo occupies a short time window around its round-trip arrival 2·|r|/c, but a single computation over ALL scatterers must use a time grid spanning from the earliest to the latest arrival. With scatterers spread over centimetres of depth that grid is long and mostly empty, and its FFT length nfft (and, for the spectral form, the number of in-band frequency bins ∝ nfft) grows with the depth span. Splitting the scatterers into depth-ordered groups lets each group use a grid only as long as its own arrival window — much shorter transforms, same summed RF.

How the count is chosen:

  1. Don't bin when it cannot pay: fewer than _MIN_SCATTERERS_PER_BIN scatterers, fewer than 2 output channels, or all scatterers at the same depth (arrival spread under one sample).
  2. Otherwise target ~_MIN_SCATTERERS_PER_BIN scatterers per bin (P // _MIN_SCATTERERS_PER_BIN). More bins mean shorter windows but also more fixed per-bin overhead (SIR rebuild + FFT setup), and nfft can never drop below the excitation length — so past this occupancy, extra bins are pure overhead. This target is the empirical balance.
  3. Never more bins than the arrival spread in samples: a bin thinner than one sample cannot shrink its window any further.

_validate_scatterer_inputs

_validate_scatterer_inputs(positions_mm, amplitudes)

Normalise and validate positions + amplitudes, return (points_m, amps).

positions_mm may also be a grid dict (x_extent/dx/… keys in mm, exactly as Emission takes): the regular lattice of point targets it describes is built automatically — handy with per_scatterer=True to map the PSF across the field. A regular lattice is NOT a tissue phantom (its periodicity returns coherent lattice echoes, not speckle); for phantoms draw random scatterers, e.g. with [make_phantom][esdiva.utilities.phantom.make_phantom].

A single acquisition sees ONE cloud, so a per-event (N_events, N_scat, 3) stack of moving scatterers is refused here — that shape belongs to sequence_rf, which fires one emission per event.

_per_event_cloud staticmethod

_per_event_cloud(positions_mm, amplitudes, n_events)

Resolve a static or per-event scatterer cloud into an event indexer.

A sequence may hold the medium still (one cloud, reused every emission) or let it move: blood in a vessel travels v/PRF between emissions, and it is exactly that inter-emission displacement — a round-trip path change of twice the axial step — that a Doppler estimator reads as phase. Both are expressed by the shape of the input: (N_scat, 3) holds still, (N_events, N_scat, 3) gives each emission its own cloud. Amplitudes follow the same rule, so a scatterer that leaves the region of interest (or a contrast bubble that is destroyed) is retired by setting its amplitude to zero for the later events.

Parameters:

Name Type Description Default
positions_mm (N_scat, 3) or (N_events, N_scat, 3) numpy.ndarray or dict

Scatterer positions in mm, or a grid dict (static clouds only).

required
amplitudes (N_scat,) or (N_events, N_scat) numpy.ndarray or None

Scattering amplitudes. None defaults to ones.

required
n_events int

Number of TX events in the sequence.

required

Returns:

Name Type Description
pick callable

pick(i) returns the (positions, amplitudes) of event i.

moving bool

True when the cloud differs between events.

Raises:

Type Description
ValueError

If a per-event positions/amplitudes array does not have one entry per TX event, or the two disagree on the scatterer count.

TypeError

If a grid dict is combined with per-event amplitudes — a lattice is a fixed map of point targets, it has no trajectory.

show

show(
    scatterer_positions_mm=None,
    amplitudes=None,
    *,
    TX_color="Delays",
    RX_color="Delays",
    TX_show_edges=False,
    RX_show_edges=False,
    TX_kwargs=None,
    RX_kwargs=None,
    window_size=(900, 700),
    notebook=False,
    jupyter_backend=None,
    legend=True,
    scale=1.0,
    save_path=None,
    off_screen=None,
    return_plotter=False,
    db_scale=True,
    **kwargs,
)

Interactive 3-D preview of the pulse-echo setup (TX, RX, scatterers).

Renders the transmit and receive apertures (their patch meshes, in mm) and the scatterer cloud, each point coloured by its scattering amplitude and faded in proportion to it — a quick visual check that positions, units and aperture poses are what the simulation will see. When TX and RX are the same object the aperture is drawn once.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) array-like

Scatterer positions in mm. None draws only the apertures. A moving cloud (N_events, N_scat, 3) (as sequence_rf takes) is drawn at its FIRST emission — there is no single pose for a sequence.

None
amplitudes (N_scat,) array-like

Scattering amplitude per point. None defaults to ones (all points fully opaque). Per-event (N_events, N_scat) amplitudes are likewise shown for the first emission.

None
TX_color str or tuple

Colour of the transmit aperture. Any PyVista colour (name, hex string or RGB tuple) paints the mesh uniformly. The special strings "Delays" / "Apodization" instead colour each patch by that beamforming quantity with a colour bar, as transducer.show() does. When TX and RX show the SAME quantity, one shared colour bar (right side, common colour range) serves both; different quantities get the TX bar on the left and the RX bar on the right.

"Delays"
RX_color str or tuple

Same as TX_color, for the receive aperture.

"Delays"
TX_show_edges bool

Draw the patch edges of the transmit aperture mesh.

False
RX_show_edges bool

Draw the patch edges of the receive aperture mesh.

False
TX_kwargs dict

Extra keyword arguments for the transmit aperture's add_transducer_mesh call (e.g. {"cmap": "viridis", "opacity": 0.5}); they override the defaults above.

None
RX_kwargs dict

Same as TX_kwargs, for the receive aperture.

None
window_size (int, int)

Pixel dimensions of the render window.

(900, 700)
notebook bool

Enable Jupyter notebook rendering.

False
jupyter_backend str

Backend string passed to PyVista ('static', 'trame' …).

None
legend bool

Draw the TX/RX/Scatterers legend. Note the colour bars belong to the "Delays"/"Apodization" aperture colouring — pass plain colours (e.g. TX_color="lightsteelblue") to remove them.

True
scale float

Global resolution multiplier. Enlarges the render window, every font (grid, aperture tags, colour bars) and the saved screenshot together, so a higher value yields a larger, sharper image without changing the framing (e.g. scale=3 for print figures).

1.0
save_path str or Path

Screenshot file path (e.g. "setup.png"). When given, the scene is rendered off-screen and saved instead of opening a window.

None
off_screen bool

Render without opening a window. None (default) renders off-screen only when save_path is given. Set True when return_plotter is used to screenshot in a headless/batch run — otherwise the returned plotter has nothing rendered and plotter.screenshot() raises "Nothing to screenshot".

None
return_plotter bool

Return the PyVista Plotter object instead of showing or saving the scene. Useful for further customisation (e.g. camera position, extra meshes) before calling plotter.show() or plotter.screenshot(). The axis grid is then NOT drawn, so the caller can apply their own plotter.show_grid(...) settings.

False
db_scale bool

When True, the scatterer amplitudes are converted to dB scale for visualization.

True
**kwargs

Forwarded to the scatterer add_mesh call (e.g. point_size).

{}

Returns:

Type Description
Plotter or None

The plotter (scene assembled, grid and rendering left to the caller) when return_plotter=True; otherwise None.

sequence_rf

sequence_rf(
    scatterer_positions_mm,
    amplitudes,
    tx_events,
    *,
    downsampling=None,
    out_path=None,
    checkpoint_chunks=1,
)

Pulse-echo RF for a sequence of TX events (emission basis: PW/DW/...).

Each event sets the TX delays/apodization, then pulse_echo_rf is run (summed over scatterers). Useful as the emission basis for matrix imaging.

MOVING SCATTERERS. The medium may move between emissions: give scatterer_positions_mm a (N_events, N_scat, 3) stack, one cloud per event, and each emission sees its own cloud. Blood travelling at v covers v/PRF between emissions, and the resulting round-trip path change (twice the axial step) is the inter-emission phase a Doppler estimator reads. eSDIva has no slow-time clock of its own, so the trajectory is yours to write — one line for constant flow::

t = np.arange(n_events) / prf                       # slow-time, s
pos = pos0[None] + v_mm_s[None] * t[:, None, None]  # (N_ev, N, 3)

and any pulsatile, cardiac or decorrelating trajectory is the same call. amplitudes may vary per event too, as (N_events, N_scat): the scatterer count is fixed across the sequence, so a zero amplitude is how a scatterer that has left the region of interest — or a contrast bubble that has been destroyed — is retired.

The scatterers are FROZEN during each emission. Motion is resolved only at the event rate, so the RF carries the phase shift BETWEEN emissions, not an intra-pulse Doppler frequency shift within one echo; velocities of clinical interest move a small fraction of a wavelength during a single pulse, which is why this is the standard flow model.

With out_path, the sequence is CHECKPOINTED: each event's RF is written to disk (one compressed file per event + a contents file) the moment it finishes, and re-running the same call on the same folder skips the completed events and resumes from the first missing one. The contents file fingerprints the full simulation (probe, medium, excitation, scatterers, events); re-running with anything changed raises instead of silently mixing incompatible data.

checkpoint_chunks bounds the work lost to a crash WITHIN one event: the RF is linear in the scatterers, so the cloud is split into chunks that are simulated and checkpointed one file at a time, then summed. Every chunk carries four zero-amplitude "grid sentinel" points (just nearer than the nearest scatterer and just farther than the farthest, per aperture) so all chunks of an event share one time grid and their sum is sample-exact.

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_events, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 2-D array holds the medium still for the whole sequence; a 3-D array gives every emission its own cloud (moving scatterers), and its first axis must match tx_events.

required
amplitudes (N_scat,) or (N_events, N_scat) numpy.ndarray

Scattering amplitude of each scatterer, either fixed for the sequence or per emission.

required
tx_events list of dict

Each dict has "delays" and/or "apodization" (E,) arrays.

required
downsampling int or None

Anti-aliased time decimation factor applied per event.

None
out_path str or Path or None

Checkpoint folder (created if missing). Events already completed there are skipped; each new event is written atomically on completion, so a crash costs at most the event in flight.

None
checkpoint_chunks int

Number of scatterer chunks checkpointed per TX event (requires out_path). With N chunks a crash costs at most 1/N of an event; pick it so one chunk takes ~10–15 minutes.

1

Returns:

Name Type Description
rf (N_events, Erx, Nt) numpy.ndarray

Per-event, per-receive-element RF (channels before time).

coords dict

"t0"/"dt" of the first event, plus "t0_per_event" — an (N_events,) array of each event's beam-axis time origin. Events with differing focus have differing t0 (the time grid depends on that event's delays, and on where its scatterers are); beamform each event with its own origin. dt is shared (one sampling rate); traces are zero-padded at the END to the common Nt, so only the origin differs.

Raises:

Type Description
ValueError

If checkpoint_chunks > 1 without out_path, if a per-event positions/amplitudes array does not carry one entry per TX event, or if tx and rx are the same object while events set delays/apodization (RX weights are per receive channel, so the event's TX weights would corrupt the receive traces — pass rx=tx.copy()).

TypeError

If checkpoint_chunks > 1 with a grid-dict scatterer input, or a grid dict is combined with per-event amplitudes.

_checkpointed_pulse_echo

_checkpointed_pulse_echo(
    scatterer_positions_mm,
    amplitudes,
    *,
    per_scatterer,
    downsampling,
    out_path,
    checkpoint_chunks,
)

Checkpointed single-shot pulse-echo = a one-event sequence.

Backend for pulse_echo_rf(out_path=...): the current TX delays/apodization become an explicit event so the dataset fingerprint captures the focus state (sequence_rf fingerprints its events; bare transducer state would go unchecked and a resume could silently mix two different focus settings).

_open_sequence_dataset

_open_sequence_dataset(
    out_path,
    scatterer_positions_mm,
    amplitudes,
    tx_events,
    downsampling,
    checkpoint_chunks=1,
)

Open/create the checkpoint folder for sequence_rf.

The fingerprint covers everything that determines the RF: probe geometries (via their repr), medium, sampling, excitation, scatterer cloud and the per-event delays/apodization. Any bit-level change in these makes the resume refuse, so a checkpoint can never be silently continued with different physics. The chunk count is part of the fingerprint too (chunk files are indexed by it), but only when chunking is on, so unchunked datasets keep their original fingerprint.

_grid_sentinels_mm

_grid_sentinels_mm(pos_mm)

Four zero-amplitude points that pin the pulse-echo time grid.

The RF time window is sized from the min/max patch-centre-to-scatterer distance of the simulated point set, so two different chunks of one phantom would get slightly different (non-sample-aligned) windows and their partial RFs could not be summed. Adding to EVERY chunk one point 0.2 mm nearer than the nearest scatterer and one 0.2 mm farther than the farthest (for the TX and the RX aperture) forces one common window; with zero scattering amplitude they contribute nothing to the physics.

Parameters:

Name Type Description Default
pos_mm (N_scat, 3) numpy.ndarray

Full scatterer cloud in mm.

required

Returns:

Type Description
(4, 3) numpy.ndarray

Sentinel positions in mm.

_resolve_tx_groups staticmethod

_resolve_tx_groups(tx_groups, n_el)

Build the list of TX element-index groups for synthetic_aperture_rf.

synthetic_aperture_rf

synthetic_aperture_rf(
    scatterer_positions_mm,
    amplitudes=None,
    *,
    tx_groups="element",
    decimation=10,
    out_path=None,
    countdown=True,
    checkpoint_chunks=1,
)

Synthetic-aperture RF: each TX element/group fires alone, all RX record.

Canonical diverging-wave emission basis for matrix imaging: every group fires FLAT (zero delay, unit apodization) — this OVERRIDES any TX delays/ apodization set on the transducer. tx_groups="element" is full FMC (= Field II calc_scat_all); int N uses N-element sub-apertures; a list[list[int]] gives custom groups.

Each group is one TX event of sequence_rf, so it shares that method's checkpointing: with out_path every group (or scatterer chunk, with checkpoint_chunks) lands on disk the moment it finishes, a re-run resumes at the first missing file, and a changed setup refuses. Output Ntx_grp·Erx·Nt can be huge, hence the anti-aliased decimation (default 10×); a 10 s countdown precedes an in-RAM run so it can be aborted (a checkpointed run is always interruptible, so it starts straight away).

Parameters:

Name Type Description Default
scatterer_positions_mm (N_scat, 3) or (N_groups, N_scat, 3) numpy.ndarray

Scatterer positions in mm. A 3-D array gives each transmit group its own cloud: an FMC acquisition fires its groups one after another, so a medium that moves meanwhile is seen at a different position by each group — the motion artefact of a slow full-matrix capture. Its first axis must match the number of groups.

required
amplitudes (N_scat,) or (N_groups, N_scat) numpy.ndarray or None

Scattering coefficient at each position, fixed or per transmit group. None defaults to ones.

None
tx_groups str or int or list[list[int]]

Transmit grouping: "element" fires each element alone (full FMC), int N fires N-element sub-apertures, list[list[int]] fires custom element groups.

"element"
decimation int

Anti-aliased time decimation factor.

10
out_path str or Path or None

Checkpoint folder (an RFDataset, created if missing): one compressed file per group, crash-safe and resumable.

None
countdown bool

Print a 10 s abortable countdown before an in-RAM computation (skipped if stdout is not a TTY or out_path is given).

True
checkpoint_chunks int

Scatterer chunks checkpointed per group (requires out_path); a crash then costs at most one chunk of one group.

1

Returns:

Name Type Description
rf (Ntx_grp, Erx, Nt) numpy.ndarray

Per-group, per-receive-element RF.

coords dict

"t0"/"dt" of the decimated time axis (plus "t0_per_event", identical for all groups — they all fire flat).

_apply_focus_apod staticmethod

_apply_focus_apod(trans, focus, FoverD, apodization_type)

Set focusing delays + apodization on trans from focus (mm).

scan_focusline

scan_focusline(
    focus_mm,
    scatterer_positions_mm,
    amplitudes=None,
    *,
    FoverD=None,
    apodization_type=None,
    rx_FoverD=None,
    rx_apodization_type=None,
    output="envelope",
    downsampling=None,
)

One conventional focused scan line (Field II calc_scat match).

Recomputes the TX and RX focus + apodization FROM focus_mm (reusing each transducer's compute_delays / compute_apodization), then beamforms ON RECEIVE INSIDE THE SIR KERNEL via _focused_sum_rf (focused_sum=True): every RX patch carries its focusing delay + apodization and the kernel sums them into one line — exactly Field II calc_scat's internal receive beamforming. This replaces the old per-element-RF + external DAS_focused_scanline path, so it is ~E_rx× cheaper (one FFT pair, no per-channel loop) and applies the receive focus at corner-time resolution (no sample-interpolation loss). Loop focus_mm externally to build a B-mode, e.g. img[i] = sim.scan_focusline(...)[0].

By default the RX aperture mirrors the TX focus + apodization (the usual focused-imaging case, and what Field II's sesr.m does: identical xdc_apodization on emit and receive). Pass rx_FoverD / rx_apodization_type to give the receive aperture a different profile.

Parameters:

Name Type Description Default
focus_mm (3,) array_like

Focal point [x, y, z] in mm (the per-line looped variable).

required
scatterer_positions_mm (N_scat, 3) numpy.ndarray

Scatterer positions in mm.

required
amplitudes (N_scat,) numpy.ndarray or None

Scattering coefficient at each position. None defaults to ones.

None
FoverD float or None

TX F-number for the active aperture. Used only when apodization_type is set (defaults to 2.0 there if None).

None
apodization_type str or None

TX apodization: "hanning" / "rect" (F/D=1) / "gaussian" / custom. None → uniform full aperture (no taper).

None
rx_FoverD float or None

RX F-number. None → mirror FoverD (identical TX/RX aperture).

None
rx_apodization_type str or None

RX apodization type. None → mirror apodization_type.

None
output (envelope, rf)

Return the Hilbert envelope (default) or the raw beamformed RF line.

"envelope"
downsampling int or None

Anti-aliased time decimation factor.

None

Returns:

Name Type Description
out (Nt,) numpy.ndarray

Envelope (or RF line) of the beamformed scan line at lateral focus[0].

coords dict

"t0"/"dt" of the line's time axis (beam-axis referenced).

_validate_method staticmethod

_validate_method(method)

_compute_pe_time_grid

_compute_pe_time_grid(points_m)

Time grid covering both TX and RX propagation paths.

Returns the combined pulse-echo window (pe_t0, dt, pe_T) and the two one-way windows (tx_t0, tx_T, rx_t0, rx_T). The spectral form references its TX and RX spectra to tx_t0 / rx_t0 so their product lands at pe_t0 = tx_t0 + rx_t0.

_ensure_conv

_ensure_conv(sir_method)

Lazily build (and cache) the ReceptionConventional used for the conventional branch, with its SIR-sampling kernel set to sir_method ("fst"/"sdi"/ "auto").

Shares the same tx/rx objects (so focusing state set by sequence_rf/scan_focusline/synthetic_aperture_rf is seen) and mirrors the medium params. The RX-non-default warning already fired at this object's construction, so suppress the duplicate here.

_band_range staticmethod

_band_range(band_mag, tol=0.0001)

In-band frequency slice [b0, b1) where the pulse filter is non-negligible.

The received signal is shaped by the excitation × impulse-response magnitude band_mag; outside the band where it exceeds tol of its peak the result is ~0, so the spectral form's spectra need not be evaluated there. Returns the contiguous span covering every significant bin (the whole range if the drive is wideband).

_pe_setup

_pe_setup(
    points_m,
    *,
    per_scatterer,
    focused_sum,
    label,
    grid_override=None,
)

Common time-grid, FFT-filter and band-range setup for the spectral core.

Returns a dict of everything the spectral (and ReceptionPaired) cores share.

grid_override (a (pe_t0, dt, pe_T, tx_t0, rx_t0) tuple) bypasses the natural time grid — used by the depth-binned spectral path, where each bin's window is snapped to the global sample lattice so per-bin results add back at an integer offset. pe_T (and hence nfft, the band-bin count) is then the SHORT per-bin window, not the whole-field span.

_rf_spectral

_rf_spectral(
    points_m,
    amps,
    *,
    downsampling,
    per_scatterer,
    focused_sum,
)

Spectral PE core: closed-form one-way SIR spectra, multiplied — no forward FFT.

H_TX and each receive element's H_RX are evaluated only on the in-band frequencies; their product is the two-way SIR spectrum. For the summed RF the scatterers are amplitude-summed in the frequency domain (amps @ H_TX·H_RX), the shared filter G = fs·IR is applied per bin (the excitation weights each TX element's H_TX,t, so global and per-element drives share one path), and one inverse FFT per element returns the RF. Attenuation multiplies each (scatterer, element) product by H_att over the round trip TX centre → scatterer → element centre.

Depth binning. The in-band bin count is N_band = (BW/fs)·nfft and nfft ≈ next_pow2(pe_T) spans the arrival window of ALL scatterers, so a deep or wide field inflates N_band — and the spectrum build costs P·M·N_band, the dominant term. Grouping scatterers by depth lets each bin use a SHORT window (small nfft → small N_band), cutting both the build and the per-element product-sum by nfft_full/nfft_bin. Each bin shares one global sample lattice, so its RF adds back at an integer sample offset (no resampling). Used only for the summed RF (per_scatterer=False); the per-scatterer PSF keeps one window.

_n_spectral_out

_n_spectral_out(focused_sum)

Number of output channels the spectral core produces (1 if focused_sum).

_build_csr staticmethod

_build_csr(groups)

Lay per-element patch arrays out element-by-element, CSR-style.

The fused two-way kernel reads an aperture as one set of patch arrays in which element e occupies the contiguous block ptr[e]:ptr[e+1]. This concatenates the per-element groups into that layout and returns the patch arrays, their tangent frames, and the offsets ptr.

_tx_layout

_tx_layout(n_drives)

TX patch arrays + element offsets ptr: one block per element when each element has its own drive (n_drives > 1), else the whole aperture as one.

_spectral_filters

_spectral_filters(s)

Shared filter G = fs·ir_tx·ir_rx, TX drives and the attenuation spec.

fs turns the product of two continuous SIR spectra into the sampled-RF scale (rfft(h[n]) ≈ fs·H per SIR, and the discrete convolution carries one dt). The drive spectra (E_tx or 1, N_band) weight each TX element's H_TX,t (one row of ones without excitation). Attenuation (None when alpha0 is unset) is referenced to the TX aperture centre and each receive element centre (the aperture centroid for a focused sum).

_spectral_h_rx

_spectral_h_rx(s, e_rx, points_m, omega_band)

One receive element's closed-form SIR spectrum H_RX → (P, N_band).

_spectral_summed_from_setup

_spectral_summed_from_setup(s, points_m, amps, rx_csr)

Summed spectral RF for one (sub-field, window) → (n_out, pe_T) float32.

Forms the two-way SIR spectrum H_TX·H_RX for every receive element and amplitude-sums it over scatterers in a single fused parallel pass (the analogue of conventional's amps @ H_pe): the transmit one-way spectrum is built once per scatterer and reused across all receive elements, with nothing of size (P, N_band) materialised. The shared filter G = fs·IR is then applied per bin and one inverse FFT per element returns the RF. Reused for the single-window path and for each depth bin (the window — hence nfft and the band-bin count — comes from s); rx_csr is the receive aperture laid out element-by-element.

The fused kernel accumulates the scatterer sum in complex128 internally — cheap because N_band is the small in-band bin count.

_rf_spectral_binned

_rf_spectral_binned(
    points_m, amps, *, downsampling, focused_sum, n_bins
)

Summed spectral RF, split into depth bins for short per-bin windows.

Same physical result as the single-window spectral path, but scatterers are grouped by depth so each bin spans a tight arrival window → small nfft → small in-band bin count N_band (the spectral form's dominant cost factor). All bins share one global sample lattice (t0_g, the reported t0): each bin's window is snapped to that lattice and its RF added back at the integer sample offset n0 — no resampling. Only the summed RF uses this; per-scatterer does not.