From b7aa5143a4e170657eedb3d02c44fc6c27a2c819 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Mon, 27 Apr 2026 16:42:13 -0500 Subject: [PATCH 1/3] ADD: GitHub Actions workflow to publish to PyPI on release Automatically builds and publishes to PyPI when a GitHub Release is published. Patches pyproject.toml with the release tag version before building so the package version always matches the tag. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/release.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2def77c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release to PyPI + +on: + release: + types: [published] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # required for PyPI trusted publisher (OIDC) + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: python -m pip install --upgrade pip build + + # Strip a leading 'v' from the tag (e.g. v1.2.3 → 1.2.3) and write it + # into pyproject.toml so the published package version matches the tag. + - name: Sync version with release tag + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml + echo "Building version $VERSION" + + - name: Build distribution + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From b25ca02ae65d7c7439d2bf7e889c3adb33cacacb Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 30 Apr 2026 16:27:06 -0500 Subject: [PATCH 2/3] ADD: gate counts per dBZ threshold to radar preprocessing output Adds n_gates_10dbz through n_gates_50dbz columns to the DataFrame returned by preprocess_radar_data, counting gates with reflectivity exceeding each threshold. Updates the integration test to match. Co-Authored-By: Claude Sonnet 4.6 --- lars/preprocessing/radar_preprocessing.py | 7 +++++-- tests/integration/test_radar_preprocessing.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lars/preprocessing/radar_preprocessing.py b/lars/preprocessing/radar_preprocessing.py index 9596037..a8a2974 100644 --- a/lars/preprocessing/radar_preprocessing.py +++ b/lars/preprocessing/radar_preprocessing.py @@ -48,7 +48,9 @@ def preprocess_radar_data(file_path, output_path, date=None, for date_str in date: file_list2.extend([f for f in file_list if date_str in f]) file_list = file_list2 - out_df = pd.DataFrame(columns=['file_path', 'time', 'label', 'ref_min', 'ref_max']) + dbz_thresholds = [10, 20, 30, 40, 50] + gate_cols = [f'n_gates_{t}dbz' for t in dbz_thresholds] + out_df = pd.DataFrame(columns=['file_path', 'time', 'label', 'ref_min', 'ref_max'] + gate_cols) if not "vmin" in kwargs: kwargs['vmin'] = -20 if not "vmax" in kwargs: @@ -80,6 +82,7 @@ def preprocess_radar_data(file_path, output_path, date=None, sweep[radar_field] > min_ref).values ref_min = np.nanmin(masked) ref_max = np.nanmax(masked) + gate_counts = [int(np.sum(masked > t)) for t in dbz_thresholds] ax.axis('off') ax.set_title('') ax.set_ylabel('') @@ -96,7 +99,7 @@ def preprocess_radar_data(file_path, output_path, date=None, os.path.basename(file).replace('.nc', '.png')), dpi=dpi, bbox_inches='tight', pad_inches=0) plt.close(fig) - out_df.loc[len(out_df)] = [file_name, time_str, label, ref_min, ref_max] + out_df.loc[len(out_df)] = [file_name, time_str, label, ref_min, ref_max] + gate_counts else: print(f"Sweep mode is not PPI or sector scan in {file}, skipping.") diff --git a/tests/integration/test_radar_preprocessing.py b/tests/integration/test_radar_preprocessing.py index 8ce92fa..b959cdc 100644 --- a/tests/integration/test_radar_preprocessing.py +++ b/tests/integration/test_radar_preprocessing.py @@ -70,7 +70,10 @@ def test_dataframe_row_count(preprocessing_output): def test_dataframe_columns(preprocessing_output): _, label_df = preprocessing_output - assert set(label_df.columns) == {"file_path", "label", "ref_min", "ref_max"} + assert set(label_df.columns) == { + "file_path", "label", "ref_min", "ref_max", + "n_gates_10dbz", "n_gates_20dbz", "n_gates_30dbz", "n_gates_40dbz", "n_gates_50dbz", + } def test_labels_are_unknown(preprocessing_output): From 36827e499f2be1884f4c83f4e7d29bea2202ccac Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 30 Apr 2026 16:30:36 -0500 Subject: [PATCH 3/3] ADD: per-threshold coverage percentages and customizable dBZ thresholds Adds pct_gates_dbz columns alongside the existing n_gates_dbz columns, expressing each gate count as a percentage of total sweep gates. Thresholds are now controlled by a dbz_thresholds parameter (default (10, 20, 30, 40, 50)) so callers can supply any arbitrary list. Co-Authored-By: Claude Sonnet 4.6 --- lars/preprocessing/radar_preprocessing.py | 22 ++++++++++++++----- tests/integration/test_radar_preprocessing.py | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lars/preprocessing/radar_preprocessing.py b/lars/preprocessing/radar_preprocessing.py index a8a2974..07a3116 100644 --- a/lars/preprocessing/radar_preprocessing.py +++ b/lars/preprocessing/radar_preprocessing.py @@ -11,6 +11,7 @@ def preprocess_radar_data(file_path, output_path, date=None, radar_field='corrected_reflectivity', x_bounds=(-150000, 150000), y_bounds=(-150000, 150000), size_px=256, dpi=150, min_ref=-99., + dbz_thresholds=(10, 20, 30, 40, 50), **kwargs): """ Preprocess cf/Radial radar data from a given file path. This module will load the radar data, @@ -28,7 +29,9 @@ def preprocess_radar_data(file_path, output_path, date=None, y_bounds (tuple): The y-axis bounds for plotting in meters. size_px (int): Width and height of the output PNG in pixels. Default is 256. dpi (int): Dots per inch for the saved figure. Default is 150. - min_ref (float): The minimum reflectivity to consider + min_ref (float): The minimum reflectivity to consider. + dbz_thresholds (sequence of float): Reflectivity thresholds in dBZ for which gate + counts and coverage percentages are computed. Default is (10, 20, 30, 40, 50). **kwargs: @@ -38,8 +41,10 @@ def preprocess_radar_data(file_path, output_path, date=None, ------- label_df: pd.DataFrame DataFrame containing labels, paths, and times for the radar data. + Includes n_gates_dbz (count) and pct_gates_dbz (% of total gates) + columns for each threshold T in dbz_thresholds. """ - + file_list = glob.glob(file_path + '/*.nc') if date is not None: if isinstance(date, str): @@ -48,9 +53,11 @@ def preprocess_radar_data(file_path, output_path, date=None, for date_str in date: file_list2.extend([f for f in file_list if date_str in f]) file_list = file_list2 - dbz_thresholds = [10, 20, 30, 40, 50] - gate_cols = [f'n_gates_{t}dbz' for t in dbz_thresholds] - out_df = pd.DataFrame(columns=['file_path', 'time', 'label', 'ref_min', 'ref_max'] + gate_cols) + dbz_thresholds = list(dbz_thresholds) + count_cols = [f'n_gates_{t}dbz' for t in dbz_thresholds] + pct_cols = [f'pct_gates_{t}dbz' for t in dbz_thresholds] + out_df = pd.DataFrame(columns=['file_path', 'time', 'label', 'ref_min', 'ref_max'] + + count_cols + pct_cols) if not "vmin" in kwargs: kwargs['vmin'] = -20 if not "vmax" in kwargs: @@ -82,7 +89,9 @@ def preprocess_radar_data(file_path, output_path, date=None, sweep[radar_field] > min_ref).values ref_min = np.nanmin(masked) ref_max = np.nanmax(masked) + total_gates = masked.size gate_counts = [int(np.sum(masked > t)) for t in dbz_thresholds] + gate_pcts = [round(n / total_gates * 100, 4) for n in gate_counts] ax.axis('off') ax.set_title('') ax.set_ylabel('') @@ -99,7 +108,8 @@ def preprocess_radar_data(file_path, output_path, date=None, os.path.basename(file).replace('.nc', '.png')), dpi=dpi, bbox_inches='tight', pad_inches=0) plt.close(fig) - out_df.loc[len(out_df)] = [file_name, time_str, label, ref_min, ref_max] + gate_counts + out_df.loc[len(out_df)] = ([file_name, time_str, label, ref_min, ref_max] + + gate_counts + gate_pcts) else: print(f"Sweep mode is not PPI or sector scan in {file}, skipping.") diff --git a/tests/integration/test_radar_preprocessing.py b/tests/integration/test_radar_preprocessing.py index b959cdc..66338ed 100644 --- a/tests/integration/test_radar_preprocessing.py +++ b/tests/integration/test_radar_preprocessing.py @@ -73,6 +73,7 @@ def test_dataframe_columns(preprocessing_output): assert set(label_df.columns) == { "file_path", "label", "ref_min", "ref_max", "n_gates_10dbz", "n_gates_20dbz", "n_gates_30dbz", "n_gates_40dbz", "n_gates_50dbz", + "pct_gates_10dbz", "pct_gates_20dbz", "pct_gates_30dbz", "pct_gates_40dbz", "pct_gates_50dbz", }