added s21 s11 normalisations
This commit is contained in:
@@ -1,21 +1,23 @@
|
|||||||
{
|
{
|
||||||
"open_air": false,
|
"open_air": true,
|
||||||
"subtract_mean_ascan": true,
|
"ach_norm_enabled": false,
|
||||||
|
"s11_norm_enabled": false,
|
||||||
|
"subtract_mean_ascan": false,
|
||||||
"axis": "abs",
|
"axis": "abs",
|
||||||
"cut": 0.546,
|
"cut": 0.183,
|
||||||
"max": 0.5,
|
"max": 2.0,
|
||||||
"gain": 1.0,
|
"gain": 1.5,
|
||||||
"start_freq": 400.0,
|
"start_freq": 1730.0,
|
||||||
"stop_freq": 5170.0,
|
"stop_freq": 7320.0,
|
||||||
"clear_history": false,
|
"clear_history": false,
|
||||||
"sigma": 0.54,
|
"sigma": 1.38,
|
||||||
"border_border_m": 0.09,
|
"border_border_m": 0.09,
|
||||||
"if_normalize": false,
|
"if_normalize": false,
|
||||||
"if_draw_level": false,
|
"if_draw_level": false,
|
||||||
"detection_level": 8.0,
|
"detection_level": 8.0,
|
||||||
"apply_eps_correction": true,
|
"apply_eps_correction": true,
|
||||||
"eps_r": 3.3,
|
"eps_r": 3.7,
|
||||||
"eps_boundary_m": 0.12,
|
"eps_boundary_m": 0.0,
|
||||||
"data_limit": 500,
|
"data_limit": 500,
|
||||||
"y_min": -50,
|
"y_min": -50,
|
||||||
"y_max": 40,
|
"y_max": 40,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -38,6 +39,12 @@ class BScanProcessor(BaseProcessor):
|
|||||||
|
|
||||||
# Local plot history (separate from sweep history maintained by BaseProcessor)
|
# Local plot history (separate from sweep history maintained by BaseProcessor)
|
||||||
self._plot_history: list[dict[str, Any]] = []
|
self._plot_history: list[dict[str, Any]] = []
|
||||||
|
self._ach_norm_curve: NDArray[np.complex128] | None = None
|
||||||
|
self._ach_norm_mtime: float | None = None
|
||||||
|
self._s11_norm_curve_1: NDArray[np.complex128] | None = None
|
||||||
|
self._s11_norm_curve_2: NDArray[np.complex128] | None = None
|
||||||
|
self._s11_norm_mtime_1: float | None = None
|
||||||
|
self._s11_norm_mtime_2: float | None = None
|
||||||
|
|
||||||
logger.info("BScanProcessor initialized", processor_id=self.processor_id)
|
logger.info("BScanProcessor initialized", processor_id=self.processor_id)
|
||||||
|
|
||||||
@@ -49,6 +56,8 @@ class BScanProcessor(BaseProcessor):
|
|||||||
"""Return default configuration values."""
|
"""Return default configuration values."""
|
||||||
return {
|
return {
|
||||||
"open_air": False, # Toggle for reference usage
|
"open_air": False, # Toggle for reference usage
|
||||||
|
"ach_norm_enabled": False, # Toggle ACH normalization from JSON
|
||||||
|
"s11_norm_enabled": False, # Toggle S21 normalization using two S11 files
|
||||||
"subtract_mean_ascan": False, # Subtract mean A-scan from each sweep in B-scan
|
"subtract_mean_ascan": False, # Subtract mean A-scan from each sweep in B-scan
|
||||||
"axis": "abs", # "real", "abs", or "phase"
|
"axis": "abs", # "real", "abs", or "phase"
|
||||||
# "data_limitation": None, # None, "ph_only_1", "ph_only_2"
|
# "data_limitation": None, # None, "ph_only_1", "ph_only_2"
|
||||||
@@ -65,7 +74,7 @@ class BScanProcessor(BaseProcessor):
|
|||||||
"detection_level" : 5,
|
"detection_level" : 5,
|
||||||
"apply_eps_correction": False,
|
"apply_eps_correction": False,
|
||||||
"eps_r": 4.0,
|
"eps_r": 4.0,
|
||||||
"eps_boundary_m": 0.5,
|
"eps_boundary_m": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ui_parameters(self) -> list[UIParameter]:
|
def get_ui_parameters(self) -> list[UIParameter]:
|
||||||
@@ -79,6 +88,18 @@ class BScanProcessor(BaseProcessor):
|
|||||||
type="toggle",
|
type="toggle",
|
||||||
value=cfg["open_air"],
|
value=cfg["open_air"],
|
||||||
),
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="ach_norm_enabled",
|
||||||
|
label="\u041d\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u043a\u0430 \u0410\u0427\u0425 \u043f\u043e s21",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["ach_norm_enabled"],
|
||||||
|
),
|
||||||
|
UIParameter(
|
||||||
|
name="s11_norm_enabled",
|
||||||
|
label="\u041d\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u043a\u0430 \u0410\u0427\u0425 \u043f\u043e s11",
|
||||||
|
type="toggle",
|
||||||
|
value=cfg["s11_norm_enabled"],
|
||||||
|
),
|
||||||
UIParameter(
|
UIParameter(
|
||||||
name="subtract_mean_ascan",
|
name="subtract_mean_ascan",
|
||||||
label="Вычесть средний A-скан",
|
label="Вычесть средний A-скан",
|
||||||
@@ -181,20 +202,20 @@ class BScanProcessor(BaseProcessor):
|
|||||||
# --- NEW: epsilon correction controls ---
|
# --- NEW: epsilon correction controls ---
|
||||||
UIParameter(
|
UIParameter(
|
||||||
name="apply_eps_correction",
|
name="apply_eps_correction",
|
||||||
label="Учет ε ниже границы (новая)",
|
label="Учет ε ниже границы",
|
||||||
type="toggle",
|
type="toggle",
|
||||||
value=cfg["apply_eps_correction"],
|
value=cfg["apply_eps_correction"],
|
||||||
),
|
),
|
||||||
UIParameter(
|
UIParameter(
|
||||||
name="eps_r",
|
name="eps_r",
|
||||||
label="εr ниже границы (новая)",
|
label="ε ниже границы",
|
||||||
type="slider",
|
type="slider",
|
||||||
value=cfg["eps_r"],
|
value=cfg["eps_r"],
|
||||||
options={"min": 1.0, "max": 30.0, "step": 0.1, "dtype": "float"},
|
options={"min": 1.0, "max": 30.0, "step": 0.1, "dtype": "float"},
|
||||||
),
|
),
|
||||||
UIParameter(
|
UIParameter(
|
||||||
name="eps_boundary_m",
|
name="eps_boundary_m",
|
||||||
label="Граница среды (ε) (м) (новая)",
|
label="Граница среды (ε) (м)",
|
||||||
type="slider",
|
type="slider",
|
||||||
value=cfg["eps_boundary_m"],
|
value=cfg["eps_boundary_m"],
|
||||||
options={"min": 0.0, "max": 2.5, "step": 0.01, "dtype": "float"},
|
options={"min": 0.0, "max": 2.5, "step": 0.01, "dtype": "float"},
|
||||||
@@ -312,6 +333,10 @@ class BScanProcessor(BaseProcessor):
|
|||||||
complex_data = self._subtract_reference(complex_data, reference_complex)
|
complex_data = self._subtract_reference(complex_data, reference_complex)
|
||||||
logger.debug("Applied open-air reference subtraction")
|
logger.debug("Applied open-air reference subtraction")
|
||||||
|
|
||||||
|
# Optional ACH normalization using fixed JSON file
|
||||||
|
complex_data = self._apply_ach_normalization(complex_data)
|
||||||
|
complex_data = self._apply_s11_normalization(complex_data)
|
||||||
|
|
||||||
# Keep frequency controls in sync with the current VNA config
|
# Keep frequency controls in sync with the current VNA config
|
||||||
self._update_frequency_ranges(vna_config)
|
self._update_frequency_ranges(vna_config)
|
||||||
|
|
||||||
@@ -675,6 +700,222 @@ class BScanProcessor(BaseProcessor):
|
|||||||
logger.error("Reference subtraction failed", error=repr(exc))
|
logger.error("Reference subtraction failed", error=repr(exc))
|
||||||
return signal # Non-fatal; continue with original signal
|
return signal # Non-fatal; continue with original signal
|
||||||
|
|
||||||
|
def _get_ach_norm_file_path(self) -> Path:
|
||||||
|
"""Return fixed ACH normalization JSON path."""
|
||||||
|
return Path(__file__).resolve().parents[3] / "references" / "normalisation.json"
|
||||||
|
|
||||||
|
def _apply_ach_normalization(
|
||||||
|
self,
|
||||||
|
signal: NDArray[np.complex128],
|
||||||
|
) -> NDArray[np.complex128]:
|
||||||
|
"""
|
||||||
|
Apply ACH normalization from JSON:
|
||||||
|
signal = signal / calibrated_points
|
||||||
|
"""
|
||||||
|
if not self._config.get("ach_norm_enabled", False):
|
||||||
|
return signal
|
||||||
|
|
||||||
|
norm_curve = self._load_ach_norm_curve()
|
||||||
|
if norm_curve is None or norm_curve.size == 0:
|
||||||
|
logger.warning("ACH normalization enabled but no valid calibrated_points found")
|
||||||
|
return signal
|
||||||
|
|
||||||
|
n = min(signal.size, norm_curve.size)
|
||||||
|
if n == 0:
|
||||||
|
return signal
|
||||||
|
|
||||||
|
denom = norm_curve[:n].copy()
|
||||||
|
eps = 1e-12
|
||||||
|
zero_mask = np.abs(denom) < eps
|
||||||
|
if np.any(zero_mask):
|
||||||
|
denom[zero_mask] = eps + 0j
|
||||||
|
|
||||||
|
logger.debug("Applied ACH normalization", points=n)
|
||||||
|
out = signal.copy()
|
||||||
|
denom_abs = np.abs(denom)
|
||||||
|
denom_abs[denom_abs < eps] = eps
|
||||||
|
out[:n] = out[:n] / denom_abs
|
||||||
|
# out[:n] = out[:n] / denom
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _load_ach_norm_curve(self) -> NDArray[np.complex128] | None:
|
||||||
|
"""Load normalization curve from fixed JSON file with mtime cache."""
|
||||||
|
file_path = self._get_ach_norm_file_path()
|
||||||
|
if not file_path.exists():
|
||||||
|
logger.warning("ACH normalization file not found", file=str(file_path))
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
mtime = file_path.stat().st_mtime
|
||||||
|
if self._ach_norm_curve is not None and self._ach_norm_mtime == mtime:
|
||||||
|
return self._ach_norm_curve
|
||||||
|
|
||||||
|
payload = json.loads(file_path.read_text(encoding="utf-8"))
|
||||||
|
points = self._find_last_calibrated_points(payload)
|
||||||
|
if not points:
|
||||||
|
return None
|
||||||
|
|
||||||
|
curve = self._points_to_complex(points)
|
||||||
|
if curve is None or curve.size == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._ach_norm_curve = curve
|
||||||
|
self._ach_norm_mtime = mtime
|
||||||
|
logger.info("Loaded ACH normalization curve", file=str(file_path), points=curve.size)
|
||||||
|
return curve
|
||||||
|
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("Failed to load ACH normalization file", file=str(file_path), error=repr(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_last_calibrated_points(self, node: Any) -> list[Any] | None:
|
||||||
|
"""Recursively find the latest non-empty calibrated_points list."""
|
||||||
|
found: list[Any] | None = None
|
||||||
|
|
||||||
|
if isinstance(node, dict):
|
||||||
|
local = node.get("calibrated_points")
|
||||||
|
if isinstance(local, list) and local:
|
||||||
|
found = local
|
||||||
|
for value in node.values():
|
||||||
|
nested = self._find_last_calibrated_points(value)
|
||||||
|
if nested:
|
||||||
|
found = nested
|
||||||
|
return found
|
||||||
|
|
||||||
|
if isinstance(node, list):
|
||||||
|
for item in node:
|
||||||
|
nested = self._find_last_calibrated_points(item)
|
||||||
|
if nested:
|
||||||
|
found = nested
|
||||||
|
return found
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _points_to_complex(self, points: list[Any]) -> NDArray[np.complex128] | None:
|
||||||
|
"""Convert JSON points (real, imag) into complex ndarray."""
|
||||||
|
out: list[complex] = []
|
||||||
|
for point in points:
|
||||||
|
real: Any = None
|
||||||
|
imag: Any = None
|
||||||
|
|
||||||
|
if isinstance(point, dict):
|
||||||
|
real = point.get("real", point.get("r"))
|
||||||
|
imag = point.get("imag", point.get("i"))
|
||||||
|
elif isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||||
|
real, imag = point[0], point[1]
|
||||||
|
|
||||||
|
if real is None or imag is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
out.append(complex(float(real), float(imag)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
return None
|
||||||
|
return np.asarray(out, dtype=np.complex128)
|
||||||
|
|
||||||
|
def _get_s11_norm_file_paths(self) -> tuple[Path, Path]:
|
||||||
|
"""Return fixed file paths for S11-based normalization."""
|
||||||
|
candidates = [
|
||||||
|
Path(__file__).resolve().parents[3] / "references",
|
||||||
|
Path(__file__).resolve().parents[4] / "references",
|
||||||
|
]
|
||||||
|
base_dir = next((p for p in candidates if p.exists()), candidates[0])
|
||||||
|
file_1 = base_dir / "s11_blue_bl.json"
|
||||||
|
file_2 = base_dir / "s11_blue_gr.json"
|
||||||
|
return file_1, file_2
|
||||||
|
|
||||||
|
def _apply_s11_normalization(
|
||||||
|
self,
|
||||||
|
signal: NDArray[np.complex128],
|
||||||
|
) -> NDArray[np.complex128]:
|
||||||
|
"""
|
||||||
|
Apply S11-based normalization:
|
||||||
|
s21 = s21 / sqrt((1-|s11_1|^2) * (1-|s11_2|^2))
|
||||||
|
"""
|
||||||
|
if not self._config.get("s11_norm_enabled", False):
|
||||||
|
return signal
|
||||||
|
|
||||||
|
curves = self._load_s11_norm_curves()
|
||||||
|
if curves is None:
|
||||||
|
logger.warning("S11 normalization enabled but S11 curves are unavailable")
|
||||||
|
return signal
|
||||||
|
|
||||||
|
s11_1, s11_2 = curves
|
||||||
|
n = min(signal.size, s11_1.size, s11_2.size)
|
||||||
|
if n == 0:
|
||||||
|
return signal
|
||||||
|
|
||||||
|
abs_s11_1_sq = np.abs(s11_1[:n]) ** 2
|
||||||
|
abs_s11_2_sq = np.abs(s11_2[:n]) ** 2
|
||||||
|
product = (1.0 - abs_s11_1_sq) * (1.0 - abs_s11_2_sq)
|
||||||
|
|
||||||
|
eps = 1e-12
|
||||||
|
denom = np.sqrt(np.clip(product, eps, None))
|
||||||
|
|
||||||
|
out = signal.copy()
|
||||||
|
out[:n] = out[:n] / denom
|
||||||
|
logger.debug("Applied S11 normalization", points=n)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _load_s11_norm_curves(self) -> tuple[NDArray[np.complex128], NDArray[np.complex128]] | None:
|
||||||
|
"""Load both S11 normalization curves from fixed sweep_data.json files."""
|
||||||
|
file_1, file_2 = self._get_s11_norm_file_paths()
|
||||||
|
curve_1 = self._load_sweep_points_curve(
|
||||||
|
file_path=file_1,
|
||||||
|
curve_attr="_s11_norm_curve_1",
|
||||||
|
mtime_attr="_s11_norm_mtime_1",
|
||||||
|
)
|
||||||
|
curve_2 = self._load_sweep_points_curve(
|
||||||
|
file_path=file_2,
|
||||||
|
curve_attr="_s11_norm_curve_2",
|
||||||
|
mtime_attr="_s11_norm_mtime_2",
|
||||||
|
)
|
||||||
|
|
||||||
|
if curve_1 is None or curve_2 is None:
|
||||||
|
return None
|
||||||
|
return curve_1, curve_2
|
||||||
|
|
||||||
|
def _load_sweep_points_curve(
|
||||||
|
self,
|
||||||
|
file_path: Path,
|
||||||
|
curve_attr: str,
|
||||||
|
mtime_attr: str,
|
||||||
|
) -> NDArray[np.complex128] | None:
|
||||||
|
"""Load complex curve from sweep_data.json['points'] with mtime cache."""
|
||||||
|
if not file_path.exists():
|
||||||
|
logger.warning("S11 normalization file not found", file=str(file_path))
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
mtime = file_path.stat().st_mtime
|
||||||
|
cached_curve = getattr(self, curve_attr)
|
||||||
|
cached_mtime = getattr(self, mtime_attr)
|
||||||
|
if cached_curve is not None and cached_mtime == mtime:
|
||||||
|
return cached_curve
|
||||||
|
|
||||||
|
payload = json.loads(file_path.read_text(encoding="utf-8"))
|
||||||
|
points = self._find_last_calibrated_points(payload)
|
||||||
|
if not isinstance(points, list) or not points:
|
||||||
|
logger.warning("Invalid points in S11 normalization file", file=str(file_path))
|
||||||
|
return None
|
||||||
|
|
||||||
|
curve = self._points_to_complex(points)
|
||||||
|
if curve is None or curve.size == 0:
|
||||||
|
logger.warning("Failed to parse points in S11 normalization file", file=str(file_path))
|
||||||
|
return None
|
||||||
|
|
||||||
|
setattr(self, curve_attr, curve)
|
||||||
|
setattr(self, mtime_attr, mtime)
|
||||||
|
logger.info("Loaded S11 normalization curve", file=str(file_path), points=curve.size)
|
||||||
|
return curve
|
||||||
|
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("Failed to load S11 normalization file", file=str(file_path), error=repr(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
def _update_frequency_ranges(self, vna_config: dict[str, Any]) -> None:
|
def _update_frequency_ranges(self, vna_config: dict[str, Any]) -> None:
|
||||||
"""Clamp configured frequency sliders to VNA limits."""
|
"""Clamp configured frequency sliders to VNA limits."""
|
||||||
if not vna_config:
|
if not vna_config:
|
||||||
|
|||||||
Reference in New Issue
Block a user