Shared Memory Protocol¶
EffUSIve uses Python's multiprocessing.shared_memory transport to exchange images,
metadata, commands, and status between the Python napari process and the MATLAB worker
subprocess. Each acquisition run gets its own uniquely named segment set (for example
cf_meta_<run_token>), created by Python before the worker launches, zero-initialised,
and opened by MATLAB on first use.
Segment Summary¶
The table uses logical channel names. At runtime each one is suffixed with a session token so multiple acquisitions cannot collide with stale segments from a previous run.
| Logical name | Runtime example | Size | Written by | Read by | Purpose |
|---|---|---|---|---|---|
cf_meta |
cf_meta_20260519_153012_123456 |
44 bytes | MATLAB (setup + publish) | Python | Frame counter, image dimensions, depth axes, runtime flags, hardware timestamp |
cf_bmode |
cf_bmode_... |
1 MB | MATLAB (publish) | Python | Raw B-mode image (float32, un-normalized) |
cf_pdi |
cf_pdi_... |
1 MB | MATLAB (publish) | Python | Raw PDI image (float32, un-normalized) |
cf_cmd |
cf_cmd_... |
116 bytes | Python | MATLAB | Operator commands and request IDs |
cf_rf |
cf_rf_... |
~1 MB | MATLAB (publish) | Python | Raw RF snapshot header + int16 data |
cf_stack |
cf_stack_... |
16 bytes | MATLAB (control) | Python | Z-stack state machine status |
cf_ack |
cf_ack_... |
36 bytes | MATLAB (control) | Python | Command acknowledgement IDs |
Image segments (cf_bmode, cf_pdi) are allocated for the maximum supported image size of 512 × 512 pixels. Only the first nz × nx elements are valid; dimensions are read from cf_meta.
Segment Layouts¶
cf_meta / cf_meta_* (44 bytes)¶
Written by effusive.napari.setup (dimensions and axes) and by publishProcessedFrame (counter, flags, timestamp) each frame.
[0:8] uint64 frame_counter incremented every published frame
[8:12] int32 nz image height in pixels (set at setup)
[12:16] int32 nx image width in pixels (set at setup)
[16:20] float32 z_start_mm depth axis start (mm)
[20:24] float32 z_end_mm depth axis end (mm)
[24:28] float32 x_start_mm lateral axis start (mm)
[28:32] float32 x_end_mm lateral axis end (mm)
[32:36] uint32 runtime_flags bitmask (see below)
[36:44] float64 ensemble_time_s hardware ensemble timestamp (s); 0 if unavailable
Runtime flag bits:
| Bit | Constant | Meaning |
|---|---|---|
| 0 | META_FLAG_SAVE_ACTIVE |
EchoFrame is currently saving to disk |
| 1 | META_FLAG_FREEZE_ACTIVE |
Acquisition is frozen in VSX |
cf_bmode / cf_bmode_* and cf_pdi / cf_pdi_* (1 MB each)¶
Row-major float32 arrays of shape [nz, nx], written transpose-then-flatten from MATLAB. Both carry
raw, un-normalized amplitude/power straight from EchoFrame -- no log-compression or scaling happens
on the MATLAB side.
Python reads these with numpy.ndarray(..., dtype=numpy.float32, buffer=shm.buf), copies nz × nx
elements, then log-compresses each to dB relative to that frame's own peak
(ShmReader.read_bmode/read_pdi in shared_memory.py):
cf_bmode:20 * log10(raw / raw.max()), clipped toBMODE_CONTRAST_LIMITS([-60, 0]dB).cf_pdi:10 * log10(raw / raw.max()), clipped toPDI_CONTRAST_LIMITS([-30, 0]dB).
Doing this on the Python side keeps the max-reduce and log10 work off the VSX-blocking MATLAB callback chain; it runs on napari's decoupled polling tick instead, which cannot stall acquisition.
cf_cmd / cf_cmd_* (116 bytes)¶
Written entirely by Python each command; MATLAB reads it every frame in processRuntimeControl.
[0] uint8 vsExit 0=run, 1=stop VSX
[1] uint8 freeze 0=run, 1=freeze acquisition
[2] uint8 save_to_disk master save switch (1=enabled)
[3] uint8 svd_update_flag 1=apply new SVD threshold
[4:6] int16 svd_threshold SVD rejection threshold (0-100 %; stored as percent×100)
[6] uint8 voltage_update_flag 1=apply new transmit voltage
[7] pad
[8:12] float32 voltage_v transmit voltage (V)
[12] uint8 tx_aperture_update_flag 1=apply TX aperture
[13] uint8 rx_aperture_update_flag 1=apply RX aperture
[14:16] int16 tx_aperture TX aperture (0-100 %)
[16:18] int16 rx_aperture RX aperture (0-100 %)
[18] uint8 show_rf rising edge triggers RF snapshot
[19] uint8 crop_update_flag 1=apply new ROI crop
[20] uint8 crop_reset_flag 1=reset crop to full FOV
[21] pad
[22:24] int16 roi_top crop top row (0-based)
[24:26] int16 roi_bottom crop bottom row (0-based)
[26:28] int16 roi_left crop left column (0-based)
[28:30] int16 roi_right crop right column (0-based)
[30] uint8 save_rf 1=save raw RF data
[31] uint8 save_rf_time_tag 1=save RF hardware timestamps
[32] uint8 save_bf 1=save beamformed IQ data
[33] uint8 save_pdi 1=save power Doppler data
[34] uint8 tgc_update_flag 1=apply new TGC control points
[35] pad
[36:52] int16×8 tgc_point_1..8 Verasonics TGC control points (0-1023 each)
[52] uint8 stack_start_flag one-shot: 1=start z-stack
[53] uint8 stack_abort_flag one-shot: 1=abort active z-stack
[54] uint8 stack_use_dummy_motor 0=real Zaber motor, 1=dummy backend
[55] pad
[56:60] int32 stack_start_um elevation start position (μm)
[60:64] int32 stack_step_um step size between slices (μm)
[64:68] int32 stack_n_slices number of slices
[68:72] int32 stack_npdi_per_slice PDI frames to average per slice
[72:76] int32 stack_settle_ms motor settle time (ms)
[76:80] int32 stack_jog_um one-shot relative jog (μm)
[80:84] uint32 freeze_req_id request counter for freeze/unfreeze
[84:88] uint32 save_req_id request counter for save toggle
[88:92] uint32 svd_req_id request counter for SVD update
[92:96] uint32 voltage_req_id request counter for voltage update
[96:100] uint32 tx_aperture_req_id request counter for TX aperture update
[100:104] uint32 rx_aperture_req_id request counter for RX aperture update
[104:108] uint32 tgc_req_id request counter for TGC update
[108:112] uint32 crop_req_id request counter for crop update
[112:116] uint32 stack_req_id request counter for z-stack command
cf_rf / cf_rf_* (~1 MB)¶
Written by publishProcessedFrame when an RF snapshot is requested (rising edge of show_rf). Format: a 16-byte header followed by raw RF int16 data.
[0:8] uint64 rf_counter incremented each time a snapshot is written
[8:12] int32 nsamples number of samples (rows)
[12:16] int32 ncols number of channels (columns)
[16:] int16 data row-major RF data, shape [nsamples, ncols]
Maximum dimensions are 1024 samples × 512 channels; the actual dimensions vary with probe and imaging depth.
cf_stack / cf_stack_* (16 bytes)¶
Written by processRuntimeControl whenever the z-stack state changes.
[0] uint8 stack_active 1 while a z-stack acquisition is running
[1] uint8 stack_error 1 if the z-stack encountered an error
[2:4] int16 status_code state machine state (see below)
[4:8] int32 current_slice current slice index (0-based; -1=not started)
[8:12] int32 total_slices total slices for this run
[12:16] int32 target_position_um current motor target position (μm)
Z-stack status codes:
| Code | Constant | Meaning |
|---|---|---|
| 0 | STACK_STATUS_IDLE |
No active stack |
| 1 | STACK_STATUS_INITIALIZING |
Starting up, computing slice positions |
| 2 | STACK_STATUS_MOVING_TO_SLICE |
Motor moving to target position |
| 3 | STACK_STATUS_SETTLING |
Waiting for motor settle delay |
| 4 | STACK_STATUS_ACCUMULATING |
Collecting PDI frames for current slice |
| 5 | STACK_STATUS_FINALIZING_SLICE |
Averaging accumulated frames |
| 6 | STACK_STATUS_COMPLETING |
Stack done, exporting NIfTI files |
| 7 | STACK_STATUS_ABORTING |
Aborting on user request or vsExit |
| 8 | STACK_STATUS_ERROR |
Motor or processing error |
cf_ack / cf_ack_* (36 bytes)¶
Nine uint32 acknowledgement counters written by processRuntimeControl when a command is applied. Python matches an ack ID against the request ID it sent to confirm the command was processed.
[0:4] uint32 freeze_ack_id
[4:8] uint32 save_ack_id
[8:12] uint32 svd_ack_id
[12:16] uint32 voltage_ack_id
[16:20] uint32 tx_aperture_ack_id
[20:24] uint32 rx_aperture_ack_id
[24:28] uint32 tgc_ack_id
[28:32] uint32 crop_ack_id
[32:36] uint32 stack_ack_id
Request/Ack Protocol¶
Shared memory offers no atomic read-modify-write primitives. To avoid the UI re-applying a stale command on every frame, each command type carries a monotonically-incrementing uint32 request ID alongside its payload fields.
Python side (napari widget; logical names shown)
1. Increment the per-command request counter.
2. Write the new counter value and payload into cf_cmd.
MATLAB side (processRuntimeControl, every frame)
3. Read cf_cmd.
4. Compare the incoming req_id against the last-seen req_id (persistent).
5. If the ID has changed, apply the command.
6. Write the req_id into the corresponding cf_ack field.
7. Update the persistent last-seen req_id.
Python side (optional wait for confirmation)
8. Poll cf_ack until ack_id == req_id.
A request ID of zero is treated as "no change" by MATLAB (persistent variables initialise to zero, matching the initial cf_cmd state). The first real command from Python uses ID 1 or higher.
Lifecycle¶
Python generates a fresh set of shared-memory names for each acquisition, calls
shared_memory.create_segments(...), and passes those resolved names into the MATLAB
worker config. This creates fresh zero-initialised segments and avoids collisions with
stale names from a previous session.
MATLAB opens segments lazily: processRuntimeControl opens cf_cmd_*, cf_stack_*,
and cf_ack_* on its first call; publishProcessedFrame opens cf_bmode_*,
cf_pdi_*, cf_meta_*, and cf_rf_* on the first frame with frameReady=true.
When the session ends Python sends vsExit=1 in cf_cmd_*, waits for the worker to
exit, then closes and unlinks the segments via shared_memory.close_segments(...).