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 @@
-
+
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"
}