Skip to content

Conversation

@wesselvannierop
Copy link
Collaborator

@wesselvannierop wesselvannierop commented Jan 6, 2026

This PR aims to integrate support for harmonic imaging into the zea data format and code.

Most importantly fixes some bugs around focused beamforming, and different results when using lens correction. Updates the data format to have the parameters needed for harmonic imaging and focused transmits.

  • Add demodulation_frequency to data format and scan class.
  • center_frequency now refers to transmit center frequency by default. If we need the probe center_frequency, this is currently not stored.
  • Refactored lens corrected beamforming to have the same functionality with focusing.
  • Fix focused beamforming (only considered z-axis previously), and adding transmit_origin to data format.
  • Improve polar grid (adds distance_to_apex) to show the full aperture width.
  • Add option to supply a convert.yaml in the verasonics conversion script, which supplies the first_frame in the buffer (if missing from the .mat file) and frames that need to be converted.
  • Clean up and improve Verasonics data conversion script.
  • Add make_tgc_curve
  • Fix init_device("cpu") in jax backend.
  • Add _check_validity_of_dependencies to Parameters, which checks all properties for their dependencies, statically. This basically tests whether the Scan class definition is without bugs.
  • Add minval and maxval to valid keys of Normalize
  • Fix Scan.extent

Summary by CodeRabbit

  • New Features

    • Added demodulation_frequency and per‑transmit transmit_origins; TGC curve generator and a band‑pass filter operation; per‑file conversion config for Verasonics.
  • Documentation

    • Clarified frequency wording to reference transmit pulses; expanded scan, focus and dataset field descriptions.
  • Improvements

    • Better Verasonics framing/buffer handling; scan/display support distance_to_apex and transmit‑origin metadata for polar/grid computations.
  • Tests

    • Added tests for TGC, band‑pass filter, Verasonics conversion, parameter dependency validation, and transmit-related flows.

✏️ Tip: You can customize this high-level summary in your review settings.

@wesselvannierop wesselvannierop self-assigned this Jan 14, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
zea/display.py (1)

310-346: Missing distance_to_apex in scan_convert wrapper function.

The scan_convert function calls scan_convert_2d but does not accept or forward the distance_to_apex parameter. Users calling scan_convert for 2D images cannot utilize this new feature.

🔧 Suggested fix
 def scan_convert(
     image,
     rho_range: Tuple[float, float] = None,
     theta_range: Tuple[float, float] = None,
     phi_range: Tuple[float, float] = None,
     resolution: Union[float, None] = None,
     coordinates: Union[None, np.ndarray] = None,
     fill_value: float = 0.0,
     order: int = 1,
     with_batch_dim: bool = False,
+    distance_to_apex: float = 0.0,
 ):
     """Scan convert image based on number of dimensions."""
     if len(image.shape) == 2 + int(with_batch_dim):
         return scan_convert_2d(
             image,
             rho_range,
             theta_range,
             resolution,
             coordinates,
             fill_value,
             order,
+            distance_to_apex,
         )
zea/ops/__init__.py (1)

123-142: Breaking change: LowPassFilter renamed to LowPassFilterIQ.

The renaming from LowPassFilter to LowPassFilterIQ is a breaking change for external users. No backward compatibility or deprecation measures are in place. Consider adding a deprecation alias or migration guidance if maintaining backward compatibility is a requirement.

🤖 Fix all issues with AI agents
In `@zea/data/convert/verasonics.py`:
- Around line 181-197: The docstring for dereference_all claims it returns an
np.ndarray but the function currently returns a Python list; update
dereference_all so it returns a numpy array by converting dereferenced_data to
np.array(...) before returning (and ensure numpy is imported), or alternatively
update the docstring to state it returns a list—prefer converting the list to an
ndarray to match the existing docstring; see dereference_all,
get_reference_size, and dereference_index to locate the code to change.
- Around line 597-618: get_raw_data_order passes n_frames as the second argument
to get_indices_to_reorder but the method signature expects buffer_index as the
second parameter; change the call in get_raw_data_order to pass buffer_index
(the function parameter) instead of n_frames so get_indices_to_reorder can call
get_frame_count(buffer_index) correctly; locate the call to
get_indices_to_reorder in get_raw_data_order and replace the second argument
with buffer_index.

In `@zea/data/data_format.py`:
- Around line 645-646: The docstring for focus_distances incorrectly documents
its shape as (n_tx, n_el); update the docstring for the parameter
focus_distances in data_format.py to state the correct shape is (n_tx,) to match
the dataset description and Scan usage, and scan the surrounding docstring
entries (e.g., transmit_origins) to ensure consistency with how focus_distances
is used in the Scan class and other functions.

In `@zea/scan.py`:
- Around line 252-274: distance_to_apex can divide by zero when max_angle is 0;
modify the method (distance_to_apex) to guard the tan computation: compute
max_angle = np.max(np.abs(self.polar_limits)) as before, then compute t =
np.tan(max_angle) and if np.isclose(t, 0.0) (or t == 0) return float('inf') (or
another sentinel) instead of dividing; otherwise compute distance_to_apex =
(self.aperture_size[0] / 2) / t and return it. Ensure you still handle the
existing branches (params override and aperture_size None) unchanged.
🧹 Nitpick comments (11)
zea/ops/pipeline.py (1)

1277-1297: Minor docstring inconsistency in output shape description.

The input shape docstring at line 1282 uses prod(grid.shape), but the output shape at line 1286 still uses grid_size_z*grid_size_x. For consistency with DelayAndSum (line 1208) and the input shape description, consider updating:

📝 Suggested docstring fix
         Returns:
             dict: Dictionary containing beamformed_data
-                of shape `(grid_size_z*grid_size_x, n_ch)`
+                of shape `(prod(grid.shape), n_ch)`
                 with optional batch dimension.
zea/probes.py (1)

61-62: Consider tracking this TODO in an issue.

The comment flags a valid concern about the data format no longer storing the actual probe center frequency. To ensure this doesn't get lost, consider creating a GitHub issue to track this design decision.

Would you like me to help open an issue to track this technical debt?

zea/func/__init__.py (1)

56-57: LGTM - New TGC function exported.

The make_tgc_curve function is correctly imported and added to __all__, making it publicly available through zea.func.

Nit: The ordering in __all__ differs from the import order - make_tgc_curve appears before upmix in the import (line 56-57) but after it in __all__ (line 110). Consider keeping consistent alphabetical ordering in both places.

Also applies to: 110-110

tests/test_parameters.py (2)

39-53: LGTM! Test class correctly sets up invalid dependency scenario.

The DummyInvalidParameters class properly defines a computed property (computed2) that depends on a non-existent dependency (non_existing_dependency), which will trigger the validation logic in _check_validity_of_dependencies.

Minor note: The docstring says "invalid parameter type" but the class actually demonstrates an invalid dependency, not an invalid type. Consider updating:

📝 Suggested docstring fix
 class DummyInvalidParameters(Parameters):
-    """A simple test class with an invalid parameter type."""
+    """A simple test class with an invalid dependency."""

148-152: LGTM! Test correctly validates invalid dependency detection.

The test properly verifies that constructing a Parameters subclass with an invalid dependency raises a ValueError. This ensures the _check_validity_of_dependencies mechanism catches typos and missing dependencies at class instantiation time.

Consider adding a match parameter to make the test more specific about the expected error message:

📝 Optional: More specific error matching
 def test_catch_invalid_dependency():
     """Test that invalid dependencies raise an error."""
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="non_existing_dependency"):
         DummyInvalidParameters(param1=5)
tests/data/test_conversion_scripts.py (1)

657-663: Use next() with a default or handle StopIteration.

Using __next__() directly on a generator can raise StopIteration if no .mat files exist. While this is a test file and the MAT file is guaranteed to exist from the test setup, using the built-in next() function is more idiomatic.

Suggested improvement
-    filepath = Path(src).glob("*.mat").__next__()
+    filepath = next(Path(src).glob("*.mat"))
zea/ops/ultrasound.py (1)

476-531: Missing jittable=False in BandPassFilter.__init__.

The BandPassFilter class recomputes filter taps on every call, similar to LowPassFilterIQ. However, unlike LowPassFilterIQ which explicitly sets jittable=False, BandPassFilter doesn't set this flag and instead only conditionally validates based on self._jit_compile. This inconsistency could lead to issues if someone tries to JIT-compile a pipeline containing this operation.

Suggested fix to explicitly set jittable=False
         super().__init__(
             axis=axis,
             complex_channels=False,
             filter_key=f"band_pass_{self._random_suffix}",
+            jittable=False,
             **kwargs,
         )
zea/scan.py (1)

820-820: Address the TODO comment for 3D coordinates.

The TODO indicates that grid_size_y is not being used in coordinates_3d, which suggests this method may be incomplete for proper 3D scan conversion.

Would you like me to open an issue to track fixing the 3D coordinates computation to properly include grid_size_y?

zea/data/convert/verasonics.py (3)

574-574: Consider using yaml.SafeLoader instead of yaml.FullLoader.

yaml.FullLoader can still instantiate arbitrary Python objects in certain cases. Since convert.yaml is a configuration file that could potentially be user-supplied, using yaml.SafeLoader is the safer choice for parsing simple data structures like this schema.

Suggested fix
-                data = yaml.load(file, Loader=yaml.FullLoader)
+                data = yaml.load(file, Loader=yaml.SafeLoader)

