From fcafacd51e322a2bb69da635d22dd2edf4296978 Mon Sep 17 00:00:00 2001 From: RFingAdam Date: Tue, 10 Feb 2026 12:35:04 -0600 Subject: [PATCH 1/4] fix: Disable interactive matplotlib during batch processing plt.ioff() before batch loops prevents figure windows from briefly appearing when saving plots. plt.ion() restored on completion or early return. Co-Authored-By: Claude Opus 4.6 --- plot_antenna/file_utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plot_antenna/file_utils.py b/plot_antenna/file_utils.py index a06769f..8c84f57 100644 --- a/plot_antenna/file_utils.py +++ b/plot_antenna/file_utils.py @@ -1044,8 +1044,14 @@ def batch_process_passive_scans( ``save_base`` if provided. """ import os + + import matplotlib.pyplot as plt + from .plotting import plot_2d_passive_data, plot_passive_3d_component + # Disable interactive mode so figures don't pop up during batch processing + plt.ioff() + # Find all HPOL and VPOL files files = os.listdir(folder_path) hpol_files = [f for f in files if f.endswith("AP_HPol.txt")] @@ -1061,6 +1067,7 @@ def batch_process_passive_scans( if not pairs: print(f"No HPOL/VPOL pairs found in {folder_path}.") + plt.ion() return for h_path, v_path in pairs: @@ -1174,6 +1181,9 @@ def batch_process_passive_scans( save_path=maritime_sub, ) + # Re-enable interactive mode after batch processing + plt.ion() + def batch_process_active_scans( folder_path, @@ -1207,15 +1217,22 @@ def batch_process_active_scans( 4. Saves results to per‑file subfolders in ``save_base`` if provided. """ import os + + import matplotlib.pyplot as plt + from .plotting import plot_active_2d_data, plot_active_3d_data from .calculations import calculate_active_variables + # Disable interactive mode so figures don't pop up during batch processing + plt.ioff() + # Find all TRP files in the folder files = os.listdir(folder_path) trp_files = [f for f in files if f.endswith(".txt") and "TRP" in f.upper()] if not trp_files: print(f"No TRP files found in {folder_path}.") + plt.ion() return for trp_file in trp_files: @@ -1341,3 +1358,6 @@ def batch_process_active_scans( except Exception as e: print(f" ✗ Error processing {trp_file}: {e}") + + # Re-enable interactive mode after batch processing + plt.ion() From 01ce94fbe294d287f1f4cf60c5d82cf765a315ca Mon Sep 17 00:00:00 2001 From: RFingAdam Date: Wed, 11 Feb 2026 10:45:19 -0600 Subject: [PATCH 2/4] feat: Add horizon TRP, efficiency, and enhanced statistics - Added _compute_partial_trp() helper for integrating power/gain over partial or full sphere with sin(theta) weighting - Horizon stats table now includes: Horizon TRP, Full Sphere TRP, Horizon Efficiency (%), with appropriate labels for gain vs power - Replaced single theta=90 mini polar with multi-cut polar showing 3-5 cuts spanning the full horizon band - Added statistics annotation to 3D masked pattern plot showing max/min/avg, horizon TRP, full TRP, and efficiency - Fixed plt.cm.viridis Pylance warnings using cm.get_cmap() Co-Authored-By: Claude Opus 4.6 --- plot_antenna/plotting.py | 151 ++++++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 19 deletions(-) diff --git a/plot_antenna/plotting.py b/plot_antenna/plotting.py index 8974556..4c72bc3 100644 --- a/plot_antenna/plotting.py +++ b/plot_antenna/plotting.py @@ -2203,7 +2203,7 @@ def plot_conical_cuts( else: ax = fig.add_subplot(111) - colors = plt.cm.viridis(np.linspace(0, 1, len(theta_cuts))) + colors = cm.get_cmap("viridis")(np.linspace(0, 1, len(theta_cuts))) for i, theta_cut in enumerate(theta_cuts): theta_idx = np.argmin(np.abs(theta_deg - theta_cut)) @@ -2289,7 +2289,7 @@ def plot_gain_over_azimuth( fig = plt.figure(figsize=(12, 6)) ax = fig.add_subplot(111) - colors = plt.cm.viridis(np.linspace(0, 1, len(theta_cuts))) + colors = cm.get_cmap("viridis")(np.linspace(0, 1, len(theta_cuts))) for i, theta_cut in enumerate(theta_cuts): theta_idx = np.argmin(np.abs(theta_deg - theta_cut)) @@ -2338,6 +2338,44 @@ def plot_gain_over_azimuth( plt.show() +def _compute_partial_trp(theta_deg, phi_deg, gain_2d, theta_min=None, theta_max=None): + """ + Compute TRP (Total Radiated Power) over a partial or full sphere. + + Uses numerical integration with sin(theta) weighting: + TRP = ΔΘ·ΔΦ / (4π) · ΣΣ P_linear(θ,φ) · sin(θ) + + Parameters: + theta_deg: 1D array of theta angles in degrees + phi_deg: 1D array of phi angles in degrees + gain_2d: 2D array (n_theta, n_phi) of gain/power in dB scale + theta_min: Optional lower bound (degrees). None = use full range. + theta_max: Optional upper bound (degrees). None = use full range. + + Returns: + TRP in dB (10*log10 of linear TRP), or -inf if no data. + """ + if theta_min is not None and theta_max is not None: + mask = (theta_deg >= theta_min) & (theta_deg <= theta_max) + sel_theta = theta_deg[mask] + sel_gain = gain_2d[mask, :] + else: + sel_theta = theta_deg + sel_gain = gain_2d + + if sel_gain.size == 0: + return float("-inf") + + d_theta = np.deg2rad(np.mean(np.diff(sel_theta))) if len(sel_theta) > 1 else np.deg2rad(5.0) + d_phi = np.deg2rad(np.mean(np.diff(phi_deg))) if len(phi_deg) > 1 else np.deg2rad(5.0) + + lin = 10 ** (sel_gain / 10) + sin_w = np.sin(np.deg2rad(sel_theta)) + # Integrate: ΔΘ·ΔΦ/(4π) · Σ_θ Σ_φ P(θ,φ)·sin(θ) + integrated = d_theta * d_phi / (4 * np.pi) * np.sum(lin * sin_w[:, np.newaxis]) + return 10 * np.log10(integrated) if integrated > 0 else float("-inf") + + def plot_horizon_statistics( theta_deg, phi_deg, @@ -2374,7 +2412,7 @@ def plot_horizon_statistics( print(f"[Maritime] No data in theta range {theta_min}-{theta_max} deg") return - # Statistics + # ----- Basic statistics ----- max_gain = np.max(horizon_gain) min_gain = np.min(horizon_gain) lin = 10 ** (horizon_gain / 10) @@ -2384,8 +2422,7 @@ def plot_horizon_statistics( coverage_limit = max_gain + gain_threshold # threshold is negative coverage_pct = 100.0 * np.sum(horizon_gain >= coverage_limit) / horizon_gain.size - # MEG: Mean Effective Gain with sin(theta) weighting (passive only) - # For active power data, sin-weighted average EIRP is shown instead. + # Sin-weighted average (MEG for passive, avg EIRP for active) theta_rad = np.deg2rad(horizon_theta) sin_weights = np.sin(theta_rad) n_phi = horizon_gain.shape[1] @@ -2393,38 +2430,54 @@ def plot_horizon_statistics( meg_lin = np.sum(weighted_lin) / (np.sum(sin_weights) * n_phi) meg_dB = 10 * np.log10(meg_lin) if meg_lin > 0 else float("-inf") - # Null detection: find the minimum point + # Null detection null_flat_idx = np.argmin(horizon_gain) null_theta_idx, null_phi_idx = np.unravel_index(null_flat_idx, horizon_gain.shape) null_depth = min_gain - max_gain null_location = f"θ={horizon_theta[null_theta_idx]:.0f}°, φ={phi_deg[null_phi_idx]:.0f}°" - # Create figure with table and mini polar plot - fig = plt.figure(figsize=(14, 6)) - gs = fig.add_gridspec(1, 2, width_ratios=[1.5, 1]) + # ----- TRP / efficiency ----- + trp_horizon_dB = _compute_partial_trp( + theta_deg, phi_deg, gain_2d, theta_min=theta_min, theta_max=theta_max + ) + trp_full_dB = _compute_partial_trp(theta_deg, phi_deg, gain_2d) + if trp_full_dB > -100 and trp_horizon_dB > -100: + horizon_eff = 10 ** ((trp_horizon_dB - trp_full_dB) / 10) * 100.0 + else: + horizon_eff = 0.0 - # Left: statistics table + # ----- Figure: table (left) + multi-cut polar (right) ----- + fig = plt.figure(figsize=(16, 7)) + gs = fig.add_gridspec(1, 2, width_ratios=[1.4, 1]) + + # --- Left: statistics table --- ax_table = fig.add_subplot(gs[0]) ax_table.axis("off") - # Label the sin-weighted metric appropriately for gain vs power if data_label == "Gain": meg_label = "MEG (sin-θ weighted)" + trp_label = "Partial Directivity (horizon)" + trp_full_label = "Total Directivity (full sphere)" else: meg_label = "Avg EIRP (sin-θ weighted)" + trp_label = "Horizon TRP" + trp_full_label = "Full Sphere TRP" table_data = [ ["Max " + data_label, f"{max_gain:.1f} {data_unit}"], ["Min " + data_label, f"{min_gain:.1f} {data_unit}"], ["Avg " + data_label + " (linear)", f"{avg_gain:.1f} {data_unit}"], [meg_label, f"{meg_dB:.1f} {data_unit}"], + [trp_label, f"{trp_horizon_dB:.1f} {data_unit}"], + [trp_full_label, f"{trp_full_dB:.1f} {data_unit}"], + ["Horizon Efficiency", f"{horizon_eff:.1f}%"], [ f"Coverage (>{coverage_limit:.1f} {data_unit})", f"{coverage_pct:.1f}%", ], ["Null Depth", f"{null_depth:.1f} dB"], ["Null Location", null_location], - ["Theta Range", f"{theta_min}° - {theta_max}°"], + ["Theta Range", f"{theta_min}° – {theta_max}°"], ["Frequency", f"{frequency} MHz"], ] @@ -2437,7 +2490,7 @@ def plot_horizon_statistics( ) table.auto_set_font_size(False) table.set_fontsize(10) - table.scale(1, 1.6) + table.scale(1, 1.5) # Style header row for j in range(2): @@ -2451,17 +2504,38 @@ def plot_horizon_statistics( pad=20, ) - # Right: mini polar plot at theta=90 + # --- Right: multi-cut polar plot across horizon band --- ax_polar = fig.add_subplot(gs[1], projection="polar") - theta_90_idx = np.argmin(np.abs(theta_deg - 90)) - cut_gain = gain_2d[theta_90_idx, :] + # Pick 3-5 representative cuts spanning the horizon band + band_thetas = np.array( + [ + t + for t in [theta_min, (theta_min + 90) / 2, 90, (90 + theta_max) / 2, theta_max] + if theta_min <= t <= theta_max + ] + ) + # Remove near-duplicates + band_thetas = np.unique(np.round(band_thetas, 1)) + cmap = cm.get_cmap("viridis") + colors = cmap(np.linspace(0, 1, len(band_thetas))) + phi_rad = np.deg2rad(phi_deg) phi_plot = np.append(phi_rad, phi_rad[0]) - gain_plot = np.append(cut_gain, cut_gain[0]) - ax_polar.plot(phi_plot, gain_plot, "b-", linewidth=2) + + for i, tc in enumerate(band_thetas): + t_idx = np.argmin(np.abs(theta_deg - tc)) + cut = gain_2d[t_idx, :] + cut_plot = np.append(cut, cut[0]) + ax_polar.plot(phi_plot, cut_plot, color=colors[i], linewidth=1.5, label=f"θ={tc:.0f}°") + ax_polar.set_theta_zero_location("N") ax_polar.set_theta_direction(-1) - ax_polar.set_title(f"θ=90° Cut", pad=20, fontsize=11) + ax_polar.set_title( + f"Horizon Cuts ({data_unit})\n{theta_min}°–{theta_max}°", + pad=20, + fontsize=11, + ) + ax_polar.legend(loc="upper right", bbox_to_anchor=(1.35, 1.0), fontsize=8) ax_polar.grid(True, alpha=0.3) plt.tight_layout() @@ -2634,6 +2708,45 @@ def plot_3d_pattern_masked( cbar = fig.colorbar(mappable, ax=ax, pad=0.1, shrink=0.75) cbar.set_label(f"{data_label} ({data_unit})") + # ----- Horizon band statistics annotation ----- + band_mask = (theta_deg >= theta_highlight_min) & (theta_deg <= theta_highlight_max) + band_data = gain_2d[band_mask, :] + if band_data.size > 0: + band_max = np.max(band_data) + band_min = np.min(band_data) + band_lin = 10 ** (band_data / 10) + band_avg = 10 * np.log10(np.mean(band_lin)) + + trp_horizon = _compute_partial_trp( + theta_deg, + phi_deg, + gain_2d, + theta_min=theta_highlight_min, + theta_max=theta_highlight_max, + ) + trp_full = _compute_partial_trp(theta_deg, phi_deg, gain_2d) + if trp_full > -100 and trp_horizon > -100: + eff = 10 ** ((trp_horizon - trp_full) / 10) * 100.0 + else: + eff = 0.0 + + stats_text = ( + f"Horizon Band ({theta_highlight_min}–{theta_highlight_max}°)\n" + f"Max: {band_max:.1f} Min: {band_min:.1f} Avg: {band_avg:.1f} {data_unit}\n" + f"Horizon TRP: {trp_horizon:.1f} {data_unit} " + f"Full TRP: {trp_full:.1f} {data_unit}\n" + f"Horizon Efficiency: {eff:.1f}%" + ) + ax.text2D( + 0.02, + 0.02, + stats_text, + transform=ax.transAxes, + fontsize=9, + verticalalignment="bottom", + bbox=dict(facecolor="white", edgecolor="gray", alpha=0.85, boxstyle="round,pad=0.4"), + ) + plt.tight_layout() if save_path: From 68894a8c176bb2d131b9e9ba174dbdc38acad2c4 Mon Sep 17 00:00:00 2001 From: RFingAdam Date: Wed, 11 Feb 2026 10:45:49 -0600 Subject: [PATCH 3/4] docs: Add v4.1.4 release notes Co-Authored-By: Claude Opus 4.6 --- RELEASE_NOTES.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 91ad94d..236aca0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,22 @@ # RFlect - Release Notes +## Version 4.1.4 (02/11/2026) + +**Feature release — horizon band TRP, efficiency calculations, and enhanced maritime statistics.** + +### New Features +- **Horizon TRP**: Integrated radiated power over the horizon band (the "donut" between theta min/max), computed using sin(θ)-weighted numerical integration +- **Full Sphere TRP**: Total radiated power integrated over the entire measurement sphere for reference +- **Horizon Efficiency**: Percentage of total radiated power concentrated in the horizon band — the key figure of merit for maritime antennas +- **3D pattern statistics**: The 3D masked horizon plot now includes an annotation box with max/min/avg, horizon TRP, full TRP, and efficiency + +### Improvements +- **Multi-cut polar plot**: Horizon statistics page now shows 3–5 polar cuts spanning the full horizon band instead of a single θ=90° cut +- **Enhanced statistics table**: Added Horizon TRP, Full Sphere TRP, and Horizon Efficiency rows with appropriate labels (Gain/Directivity for passive, Power/TRP for active) +- **Batch processing**: Disabled interactive matplotlib during batch jobs so figure windows no longer briefly pop up and interfere with queuing additional work + +--- + ## Version 4.1.3 (02/10/2026) **Patch release — coverage threshold and horizon statistics fixes.** From 824517df58a648e280d826901e61493a8b6449fc Mon Sep 17 00:00:00 2001 From: RFingAdam Date: Wed, 11 Feb 2026 10:45:50 -0600 Subject: [PATCH 4/4] =?UTF-8?q?Bump=20version:=204.1.3=20=E2=86=92=204.1.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- README.md | 2 +- plot_antenna/__init__.py | 2 +- pyproject.toml | 2 +- settings.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2ab88a5..a981e6a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.1.3 +current_version = 4.1.4 commit = True tag = True tag_name = v{new_version} diff --git a/README.md b/README.md index 11802e3..42c9d4c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Version + Version Python License Tests diff --git a/plot_antenna/__init__.py b/plot_antenna/__init__.py index 47789b4..37fb8ac 100644 --- a/plot_antenna/__init__.py +++ b/plot_antenna/__init__.py @@ -16,7 +16,7 @@ - Professional report generation """ -__version__ = "4.1.3" +__version__ = "4.1.4" __author__ = "Adam" __license__ = "GPL-3.0" diff --git a/pyproject.toml b/pyproject.toml index ebf4bdc..5df96fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rflect" -version = "4.1.3" +version = "4.1.4" description = "Antenna measurement visualization and analysis tool" readme = "README.md" requires-python = ">=3.11" diff --git a/settings.json b/settings.json index 89251a3..f53f198 100644 --- a/settings.json +++ b/settings.json @@ -1,3 +1,3 @@ { - "CURRENT_VERSION": "v4.1.3" + "CURRENT_VERSION": "v4.1.4" }