Add RFG UDP source to rph_radar
This commit is contained in:
@@ -165,6 +165,8 @@ def run_config_from_dict(payload: object) -> RunConfigModel:
|
||||
"stimulus_switch_positions",
|
||||
model.radar.multi_device.stimulus_switch_positions,
|
||||
)
|
||||
model.radar.rph_radar.source = _read_str(rph_radar_payload, "source", model.radar.rph_radar.source)
|
||||
model.radar.rph_radar.udp_bind = _read_str(rph_radar_payload, "udp_bind", model.radar.rph_radar.udp_bind)
|
||||
model.radar.rph_radar.project_dir = _read_str(
|
||||
rph_radar_payload, "project_dir", model.radar.rph_radar.project_dir
|
||||
)
|
||||
|
||||
@@ -89,8 +89,12 @@ class RphRadarBandModel:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RphRadarModel:
|
||||
"""External RadioPhotonic radar (RPH) acquisition process settings."""
|
||||
"""RadioPhotonic radar (RPH) acquisition: E-502 collector process or RFG board UDP stream."""
|
||||
|
||||
# "e502": the C++ collector streams over tty_path (project_dir..stop_timeout_s are its
|
||||
# settings); "rfg_udp": the RFG ADC board sends averaged sweep points to udp_bind.
|
||||
source: str = "e502"
|
||||
udp_bind: str = "0.0.0.0:5000"
|
||||
project_dir: str = ""
|
||||
executable_path: str = ""
|
||||
tty_path: str = ""
|
||||
@@ -107,12 +111,12 @@ class RphRadarModel:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LaserManualModeModel:
|
||||
"""Manual laser-control setpoints."""
|
||||
"""Manual laser-control setpoints (defaults: the lab GUI, RadioPhotonic_PCB_PC_software)."""
|
||||
|
||||
temp1: float = 25.0
|
||||
temp2: float = 25.0
|
||||
current1: float = 30.0
|
||||
current2: float = 30.0
|
||||
temp1: float = 24.0
|
||||
temp2: float = 24.9
|
||||
current1: float = 97.3
|
||||
current2: float = 102.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -120,15 +124,15 @@ class LaserVariationModeModel:
|
||||
"""Laser-control variation task parameters."""
|
||||
|
||||
variation_type: str = "CHANGE_CURRENT_LD1"
|
||||
static_temp1: float = 25.0
|
||||
static_temp2: float = 25.0
|
||||
static_current1: float = 30.0
|
||||
static_current2: float = 30.0
|
||||
min_value: float = 30.0
|
||||
max_value: float = 35.0
|
||||
step: float = 0.1
|
||||
time_step: int = 20
|
||||
delay_time: int = 3
|
||||
static_temp1: float = 24.0
|
||||
static_temp2: float = 24.9
|
||||
static_current1: float = 97.3
|
||||
static_current2: float = 102.0
|
||||
min_value: float = 97.3
|
||||
max_value: float = 107.0
|
||||
step: float = 0.05
|
||||
time_step: int = 50
|
||||
delay_time: int = 10
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -138,10 +142,10 @@ class LaserControlModel:
|
||||
enabled: bool = False
|
||||
port: str = ""
|
||||
mode: str = "manual"
|
||||
pi_coeff1_p: int = 2560
|
||||
pi_coeff1_i: int = 128
|
||||
pi_coeff2_p: int = 2560
|
||||
pi_coeff2_i: int = 128
|
||||
pi_coeff1_p: int = 1280
|
||||
pi_coeff1_i: int = 25
|
||||
pi_coeff2_p: int = 1280
|
||||
pi_coeff2_i: int = 179
|
||||
manual: LaserManualModeModel = field(default_factory=LaserManualModeModel)
|
||||
variation: LaserVariationModeModel = field(default_factory=LaserVariationModeModel)
|
||||
|
||||
@@ -410,10 +414,14 @@ class RunConfigModel:
|
||||
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),
|
||||
rph = radar.rph_radar
|
||||
# The RFG source always runs the profile's sweep (its length and step timing), even when
|
||||
# the laser is started elsewhere; E-502 keys stay as they were before the source field.
|
||||
rfg = rph.source == "rfg_udp"
|
||||
physical = (asdict(rph.band), asdict(rph.phase_calibration), rph.source if rfg else rph.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)
|
||||
laser.pi_coeff2_p, laser.pi_coeff2_i) if laser.enabled or rfg 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"]
|
||||
|
||||
@@ -4,11 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict
|
||||
import ipaddress
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from python_app.hardware_full.laser_control.conversions import variation_step_count
|
||||
from python_app.hardware_full.laser_control.validators import validate_variation
|
||||
from python_app.models.json_fields import (
|
||||
finite_number,
|
||||
read_bool as _require_bool, read_int as _require_int, read_str as _require_str,
|
||||
@@ -138,6 +141,8 @@ def validate_acquisition_config(config: RunConfigModel) -> None:
|
||||
|
||||
if config.is_rph_radar:
|
||||
validate_rph_radar_model(config.radar.rph_radar)
|
||||
if config.radar.rph_radar.source == "rfg_udp":
|
||||
validate_rfg_source(config)
|
||||
|
||||
for switch, physical_positions in (
|
||||
(config.input_switch, config.matrix_input_switch_positions),
|
||||
@@ -170,8 +175,40 @@ def validate_acquisition_config(config: RunConfigModel) -> None:
|
||||
raise ValueError(f"rings.{name}.slot_size_bytes must be at least {minimum} for this sweep")
|
||||
|
||||
|
||||
def parse_udp_bind(text: str) -> tuple[str, int]:
|
||||
"""``HOST:PORT`` (empty host = all interfaces) -> socket address."""
|
||||
host, colon, port = text.rpartition(":")
|
||||
try:
|
||||
ipaddress.IPv4Address(host or "0.0.0.0")
|
||||
except ValueError:
|
||||
colon = ""
|
||||
if not colon or not port.isdigit() or not 1 <= int(port) <= 65535:
|
||||
raise ValueError(f"radar.rph_radar.udp_bind must be IPV4:PORT, got {text!r}")
|
||||
return host or "0.0.0.0", int(port)
|
||||
|
||||
|
||||
def validate_rfg_source(config: RunConfigModel) -> None:
|
||||
"""The RFG board numbers steps of the laser current sweep; its exact length must be known."""
|
||||
laser = config.radar.laser_control
|
||||
if laser.mode.strip().lower() != "variation":
|
||||
raise ValueError("radar.rph_radar.source rfg_udp requires radar.laser_control.mode 'variation'")
|
||||
variation = laser.variation
|
||||
validate_variation(variation.min_value, variation.max_value, variation.step,
|
||||
variation.time_step, variation.delay_time)
|
||||
if variation_step_count(variation.min_value, variation.max_value, variation.step) < 2:
|
||||
raise ValueError("The laser current sweep must have at least two steps")
|
||||
# Without a source-side marker a sweep after a switch cannot be proven fresh, so every
|
||||
# capture path (run combos and the full calibration matrix) must see a single pair.
|
||||
if config.input_switch.positions != 1 or config.output_switch.positions != 1:
|
||||
raise ValueError("radar.rph_radar.source rfg_udp supports one antenna pair: both switches need 1 position")
|
||||
|
||||
|
||||
def validate_rph_radar_model(adc: RphRadarModel) -> None:
|
||||
"""Reject phase/grid errors before stopping a running pipeline."""
|
||||
if adc.source not in ("e502", "rfg_udp"):
|
||||
raise ValueError("radar.rph_radar.source must be e502 or rfg_udp")
|
||||
if adc.source == "rfg_udp":
|
||||
parse_udp_bind(adc.udp_bind)
|
||||
phase, band = adc.phase_calibration, adc.band
|
||||
for name, value in {**asdict(phase), **asdict(band)}.items():
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
||||
|
||||
Reference in New Issue
Block a user