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_RXfrom the closed-form SIR spectrum, in-band only."temporal"(="sdi") /"fst"/"auto"— sampled two-way SIR convolution (delegated to the conventionalReceptionConventionalbackend; 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 spectraH_TX·H_RXon the in-band bins only. No forward FFT, cost linear in patches, exact."fst"/"sdi"/"auto"— sampleh_tx/h_rxand convolve, delegated toReceptionConventional. 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 |
None
|
method
|
str
|
Pulse-echo formulation: |
"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 |
"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 → |
False
|
downsampling
|
int or None
|
Anti-aliased time decimation factor. |
None
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed separately (requires |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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 |
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
|
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(N_events, Erx, Nt) numpy.ndarray
|
Per-event, per-receive-element RF (channels before time). |
coords |
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If |
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"
|
decimation
|
int
|
Anti-aliased time decimation factor. |
10
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
countdown
|
bool
|
Print a 10 s abortable countdown before an in-RAM computation
(skipped if stdout is not a TTY or |
True
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed per group (requires |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(Ntx_grp, Erx, Nt) numpy.ndarray
|
Per-group, per-receive-element RF. |
coords |
dict
|
|
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 |
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
|
None
|
apodization_type
|
str or None
|
TX apodization: |
None
|
rx_FoverD
|
float or None
|
RX F-number. None → mirror |
None
|
rx_apodization_type
|
str or None
|
RX apodization type. None → mirror |
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
|
|
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 |
None
|
amplitudes
|
(N_scat,) array-like
|
Scattering amplitude per point. None defaults to ones
(all points fully opaque). Per-event |
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"
|
RX_color
|
str or tuple
|
Same as |
"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
|
None
|
RX_kwargs
|
dict
|
Same as |
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 ( |
None
|
legend
|
bool
|
Draw the TX/RX/Scatterers legend. Note the colour bars belong to
the |
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. |
1.0
|
save_path
|
str or Path
|
Screenshot file path (e.g. |
None
|
off_screen
|
bool
|
Render without opening a window. |
None
|
return_plotter
|
bool
|
Return the PyVista |
False
|
db_scale
|
bool
|
When True, the scatterer amplitudes are converted to dB scale for visualization. |
True
|
**kwargs
|
Forwarded to the scatterer |
{}
|
Returns:
| Type | Description |
|---|---|
Plotter or None
|
The plotter (scene assembled, grid and rendering left to the
caller) when |
set
¶
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 |
required |
value
|
object
|
New value; for |
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 |
None
|
method
|
str
|
SIR computation method: |
"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"
|
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 → |
False
|
downsampling
|
int or None
|
Anti-aliased time decimation factor. |
None
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed separately (requires |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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 |
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
|
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(N_events, Erx, Nt) numpy.ndarray
|
Per-event, per-receive-element RF (channels before time). |
coords |
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If |
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"
|
decimation
|
int
|
Anti-aliased time decimation factor. |
10
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
countdown
|
bool
|
Print a 10 s abortable countdown before an in-RAM computation
(skipped if stdout is not a TTY or |
True
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed per group (requires |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(Ntx_grp, Erx, Nt) numpy.ndarray
|
Per-group, per-receive-element RF. |
coords |
dict
|
|
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 |
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
|
None
|
apodization_type
|
str or None
|
TX apodization: |
None
|
rx_FoverD
|
float or None
|
RX F-number. None → mirror |
None
|
rx_apodization_type
|
str or None
|
RX apodization type. None → mirror |
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
|
|
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 |
None
|
amplitudes
|
(N_scat,) array-like
|
Scattering amplitude per point. None defaults to ones
(all points fully opaque). Per-event |
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"
|
RX_color
|
str or tuple
|
Same as |
"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
|
None
|
RX_kwargs
|
dict
|
Same as |
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 ( |
None
|
legend
|
bool
|
Draw the TX/RX/Scatterers legend. Note the colour bars belong to
the |
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. |
1.0
|
save_path
|
str or Path
|
Screenshot file path (e.g. |
None
|
off_screen
|
bool
|
Render without opening a window. |
None
|
return_plotter
|
bool
|
Return the PyVista |
False
|
db_scale
|
bool
|
When True, the scatterer amplitudes are converted to dB scale for visualization. |
True
|
**kwargs
|
Forwarded to the scatterer |
{}
|
Returns:
| Type | Description |
|---|---|
Plotter or None
|
The plotter (scene assembled, grid and rendering left to the
caller) when |
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
¶
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 |
{}
|
excitation
instance-attribute
¶
_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"),
}
_apply_settable
¶
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 |
TypeError
|
If |
_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
¶
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, |
shift |
float
|
Sub-sample remainder |
_require_rigid
staticmethod
¶
Refuse baffle="soft" in a core that models the rigid baffle only.
_group_patches_by_element
staticmethod
¶
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 → |
False
|
downsampling
|
int or None
|
Anti-aliased time decimation factor. |
None
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed separately (requires |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
_focused_sum_rf
¶
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).
_reset_time_log
¶
Zero the per-phase wall-clock log at the start of an RF computation.
_timed_h_sir
¶
compute_h_sir timed into time_log["sir_s"] (the SIR kernel cost).
_refresh_sub_elem_attributes
¶
Extract patch arrays from both TX and RX transducers.
set
¶
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 |
required |
value
|
object
|
New value; for |
required |
_extract_rx_element_patches
¶
Pre-extract per-RX-element patch arrays.
_rx_groups
¶
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
¶
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
¶
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_t0counts from the first-firing element, so the transmit bulktx.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 sagR − √(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 the2z/ca beamformer assumes, so the sag is SUBTRACTED fromt0(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 incoords["pulse_center_lag_s"]as provenance — it is already applied, so a beamformer must NOT add it again.
_pulse_center_lag_s
¶
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
¶
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
¶
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:
- Don't bin when it cannot pay: fewer than
_MIN_SCATTERERS_PER_BINscatterers, fewer than 2 output channels, or all scatterers at the same depth (arrival spread under one sample). - Otherwise target ~
_MIN_SCATTERERS_PER_BINscatterers per bin (P // _MIN_SCATTERERS_PER_BIN). More bins mean shorter windows but also more fixed per-bin overhead (SIR rebuild + FFT setup), andnfftcan never drop below the excitation length — so past this occupancy, extra bins are pure overhead. This target is the empirical balance. - Never more bins than the arrival spread in samples: a bin thinner than one sample cannot shrink its window any further.
_validate_scatterer_inputs
¶
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
¶
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
|
|
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 |
None
|
amplitudes
|
(N_scat,) array-like
|
Scattering amplitude per point. None defaults to ones
(all points fully opaque). Per-event |
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"
|
RX_color
|
str or tuple
|
Same as |
"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
|
None
|
RX_kwargs
|
dict
|
Same as |
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 ( |
None
|
legend
|
bool
|
Draw the TX/RX/Scatterers legend. Note the colour bars belong to
the |
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. |
1.0
|
save_path
|
str or Path
|
Screenshot file path (e.g. |
None
|
off_screen
|
bool
|
Render without opening a window. |
None
|
return_plotter
|
bool
|
Return the PyVista |
False
|
db_scale
|
bool
|
When True, the scatterer amplitudes are converted to dB scale for visualization. |
True
|
**kwargs
|
Forwarded to the scatterer |
{}
|
Returns:
| Type | Description |
|---|---|
Plotter or None
|
The plotter (scene assembled, grid and rendering left to the
caller) when |
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 |
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 |
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
|
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(N_events, Erx, Nt) numpy.ndarray
|
Per-event, per-receive-element RF (channels before time). |
coords |
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If |
_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
¶
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
¶
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"
|
decimation
|
int
|
Anti-aliased time decimation factor. |
10
|
out_path
|
str or Path or None
|
Checkpoint folder (an |
None
|
countdown
|
bool
|
Print a 10 s abortable countdown before an in-RAM computation
(skipped if stdout is not a TTY or |
True
|
checkpoint_chunks
|
int
|
Scatterer chunks checkpointed per group (requires |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
rf |
(Ntx_grp, Erx, Nt) numpy.ndarray
|
Per-group, per-receive-element RF. |
coords |
dict
|
|
_apply_focus_apod
staticmethod
¶
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 |
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
|
None
|
apodization_type
|
str or None
|
TX apodization: |
None
|
rx_FoverD
|
float or None
|
RX F-number. None → mirror |
None
|
rx_apodization_type
|
str or None
|
RX apodization type. None → mirror |
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
|
|
_compute_pe_time_grid
¶
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
¶
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
¶
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
¶
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
¶
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
¶
Number of output channels the spectral core produces (1 if focused_sum).
_build_csr
staticmethod
¶
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 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
¶
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
¶
One receive element's closed-form SIR spectrum H_RX → (P, N_band).
_spectral_summed_from_setup
¶
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
¶
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.