Compare commits

...
11 Commits
Author SHA1 Message Date
BogatskiyG 8a419302d5 optimization fix 2026-08-05 15:40:41 +03:00
sad-santa 63138d8d9c added notch toggle 2026-04-30 14:23:15 +03:00
sad-santa 6c1a1151bc hardcoded filters 2026-04-30 13:59:21 +03:00
sad-santa 4da9e9711a del one dot 2026-04-22 13:12:37 +03:00
morlo 7192577e3a add ... 2026-02-17 18:22:06 +03:00
sad-santa 4897209773 added s21 s11 normalisations 2026-02-17 17:29:24 +03:00
sad-santa 80335a9983 checkpoint before ach normalization feature 2026-02-12 13:33:44 +03:00
sad-santa da799aed29 added scan averaging and consideration of the epsilon environmen 2026-02-03 12:01:04 +03:00
mipt-user 14bd16ca9d new presets added 2025-12-03 18:41:36 +03:00
mipt-user 3124b7141a Merge branch 'master' of 10.55.228.171:/home/mipt-user/projects/vna_system 2025-11-26 13:52:11 +03:00
sad-santa c1f5d6580e added requirement 2025-11-21 19:54:05 +03:00
75 changed files with 197009 additions and 34 deletions
+1
View File
@@ -4,4 +4,5 @@ fastapi==0.117.1
numpy==2.3.3 numpy==2.3.3
matplotlib==3.10.6 matplotlib==3.10.6
plotly==6.3.0 plotly==6.3.0
scipy==1.16.3
# ??? kaleido==1.1.0 # ??? kaleido==1.1.0
@@ -0,0 +1,18 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sol-new-1m-cable_1",
"standards": [
"open",
"short",
"load"
],
"created_timestamp": "2025-12-15T14:46:10.474190",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sol-new-1m-cable_1",
"standard": "load",
"sweep_number": 23,
"sweep_timestamp": 1765799144.3783984,
"created_timestamp": "2025-12-15T14:46:10.471566",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sol-new-1m-cable_1",
"standard": "open",
"sweep_number": 21,
"sweep_timestamp": 1765799091.847419,
"created_timestamp": "2025-12-15T14:46:10.429933",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sol-new-1m-cable_1",
"standard": "short",
"sweep_number": 22,
"sweep_timestamp": 1765799117.1214204,
"created_timestamp": "2025-12-15T14:46:10.455744",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"standards": [
"through"
],
"created_timestamp": "2025-11-05T20:50:07.441271",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"standard": "through",
"sweep_number": 14,
"sweep_timestamp": 1762365005.456789,
"created_timestamp": "2025-11-05T20:50:07.440235",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 100.0
},
"calibration_name": "blue_ann_through_100hz",
"standards": [
"through"
],
"created_timestamp": "2025-12-04T15:04:35.142648",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 100.0
},
"calibration_name": "blue_ann_through_100hz",
"standard": "through",
"sweep_number": 23,
"sweep_timestamp": 1764849859.7482991,
"created_timestamp": "2025-12-04T15:04:35.140647",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 100.0
},
"calibration_name": "blue_ann_through_20hz",
"standards": [
"through"
],
"created_timestamp": "2025-12-04T15:04:56.454793",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 100.0
},
"calibration_name": "blue_ann_through_20hz",
"standard": "through",
"sweep_number": 24,
"sweep_timestamp": 1764849893.2069495,
"created_timestamp": "2025-12-04T15:04:56.453793",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_tair_451U_x2+rfc18-1m",
"standards": [
"through"
],
"created_timestamp": "2025-12-19T17:03:08.354734",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_tair_451U_x2+rfc18-1m",
"standard": "through",
"sweep_number": 4,
"sweep_timestamp": 1766152953.8570125,
"created_timestamp": "2025-12-19T17:03:08.349898",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "blue_ann_through_1khz",
"standards": [
"through"
],
"created_timestamp": "2025-12-04T15:05:28.008925",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "blue_ann_through_1khz",
"standard": "through",
"sweep_number": 25,
"sweep_timestamp": 1764849920.0909598,
"created_timestamp": "2025-12-04T15:05:28.007879",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "blueann_through_1khz",
"standards": [
"through"
],
"created_timestamp": "2025-12-04T15:03:58.727780",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "blueann_through_1khz",
"standard": "through",
"sweep_number": 22,
"sweep_timestamp": 1764849814.6352062,
"created_timestamp": "2025-12-04T15:03:58.726778",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "thought-25-12-15",
"standards": [
"through"
],
"created_timestamp": "2025-12-15T13:36:06.578246",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "thought-25-12-15",
"standard": "through",
"sweep_number": 31,
"sweep_timestamp": 1765794952.1075795,
"created_timestamp": "2025-12-15T13:36:06.577088",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"standards": [
"through"
],
"created_timestamp": "2025-11-05T20:50:07.441271",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"mode": "s21",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"standard": "through",
"sweep_number": 14,
"sweep_timestamp": 1762365005.456789,
"created_timestamp": "2025-11-05T20:50:07.440235",
"total_points": 1000
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start400_stop6000_points1000_bw20hz.bin",
"mode": "s21",
"start_freq": 400000000.0,
"stop_freq": 6000000000.0,
"points": 1000,
"bandwidth": 20.0
},
"calibration_name": "blue_through_20hz",
"standards": [
"through"
],
"created_timestamp": "2025-12-05T19:15:54.254887",
"is_complete": true
}
@@ -0,0 +1,16 @@
{
"preset": {
"filename": "s21_start400_stop6000_points1000_bw20hz.bin",
"mode": "s21",
"start_freq": 400000000.0,
"stop_freq": 6000000000.0,
"points": 1000,
"bandwidth": 20.0
},
"calibration_name": "blue_through_20hz",
"standard": "through",
"sweep_number": 4,
"sweep_timestamp": 1764949677.0854876,
"created_timestamp": "2025-12-05T19:15:54.252679",
"total_points": 1000
}
+1 -1
View File
@@ -33,7 +33,7 @@ VNA_VID = 0x0483 # STMicroelectronics
VNA_PID = 0x5740 # STM32 Virtual ComPort VNA_PID = 0x5740 # STM32 Virtual ComPort
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Simulator mode settings # Simulator mode settings..
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
USE_SIMULATOR = True # Set to True to use simulator instead of real device USE_SIMULATOR = True # Set to True to use simulator instead of real device
SIMULATOR_SWEEP_FILE = BASE_DIR / "binary_input" / "sweep_example" / "example.json" SIMULATOR_SWEEP_FILE = BASE_DIR / "binary_input" / "sweep_example" / "example.json"
+7 -1
View File
@@ -894,7 +894,13 @@ class BaseProcessor:
"timestamp": entry.get("timestamp"), "timestamp": entry.get("timestamp"),
}) })
logger.info("History imported", processor_id=self.processor_id, records=len(history_data)) self._trim_history()
logger.info(
"History imported",
processor_id=self.processor_id,
records=len(history_data),
kept=len(self._sweep_history),
)
@staticmethod @staticmethod
def _points_to_list(points: Any) -> list[tuple[float, float]]: def _points_to_list(points: Any) -> list[tuple[float, float]]:
@@ -1,17 +1,24 @@
{ {
"open_air": true, "open_air": false,
"axis": "abs", "ach_norm_enabled": false,
"cut": 0.2, "s11_norm_enabled": false,
"max": 1.5, "subtract_mean_ascan": false,
"gain": 1.0, "hardcoded_notch_enabled": false,
"start_freq": 400.0, "axis": "real",
"stop_freq": 3000.0, "cut": 0.0,
"max": 3.0,
"gain": 1.5,
"start_freq": 1730.0,
"stop_freq": 6000.0,
"clear_history": false, "clear_history": false,
"sigma": 2.0, "sigma": 1.38,
"border_border_m": 0.3, "border_border_m": 0.09,
"if_normalize": true, "if_normalize": false,
"if_draw_level": true, "if_draw_level": false,
"detection_level": 8.0, "detection_level": 8.0,
"apply_eps_correction": true,
"eps_r": 3.7,
"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
@@ -15,6 +16,31 @@ from vna_system.core.config import SPEED_OF_LIGHT_M_S
logger = get_component_logger(__file__) logger = get_component_logger(__file__)
# -----------------------------------------------------------------------------
# Hardcoded notch filter configuration (Hz)
# Keep this list simple for manual on/off by commenting lines.
# If list is empty, filter is bypassed safely.
# -----------------------------------------------------------------------------
_HARDCODED_NOTCH_BANDS_HZ: list[tuple[float, float]] = [
(521805341, 559802333),
(565570894, 597568846),
(654160139, 726852100),
(774625224, 866509567),
(923844514, 961841506),
(1710023801, 1762573222),
(1830537144, 1889723898),
(1976018004, 2008015956),
(2095044545, 2133041601),
(2168370432, 2228168766),
(2251665672, 2277664776),
(2308722546, 2346719602),
(2373748986, 2405746938),
(2491168130, 2578479270),
(2647818717, 2698980425),
]
_HARDCODED_NOTCH_TAPER_WIDTH_HZ = 40e6
_HARDCODED_NOTCH_TAPER_TYPE = "cosine" # "cosine" or "hard"
class BScanProcessor(BaseProcessor): class BScanProcessor(BaseProcessor):
""" """
@@ -34,10 +60,16 @@ class BScanProcessor(BaseProcessor):
super().__init__("bscan", config_dir) super().__init__("bscan", config_dir)
# Increase history size for multi-sweep plotting # Increase history size for multi-sweep plotting
self._max_history = 50 self._max_history = 1000
# 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 +81,10 @@ 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
"hardcoded_notch_enabled": True, # Enable/disable hardcoded notch filtering
"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"
"cut": 0.824, # Cut parameter (meters) "cut": 0.824, # Cut parameter (meters)
@@ -62,6 +98,9 @@ class BScanProcessor(BaseProcessor):
"if_normalize" : False, "if_normalize" : False,
"if_draw_level" : False, "if_draw_level" : False,
"detection_level" : 5, "detection_level" : 5,
"apply_eps_correction": False,
"eps_r": 4.0,
"eps_boundary_m": 0.0,
} }
def get_ui_parameters(self) -> list[UIParameter]: def get_ui_parameters(self) -> list[UIParameter]:
@@ -75,6 +114,30 @@ 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(
name="subtract_mean_ascan",
label="Вычесть средний A-скан",
type="toggle",
value=cfg["subtract_mean_ascan"],
),
UIParameter(
name="hardcoded_notch_enabled",
label="Notch filter",
type="toggle",
value=cfg["hardcoded_notch_enabled"],
),
UIParameter( UIParameter(
name="axis", name="axis",
label="Ось", label="Ось",
@@ -101,7 +164,7 @@ class BScanProcessor(BaseProcessor):
label="Макс. глубина (м)", label="Макс. глубина (м)",
type="slider", type="slider",
value=cfg["max"], value=cfg["max"],
options={"min": 0.1, "max": 5.0, "step": 0.1, "dtype": "float"}, options={"min": 0.1, "max": 15.0, "step": 0.1, "dtype": "float"},
), ),
UIParameter( UIParameter(
name="gain", name="gain",
@@ -168,6 +231,27 @@ class BScanProcessor(BaseProcessor):
value=cfg["clear_history"], value=cfg["clear_history"],
options={"action": "Очистить накопленную историю графика"}, options={"action": "Очистить накопленную историю графика"},
), ),
# --- NEW: epsilon correction controls ---
UIParameter(
name="apply_eps_correction",
label="Учет ε ниже границы",
type="toggle",
value=cfg["apply_eps_correction"],
),
UIParameter(
name="eps_r",
label="ε ниже границы",
type="slider",
value=cfg["eps_r"],
options={"min": 1.0, "max": 30.0, "step": 0.1, "dtype": "float"},
),
UIParameter(
name="eps_boundary_m",
label="Граница среды (ε) (м)",
type="slider",
value=cfg["eps_boundary_m"],
options={"min": 0.0, "max": 2.5, "step": 0.01, "dtype": "float"},
),
] ]
def update_config(self, updates: dict[str, Any]) -> None: def update_config(self, updates: dict[str, Any]) -> None:
@@ -236,6 +320,45 @@ class BScanProcessor(BaseProcessor):
# Processing # Processing
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def add_sweep_data(
self,
sweep_data: Any,
calibrated_data: Any,
vna_config: Any,
reference_data: Any = None,
reference_info: Any = None,
raw_reference_data: Any = None,
calibration_standards: dict | None = None,
) -> ProcessedResult | None:
"""
Add the latest sweep and process it incrementally.
Overridden to avoid BaseProcessor's default of calling `self.recalculate()`,
which for BScanProcessor rebuilds the *entire* plot history from scratch.
Doing that on every incoming sweep is O(history_size) per sweep, so with a
large `_max_history` a live stream turns into O(n^2) work and floods the
websocket with full-heatmap payloads on every sweep. Here we only process
the new sweep and append it to the existing plot history; a full rebuild
is still done by `recalculate()` when the user changes config or loads history.
"""
with self._lock:
self._sweep_history.append(
{
"sweep_data": sweep_data,
"calibrated_data": calibrated_data,
"vna_config": self._snapshot_vna_config(vna_config),
"reference_data": reference_data,
"reference_info": reference_info,
"raw_reference_data": raw_reference_data,
"calibration_standards": calibration_standards,
"timestamp": datetime.now().timestamp(),
}
)
self._trim_history()
latest_vna_config = self._sweep_history[-1]["vna_config"]
return self._process_data(sweep_data, calibrated_data, latest_vna_config)
def process_sweep( def process_sweep(
self, self,
sweep_data: SweepData, sweep_data: SweepData,
@@ -281,6 +404,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)
@@ -310,8 +437,11 @@ class BScanProcessor(BaseProcessor):
all_sweep_numbers = list(range(1, len(self._plot_history) + 1)) all_sweep_numbers = list(range(1, len(self._plot_history) + 1))
all_timestamps = [record["timestamp"] for record in self._plot_history] all_timestamps = [record["timestamp"] for record in self._plot_history]
adjusted_time_domain = self._apply_mean_ascan_subtraction(all_time_domain)
latest_time_domain = adjusted_time_domain[-1] if adjusted_time_domain else analysis["time_data"].tolist()
return { return {
"time_domain_data": analysis["time_data"].tolist(), # Latest sweep "time_domain_data": latest_time_domain, # Latest sweep (after mean subtraction if enabled)
"distance_data": analysis["distance"].tolist(), # Latest sweep "distance_data": analysis["distance"].tolist(), # Latest sweep
"frequency_range": analysis["freq_range"], "frequency_range": analysis["freq_range"],
"reference_used": bool(self._config["open_air"] and reference_data is not None), "reference_used": bool(self._config["open_air"] and reference_data is not None),
@@ -319,7 +449,7 @@ class BScanProcessor(BaseProcessor):
"points_processed": int(complex_data.size), "points_processed": int(complex_data.size),
"plot_history_count": len(self._plot_history), "plot_history_count": len(self._plot_history),
# Full history data # Full history data
"all_time_domain_data": all_time_domain, "all_time_domain_data": adjusted_time_domain,
"all_distance_data": all_distance, "all_distance_data": all_distance,
"all_sweep_numbers": all_sweep_numbers, "all_sweep_numbers": all_sweep_numbers,
"all_timestamps": all_timestamps, "all_timestamps": all_timestamps,
@@ -383,9 +513,12 @@ class BScanProcessor(BaseProcessor):
z_values: list[float] = [] z_values: list[float] = []
z_values_square = np.zeros((len(history[0]["distance_data"]),len(history)),dtype=float) z_values_square = np.zeros((len(history[0]["distance_data"]),len(history)),dtype=float)
time_series = [record["time_domain_data"] for record in history]
adjusted_time_series = self._apply_mean_ascan_subtraction(time_series)
for sweep_index, item in enumerate(history, start=1): for sweep_index, item in enumerate(history, start=1):
depths = item["distance_data"] depths = item["distance_data"]
amps = item["time_domain_data"] amps = adjusted_time_series[sweep_index - 1] if sweep_index - 1 < len(adjusted_time_series) else item["time_domain_data"]
if self._config['if_normalize']: if self._config['if_normalize']:
depth_mask = np.array(depths) < Y_VALUE depth_mask = np.array(depths) < Y_VALUE
@@ -492,6 +625,25 @@ class BScanProcessor(BaseProcessor):
} }
] ]
if self._config.get("apply_eps_correction", False):
eps_boundary = float(self._config.get("eps_boundary_m", 0.0))
layout["shapes"] = layout.get("shapes", []) + [
{
"type": "line",
"xref": "paper",
"yref": "y",
"x0": 0,
"x1": 1,
"y0": eps_boundary,
"y1": eps_boundary,
"line": {
"width": 2,
"dash": "dot",
"color": "#00E5FF",
},
}
]
if detected_trace is not None: if detected_trace is not None:
return {"data": [heatmap_trace,detected_trace], "layout": layout} return {"data": [heatmap_trace,detected_trace], "layout": layout}
return {"data": [heatmap_trace], "layout": layout} return {"data": [heatmap_trace], "layout": layout}
@@ -619,6 +771,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:
@@ -640,6 +1008,59 @@ class BScanProcessor(BaseProcessor):
self._config["start_freq"] = new_start self._config["start_freq"] = new_start
self._config["stop_freq"] = new_stop self._config["stop_freq"] = new_stop
def _apply_hardcoded_notch_filter(
self,
s_complex: NDArray[np.complex128],
freq_hz: NDArray[np.floating],
) -> tuple[NDArray[np.complex128], NDArray[np.floating]]:
"""
Apply manual hardcoded notch filtering in frequency domain.
Notes
-----
- Designed for simple manual editing of frequency bands.
- If `_HARDCODED_NOTCH_BANDS_HZ` is empty, this is a passthrough.
"""
mask = np.ones_like(freq_hz, dtype=float)
bands = _HARDCODED_NOTCH_BANDS_HZ
if not bool(self._config.get("hardcoded_notch_enabled", True)):
return s_complex, mask
if not bands:
return s_complex, mask
taper_width_hz = float(_HARDCODED_NOTCH_TAPER_WIDTH_HZ)
taper_type = str(_HARDCODED_NOTCH_TAPER_TYPE).lower()
if taper_width_hz <= 0.0:
taper_type = "hard"
for f_low, f_high in bands:
trans_low = f_low - taper_width_hz
trans_high = f_high + taper_width_hz
inside = (freq_hz >= f_low) & (freq_hz <= f_high)
left_edge = (freq_hz >= trans_low) & (freq_hz < f_low)
right_edge = (freq_hz > f_high) & (freq_hz <= trans_high)
if taper_type == "cosine":
# Left transition: 1 -> 0
arg_l = np.pi * (freq_hz[left_edge] - trans_low) / taper_width_hz
mask[left_edge] *= 0.5 * (1.0 + np.cos(arg_l))
# Right transition: 0 -> 1
arg_r = np.pi * (freq_hz[right_edge] - f_high) / taper_width_hz
mask[right_edge] *= 0.5 * (1.0 - np.cos(arg_r))
mask[inside] = 0.0
else:
mask[inside] = 0.0
mask[left_edge] = 0.0
mask[right_edge] = 0.0
return s_complex * mask, mask
def _perform_data_analysis( def _perform_data_analysis(
self, self,
complex_data: NDArray[np.complex128], complex_data: NDArray[np.complex128],
@@ -666,6 +1087,9 @@ class BScanProcessor(BaseProcessor):
# Frequency vector over current data length # Frequency vector over current data length
freq_axis = np.linspace(freq_start, freq_stop, complex_data.size, dtype=float) freq_axis = np.linspace(freq_start, freq_stop, complex_data.size, dtype=float)
# Hardcoded frequency-domain notch filter (manual on/off via comments)
complex_data, _ = self._apply_hardcoded_notch_filter(complex_data, freq_axis)
# print("freq_axis", freq_axis.shape, freq_axis[0], freq_axis[-1]) # print("freq_axis", freq_axis.shape, freq_axis[0], freq_axis[-1])
# Optionally normalize amplitude (phase-only modes) # Optionally normalize amplitude (phase-only modes)
@@ -786,6 +1210,59 @@ class BScanProcessor(BaseProcessor):
depth_fallback = np.linspace(0.0, 1.0, s_array.size, dtype=float) depth_fallback = np.linspace(0.0, 1.0, s_array.size, dtype=float)
return depth_fallback, np.abs(s_array).astype(float, copy=False) return depth_fallback, np.abs(s_array).astype(float, copy=False)
def _apply_eps_depth_correction(
self,
depth_out: NDArray[np.floating],
) -> NDArray[np.floating]:
"""
NEW FEATURE:
Compress depth axis below eps_boundary_m using epsilon.
Inputs
------
depth_out:
One-way depth axis (meters). This is what you later plot on Y.
Config keys used
---------------
apply_eps_correction: bool
If False -> do nothing.
eps_r: float
Relative permittivity below boundary. Compression factor = 1/sqrt(eps_r).
eps_boundary_m: float
Depth (m) where the medium changes.
Mapping
-------
d0 = eps_boundary_m
if d <= d0: unchanged
if d > d0: d_corr = d0 + (d - d0)/sqrt(eps_r)
"""
# 1) Check toggle
if not self._config.get("apply_eps_correction", False):
return depth_out
# 2) Read eps_r
eps_r = float(self._config.get("eps_r", 1.0))
if eps_r <= 1.0:
return depth_out
# 3) Read epsilon boundary depth
d0 = float(self._config.get("eps_boundary_m", 0.0))
# 4) Boundary at/under 0 -> compress everything
if d0 <= 0.0:
return depth_out / np.sqrt(eps_r)
# 5) Copy to avoid mutating input array
depth_corr = depth_out.astype(float, copy=True)
# 6) Apply only below boundary
mask = depth_corr > d0
depth_corr[mask] = d0 + (depth_corr[mask] - d0) / np.sqrt(eps_r)
return depth_corr
def _apply_depth_processing( def _apply_depth_processing(
self, self,
depth_m: NDArray[np.floating], depth_m: NDArray[np.floating],
@@ -824,6 +1301,8 @@ class BScanProcessor(BaseProcessor):
# Convert to one-way depth relative to the cut # Convert to one-way depth relative to the cut
depth_out = (depth_win - lo) / 2.0 depth_out = (depth_win - lo) / 2.0
depth_out = self._apply_eps_depth_correction(depth_out)
# Depth-dependent gain (safe for zero depth with exponent >= 0) # Depth-dependent gain (safe for zero depth with exponent >= 0)
with np.errstate(invalid="ignore"): with np.errstate(invalid="ignore"):
@@ -838,6 +1317,31 @@ class BScanProcessor(BaseProcessor):
logger.error("Depth processing failed", error=repr(exc)) logger.error("Depth processing failed", error=repr(exc))
return depth_m, response return depth_m, response
def _apply_mean_ascan_subtraction(
self,
time_domain_series: list[list[float]],
) -> list[list[float]]:
"""
Subtract mean A-scan (across sweeps) from each A-scan in the B-scan.
This operates on time-domain data after reference subtraction and IFFT.
"""
if not self._config.get("subtract_mean_ascan", False):
return time_domain_series
lengths = {len(series) for series in time_domain_series}
if len(lengths) != 1:
logger.warning(
"Mean A-scan subtraction skipped due to inconsistent lengths",
lengths=sorted(lengths),
)
return time_domain_series
data = np.asarray(time_domain_series, dtype=float)
mean_trace = np.mean(data, axis=0)
adjusted = data - mean_trace
return [row.tolist() for row in adjusted]
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# State export override # State export override
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -862,7 +1366,7 @@ class BScanProcessor(BaseProcessor):
all_timestamps = [record["timestamp"] for record in self._plot_history] all_timestamps = [record["timestamp"] for record in self._plot_history]
state["plot_data"] = { state["plot_data"] = {
"all_time_domain_data": all_time_domain, "all_time_domain_data": self._apply_mean_ascan_subtraction(all_time_domain),
"all_distance_data": all_distance, "all_distance_data": all_distance,
"all_sweep_numbers": all_sweep_numbers, "all_sweep_numbers": all_sweep_numbers,
"all_timestamps": all_timestamps, "all_timestamps": all_timestamps,
+36 -14
View File
@@ -38,6 +38,16 @@ class ProcessorWebSocketHandler:
self.active_connections: set[WebSocket] = set() self.active_connections: set[WebSocket] = set()
# Starlette's WebSocket.send() is not safe to call concurrently from
# multiple tasks on the same connection (it corrupts internal ASGI
# protocol state, raising AssertionError and killing the connection).
# Broadcasts triggered by unrelated processor results (scheduled from
# worker threads) can otherwise race with a direct reply to a client
# command, especially once a payload is large enough to take a while
# to transmit (e.g. a big B-scan history). Serialize all sends per
# connection with a lock to eliminate that race.
self._send_locks: dict[WebSocket, asyncio.Lock] = {}
# Main FastAPI/uvicorn event loop handle (set on first connection). # Main FastAPI/uvicorn event loop handle (set on first connection).
self._loop: asyncio.AbstractEventLoop | None = None self._loop: asyncio.AbstractEventLoop | None = None
@@ -138,10 +148,16 @@ class ProcessorWebSocketHandler:
return return
try: try:
result = self.processor_manager.recalculate_processor(processor_id, config_updates) # Note: recalculate_processor() already broadcasts the result to every
if result: # connected client (including this one) via the manager's result
await websocket.send_text(json.dumps(self._result_to_message(processor_id, result))) # callbacks, so we must NOT also send it here directly — two
else: # concurrent writes to the same connection (the broadcast task and
# this handler) can race on the ASGI transport and corrupt/close it,
# especially with large payloads that take a while to transmit.
result = await asyncio.to_thread(
self.processor_manager.recalculate_processor, processor_id, config_updates
)
if not result:
await self._send_error(websocket, f"Нет результата от процессора {processor_id}") await self._send_error(websocket, f"Нет результата от процессора {processor_id}")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Recalculation failed") logger.error("Recalculation failed")
@@ -197,10 +213,14 @@ class ProcessorWebSocketHandler:
return return
try: try:
result = self.processor_manager.load_processor_history(processor_id, history_data, config) # See note in _handle_recalculate: load_processor_history() already
if result: # broadcasts the result to all clients via result callbacks, so no
await websocket.send_text(json.dumps(self._result_to_message(processor_id, result))) # explicit send here (avoids racing two concurrent writes on the
else: # same connection, which reliably crashes it for large histories).
result = await asyncio.to_thread(
self.processor_manager.load_processor_history, processor_id, history_data, config
)
if not result:
await self._send_error(websocket, f"Нет результата от процессора {processor_id} после загрузки истории") await self._send_error(websocket, f"Нет результата от процессора {processor_id} после загрузки истории")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("History load failed", processor_id=processor_id, error=repr(exc)) logger.error("History load failed", processor_id=processor_id, error=repr(exc))
@@ -224,10 +244,12 @@ class ProcessorWebSocketHandler:
return return
try: try:
result = self.processor_manager.append_processor_history(processor_id, history_data) # See note in _handle_recalculate: append_processor_history() already
if result: # broadcasts the result to all clients via result callbacks.
await websocket.send_text(json.dumps(self._result_to_message(processor_id, result))) result = await asyncio.to_thread(
else: self.processor_manager.append_processor_history, processor_id, history_data
)
if not result:
await self._send_error(websocket, f"Нет результата от процессора {processor_id} после дополнения истории") await self._send_error(websocket, f"Нет результата от процессора {processor_id} после дополнения истории")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("History append failed", processor_id=processor_id, error=repr(exc)) logger.error("History append failed", processor_id=processor_id, error=repr(exc))
@@ -247,7 +269,7 @@ class ProcessorWebSocketHandler:
return return
try: try:
response = self.processor_manager.build_processor_state(processor_id) response = await asyncio.to_thread(self.processor_manager.build_processor_state, processor_id)
await websocket.send_text(json.dumps(response)) await websocket.send_text(json.dumps(response))
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Error getting processor state", processor_id=processor_id, error=repr(exc)) logger.error("Error getting processor state", processor_id=processor_id, error=repr(exc))
@@ -293,7 +315,7 @@ class ProcessorWebSocketHandler:
return return
# Recalculate and send updated result to all clients # Recalculate and send updated result to all clients
result = processor.recalculate() result = await asyncio.to_thread(processor.recalculate)
if result: if result:
# Broadcast to all connected clients # Broadcast to all connected clients
message_str = json.dumps(self._result_to_message(processor_id, result)) message_str = json.dumps(self._result_to_message(processor_id, result))
+1 -1
View File
@@ -363,7 +363,7 @@ class VNASettingsManager:
return summary return summary
def _wait_for_new_sweep(self, sweep_buffer, current_sweep_number: int, timeout_seconds: float = 5.0) -> SweepData | None: def _wait_for_new_sweep(self, sweep_buffer, current_sweep_number: int, timeout_seconds: float = 25.0) -> SweepData | None:
""" """
Wait for a new sweep to appear in the buffer with a higher sweep number. Wait for a new sweep to appear in the buffer with a higher sweep number.
+4
View File
@@ -99,6 +99,10 @@ def main() -> None:
port=port, port=port,
log_level="info", log_level="info",
reload=False, reload=False,
# Default 16MB is too small for uploading large B-scan history files
# (hundreds of sweeps x 1000 points each easily exceeds it); the
# connection is silently reset with no application-level error.
ws_max_size=256 * 1024 * 1024,
) )
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
{
"name": "blue_antenna_100hz",
"timestamp": "2025-12-04T15:17:04.559000",
"preset_filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "blue_ann_through_100hz",
"preset_filename": "s21_start100_stop8800_points1000_bw100hz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "openair",
"timestamp": "2025-12-03T18:01:54.377387",
"preset_filename": "s21_start100_stop8800_points1000_bw100hz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"preset_filename": "s21_start100_stop8800_points1000_bw100hz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "11-21-b",
"timestamp": "2025-11-21T14:35:28.246450",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_noamp_451U_x2",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "2025-11-12_b",
"timestamp": "2025-11-12T18:10:13.091024",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_noamp_451U_x2",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "2026-01-30_floor",
"timestamp": "2026-01-30T16:56:51.990616",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "25-11-13",
"timestamp": "2025-11-13T16:30:36.164757",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_noamp_451U_x2",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "25-11-14",
"timestamp": "2025-11-14T12:41:59.918674",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_noamp_451U_x2",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "blue_antenna_1khz",
"timestamp": "2025-12-04T15:16:19.336172",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "blue_ann_through_1khz",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "blue_n2",
"timestamp": "2026-01-29T18:36:21.755535",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "green_ann_1khz",
"timestamp": "2025-12-05T20:43:59.532520",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "blue_ann_through_1khz",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "green_open_air_after-fix_wide-config",
"timestamp": "2025-12-15T13:43:49.637105",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "thought-25-12-15",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "long_cable_green_ann",
"timestamp": "2025-12-19T17:09:30.725124",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_tair_451U_x2+rfc18-1m",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "open_air_green_213",
"timestamp": "2025-12-12T17:55:58.564470",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "blue_ann_through_1khz",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "blue_antenna_20hz",
"timestamp": "2025-12-05T19:24:34.272799",
"preset_filename": "s21_start400_stop6000_points1000_bw20hz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "blue_through_20hz",
"preset_filename": "s21_start400_stop6000_points1000_bw20hz.bin"
}
}
}
@@ -0,0 +1,12 @@
{
"name": "opair",
"timestamp": "2025-12-03T18:29:32.724722",
"preset_filename": "s21_start400_stop6000_points1000_bw20hz.bin",
"description": "",
"metadata": {
"calibration": {
"calibration_name": "T_N2_451U_x2_noamp_20251105",
"preset_filename": "s21_start400_stop6000_points1000_bw20hz.bin"
}
}
}