diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8171787..2ab88a5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.1.2 +current_version = 4.1.3 commit = True tag = True tag_name = v{new_version} diff --git a/README.md b/README.md index 8ecced8..11802e3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
-
+
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 47cd445..91ad94d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,17 @@
# RFlect - Release Notes
+## Version 4.1.3 (02/10/2026)
+
+**Patch release — coverage threshold and horizon statistics fixes.**
+
+### Bug Fixes
+- **Coverage threshold reference line**: The -3 dB reference line in conical cuts and GOA plots was hardcoded at `y=-3` on the Y-axis (meaningless for active dBm data). Now drawn relative to peak using the coverage threshold setting, and legend shows the absolute value (e.g., "-3 dB ref (17.5 dBm)")
+- **Configurable threshold**: Changing the coverage threshold setting (e.g., to -6 dB) now applies to the reference line in all maritime Cartesian plots
+- **MEG denominator bug**: Fixed incorrect array shape reference in MEG calculation (`gain_2d.shape[1]` vs `horizon_gain.shape[1]`)
+- **MEG label for active data**: Horizon statistics table now shows "Avg EIRP (sin-θ weighted)" for active power data instead of "MEG" which is only meaningful for passive gain
+
+---
+
## Version 4.1.2 (02/10/2026)
**Patch release — maritime plot title corrections.**
diff --git a/plot_antenna/__init__.py b/plot_antenna/__init__.py
index f66edc5..47789b4 100644
--- a/plot_antenna/__init__.py
+++ b/plot_antenna/__init__.py
@@ -16,7 +16,7 @@
- Professional report generation
"""
-__version__ = "4.1.2"
+__version__ = "4.1.3"
__author__ = "Adam"
__license__ = "GPL-3.0"
diff --git a/plot_antenna/plotting.py b/plot_antenna/plotting.py
index 64d4f37..8974556 100644
--- a/plot_antenna/plotting.py
+++ b/plot_antenna/plotting.py
@@ -2176,6 +2176,7 @@ def plot_conical_cuts(
theta_cuts=None,
data_label="Gain",
data_unit="dBi",
+ gain_threshold=-3.0,
polar=True,
save_path=None,
):
@@ -2235,7 +2236,20 @@ def plot_conical_cuts(
fontsize=14,
)
ax.grid(True, alpha=0.3)
- ax.axhline(y=-3, color="red", linestyle="--", alpha=0.5, linewidth=1, label="-3 dB ref")
+ # Reference line relative to peak using the coverage threshold setting
+ all_cut_data = np.concatenate(
+ [gain_2d[np.argmin(np.abs(theta_deg - tc)), :] for tc in theta_cuts]
+ )
+ peak_val = np.max(all_cut_data)
+ ref_line = peak_val + gain_threshold # gain_threshold is negative
+ ax.axhline(
+ y=ref_line,
+ color="red",
+ linestyle="--",
+ alpha=0.5,
+ linewidth=1,
+ label=f"{gain_threshold:.0f} dB ref ({ref_line:.1f} {data_unit})",
+ )
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.0), fontsize=8)
ax.grid(True, alpha=0.3)
@@ -2259,6 +2273,7 @@ def plot_gain_over_azimuth(
theta_cuts=None,
data_label="Gain",
data_unit="dBi",
+ gain_threshold=-3.0,
save_path=None,
):
"""
@@ -2281,7 +2296,18 @@ def plot_gain_over_azimuth(
cut_gain = gain_2d[theta_idx, :]
ax.plot(phi_deg, cut_gain, color=colors[i], label=f"θ={theta_cut}°", linewidth=1.5)
- ax.axhline(y=-3, color="red", linestyle="--", alpha=0.5, linewidth=1, label="-3 dB ref")
+ # Compute stats across all cuts first (used for ref line and summary)
+ all_cuts = np.concatenate([gain_2d[np.argmin(np.abs(theta_deg - tc)), :] for tc in theta_cuts])
+ peak_val = np.max(all_cuts)
+ ref_line = peak_val + gain_threshold # gain_threshold is negative
+ ax.axhline(
+ y=ref_line,
+ color="red",
+ linestyle="--",
+ alpha=0.5,
+ linewidth=1,
+ label=f"{gain_threshold:.0f} dB ref ({ref_line:.1f} {data_unit})",
+ )
ax.set_xlabel("Phi (degrees)")
ax.set_ylabel(f"{data_label} ({data_unit})")
theta_range_str = f"θ={theta_cuts[0]}–{theta_cuts[-1]}°"
@@ -2289,12 +2315,10 @@ def plot_gain_over_azimuth(
ax.legend(loc="upper right", bbox_to_anchor=(1.2, 1.0), fontsize=8)
ax.grid(True, alpha=0.3)
- # Summary annotation: max / min / avg across all cuts
- all_cuts = np.concatenate([gain_2d[np.argmin(np.abs(theta_deg - tc)), :] for tc in theta_cuts])
- max_v = np.max(all_cuts)
+ # Summary annotation reusing all_cuts computed above
min_v = np.min(all_cuts)
avg_v = 10 * np.log10(np.mean(10 ** (all_cuts / 10)))
- summary = f"Max: {max_v:.1f} {data_unit} Min: {min_v:.1f} {data_unit} Avg: {avg_v:.1f} {data_unit}"
+ summary = f"Max: {peak_val:.1f} {data_unit} Min: {min_v:.1f} {data_unit} Avg: {avg_v:.1f} {data_unit}"
ax.annotate(
summary,
xy=(0.5, -0.12),
@@ -2360,14 +2384,13 @@ 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
+ # MEG: Mean Effective Gain with sin(theta) weighting (passive only)
+ # For active power data, sin-weighted average EIRP is shown instead.
theta_rad = np.deg2rad(horizon_theta)
sin_weights = np.sin(theta_rad)
- # Broadcast sin_weights to match gain shape: (n_horizon_theta, 1) * (n_horizon_theta, n_phi)
+ n_phi = horizon_gain.shape[1]
weighted_lin = lin * sin_weights[:, np.newaxis]
- meg_lin = np.sum(weighted_lin) / np.sum(
- np.tile(sin_weights[:, np.newaxis], (1, gain_2d.shape[1]))
- )
+ 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
@@ -2384,12 +2407,21 @@ def plot_horizon_statistics(
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)"
+ else:
+ meg_label = "Avg EIRP (sin-θ weighted)"
+
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 (sin-θ weighted)", f"{meg_dB:.1f} {data_unit}"],
- [f"Coverage (>{coverage_limit:.1f} {data_unit})", f"{coverage_pct:.1f}%"],
+ [meg_label, f"{meg_dB:.1f} {data_unit}"],
+ [
+ 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}°"],
@@ -2670,6 +2702,7 @@ def generate_maritime_plots(
theta_cuts=theta_cuts,
data_label=data_label,
data_unit=data_unit,
+ gain_threshold=gain_threshold,
polar=True,
save_path=save_path,
)
@@ -2683,6 +2716,7 @@ def generate_maritime_plots(
theta_cuts=theta_cuts,
data_label=data_label,
data_unit=data_unit,
+ gain_threshold=gain_threshold,
save_path=save_path,
)
diff --git a/pyproject.toml b/pyproject.toml
index 6331b99..ebf4bdc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rflect"
-version = "4.1.2"
+version = "4.1.3"
description = "Antenna measurement visualization and analysis tool"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/settings.json b/settings.json
index 7b160ee..89251a3 100644
--- a/settings.json
+++ b/settings.json
@@ -1,3 +1,3 @@
{
- "CURRENT_VERSION": "v4.1.2"
+ "CURRENT_VERSION": "v4.1.3"
}