Rename kamil_adc to rph_radar and fix its data path

This commit is contained in:
Ayzen
2026-10-06 18:15:18 +03:00
parent aef549ea76
commit a185e61495
94 changed files with 2851 additions and 9485 deletions
+33 -68
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
import hashlib
import json
import logging
@@ -45,7 +45,7 @@ class RadarMultiDeviceModel:
"""Multi-device LibreVNA topology settings."""
slave_serials: list[str] = field(default_factory=list)
force_external_reference: bool = True
force_external_reference: bool = False
recovery_attempts: int = 3
output_switch_positions: int = 1 # 1 = свитча нет
input_switch_positions: int = 1 # 1 = свитча нет
@@ -56,7 +56,7 @@ class RadarMultiDeviceModel:
@dataclass(slots=True)
class KamilAdcPhaseCalibrationModel:
class RphRadarPhaseCalibrationModel:
"""Affine law mapping the reference signal's unwrapped phase to frequency.
Two fixed anchor points ``(phase0_rad, freq0_hz)`` and ``(phase1_rad, freq1_hz)``
@@ -73,7 +73,7 @@ class KamilAdcPhaseCalibrationModel:
@dataclass(slots=True)
class KamilAdcBandModel:
class RphRadarBandModel:
"""Fixed frequency window every sweep is cropped to and resampled onto.
Each sweep is resampled onto ``linspace(start_hz, stop_hz, points)`` so all
@@ -88,8 +88,8 @@ class KamilAdcBandModel:
@dataclass(slots=True)
class KamilAdcModel:
"""External Kamil ADC acquisition process settings."""
class RphRadarModel:
"""External RadioPhotonic radar (RPH) acquisition process settings."""
project_dir: str = ""
executable_path: str = ""
@@ -99,10 +99,10 @@ class KamilAdcModel:
startup_timeout_s: float = 5.0
sweep_timeout_s: float = 5.0
stop_timeout_s: float = 2.0
phase_calibration: KamilAdcPhaseCalibrationModel = field(
default_factory=KamilAdcPhaseCalibrationModel
phase_calibration: RphRadarPhaseCalibrationModel = field(
default_factory=RphRadarPhaseCalibrationModel
)
band: KamilAdcBandModel = field(default_factory=KamilAdcBandModel)
band: RphRadarBandModel = field(default_factory=RphRadarBandModel)
@dataclass(slots=True)
@@ -129,14 +129,11 @@ class LaserVariationModeModel:
step: float = 0.1
time_step: int = 20
delay_time: int = 3
# Max allowed |measured - target| laser temperature before the temperature
# checker warns, °C. Applied independently to both lasers (temp1/temp2).
temp_tolerance_c: float = 0.03
@dataclass(slots=True)
class LaserControlModel:
"""Laser-control board settings applied before Kamil ADC acquisition."""
"""Laser-control board settings applied before RadioPhotonic radar (RPH) acquisition."""
enabled: bool = False
port: str = ""
@@ -158,7 +155,7 @@ class RadarModel:
driver_mode: str = "mock"
sweep: RadarSweepModel = field(default_factory=RadarSweepModel)
multi_device: RadarMultiDeviceModel = field(default_factory=RadarMultiDeviceModel)
kamil_adc: KamilAdcModel = field(default_factory=KamilAdcModel)
rph_radar: RphRadarModel = field(default_factory=RphRadarModel)
laser_control: LaserControlModel = field(default_factory=LaserControlModel)
@@ -337,7 +334,7 @@ class RunConfigModel:
LIBREVNA_MODEL = "librevna"
LIBREVNA_MULTI_MODEL = "librevna_multi"
KAMIL_ADC_MODEL = "kamil_adc"
RPH_RADAR_MODEL = "rph_radar"
MULTI_DEVICE_INPUT_POSITIONS = 4
MULTI_DEVICE_OUTPUT_POSITIONS = 2
# Stimulus ports the master itself owns. The virtual output axis is this times the
@@ -400,72 +397,40 @@ class RunConfigModel:
def build_runtime_combos(self) -> list[ComboModel]:
"""Build the combo matrix from the effective switch axis sizes."""
return self.build_full_combos(self.input_switch.positions, self.output_switch.positions)
@property
def is_kamil_adc(self) -> bool:
"""Return whether this config targets the external Kamil ADC acquisition path."""
return self.radar.model == self.KAMIL_ADC_MODEL
def is_rph_radar(self) -> bool:
"""Return whether this config targets the external RadioPhotonic radar (RPH) acquisition path."""
return self.radar.model == self.RPH_RADAR_MODEL
def radar_key(self) -> str:
"""Stable hardware/sweep identity used to find saved preprocess sets."""
radar, sweep = self.radar, self.radar.sweep
if self.is_rph_radar:
laser = radar.laser_control
mode = laser.mode.strip().lower()
optical = asdict(laser.variation if mode == "variation" else laser.manual)
physical = (asdict(radar.rph_radar.band), asdict(radar.rph_radar.phase_calibration),
radar.rph_radar.args, (self.input_switch.positions, self.output_switch.positions),
(mode, optical, laser.pi_coeff1_p, laser.pi_coeff1_i,
laser.pi_coeff2_p, laser.pi_coeff2_i) if laser.enabled else None)
digest = hashlib.sha256(json.dumps(physical, sort_keys=True).encode()).hexdigest()[:16]
return f"{radar.model}_{digest}"
serials = [radar.serial or "no_serial"]
serials.extend(value.strip() or "no_serial" for value in self._radar_key_extra_parts())
if self.is_multi_device:
serials.extend(value.strip() or "no_serial" for value in radar.multi_device.slave_serials)
serial = "_".join(sanitize_path_component(value) for value in serials)
points = "adc" if self.is_kamil_adc else str(int(sweep.points))
points = str(int(sweep.points))
# Switch positions decide what each combo index means, so sets from another matrix never match.
matrix = (f"_out{self.matrix_output_switch_positions}_in{self.matrix_input_switch_positions}"
f"_stim{self.matrix_stimulus_switch_positions}") if self.is_multi_device else ""
return (
f"{radar.model}_{serial}"
f"_st{_format_float_for_key(sweep.start_hz)}_sp{_format_float_for_key(sweep.stop_hz)}"
f"_p{points}_if{_format_float_for_key(sweep.if_bandwidth_hz)}"
f"_pw{_format_float_for_key(sweep.power_dbm)}"
f"_pw{_format_float_for_key(sweep.power_dbm)}{matrix}"
)
def _radar_key_extra_parts(self) -> list[str]:
"""Return model-specific identity parts that affect captured data."""
if self.is_multi_device:
return list(self.radar.multi_device.slave_serials)
if self.is_kamil_adc:
payload = {
"kamil_adc": {
"project_dir": self.radar.kamil_adc.project_dir,
"executable_path": self.radar.kamil_adc.executable_path,
"tty_path": self.radar.kamil_adc.tty_path,
"args": list(self.radar.kamil_adc.args),
"env": dict(sorted(self.radar.kamil_adc.env.items())),
},
"laser_control": {
"enabled": self.radar.laser_control.enabled,
"port": self.radar.laser_control.port,
"mode": self.radar.laser_control.mode,
"pi_coeff1_p": self.radar.laser_control.pi_coeff1_p,
"pi_coeff1_i": self.radar.laser_control.pi_coeff1_i,
"pi_coeff2_p": self.radar.laser_control.pi_coeff2_p,
"pi_coeff2_i": self.radar.laser_control.pi_coeff2_i,
"manual": {
"temp1": self.radar.laser_control.manual.temp1,
"temp2": self.radar.laser_control.manual.temp2,
"current1": self.radar.laser_control.manual.current1,
"current2": self.radar.laser_control.manual.current2,
},
"variation": {
"variation_type": self.radar.laser_control.variation.variation_type,
"static_temp1": self.radar.laser_control.variation.static_temp1,
"static_temp2": self.radar.laser_control.variation.static_temp2,
"static_current1": self.radar.laser_control.variation.static_current1,
"static_current2": self.radar.laser_control.variation.static_current2,
"min_value": self.radar.laser_control.variation.min_value,
"max_value": self.radar.laser_control.variation.max_value,
"step": self.radar.laser_control.variation.step,
"time_step": self.radar.laser_control.variation.time_step,
"delay_time": self.radar.laser_control.variation.delay_time,
},
},
}
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"))
digest = hashlib.sha256(encoded.encode("utf-8")).hexdigest()[:16]
return [f"kamil_{digest}"]
return []
def apply_device_model_constraints(self) -> None:
"""Apply only required wire-format constraints for the selected device model."""
if not self.is_multi_device: