Skip to content

[doc] marimo examples#57

Merged
charlesbmi merged 12 commits intomainfrom
feature/marimo
Dec 13, 2025
Merged

[doc] marimo examples#57
charlesbmi merged 12 commits intomainfrom
feature/marimo

Conversation

@charlesbmi
Copy link
Copy Markdown
Collaborator

Introduction

Marimo is a nice way to add interactivity to our code and notebooks. In particular, we can have a quick up and running GUI with sliders for changing our beamforming parameters, and make it a nice example.

Changes

  • port plane_wave_compound.py and doppler.py examples to marimo
  • add marimo to dependencies

Behavior

runnable, e.g. with uv run marimo run examples/marimo/plane_wave_compound.py

Review checklist

  • All existing tests and checks pass
  • Unit tests covering the new feature or bugfix have been added
  • The documentation has been updated if necessary

@charlesbmi charlesbmi self-assigned this Dec 12, 2025
@qodo-code-review
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

ImportError UX

The raised ImportError message for missing pymust includes an emoji and may not clearly guide users in non-emoji terminals; consider a clearer instruction or optional fallback to disable the demo gracefully when pymust is absent.

try:
    import pymust
except ImportError as err:
    raise ImportError("⚠️  PyMUST is currently required for RF-to-IQ demodulation.") from err

# Convenience constant
Download Robustness

The dataset download uses a fixed expected size and hash; network issues or mirror changes could cause brittle failures—consider clearer error messages and retry/backoff or alternative source handling.

@app.cell
def _(cached_download, hashlib):
    # Download PICMUS Challenge Dataset (runs once, cached)
    url = "http://www.ustb.no/datasets/PICMUS_experiment_resolution_distortion.uff"
    uff_path = cached_download(
        url,
        expected_size=145_518_524,
        expected_hash="c93af0781daeebf771e53a42a629d4f311407410166ef2f1d227e9d2a1b8c641",
        digest=hashlib.sha256,
        filename="PICMUS_experiment_resolution_distortion.uff",
    )
    return (uff_path,)
Ruff Exclude Scope

Excluding all files under examples/marimo/* from Ruff may hide real issues; consider narrowing the exclude to lint but ignore specific rules, or add # noqa where needed.

wheel.packages = ["src/mach"]

[tool.ruff]
target-version = "py310"
line-length = 120
fix = true
exclude = ["examples/marimo/*"]

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Dec 12, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Refactor example logic to avoid duplication

To improve maintainability, extract the duplicated data processing and
beamforming logic from the new interactive examples into shared, reusable
functions. This will prevent code drift between the static and interactive
examples.

Examples:

examples/marimo/plane_wave_compound.py [87-122]
def _(cached_download, hashlib):
    # Download PICMUS Challenge Dataset (runs once, cached)
    url = "http://www.ustb.no/datasets/PICMUS_experiment_resolution_distortion.uff"
    uff_path = cached_download(
        url,
        expected_size=145_518_524,
        expected_hash="c93af0781daeebf771e53a42a629d4f311407410166ef2f1d227e9d2a1b8c641",
        digest=hashlib.sha256,
        filename="PICMUS_experiment_resolution_distortion.uff",
    )

 ... (clipped 26 lines)
examples/marimo/doppler.py [91-117]
def _(download_pymust_doppler_data, extract_pymust_params):
    # Load PyMUST data (cached after first download)
    mat_data = download_pymust_doppler_data()
    params = extract_pymust_params(mat_data)
    return mat_data, params


@app.cell
def _(mat_data, params, pymust):
    # Convert RF to IQ format

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

// In examples/plane_wave_compound.py (original script)
// ... imports ...
url = "http://www.ustb.no/datasets/PICMUS_experiment_resolution_distortion.uff"
uff_path = cached_download(url, ...)
uff_file = Uff(str(uff_path))
channel_data = uff_file.read("/channel_data")
scan = uff_file.read("/scan")
base_kwargs = create_beamforming_setup(...)
result = experimental.beamform(**base_kwargs)
// ... plot result ...

// In examples/marimo/plane_wave_compound.py (new interactive example)
@app.cell
def download_data():
    url = "http://www.ustb.no/datasets/PICMUS_experiment_resolution_distortion.uff"
    uff_path = cached_download(url, ...)
    return uff_path

@app.cell
def load_data(uff_path):
    uff_file = Uff(str(uff_path))
    channel_data = uff_file.read("/channel_data")
    scan = uff_file.read("/scan")
    return channel_data, scan

@app.cell
def beamform_and_visualize(...):
    // ... slice data based on UI controls ...
    result = experimental.beamform(...)
    // ... plot result ...

After:

// Create a new shared file, e.g., examples/common.py
def get_picmus_data():
    url = "http://www.ustb.no/datasets/PICMUS_experiment_resolution_distortion.uff"
    uff_path = cached_download(url, ...)
    uff_file = Uff(str(uff_path))
    channel_data = uff_file.read("/channel_data")
    scan = uff_file.read("/scan")
    return channel_data, scan

// In examples/plane_wave_compound.py (refactored script)
from examples.common import get_picmus_data
channel_data, scan = get_picmus_data()
base_kwargs = create_beamforming_setup(channel_data, scan, ...)
result = experimental.beamform(**base_kwargs)
// ... plot result ...

// In examples/marimo/plane_wave_compound.py (refactored interactive example)
from examples.common import get_picmus_data
@app.cell
def load_data():
    channel_data, scan = get_picmus_data()
    return channel_data, scan

@app.cell
def beamform_and_visualize(channel_data, scan, ...):
    // ... create setup and beamform ...
    // ... plot result ...
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies significant code duplication between the new interactive examples and existing ones, which poses a long-term maintenance challenge.

Medium
Possible issue
Prevent crash on empty selection

Add a check to see if selected_angles is empty before calling min() and max() to
prevent a ValueError when generating the plot title.

examples/marimo/plane_wave_compound.py [223-233]

 # Get angle range for selected plane waves
 selected_angles = angles_deg_all[pw_start:pw_end]
 n_pws = pw_end - pw_start
 n_chs = ch_end - ch_start
 
+if selected_angles:
+    title_pw_part = f"{n_pws} Plane Waves ({min(selected_angles):.0f}° to {max(selected_angles):.0f}°)"
+else:
+    title_pw_part = f"{n_pws} Plane Waves"
+
 ax.set_title(
-    f"PICMUS: {n_pws} Plane Waves ({min(selected_angles):.0f}° to {max(selected_angles):.0f}°), "
-    f"{n_chs} Channels\n"
+    f"PICMUS: {title_pw_part}, {n_chs} Channels\n"
     f"F-number: {f_number.value:.1f}, SoS: {sound_speed.value} m/s",
     fontsize=12,
 )
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a ValueError that would crash the interactive application if a user selects an empty range, and the proposed fix is correct and improves robustness.

Medium
Handle empty data selection gracefully

Check if the user has selected an empty range for channels or plane waves. If
so, display a message instead of calling experimental.beamform with empty data
arrays to prevent a crash.

examples/marimo/plane_wave_compound.py [165-193]

 # Interactive Visualization
 # -------------------------
 # This cell reactively updates when any slider changes
 
 # Extract slider values
 ch_start, ch_end = channel_range.value
 pw_start, pw_end = pw_range.value
 
-# Slice the data based on user selections
-sliced_channel_data = base_kwargs["channel_data"][pw_start:pw_end, ch_start:ch_end, :, :]
-sliced_rx_coords = base_kwargs["rx_coords_m"][ch_start:ch_end, :]
-sliced_tx_arrivals = base_kwargs["tx_wave_arrivals_s"][pw_start:pw_end, :]
+if ch_start == ch_end or pw_start == pw_end:
+    # Create an empty plot with a message for empty selections
+    fig, ax = plt.subplots(figsize=(8, 8), dpi=150)
+    ax.text(0.5, 0.5, "Empty selection for channels or plane waves.",
+            horizontalalignment='center', verticalalignment='center',
+            transform=ax.transAxes)
+    plt.close(fig)
+else:
+    # Slice the data based on user selections
+    sliced_channel_data = base_kwargs["channel_data"][pw_start:pw_end, ch_start:ch_end, :, :]
+    sliced_rx_coords = base_kwargs["rx_coords_m"][ch_start:ch_end, :]
+    sliced_tx_arrivals = base_kwargs["tx_wave_arrivals_s"][pw_start:pw_end, :]
 
-# Update beamforming kwargs with current slider values
-current_kwargs = {
-    "channel_data": sliced_channel_data,
-    "rx_coords_m": sliced_rx_coords,
-    "scan_coords_m": base_kwargs["scan_coords_m"],
-    "tx_wave_arrivals_s": sliced_tx_arrivals,
-    "out": None,
-    "f_number": f_number.value,
-    "sampling_freq_hz": base_kwargs["sampling_freq_hz"],
-    "sound_speed_m_s": sound_speed.value,
-    "modulation_freq_hz": base_kwargs["modulation_freq_hz"],
-    "rx_start_s": base_kwargs["rx_start_s"],
-}
+    # Update beamforming kwargs with current slider values
+    current_kwargs = {
+        "channel_data": sliced_channel_data,
+        "rx_coords_m": sliced_rx_coords,
+        "scan_coords_m": base_kwargs["scan_coords_m"],
+        "tx_wave_arrivals_s": sliced_tx_arrivals,
+        "out": None,
+        "f_number": f_number.value,
+        "sampling_freq_hz": base_kwargs["sampling_freq_hz"],
+        "sound_speed_m_s": sound_speed.value,
+        "modulation_freq_hz": base_kwargs["modulation_freq_hz"],
+        "rx_start_s": base_kwargs["rx_start_s"],
+    }
 
-# Perform beamforming and compounding
-result = experimental.beamform(**current_kwargs)
-...
+    # Perform beamforming and compounding
+    result = experimental.beamform(**current_kwargs)
+    ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies that passing zero-sized arrays to experimental.beamform will likely cause a crash. The proposed fix prevents this and improves the application's robustness and user experience.

Medium
  • Update

@charlesbmi charlesbmi merged commit ec0081c into main Dec 13, 2025
9 checks passed
@charlesbmi charlesbmi deleted the feature/marimo branch December 13, 2025 18:59
charlesbmi added a commit that referenced this pull request Dec 15, 2025
* Add marimo to dependencies

* Add conversion of plane_wave_compound.py

* Markdown for the marimo plane-wave-compounding app

* gitignore marimo

* plane-wave-compound rename

* Fix lint

* Add README

* More excludes and marimo stuff

* Add README png

* Add Doppler example and fix some linting

* Fix trailing whitespace

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants