Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a419302d5 | ||
|
|
63138d8d9c | ||
|
|
6c1a1151bc | ||
|
|
4da9e9711a | ||
|
|
7192577e3a | ||
|
|
4897209773 | ||
|
|
80335a9983 | ||
|
|
da799aed29 | ||
|
|
14bd16ca9d | ||
|
|
3124b7141a | ||
|
|
c1f5d6580e |
@@ -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
|
||||||
BIN
Binary file not shown.
Binary file not shown.
+18
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
+16
@@ -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
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+16
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4007
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user