731-742: Consider replacing assert with explicit exception.

Using assert for validation can be problematic as assertions can be disabled with Python's -O optimization flag. For consistency with other validation in this file (e.g., read_center_frequencies uses ValueError), consider using an explicit exception.

Suggested fix
         demod_freq = self.dereference_all(self["Receive"]["demodFrequency"])
         demod_freq = np.unique(demod_freq)
-        assert demod_freq.size == 1, (
+        if demod_freq.size != 1:
+            raise ValueError(
             f"Multiple demodulation frequencies found in file: {demod_freq}. "
             "We do not support this case."
-        )
+            )

         return demod_freq.item() * 1e6

1166-1172: Replace assert with explicit exception for input validation.

Using assert for validating user input (the frames argument) is problematic because assertions can be disabled. This validation should always run regardless of optimization flags.

Suggested fix
         elif isinstance(frames, str):
-            assert "-" in frames, (
+            if "-" not in frames:
+                raise ValueError(
                 f"Invalid frames argument: {frames}. "
                 "Expected 'all' or a range of integers (e.g. '4-8')."
-            )
+                )
             start, end = frames.split("-")
             frame_indices = np.arange(int(start), int(end) + 1)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c53a1a9 and 9d4df32.

📒 Files selected for processing (33)
  • docs/source/data-acquisition.rst
  • docs/source/notebooks/pipeline/polar_grid_example.ipynb
  • docs/source/parameters.rst
  • tests/data/test_conversion_scripts.py
  • tests/test_operations.py
  • tests/test_parameters.py
  • tests/test_tensor_ops.py
  • tests/test_transmit_schemes.py
  • zea/beamform/beamformer.py
  • zea/beamform/delays.py
  • zea/beamform/lens_correction.py
  • zea/beamform/pfield.py
  • zea/beamform/pixelgrid.py
  • zea/data/convert/__main__.py
  • zea/data/convert/verasonics.py
  • zea/data/data_format.py
  • zea/data/datasets.py
  • zea/data/file.py
  • zea/display.py
  • zea/doppler.py
  • zea/func/__init__.py
  • zea/func/ultrasound.py
  • zea/internal/config/parameters.py
  • zea/internal/device.py
  • zea/internal/parameters.py
  • zea/ops/__init__.py
  • zea/ops/pipeline.py
  • zea/ops/tensor.py
  • zea/ops/ultrasound.py
  • zea/probes.py
  • zea/scan.py
  • zea/simulator.py
  • zea/tools/selection_tool.py
💤 Files with no reviewable changes (2)
  • zea/beamform/lens_correction.py
  • zea/data/datasets.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-22T20:59:30.246Z
Learnt from: tristan-deep
Repo: tue-bmd/zea PR: 136
File: zea/display.py:343-351
Timestamp: 2025-10-22T20:59:30.246Z
Learning: In zea/display.py map_coordinates, for order > 1 it is acceptable to downcast to float32 for SciPy compatibility, then cast results back to the original dtype; explicit maintenance of float64 throughout is not required.

Applied to files:

  • zea/scan.py
🧬 Code graph analysis (17)
zea/ops/tensor.py (3)
zea/ops/ultrasound.py (1)
  • valid_keys (400-402)
zea/ops/pipeline.py (1)
  • valid_keys (151-159)
zea/ops/base.py (1)
  • valid_keys (135-137)
zea/func/__init__.py (1)
zea/func/ultrasound.py (1)
  • make_tgc_curve (551-581)
zea/beamform/delays.py (1)
zea/scan.py (2)
  • focus_distances (669-676)
  • n_tx (465-467)
tests/test_transmit_schemes.py (2)
zea/scan.py (1)
  • extent (422-433)
zea/internal/dummy_scan.py (2)
  • _get_probe (63-69)
  • _get_scan (312-324)
tests/test_operations.py (1)
zea/func/ultrasound.py (1)
  • make_tgc_curve (551-581)
tests/test_parameters.py (1)
zea/internal/parameters.py (2)
  • Parameters (67-573)
  • cache_with_dependencies (22-47)
tests/test_tensor_ops.py (1)
zea/func/ultrasound.py (1)
  • get_band_pass_filter (195-246)
zea/data/data_format.py (2)
zea/data/convert/verasonics.py (2)
  • demodulation_frequency (732-742)
  • probe_name (769-784)
zea/scan.py (3)
  • demodulation_frequency (597-602)
  • transmit_origins (679-686)
  • n_tx (465-467)
zea/beamform/beamformer.py (1)
zea/beamform/lens_correction.py (1)
  • compute_lens_corrected_travel_times (6-54)
zea/display.py (1)
zea/scan.py (3)
  • distance_to_apex (266-274)
  • rho_range (779-785)
  • theta_range (788-794)
zea/simulator.py (1)
zea/data/convert/verasonics.py (1)
  • probe_center_frequency (692-695)
zea/ops/__init__.py (1)
zea/ops/ultrasound.py (2)
  • BandPassFilter (477-530)
  • LowPassFilterIQ (432-473)
zea/beamform/pixelgrid.py (1)
zea/scan.py (4)
  • polar_limits (615-623)
  • zlims (414-419)
  • distance_to_apex (266-274)
  • grid (288-313)
zea/data/file.py (1)
zea/log.py (2)
  • debug (268-273)
  • orange (104-106)
zea/internal/device.py (1)
zea/backend/autograd.py (2)
  • backend (25-27)
  • backend (30-33)
zea/func/ultrasound.py (2)
zea/data/convert/verasonics.py (4)
  • demodulation_frequency (732-742)
  • sampling_frequency (414-435)
  • n_ax (522-531)
  • tgc_gain_curve (917-941)
zea/scan.py (2)
  • demodulation_frequency (597-602)
  • tgc_gain_curve (730-735)
zea/ops/ultrasound.py (3)
zea/func/ultrasound.py (3)
  • get_band_pass_filter (195-246)
  • demodulate (409-457)
  • upmix (137-186)
zea/scan.py (2)
  • transmit_origins (679-686)
  • demodulation_frequency (597-602)
zea/utils.py (1)
  • canonicalize_axis (15-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: image
🔇 Additional comments (99)
zea/tools/selection_tool.py (1)

659-694: LGTM!

The **kwargs passthrough to the plotting helpers is a clean approach for extending customization options. The changes are backward compatible, and the docstring updates accurately describe the expected array shapes and function behavior.

One minor note: the alpha=0.5 default for the "shape" selector (line 693) is placed before **kwargs, which correctly allows callers to override it if needed. This asymmetry with the "rectangle" branch (no default alpha) is preserved from before, maintaining existing behavior.

zea/ops/tensor.py (1)

137-142: LGTM!

The valid_keys property correctly expands the set of acceptable input keys (minval, maxval) only when input_range is None, which aligns with the conditional logic in call (lines 161-163). This follows the established pattern from other operations like ultrasound.py.

zea/ops/pipeline.py (2)

215-218: LGTM!

The refactor from two append() calls to list expansion (+=) is cleaner and consistent with the pattern used for display ops at line 230.


1199-1218: LGTM!

The removal of the unused grid=None parameter simplifies the API. The method only uses kwargs[self.key] to retrieve data, so the explicit grid parameter was unnecessary. The docstring updates to prod(grid.shape) provide clearer shape documentation.

zea/internal/device.py (2)

8-9: LGTM!

The top-level keras import is appropriate for the backend check on line 274 and consistent with existing usage patterns in the codebase.


274-277: Correct approach for forcing JAX to CPU.

The dynamic import of jax and use of jax.config.update("jax_platforms", "cpu") is the recommended modern API for constraining JAX to CPU execution. The placement in _cpu_case() ensures this configuration happens before any JAX operations.

Note: JAX platform configuration must occur before JAX initialization. If any code imports and uses JAX before get_device() is called, the config update may be silently ignored. The current placement handles the typical usage flow correctly.

zea/doppler.py (1)

23-23: LGTM!

The docstring update correctly clarifies that center_frequency refers to the transmit center frequency, aligning with the broader terminology standardization in this PR.

zea/internal/config/parameters.py (1)

54-54: LGTM!

The parameter description update correctly reflects the terminology change where center_frequency now refers to the transmit pulse frequency rather than the transducer frequency.

zea/internal/parameters.py (3)

168-171: LGTM - Good defensive validation.

The early validation of dependencies during __init__ catches typos and misconfigured dependency declarations at construction time rather than at runtime when the property is accessed. This improves developer experience by failing fast with a clear error message.


378-387: Well-designed validation with clear error messaging.

The method correctly validates that all declared dependencies resolve to either leaf parameters (VALID_PARAMS) or other computed properties. The error message is actionable, identifying both the invalid dependency and the property that declared it.


461-464: LGTM!

Clean utility method that complements get_properties() by filtering to only those with declared dependencies.

zea/data/file.py (4)

303-306: LGTM! Improved warning message clarifies center_frequency semantics.

The updated warning message now explicitly mentions that the wavelength conversion assumes center_frequency is the probe center frequency, which may not always be true (especially with harmonic imaging where transmit and demodulation frequencies differ). This gives users important context about potential conversion inaccuracies.


803-804: LGTM! New validation for transmit_origins key.

The validation correctly expects transmit_origins to have shape (n_tx, 3), which aligns with the per-transmit origin support being added across the beamforming pipeline.


806-821: LGTM! Added demodulation_frequency to scalar validation list.

The addition of demodulation_frequency to the list of keys expected to have an empty shape () is consistent with the harmonic imaging support being introduced in this PR.


825-825: Good change: Demoted unknown key message from warning to debug.

Changing log.warning to log.debug for keys without defined validation is appropriate. Unknown keys aren't necessarily errors—they may be legitimate new fields or optional parameters. This reduces noise in logs while still preserving the information for debugging purposes.

zea/beamform/pfield.py (4)

58-62: LGTM! Docstring clarifications.

The updates to specify "transmit pulse" for center_frequency and clarify the shape of tx_apodizations improve documentation clarity.


359-363: LGTM! Correct refactor of phase calculation.

The change from wavenumber-based to angular-frequency-based phase calculation is mathematically sound:

  • Previously: phase involved kw (wavenumber)
  • Now: angular_frequency * delays_tx computes phase directly from frequency and time delay

This is correct because phase = ωt = 2πft, where delays_tx is a time value. The sound_speed parameter was removed since it's not needed for this time-domain delay calculation.


264-273: LGTM! Updated call site reflects refactored signature.

The call to _pfield_freq_loop correctly omits the sound_speed argument, consistent with the updated function signature.


366-408: LGTM! Internal helper signature updated consistently.

The _pfield_freq_loop function signature has been updated to remove sound_speed, and all internal calls to _pfield_freq_step are consistent with this change.

zea/simulator.py (3)

79-79: LGTM! Clarified docstring for center_frequency.

The updated docstring now explicitly states this is the "center frequency of the transmit pulse," which provides clarity for harmonic imaging scenarios where transmit and demodulation frequencies may differ.


302-302: LGTM! Consistent docstring update for get_pulse_spectrum_fn.

The docstring update aligns with the broader clarification of frequency semantics in this PR.


320-335: No evidence of a parameter rename from center_frequency to probe_center_frequency. The function has no internal usages in the codebase, and there is no changelog, deprecation warning, or git history indicating this change occurred. The parameter name probe_center_frequency disambiguates probe center frequency from the widely-used transmit center_frequency throughout the codebase, which is a valid design choice but cannot be confirmed as a breaking change without evidence of prior usage with a different parameter name.

Likely an incorrect or invalid review comment.

zea/beamform/delays.py (4)

67-72: LGTM! Docstring accurately reflects new per-transmit parameters.

The updated docstrings correctly document that focus_distances is now an array of shape (n_tx,) and clarify the per-transmit nature of the angles.


88-90: LGTM! Appropriate assertion for new parameter shape.

The assertion validates that focus_distances has the expected shape (n_tx,), providing a clear error message if callers pass incorrectly shaped data.


113-118: LGTM! Correct broadcasting for per-transmit focus distances.

The comment accurately describes the shape (n_tx, 1, 3) after expansion, and the virtual_sources calculation at line 118 correctly broadcasts focus_distances[:, None, None] to compute per-transmit virtual source positions.


54-61: The breaking change from focus_distance (scalar) to focus_distances (array of shape (n_tx,)) has already been implemented in the function signature, docstring, and all callers. All usages in zea/internal/dummy_scan.py correctly pass arrays via np.ones(n_tx) * value. No action is needed.

zea/beamform/pixelgrid.py (2)

126-126: LGTM!

The docstring update clarifies that rlims are specified with respect to each ray origin, improving documentation for the radial grid.


150-189: LGTM!

The distance_to_apex integration is well-implemented:

  • The radial limits are correctly extended to account for the apex offset.
  • The origin is properly positioned at z = -distance_to_apex.
  • The explicit trim at line 189 is a good defensive measure against floating-point rounding in np.arange.
zea/data/data_format.py (5)

201-206: LGTM!

The new demodulation_frequency and transmit_origins parameters are properly added to the _write_datasets function signature.


271-273: LGTM!

Including transmit_origins in the n_tx inference fallback is correct since its shape is (n_tx, 3).


397-411: LGTM!

The center_frequency description update and new demodulation_frequency dataset are correctly implemented with appropriate descriptions and units.


458-468: LGTM!

The transmit_origins dataset is properly added with a clear description of its shape and meaning.


699-706: LGTM!

The new parameters are correctly propagated through the data_and_parameters dictionary.

zea/beamform/beamformer.py (8)

7-7: LGTM!

The import is updated to use compute_lens_corrected_travel_times which aligns with the refactored lens correction module.


65-65: LGTM!

The transmit_origins parameter is correctly added to the tof_correction function signature.


125-143: LGTM!

The calculate_delays call is properly updated with all the new parameters including transmit_origins and lens correction parameters.


272-277: LGTM!

Good addition of input shape validation for the delay calculation inputs.


278-297: LGTM!

The conditional lens correction path is well-implemented with appropriate assertions for required parameters when lens correction is enabled.


299-321: LGTM!

The vmap call to transmit_delays is correctly configured with appropriate in_axes to iterate over transmit-specific parameters and out_axes=1 to produce the expected (n_pix, n_tx) shape.


467-569: LGTM!

The transmit_delays function is well-designed:

  • The focal geometry calculation from focus_distance, polar_angle, azimuth_angle, and transmit_origin is correct.
  • The projection-based logic to determine before/after focus handles both focused and diverging waves correctly.
  • The nan handling at line 535 addresses edge cases from inf * 0 in plane wave scenarios.
  • The min/max selection based on is_before_focus correctly implements focusing behavior.

449-464: LGTM!

Minor docstring improvement clarifying that inputs should be in SI units.

docs/source/parameters.rst (1)

143-143: LGTM!

The documentation update correctly reflects that center_frequency now refers to the transmit pulse center frequency rather than the transducer center frequency, aligning with the broader data format changes in this PR.

zea/data/convert/__main__.py (1)

96-103: LGTM!

Removing the hardcoded default for --frames and updating the help text is a good change that allows convert.yaml to take precedence while maintaining backward compatibility with the "all" default when no configuration is provided. The implementation properly handles None by checking the convert.yaml configuration first and defaulting to "all" if not specified (see read_verasonics_file method at lines 1059-1060).

docs/source/data-acquisition.rst (2)

50-50: LGTM! Documentation accurately reflects new data format fields.

The addition of demodulation_frequency and the updated description for center_frequency (now referring to transmit waveform rather than transducer) aligns well with the PR objectives for harmonic imaging support.

Also applies to: 104-106


114-114: LGTM! Improved focus_distances description.

The updated description now clearly explains that the distance is measured from the origin point on the transducer to where the beam comes to focus, which is more precise than before.

zea/ops/__init__.py (1)

171-171: LGTM! New BandPassFilter export.

The BandPassFilter class is correctly added to both the imports and __all__, maintaining consistency in the public API surface.

tests/test_tensor_ops.py (2)

745-766: LGTM! Comprehensive test coverage for band-pass filter.

Good parameterization covering standard cases, edge cases (close to Nyquist), even/odd tap counts, and minimal configurations. The comparison against scipy.signal.firwin provides solid validation of the implementation.


769-792: Test expectations are correct and match implementation.

All test cases appropriately expect ValueError to be raised. The implementation validates that f1 <= 0 triggers an error (confirming the f1 = 0 case), and the normalized cutoff frequencies meet the f2 >= 1 and f1 >= f2 boundary conditions as expected by the test cases. The tests correctly mirror scipy's behavior for these invalid input scenarios.

tests/test_operations.py (2)

21-21: LGTM!

Import correctly added for the new make_tgc_curve function.


581-613: LGTM! Well-designed test for TGC curve generation.

The test comprehensively validates the make_tgc_curve function with realistic ultrasound parameters and checks all essential properties: shape, dtype, monotonicity, initial value, and positivity.

zea/display.py (2)

39-97: LGTM! distance_to_apex parameter correctly integrated.

The parameter is properly added to the function signature, used to offset z_vec, and included in the returned parameters dictionary for downstream use.


100-165: LGTM! scan_convert_2d correctly propagates distance_to_apex.

The parameter is well-documented and correctly passed through to compute_scan_convert_2d_coordinates.

tests/test_transmit_schemes.py (4)

15-18: LGTM!

The explicit named unpacking of extent as (xmin, xmax, zmax, zmin) improves readability and correctly matches the format returned by Scan.extent (per zea/scan.py lines 421-432).


28-29: LGTM!

Consistent with the unpacking pattern used in _get_flatgrid. The width and height calculations are correct.


142-145: LGTM!

The rename from ultrasound_probe/ultrasound_scan to probe/scan is consistent with the broader API cleanup. Using scan.extent directly instead of constructing it manually is cleaner and aligns with the updated Scan.extent property.

Also applies to: 164-168


179-183: LGTM!

The test_polar_grid function correctly applies the same refactoring pattern, using the renamed variables and scan.extent for the location test.

Also applies to: 187-188, 206-209

tests/data/test_conversion_scripts.py (4)

20-21: LGTM!

Import of VerasonicsFile is needed for the new verification logic.


46-47: LGTM!

Signature update correctly propagates the src path needed for Verasonics validation.


114-137: LGTM!

The function signature and dispatch logic are correctly updated to pass src to the Verasonics verification path.


467-474: LGTM!

The test data setup correctly creates a convert.yaml file specifying the first_frame parameter, which aligns with the new YAML-based conversion configuration feature.

zea/func/ultrasound.py (7)

2-2: LGTM!

Import updated to scipy.signal which is used throughout the module.


11-14: LGTM!

The rename from center_frequency to demodulation_frequency in demodulate_not_jitable is consistent with the PR's semantic clarification that center_frequency now refers to transmit center frequency while demodulation_frequency is used for baseband processing.

Also applies to: 32-33, 69-70, 82-82, 86-87, 94-94, 102-102, 116-119


137-137: LGTM!

The upmix function signature correctly uses demodulation_frequency to match the updated demodulation semantics.

Also applies to: 145-145, 179-180


189-193: LGTM!

The _sinc helper correctly implements the normalized sinc function. Using 1.0e-20 for the zero case avoids division by zero while maintaining numerical accuracy.


409-409: LGTM!

The demodulate function correctly uses demodulation_frequency in the phasor computation for shifting the signal to baseband.

Also applies to: 419-419, 439-439, 444-451


551-581: LGTM!

The make_tgc_curve function correctly implements time-gain compensation:

  • Computes round-trip distance from time and sound speed
  • Applies two-way attenuation model (transmit + receive)
  • Converts from dB to linear scale for gain application

The formula 2 * attenuation_coef * dist_cm * (center_frequency * 1e-6) correctly accounts for frequency-dependent attenuation in dB/cm/MHz units.


195-246: Implementation verified to match scipy.signal.firwin behavior.

The manual FIR filter implementation correctly implements the window method: difference of sinc functions for band-pass, Hamming window, and unity-gain scaling at the passband center. Verification confirms this matches scipy.signal.firwin output within tight tolerances (rtol=1e-5, atol=1e-5), as validated by tests that compare against scipy directly. Edge cases and invalid inputs are properly handled and tested.

zea/ops/ultrasound.py (6)

21-21: LGTM!

Import added for the new BandPassFilter class.


126-126: LGTM!

The TOFCorrection operation correctly includes transmit_origins as both a parameter and passes it through to tof_kwargs for time-of-flight calculations.

Also applies to: 150-150, 175-175


339-339: LGTM!

The Demodulate operation correctly:

  • Uses demodulation_frequency in the call signature
  • Outputs center_frequency: 0.0 since after demodulation the signal is at baseband
  • Updates n_ch: 2 for the two-channel IQ output

Also applies to: 344-344, 350-359


378-379: LGTM!

The FirFilter class improvements are solid:

  • Docstring clarifies axis cannot be batch or complex channel dimensions
  • _check_axis validates against batch dimension
  • Runtime check in call validates axis against complex channels when complex_channels=True

Also applies to: 394-396, 408-416, 423-423


431-473: LGTM!

The rename from LowPassFilter to LowPassFilterIQ is appropriate since it enforces complex_channels=True and is specifically designed for IQ data filtering.


869-869: LGTM!

The UpMix operation correctly uses demodulation_frequency in both the signature and the call to upmix.

Also applies to: 878-878

zea/scan.py (11)

70-71: LGTM!

Documentation updates correctly reflect:

  • The probe_geometry parameter in the example
  • center_frequency now refers to transmit center frequency
  • New focus_distances and transmit_origins parameters
  • New distance_to_apex parameter for polar grids

Also applies to: 112-113, 132-135, 177-179


197-197: LGTM!

VALID_PARAMS correctly updated:

  • dynamic_range no longer has a default (removed)
  • transmit_origins added as np.ndarray
  • phi_range default set to None
  • distance_to_apex added as float

Also applies to: 220-220, 230-230, 234-234


276-313: LGTM!

The grid() method correctly:

  • Adds distance_to_apex as a dependency
  • Passes distance_to_apex to polar_pixel_grid for polar grids
  • Maintains backward compatibility for cartesian grids

421-433: LGTM!

The extent() method correctly adjusts zlims for polar grids by adding distance_to_apex to the minimum z value, aligning the coordinate system with the apex-based polar grid.


518-549: LGTM!

The new set_transmits modes ("focused", "unfocused", "plane") are well-implemented:

  • "focused": selects transmits where focus_distances > 0
  • "unfocused": selects transmits where focus_distances < 0
  • "plane": selects transmits where focus_distances == 0 or == np.inf

Error handling is appropriate when no matching transmits are found.


596-602: LGTM!

The demodulation_frequency property correctly falls back to center_frequency when not explicitly provided, maintaining backward compatibility.


625-632: LGTM!

The azimuth_angles property correctly falls back to zeros with shape (n_tx,) when not provided.


647-656: LGTM!

The per-transmit properties (t0_delays, tx_apodizations, focus_distances, transmit_origins, initial_times, tx_waveform_indices) correctly:

  • Add n_tx as a dependency for cache invalidation
  • Use self.n_tx for default array shapes
  • Apply selected_transmits filtering when returning values

Also applies to: 658-666, 668-676, 678-686, 688-694, 737-742


778-784: LGTM!

The rho_range property correctly incorporates distance_to_apex in the default range calculation for scan conversion.


796-807: LGTM!

The coordinates_2d property correctly passes distance_to_apex to compute_scan_convert_2d_coordinates.


857-868: LGTM!

The frames_per_second property correctly accesses the raw _params dictionary directly to avoid triggering the selected_transmits filtering, since FPS should be calculated based on all transmits in the acquisition.

zea/data/convert/verasonics.py (15)

173-173: LGTM!

The .item() call correctly extracts the scalar reference from a potential 0-d array, ensuring consistent behavior with the rest of the codebase.


212-215: LGTM!

Clean utility method that consolidates a common pattern for extracting integer values from h5py datasets.


339-370: LGTM!

Good use of np.stack() for consistent array construction and proper handling of the time_to_next_acq being None in accumulate mode. The frame indexing logic is sound.


620-658: LGTM!

The addition of buffer_index and first_frame_idx parameters provides good flexibility for multi-buffer scenarios. The fallback logic between first_frame_idx override and get_raw_data_order is correct.


766-766: LGTM!

Using np.stack is semantically clearer for combining arrays along a new axis.


798-810: LGTM!

Consistent use of np.stack and proper unit conversion from wavelengths to meters.


812-832: LGTM!

Clean implementation following the established pattern for reading TX-related data. Proper handling of both standard and event-based structures, with correct unit conversion.


874-897: LGTM with minor note.

Good improvement to validate that all receive events use the same bandwidth mode. The assert statements here follow the existing pattern in the file, though explicit exceptions would be more robust (as noted for demodulation_frequency).


943-973: LGTM!

Clean refactoring with proper buffer index support and use of the new cast_to_integer helper for cleaner code.


546-585: LGTM!

Well-designed configuration loading with proper schema validation and sensible fallback behavior. The filename matching logic (with and without extension) is a nice touch for user convenience.


1028-1081: LGTM!

Good integration of the convert.yaml configuration with proper parameter propagation. The new data fields (center_frequency, demodulation_frequency, transmit_origins) are correctly added to the output dictionary.


1226-1234: Verify: frames parameter not passed in event-based conversion.

When TX_Agent is present (event structure mode), read_verasonics_file is called without the frames parameter. This means convert.yaml frame configurations won't apply to event-based conversions. If this is intentional (e.g., event-based files have different frame semantics), consider adding a comment explaining why. Otherwise, consider passing frames for consistency.


1408-1414: LGTM!

Correctly changed to pass frames=args.frames directly (without or "all" fallback), allowing the None value to propagate and enabling convert.yaml configuration to take effect when no command-line frames are specified.


512-519: LGTM!

Good improvement to properly handle multi-element datasets using dereference_all with np.concatenate.


697-729: Confirm that center frequency extraction covers all waveform types used in your data.

The implementation currently only supports "parametric" waveform types for center frequency extraction and raises a ValueError for other types. If your Verasonics data uses only parametric waveforms, no action is needed. If you encounter a different waveform type, the error message will clearly identify it and you can extend the read_center_frequency method to handle it.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zea/data/convert/verasonics.py (1)

1226-1234: frames parameter is silently ignored for event-structured files.

When the file contains TX_Agent (event structure), read_verasonics_file is called without passing the frames parameter. This means the user-provided frames argument to to_zea is silently ignored for event-structured data, while it is respected for standard conversions. Either pass the frames parameter in the event-structured case or document this limitation explicitly and raise a warning when frames is specified with TX_Agent data.

🤖 Fix all issues with AI agents
In `@zea/data/convert/verasonics.py`:
- Around line 1163-1175: The code handling the input variable frames in the
Verasonics conversion logic (the branch that checks isinstance(frames, str))
fails for single-number strings like "5" allowed by _CONVERT_YAML_SCHEMA; update
the branch in the function that builds frame_indices so that if frames is a
string and not "all" you check for "-" and split into start/end when present,
otherwise treat the string as a single integer index (convert to int and create
a one-element np.arange/np.asarray accordingly), preserving sorting and type
behavior consistent with the else branch that uses np.asarray(frames).

In `@zea/scan.py`:
- Around line 661-669: The tx_apodizations method is missing n_el in its cache
dependencies: update the `@cache_with_dependencies` decorator on tx_apodizations
to include "n_el" alongside "selected_transmits" and "n_tx" so the cache
invalidates when element count changes; mirror the dependency list used by
t0_delays to ensure the default np.ones((self.n_tx, self.n_el)) path is
correctly re-evaluated when n_el changes.
- Around line 521-552: The selection branches for "focused", "unfocused", and
"plane" assign a numpy array (idx) to self._selected_transmits causing a type
mismatch with other branches that use Python lists; change each assignment to
convert idx to a list (e.g., list(idx)) before setting self._selected_transmits,
then call self._invalidate("selected_transmits") and return self as already done
so downstream code always sees a list.
♻️ Duplicate comments (1)
zea/data/convert/verasonics.py (1)

181-197: Docstring return type still inconsistent with implementation.

The docstring states the return type is list (line 189), which now matches the implementation. However, the past review suggested either updating the return to np.ndarray or updating the docstring. The docstring has been updated to say list, so this is resolved.

🧹 Nitpick comments (2)
zea/scan.py (1)

821-831: Pre-existing TODO: 3D coordinate computation missing grid_size_y.

The TODO at line 823 notes that grid_size_y is not used, which may produce incorrect 3D scan conversion results.

Would you like me to open an issue to track fixing the 3D coordinate computation?

zea/data/convert/verasonics.py (1)

573-577: Consider using yaml.SafeLoader for config parsing.

While FullLoader is safer than the default loader, SafeLoader is the recommended choice when you don't need Python object deserialization. Since this config only contains simple types (strings, integers, lists), SafeLoader would suffice and provide stronger security guarantees.

♻️ Suggested change
-            data = yaml.load(file, Loader=yaml.FullLoader)
+            data = yaml.safe_load(file)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4df32 and a2106af.

📒 Files selected for processing (3)
  • zea/data/convert/verasonics.py
  • zea/data/data_format.py
  • zea/scan.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-22T20:59:30.246Z
Learnt from: tristan-deep
Repo: tue-bmd/zea PR: 136
File: zea/display.py:343-351
Timestamp: 2025-10-22T20:59:30.246Z
Learning: In zea/display.py map_coordinates, for order > 1 it is acceptable to downcast to float32 for SciPy compatibility, then cast results back to the original dtype; explicit maintenance of float64 throughout is not required.

Applied to files:

  • zea/scan.py
🧬 Code graph analysis (1)
zea/data/data_format.py (1)
zea/scan.py (4)
  • demodulation_frequency (600-605)
  • focus_distances (672-679)
  • transmit_origins (682-689)
  • n_tx (468-470)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: image
🔇 Additional comments (23)
zea/scan.py (10)

197-234: LGTM on VALID_PARAMS updates.

The new parameters (demodulation_frequency, transmit_origins, distance_to_apex) are correctly typed, and the removal of the default for dynamic_range along with the explicit None default for phi_range are reasonable changes.


252-263: LGTM on aperture_size implementation.

The method correctly computes the aperture dimensions from probe geometry coordinates. The dependency on probe_geometry is appropriate, and returning None when unavailable is a safe fallback.


265-277: Verify the semantic correctness of returning 0.0 for near-zero angles.

The division-by-zero guard is good, but returning 0.0 when tan(max_angle) ≈ 0 may be semantically incorrect. For near-axial transmits (small angles), the apex geometrically recedes to infinity—not zero. Returning 0.0 places the apex at the transducer surface, which could produce incorrect polar grid geometry downstream.

Consider whether returning a large finite value or handling this case differently aligns with the expected behavior for nearly-axial beam configurations.


424-436: Verify polar extent adjustment logic.

For polar grids, only zlims[0] is adjusted by distance_to_apex, leaving zlims[1] unchanged. If distance_to_apex represents a coordinate offset for the polar grid origin, both bounds might need adjustment for consistent visualization. Please verify this matches the intended coordinate system semantics.


599-605: LGTM on demodulation_frequency fallback.

Defaulting to center_frequency when demodulation_frequency is not provided is a sensible choice for harmonic imaging support.


681-689: LGTM on transmit_origins implementation.

The method follows the established pattern for transmit-related properties with correct caching and selection handling.


781-811: LGTM on rho_range and coordinates_2d updates.

The integration of distance_to_apex into both the range calculation and coordinate computation is consistent and correctly threaded through the dependencies.


860-871: LGTM on frames_per_second implementation.

Using self._params.get("time_to_next_transmit") directly (bypassing transmit selection) correctly implements the documented behavior where FPS is calculated based on all transmits regardless of selection.


701-720: LGTM on n_waveforms and t_peak dependency updates.

The dependencies correctly reflect the parameters used in each method's computation.


291-302: LGTM on polar grid integration.

The distance_to_apex is correctly threaded through to polar_pixel_grid as the 5th parameter, matching its function signature. All required dependencies (polar_limits, distance_to_apex) are properly declared in __slots__.

zea/data/data_format.py (6)

201-206: LGTM!

The new demodulation_frequency and transmit_origins parameters are correctly added to the _write_datasets function signature with appropriate default values.


270-273: LGTM!

Good addition of transmit_origins to the fallback sources for inferring n_tx. This ensures datasets without raw data can still determine the number of transmits from the transmit origins array.


397-411: LGTM!

The center_frequency description is appropriately updated to clarify it refers to the transmit pulse frequency, and the new demodulation_frequency dataset is added with a clear description.


446-468: LGTM!

The focus_distances description now correctly documents the shape as (n_tx,) and clarifies the meaning. The new transmit_origins dataset is added with proper shape (n_tx, 3) and clear description of what it represents.


636-646: LGTM!

Docstrings are updated consistently with the dataset descriptions. The focus_distances shape is now correctly documented as (n_tx,), which aligns with the relevant code snippets from zea/scan.py.


699-706: LGTM!

Both new fields are correctly propagated through the data_and_parameters dictionary for downstream processing.

zea/data/convert/verasonics.py (7)

123-138: LGTM!

The schema is well-structured and uses appropriate validators. The regex ^\d+(-\d+)?$ correctly matches both single numbers (e.g., "5") and ranges (e.g., "30-99").


593-617: LGTM!

The get_indices_to_reorder and get_raw_data_order methods are correctly implemented. The circular buffer reordering logic properly uses first_frame and n_frames to compute the correct frame ordering.


692-729: LGTM!

Good separation between probe_center_frequency (probe's native frequency) and read_center_frequency/read_center_frequencies (transmit pulse frequency). The validation at lines 724-728 appropriately handles the case where multiple center frequencies exist.


812-832: LGTM!

The read_transmit_origins method follows the same pattern as other TX data readers (read_focus_distances, read_beamsteering_angles), correctly handling both regular and event-structured data with appropriate unit conversion from wavelengths to meters.


874-897: LGTM!

Clean refactoring of bandwidth_percent to use dereference_all with the decode_string function. The validation ensures consistency across all receive events.


1056-1081: LGTM!

Good integration of load_convert_config for frame selection configuration. The new fields (center_frequency, demodulation_frequency, transmit_origins) are correctly read and added to the data dictionary.


1408-1414: LGTM!

The frames argument is correctly propagated to _zea_from_verasonics_workspace for both single-file and batch conversions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zea/data/convert/verasonics.py (1)

1236-1268: Pass frames parameter to read_verasonics_file in the TX_Agent branch.

The frames parameter passed to to_zea() is not forwarded to read_verasonics_file() in the multi-event (TX_Agent) branch (lines 1251-1255), causing it to be ignored. The single-event branch correctly passes frames=frames (line 1255 in the else clause). When users explicitly call to_zea(frames="0-5"), this will be respected for single-event data but ignored for multi-event data. Pass frames=frames to maintain consistent behavior across both branches.

🤖 Fix all issues with AI agents
In `@zea/scan.py`:
- Around line 265-277: In distance_to_apex, when max_angle is nearly zero the
geometric apex is at infinity so replace the currently returned 0.0 in the
np.isclose(t, 0.0) branch with np.inf (and add a short comment explaining this
choice) so downstream polar grid calculations treat axial transmits correctly;
ensure you import or reference numpy as np and keep the rest of the
aperture_size-based computation unchanged (function name: distance_to_apex,
cached via cache_with_dependencies).
🧹 Nitpick comments (5)
zea/scan.py (3)

821-831: TODO indicates broken 3D coordinate computation.

The TODO comment notes that grid_size_y is missing from the 3D coordinate computation. The current implementation passes only (grid_size_z, grid_size_x) to compute_scan_convert_3d_coordinates, which is likely incorrect for true 3D scan conversion.

Would you like me to help address this by updating the implementation to include grid_size_y, or should I open an issue to track this separately?


839-873: Potential inconsistency between pulse_repetition_frequency and frames_per_second.

frames_per_second (line 860) intentionally uses raw _params.get("time_to_next_transmit") to compute fps from all transmits, as documented. However, pulse_repetition_frequency (line 846) uses self.time_to_next_transmit, which is filtered by selected_transmits.

If the intent is for both to compute from all transmits regardless of selection, pulse_repetition_frequency should also use _params.get. If they should differ (PRF based on selected, FPS based on all), consider adding a clarifying comment to pulse_repetition_frequency.


813-831: Consider adding distance_to_apex dependency to coordinates_3d.

coordinates_2d (line 800) includes distance_to_apex in its cache dependencies and passes it to compute_scan_convert_2d_coordinates. However, coordinates_3d does not include this dependency and doesn't pass distance_to_apex to compute_scan_convert_3d_coordinates.

If 3D scan conversion should also account for distance_to_apex, this would need to be added. This may be related to the TODO comment about the broken 3D implementation.

zea/data/convert/verasonics.py (2)

546-585: Consider adding error handling for malformed YAML.

If convert.yaml exists but contains invalid YAML syntax, yaml.load will raise an exception that propagates up without a user-friendly message. Consider wrapping the YAML loading in a try/except to provide clearer error messaging.

♻️ Suggested improvement
         if config_file.exists():
             log.info(f"Found convert config file: {log.yellow(config_file)}")
-            with open(config_file, "r", encoding="utf-8") as file:
-                data = yaml.load(file, Loader=yaml.FullLoader)
+            try:
+                with open(config_file, "r", encoding="utf-8") as file:
+                    data = yaml.load(file, Loader=yaml.FullLoader)
+            except yaml.YAMLError as e:
+                raise ValueError(
+                    f"Failed to parse convert.yaml: {config_file}\n{e}"
+                ) from e

731-742: Consider using explicit exception instead of assert for validation.

The assert statement on line 737 could be disabled with Python's -O flag in production. For critical validation that should always run, prefer an explicit if/raise pattern.

♻️ Suggested improvement
         demod_freq = self.dereference_all(self["Receive"]["demodFrequency"])
         demod_freq = np.unique(demod_freq)
-        assert demod_freq.size == 1, (
+        if demod_freq.size != 1:
+            raise ValueError(
             f"Multiple demodulation frequencies found in file: {demod_freq}. "
             "We do not support this case."
-        )
+            )
 
         return demod_freq.item() * 1e6
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2106af and 8c9efe4.

📒 Files selected for processing (2)
  • zea/data/convert/verasonics.py
  • zea/scan.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-22T20:59:30.246Z
Learnt from: tristan-deep
Repo: tue-bmd/zea PR: 136
File: zea/display.py:343-351
Timestamp: 2025-10-22T20:59:30.246Z
Learning: In zea/display.py map_coordinates, for order > 1 it is acceptable to downcast to float32 for SciPy compatibility, then cast results back to the original dtype; explicit maintenance of float64 throughout is not required.

Applied to files:

  • zea/scan.py
🧬 Code graph analysis (1)
zea/scan.py (5)
zea/internal/parameters.py (2)
  • cache_with_dependencies (22-47)
  • _invalidate (408-414)
zea/data/convert/verasonics.py (1)
  • probe_geometry (228-242)
tests/test_fnumber_mask.py (1)
  • probe_geometry (17-21)
zea/beamform/pixelgrid.py (1)
  • polar_pixel_grid (150-189)
zea/display.py (1)
  • compute_scan_convert_2d_coordinates (39-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: image
🔇 Additional comments (20)
zea/scan.py (8)

182-235: LGTM - VALID_PARAMS updates are consistent with new features.

The parameter declarations correctly add transmit_origins, demodulation_frequency, and distance_to_apex with appropriate types. The phi_range default of None aligns with its optional 3D usage.


279-316: LGTM - Grid property correctly integrates distance_to_apex.

The cache dependencies are properly updated, and the polar_pixel_grid call correctly passes distance_to_apex matching the function signature from zea/beamform/pixelgrid.py.


424-436: Verify the polar grid extent adjustment logic.

The adjustment zlims = (zlims[0] + self.distance_to_apex, zlims[1]) increases zmin for polar grids. This effectively shifts the near-field boundary away from z=0. Please confirm this is the intended behavior for visualizing polar grids - typically, distance_to_apex is subtracted from z-coordinates to account for the virtual apex being behind the transducer.


521-552: LGTM - New transmit selection modes are well-implemented.

The type inconsistency from the previous review has been addressed with .tolist() conversions. The selection logic correctly distinguishes between focused (> 0), unfocused (< 0), and plane wave (== 0 or isinf) transmits with appropriate validation.


599-605: LGTM - Simplified demodulation_frequency dependency.

The cache now correctly depends only on center_frequency since the fallback value is self.center_frequency. The removal of n_ch dependency is appropriate.


661-669: LGTM - Cache dependencies for tx_apodizations now include n_el.

The previous review's concern about missing n_el dependency has been addressed. The default return np.ones((self.n_tx, self.n_el)) is now properly invalidated when n_el changes.


681-689: LGTM - New transmit_origins property follows established patterns.

The implementation correctly mirrors other per-transmit properties with proper cache dependencies and selected_transmits indexing.


701-720: LGTM - n_waveforms and t_peak dependency updates are correct.

The n_waveforms property correctly depends on both waveform arrays, and t_peak appropriately depends on center_frequency and n_waveforms for its default computation.

zea/data/convert/verasonics.py (12)

123-138: YAML schema definition looks good.

The schema correctly validates the convert.yaml structure with appropriate type constraints for first_frame, frames, and transmits.


181-197: Docstring return type is now consistent with implementation.

The docstring correctly documents that the method returns a list. The previous review concern has been addressed.


593-617: Frame reordering logic is correct.

The get_indices_to_reorder method correctly takes first_frame and n_frames parameters, and get_raw_data_order properly computes n_frames from buffer_index before passing it. The previous review concern about incorrect argument passing appears to have been addressed.


1150-1178: Single-number frame strings are now handled correctly.

The _parse_frames_argument method now properly handles single-number strings (e.g., "5") by attempting integer conversion when no - is present. The previous review concern has been addressed.


619-689: Raw data reading with frame reordering is well-implemented.

The first_frame_idx parameter provides flexibility for explicit frame ordering when needed (e.g., from convert.yaml), while maintaining backward compatibility with automatic detection via get_raw_data_order.


697-729: Center frequency extraction is correct but limited to parametric waveforms.

The implementation correctly extracts center frequency from TW parameters and validates uniqueness. Note that only "parametric" waveform type is currently supported; other types will raise a descriptive error.


812-832: Transmit origins extraction follows established patterns.

The implementation correctly reads transmit origins, handles both regular TX and TX_Agent structures, and properly converts from wavelengths to meters.


1028-1148: read_verasonics_file updates are well-structured.

The changes correctly implement the config-driven frame handling with appropriate precedence (explicit parameter > config > default "all"). New data fields (demodulation_frequency, transmit_origins, computed center_frequency) are properly integrated into the output dictionary.


1180-1207: Frame index handling is robust with good boundary checking.

The implementation properly validates frame indices and gracefully handles out-of-bounds values. Minor note: np.unique already returns a sorted array, so the explicit sort() call on line 1196 is redundant but doesn't cause issues.


874-897: Bandwidth extraction logic is correct for supported modes.

The implementation correctly extracts bandwidth percentage from sample mode strings. The assertions follow the same pattern as elsewhere in the file.


1429-1435: CLI frame handling correctly propagates to conversion.

The frames argument is properly passed through to _zea_from_verasonics_workspace in both single-file and batch directory processing paths.


250-371: Transmit event reading properly handles buffer and frame indices.

The updates correctly propagate buffer_index to frame index computation and apply frame filtering to time_to_next_acq. The logic flow handles accumulate mode filtering appropriately.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@tristan-deep
Copy link
Collaborator

@wesselvannierop what was your experience with adding a parameter to the zea dataformat (e.g. demodulation_frequency? Were there enough checks in place already to catch potential bugs with adding such parameter? Or checks that prevented you from not documenting it in the necessary places (e.g. here)?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zea/display.py (1)

310-346: Missing distance_to_apex propagation in scan_convert wrapper.

The scan_convert wrapper doesn't expose the new distance_to_apex parameter, so users calling scan_convert instead of scan_convert_2d directly cannot utilize this feature. Consider adding the parameter to maintain API consistency.

Suggested fix
 def scan_convert(
     image,
     rho_range: Tuple[float, float] = None,
     theta_range: Tuple[float, float] = None,
     phi_range: Tuple[float, float] = None,
     resolution: Union[float, None] = None,
     coordinates: Union[None, np.ndarray] = None,
     fill_value: float = 0.0,
     order: int = 1,
     with_batch_dim: bool = False,
+    distance_to_apex: float = 0.0,
 ):
     """Scan convert image based on number of dimensions."""
     if len(image.shape) == 2 + int(with_batch_dim):
         return scan_convert_2d(
             image,
             rho_range,
             theta_range,
             resolution,
             coordinates,
             fill_value,
             order,
+            distance_to_apex,
         )
🧹 Nitpick comments (3)
zea/internal/dummy_scan.py (1)

96-130: Consider adding explicit transmit_origins for consistency.

The _get_planewave_scan and _get_multistatic_scan functions don't explicitly set transmit_origins, relying on the fallback (zeros) in Scan.transmit_origins. While this works, adding explicit transmit_origins=np.zeros((n_tx, 3)) would improve consistency with the other scan types and make the intent clearer.

docs/source/data-acquisition.rst (2)

105-107: Clarify the relationship between center_frequency and demodulation_frequency.

The description for demodulation_frequency refers to "the assumed center frequency of the transmit waveform," which is confusing since center_frequency already represents the transmit frequency. The key distinction—critical for harmonic imaging—is that these frequencies can differ when receiving at harmonic frequencies (e.g., transmit at 5 MHz, receive/demodulate at 10 MHz for 2nd harmonic).

Consider revising the description to clarify when and why these frequencies differ, and optionally mention harmonic imaging as context.

📝 Suggested clarification
   * - ``scan/center_frequency``
-     - Center frequency of the transmit waveform in Hz.
+     - Center frequency of the transmitted waveform in Hz.
   * - ``scan/demodulation_frequency``
-     - The demodulation frequency of the data in Hz. This is the assumed center frequency of the transmit waveform used to demodulate the rf data to iq data.
+     - The demodulation frequency used to convert RF data to IQ data (in Hz). For fundamental imaging, this typically equals center_frequency. For harmonic imaging, this is the harmonic frequency (e.g., 2× center_frequency for 2nd harmonic).

176-190: Consider mentioning the new optional parameters in the example or accompanying text.

The example demonstrates basic usage but doesn't include the newly added demodulation_frequency and transmit_origins parameters. While the minimal example is appropriate, adding a brief note about these optional parameters (especially for harmonic imaging use cases) could help users discover them.

📝 Optional addition

After line 175 (or within the example docstring), you could add:

  >>> # Optionally define a custom dataset element
+ >>> # Note: For harmonic imaging, also specify demodulation_frequency and transmit_origins
  >>> custom_dataset_element = DatasetElement(
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9efe4 and 47e1791.

📒 Files selected for processing (6)
  • docs/source/data-acquisition.rst
  • zea/beamform/delays.py
  • zea/data/file.py
  • zea/data/file_operations.py
  • zea/display.py
  • zea/internal/dummy_scan.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • zea/data/file.py
🧰 Additional context used
🧬 Code graph analysis (3)
zea/data/file_operations.py (2)
zea/scan.py (1)
  • transmit_origins (682-689)
zea/data/file.py (1)
  • scan (324-346)
zea/beamform/delays.py (1)
zea/scan.py (3)
  • transmit_origins (682-689)
  • n_tx (468-470)
  • focus_distances (672-679)
zea/internal/dummy_scan.py (3)
zea/scan.py (4)
  • transmit_origins (682-689)
  • n_tx (468-470)
  • t0_delays (651-659)
  • tx_apodizations (662-669)
zea/beamform/delays.py (1)
  • compute_t0_delays_focused (54-137)
tests/test_ops_infra.py (1)
  • ultrasound_probe (601-603)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: heavy-tests
  • GitHub Check: tests
  • GitHub Check: test-docs-build
🔇 Additional comments (12)
zea/data/file_operations.py (2)

82-82: LGTM! Transmit origins now correctly saved to dataset.

This addition ensures transmit_origins is persisted when saving zea datasets, aligning with the broader PR changes that introduce per-transmitter origin data throughout the pipeline.


257-272: LGTM! Identity check now includes transmit origins.

Adding scan.transmit_origins to the attributes checked ensures that compound_transmits correctly warns when transmit origins differ across transmits, which would make averaging them invalid.

zea/internal/dummy_scan.py (3)

181-211: LGTM! Diverging scan now includes transmit origins.

The transmit_origins is correctly initialized as zeros (center of aperture origin), passed to compute_t0_delays_focused, and attached to the resulting Scan object.


226-256: LGTM! Focused scan correctly propagates transmit origins.

Same pattern as diverging scan - zeros origin is appropriate for focused transmits firing from the aperture center.


271-314: LGTM! Linescan correctly builds per-transmit origins from subaperture centers.

The implementation correctly constructs transmit_origins from the probe geometry at each center element, which accurately represents the shifting transmit aperture in linescan imaging.

zea/beamform/delays.py (2)

54-90: LGTM! API updated to per-transmit origins with proper validation.

The parameter rename from origins to transmit_origins with shape (n_tx, 3) correctly reflects the per-transmit origin data model. The assertions are comprehensive and the docstrings accurately describe the expected shapes.


112-137: LGTM! Virtual source computation correctly broadcasts transmit origins.

The broadcasting logic is correct:

  • transmit_origins[:, None] → shape (n_tx, 1, 3)
  • focus_distances[:, None, None] * v → shape (n_tx, 1, 3)
  • virtual_sources → shape (n_tx, 1, 3)

This correctly computes a virtual source per transmit, then broadcasts against probe_geometry to compute element distances.

zea/display.py (3)

39-46: LGTM!

The new distance_to_apex parameter is added with a sensible default of 0.0, maintaining backward compatibility.


71-74: Verify intended behavior when distance_to_apex is large.

The z-axis range now starts at z_lim[0] + distance_to_apex but still ends at z_lim[1]. This effectively shortens the z range rather than shifting it. If distance_to_apex >= z_lim[1] - z_lim[0], z_vec will be empty, causing downstream errors.

Is this the intended behavior (cropping near-field region), or should the end also be shifted to z_lim[1] + distance_to_apex to preserve the full z extent?


100-165: LGTM!

The distance_to_apex parameter is properly added to the signature, documented in the docstring, and correctly propagated to compute_scan_convert_2d_coordinates.

docs/source/data-acquisition.rst (2)

50-50: LGTM! File structure additions are consistent.

The new fields demodulation_frequency and transmit_origins are correctly added to the file structure overview and match the detailed parameter descriptions below.

Also applies to: 55-55


115-117: LGTM! Clear relationship between focus_distances and transmit_origins.

The updated focus_distances description now references the "origin point," which is properly defined by the new transmit_origins parameter. This makes the relationship between these parameters clear and well-documented.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Co-authored-by: Vincent van de Schaft <51445631+vincentvdschaft@users.noreply.github.com>
@vincentvdschaft
Copy link
Contributor

Something seems to be broken in the beamformer on this branch. I am beamforming some BS100BW data. Here are two beamforming examples (both on data converted with the verasonics.py from this branch.

On this branch

pr

On main

main ## Code
import os

os.environ["KERAS_BACKEND"] = "jax"
os.environ["ZEA_DISABLE_CACHE"] = "1"
import matplotlib.pyplot as plt

from zea import init_device
from zea.data import load_file
from zea.display import to_8bit
from zea.ops import (
    Pipeline,
)
from zea.visualize import set_mpl_style

init_device(verbose=False)
set_mpl_style()


def plot_data(data, dynamic_range, scan):
    """Helper function to plot the data."""
    image = to_8bit(data, dynamic_range=dynamic_range)
    plt.figure()
    # Convert xlims and zlims from meters to millimeters for display
    xlims_mm = [v * 1e3 for v in scan.xlims]
    zlims_mm = [v * 1e3 for v in scan.zlims]
    plt.imshow(image, cmap="gray", extent=[xlims_mm[0], xlims_mm[1], zlims_mm[1], zlims_mm[0]])
    plt.xlabel("X (mm)")
    plt.ylabel("Z (mm)")


path = "/home/vincent/1-projects/pala_process/data/zea/PALA_InSilicoFlow_RF020.hdf5"

data, scan, probe = load_file(
    path=path,
    indices=(slice(0, 1), slice(1, 2)),
    data_type="raw_data",
)

# index the first frame
data_frame = data[0]

scan.n_ch = 2  # IQ data, should be stored in file but isn't currently
scan.xlims = (-10e-3, 10e-3)  # set x-limits for better visualization
scan.zlims = (0e-3, 12e-3)  # reduce z-limits a bit for better visualization
scan.pixels_per_wavelength = 1
dynamic_range = (-50, 0)  # set dynamic range for display


pipeline = Pipeline.from_default(
    num_patches=1,
    baseband=False,
    enable_pfield=False,
    with_batch_dim=False,
    jit_options=None,
)

parameters = pipeline.prepare_parameters(probe, scan)
parameters.pop("dynamic_range", None)  # remove dynamic_range since we will set it manually later

inputs = {pipeline.key: data_frame}

# dynamic parameters can be freely passed here as keyword arguments
outputs = pipeline(
    **inputs,
    **parameters,
    jit_compile=False,
)

image = outputs[pipeline.output_key]
# plt.imshow(image[0, :, :, 0])

plot_data(image, dynamic_range, scan)
plt.show()

@wesselvannierop
Copy link
Collaborator Author

Thanks @vincentvdschaft for checking, I will have a look why this is the case. I have indeed updated the beamformer to handle focused transmits better, but could be that I overlooked something...

@wesselvannierop
Copy link
Collaborator Author

I found that this happens when focus_distances is set to zero, I have fixed the bug and added a test for this case!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@zea/beamform/beamformer.py`:
- Around line 214-216: The docstring defaults for lens_thickness and
lens_sound_speed disagree with the function signature: change the signature
defaults from None to the documented values (lens_thickness=1e-3,
lens_sound_speed=1000) for the functions that accept apply_lens_correction,
lens_thickness, and lens_sound_speed so the parameters and docstring match;
update both occurrences where these parameters appear together (the parameter
lists containing apply_lens_correction, lens_thickness, lens_sound_speed) to use
those default values.
🧹 Nitpick comments (1)
zea/beamform/beamformer.py (1)

558-558: Consider using dynamic dtype instead of hardcoded "float32".

The hardcoded cast to "float32" could cause precision loss if the input tensors are float64. Consider using the input tensor's dtype for consistency.

♻️ Suggested fix
-    is_before_focus = ops.cast(ops.sign(focus_distance), "float32") * projection_along_beam < 0.0
+    is_before_focus = ops.cast(ops.sign(focus_distance), grid.dtype) * projection_along_beam < 0.0
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e30696b and 0f759f8.

📒 Files selected for processing (3)
  • tests/test_transmit_schemes.py
  • zea/beamform/beamformer.py
  • zea/internal/dummy_scan.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_transmit_schemes.py
  • zea/internal/dummy_scan.py
🧰 Additional context used
🧬 Code graph analysis (1)
zea/beamform/beamformer.py (2)
zea/beamform/lens_correction.py (1)
  • compute_lens_corrected_travel_times (6-54)
zea/scan.py (7)
  • transmit_origins (682-689)
  • t_peak (714-720)
  • tx_waveform_indices (741-747)
  • t0_delays (651-659)
  • n_tx (468-470)
  • focus_distances (672-679)
  • initial_times (692-699)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test-docs-build
  • GitHub Check: tests
🔇 Additional comments (9)
zea/beamform/beamformer.py (9)

7-7: LGTM!

Import correctly updated to use compute_lens_corrected_travel_times, which returns travel times in seconds rather than delays in samples, aligning with the unified delay calculation path.


65-65: LGTM!

The transmit_origins parameter is properly added to the signature and documented in the docstring with the correct shape (n_tx, 3).

Also applies to: 91-91


125-143: LGTM!

The call to calculate_delays correctly passes all the new parameters including transmit_origins and lens correction settings.


272-297: LGTM!

The conditional lens correction path is well-structured with appropriate assertions for required parameters. The unified approach computing rx_delays in seconds for both paths simplifies downstream logic.


299-310: LGTM!

The vmap configuration correctly maps over the transmit dimension for per-transmit parameters (t0_delays, tx_apodizations, focus_distances, polar_angles, initial_times, transmit_origins) while broadcasting shared parameters (grid, rx_delays).


449-464: LGTM!

The explicit broadcasting indexing grid[:, None, :] - probe_geometry[None, :, :] is clearer than implicit broadcasting and ensures correct shape alignment.


529-531: Good fix for the focus_distance=0 bug.

This correctly handles the plane wave case where focus_distance is set to zero by converting it to np.inf, ensuring the first wavefront arrival is used for all pixels. This addresses the bug reported in the PR comments about beamforming being broken when focus_distances is set to zero.


564-568: LGTM!

The min/max selection logic correctly handles:

  • First wavefront arrival for pixels before the focus (converging beam)
  • Last wavefront contribution for pixels beyond the focus (diverging beam)
  • Inactive elements are properly excluded via the offset term.

537-539: LGTM!

Good defensive handling for the edge case where np.inf * 0.0 produces NaN when computing the focal point for plane wave cases with certain beam directions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zea/data/convert/verasonics.py (1)

1254-1260: Pass frames parameter in multi-event branch.

The frames parameter is documented and passed to read_verasonics_file in the single-event branch (line 1280), but is omitted in the multi-event branch (lines 1256–1260). If a user specifies frames in multi-event mode, it will be silently ignored, defaulting to the convert.yaml configuration or "all". This should be passed consistently:

data[event] = self.read_verasonics_file(
    event=event,
    additional_functions=additional_functions,
    frames=frames,
    allow_accumulate=allow_accumulate,
)
♻️ Duplicate comments (2)
zea/beamform/beamformer.py (1)

213-217: LGTM - past review comment addressed.

The signature now correctly defaults lens_thickness and lens_sound_speed to None, and the docstrings (lines 256-259) no longer specify incorrect default values. The validation at lines 286-289 enforces these parameters when apply_lens_correction=True.

zea/data/convert/verasonics.py (1)

1155-1183: Single-number frame strings now handled correctly.

The _parse_frames_argument method now properly handles single-number strings (e.g., "5") via the try/except block at lines 1169-1173, addressing the previously flagged schema/implementation mismatch.

🧹 Nitpick comments (4)
zea/beamform/beamformer.py (3)

272-276: Consider expanding input validation for new parameters.

The validation covers core arrays but misses the newly added transmit_origins (expected shape (n_tx, 3)) and other 1D inputs like initial_times, focus_distances, and polar_angles (expected shape (n_tx,)). Adding assertions for these would provide clearer error messages if callers pass malformed data.

📝 Suggested validation expansion
     # Validate input shapes
     for arr in [t0_delays, grid, tx_apodizations, probe_geometry]:
         assert arr.ndim == 2
     assert probe_geometry.shape[0] == n_el
     assert t0_delays.shape[0] == n_tx
+    assert transmit_origins.shape == (n_tx, 3), f"transmit_origins shape mismatch: {transmit_origins.shape}"
+    for arr, name in [(initial_times, "initial_times"), (focus_distances, "focus_distances"), (polar_angles, "polar_angles")]:
+        assert arr.shape == (n_tx,), f"{name} shape mismatch: {arr.shape}"

315-317: Address TODO: Consider removing or implementing nan_to_num handling.

The commented-out nan_to_num suggests uncertainty about whether NaN values can occur. With the plane-wave handling at lines 537-539 already converting NaN to 0 for the focal point, residual NaNs in delays would indicate an unexpected edge case worth investigating.

Would you like me to help identify potential NaN sources and determine if this safeguard is necessary, or open an issue to track this investigation?


558-558: Hardcoded float32 dtype may cause precision loss.

The cast to "float32" will lose precision if the computation uses float64 tensors. Consider using the dtype from the input grid for consistency:

📝 Suggested fix
-    is_before_focus = ops.cast(ops.sign(focus_distance), "float32") * projection_along_beam < 0.0
+    is_before_focus = ops.cast(ops.sign(focus_distance), grid.dtype) * projection_along_beam < 0.0
zea/data/convert/verasonics.py (1)

731-742: Consider using ValueError instead of assert for validation.

Using assert for runtime validation can be disabled with -O flag. For robustness, consider using an explicit ValueError like in read_center_frequencies.

♻️ Suggested improvement
     `@property`
     def demodulation_frequency(self):
         """Demodulation frequency of the probe from the file in Hz."""

         demod_freq = self.dereference_all(self["Receive"]["demodFrequency"])
         demod_freq = np.unique(demod_freq)
-        assert demod_freq.size == 1, (
-            f"Multiple demodulation frequencies found in file: {demod_freq}. "
-            "We do not support this case."
-        )
+        if demod_freq.size != 1:
+            raise ValueError(
+                f"Multiple demodulation frequencies found in file: {demod_freq}. "
+                "We do not support this case."
+            )

         return demod_freq.item() * 1e6
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f759f8 and 0967236.

📒 Files selected for processing (2)
  • zea/beamform/beamformer.py
  • zea/data/convert/verasonics.py
🧰 Additional context used
🧬 Code graph analysis (2)
zea/beamform/beamformer.py (2)
zea/beamform/lens_correction.py (1)
  • compute_lens_corrected_travel_times (6-54)
zea/scan.py (5)
  • transmit_origins (682-689)
  • t_peak (714-720)
  • t0_delays (651-659)
  • grid (291-316)
  • n_tx (468-470)
zea/data/convert/verasonics.py (3)
zea/log.py (2)
  • info (276-281)
  • yellow (69-71)
zea/scan.py (1)
  • demodulation_frequency (600-605)
zea/data/data_format.py (1)
  • DatasetElement (20-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: image
🔇 Additional comments (16)
zea/beamform/beamformer.py (4)

50-106: LGTM!

The tof_correction function signature is properly extended with transmit_origins, and the docstring accurately documents all parameters including the lens correction defaults.


125-143: LGTM!

The updated call to calculate_delays correctly propagates all new parameters including transmit_origins and lens correction settings.


278-297: LGTM!

The lens correction branching logic is well-structured with proper validation of required parameters. The call to compute_lens_corrected_travel_times correctly passes parameters in the expected order per the function signature in lens_correction.py.


529-539: Good fix for the focus_distance=0 bug.

This correctly handles the plane wave case where focus_distance is set to zero by converting it to infinity, ensuring the min-time path is used for all pixels. The NaN guard at line 539 properly handles the inf * 0 edge case when beam_direction has zero components.

zea/data/convert/verasonics.py (12)

123-138: LGTM!

The schema definition is well-structured with appropriate validation for frames (supporting "all", ranges like "30-99", single numbers, and lists) and transmits.


148-179: LGTM!

The dereferencing logic correctly handles both reference and non-reference datasets with appropriate scalar extraction using .item().


181-197: Docstring return type now matches implementation.

The docstring correctly states the return type as list, which aligns with the actual implementation.


207-215: LGTM!

The utility methods decode_string and cast_to_integer provide clean abstractions for common data extraction operations.


244-248: LGTM!

Using probe_center_frequency for wavelength calculation is the correct approach, as wavelength should be based on the probe's physical characteristics rather than the transmit frequency.


546-585: LGTM!

The load_convert_config method provides flexible YAML-based configuration with proper schema validation. The fallback to match both filename with and without extension is a nice touch for usability.


587-617: LGTM!

The frame counting and reordering logic is now internally consistent. get_indices_to_reorder correctly accepts first_frame and n_frames parameters, and get_raw_data_order passes the appropriate values.


619-689: LGTM!

The read_raw_data method correctly handles the new buffer_index and first_frame_idx parameters, with proper fallback to automatic frame ordering when first_frame_idx is not provided.


874-897: LGTM!

The bandwidth extraction logic correctly parses the sample mode strings to extract the bandwidth percentage.


1028-1153: LGTM!

The read_verasonics_file method is well-refactored with proper configuration loading, error handling for the image buffer read, and inclusion of new data fields (demodulation_frequency, transmit_origins).


1185-1212: LGTM!

The get_frame_indices method provides robust frame index parsing with proper validation and graceful handling of out-of-bounds indices.


1434-1440: LGTM!

The frames argument is correctly propagated to the conversion function for both single-file and directory conversions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working data format Related to the zea data format saving and loading enhancement New feature or request ultrasound Improvements regarding ultrasound reconstruction pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants