"""Configuration resolution for the standalone generator sweep.""" from __future__ import annotations from dataclasses import dataclass import logging logger = logging.getLogger(__name__) @dataclass(slots=True, frozen=True) class GeneratorSweepConfig: """User-edited generator sweep configuration stored next to the script.""" log_level: str = "INFO" serial: str | None = None strict_protocol_version: int = 14 start_hz: float = 1_000_000.0 stop_hz: float = 6_000_000.0 step_hz: float = 0.0 points: int = 0 hold_time_ms: float = 50.0 loop: bool = True port: int = 1 power_dbm: float = -10.0 amplitude_correction: bool = False settle_timeout_ms: int = 1_000 status_poll_ms: float = 10.0 post_lock_delay_us: int = 0 gpio_chip: str = "/dev/gpiochip0" sweep_start_pin: int = 5 curr_step_pin: int = 6 curr_step_initial_level: int = 0 pwm_pin: int = 12 pwm_frequency_hz: int = 2_000_000 pwm_duty_cycle: float = 0.5 @dataclass(slots=True, frozen=True) class ResolvedGeneratorSweepConfig: """Fully validated generator sweep configuration.""" log_level: int serial: str | None strict_protocol_version: int frequencies_hz: tuple[int, ...] hold_time_s: float loop: bool port: int power_dbm: float amplitude_correction: bool settle_timeout_s: float status_poll_s: float post_lock_delay_s: float gpio_chip: str sweep_start_pin: int curr_step_pin: int curr_step_initial_level: int pwm_pin: int pwm_frequency_hz: int pwm_duty_cycle: float def _require(condition: bool, message: str) -> None: """Raise ``ValueError`` when a configuration invariant is violated.""" if not condition: raise ValueError(message) def _validate_positive_int(value: int, label: str) -> None: """Validate positive integer setting.""" _require(value > 0, f"{label} must be > 0") def _validate_non_negative_int(value: int, label: str) -> None: """Validate non-negative integer setting.""" _require(value >= 0, f"{label} must be >= 0") def _validate_non_negative_float(value: float, label: str) -> None: """Validate non-negative float setting.""" _require(value >= 0.0, f"{label} must be >= 0") def _validate_positive_float(value: float, label: str) -> None: """Validate positive float setting.""" _require(value > 0.0, f"{label} must be > 0") def _resolve_log_level(value: str) -> int: """Normalize logging level name to ``logging`` module integer constant.""" normalized = value.strip().upper() if not normalized: raise ValueError("generator_sweep.log_level must not be empty") level = logging.getLevelName(normalized) if isinstance(level, int): return level raise ValueError("generator_sweep.log_level must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL") def _build_frequency_grid(model: GeneratorSweepConfig) -> tuple[int, ...]: """Build the ordered frequency grid with inclusive stop frequency.""" start_hz = int(round(model.start_hz)) stop_hz = int(round(model.stop_hz)) step_hz = int(round(model.step_hz)) points = int(model.points) _validate_positive_int(start_hz, "generator_sweep.start_hz") _validate_positive_int(stop_hz, "generator_sweep.stop_hz") _require(stop_hz >= start_hz, "generator_sweep.stop_hz must be >= generator_sweep.start_hz") has_step = step_hz > 0 has_points = points > 0 _require(has_step != has_points, "generator_sweep must define exactly one of step_hz or points") if start_hz == stop_hz: if has_points: _require(points == 1, "generator_sweep.points must be 1 when start_hz == stop_hz") return (start_hz,) if has_step: _validate_positive_int(step_hz, "generator_sweep.step_hz") frequencies: list[int] = [] current_hz = start_hz while current_hz < stop_hz: frequencies.append(current_hz) current_hz += step_hz if not frequencies or frequencies[-1] != stop_hz: frequencies.append(stop_hz) return tuple(frequencies) _validate_positive_int(points, "generator_sweep.points") _require(points >= 2, "generator_sweep.points must be >= 2 when start_hz != stop_hz") span_hz = stop_hz - start_hz frequencies = tuple( int(round(start_hz + (span_hz * index) / float(points - 1))) for index in range(points) ) _require(frequencies[0] == start_hz, "generator_sweep.points grid does not start at start_hz") _require(frequencies[-1] == stop_hz, "generator_sweep.points grid does not end at stop_hz") _require( all(left < right for left, right in zip(frequencies, frequencies[1:])), "generator sweep grid contains duplicate or non-monotonic frequencies", ) return frequencies def resolve_generator_sweep_config(model: GeneratorSweepConfig) -> ResolvedGeneratorSweepConfig: """Validate and resolve runtime configuration for generator sweep mode.""" log_level = _resolve_log_level(model.log_level) frequencies_hz = _build_frequency_grid(model) hold_time_ms = float(model.hold_time_ms) settle_timeout_ms = int(model.settle_timeout_ms) status_poll_ms = float(model.status_poll_ms) post_lock_delay_us = int(model.post_lock_delay_us) pwm_frequency_hz = int(model.pwm_frequency_hz) sweep_start_pin = int(model.sweep_start_pin) curr_step_pin = int(model.curr_step_pin) pwm_pin = int(model.pwm_pin) curr_step_initial_level = int(model.curr_step_initial_level) _validate_non_negative_float(hold_time_ms, "generator_sweep.hold_time_ms") _validate_positive_int(settle_timeout_ms, "generator_sweep.settle_timeout_ms") _validate_positive_float(status_poll_ms, "generator_sweep.status_poll_ms") _validate_non_negative_int(post_lock_delay_us, "generator_sweep.post_lock_delay_us") _validate_positive_int(pwm_frequency_hz, "generator_sweep.pwm_frequency_hz") _validate_positive_int(int(model.strict_protocol_version), "generator_sweep.strict_protocol_version") _require(model.port in {1, 2}, "generator_sweep.port must be 1 or 2") _require(curr_step_initial_level in {0, 1}, "generator_sweep.curr_step_initial_level must be 0 or 1") _require(bool(model.gpio_chip.strip()), "generator_sweep.gpio_chip must not be empty") _validate_non_negative_int(sweep_start_pin, "generator_sweep.sweep_start_pin") _validate_non_negative_int(curr_step_pin, "generator_sweep.curr_step_pin") _validate_non_negative_int(pwm_pin, "generator_sweep.pwm_pin") _require( len({sweep_start_pin, curr_step_pin, pwm_pin}) == 3, "generator_sweep sweep_start_pin, curr_step_pin, and pwm_pin must be distinct", ) _require( 0.0 < float(model.pwm_duty_cycle) <= 1.0, "generator_sweep.pwm_duty_cycle must be within (0, 1]", ) logger.debug( "Resolved generator sweep config: points=%d start=%dHz stop=%dHz loop=%s port=%d", len(frequencies_hz), frequencies_hz[0], frequencies_hz[-1], bool(model.loop), int(model.port), ) return ResolvedGeneratorSweepConfig( log_level=log_level, serial=(model.serial.strip() or None) if isinstance(model.serial, str) else model.serial, strict_protocol_version=int(model.strict_protocol_version), frequencies_hz=frequencies_hz, hold_time_s=hold_time_ms / 1000.0, loop=bool(model.loop), port=int(model.port), power_dbm=float(model.power_dbm), amplitude_correction=bool(model.amplitude_correction), settle_timeout_s=settle_timeout_ms / 1000.0, status_poll_s=status_poll_ms / 1000.0, post_lock_delay_s=post_lock_delay_us / 1_000_000.0, gpio_chip=model.gpio_chip.strip(), sweep_start_pin=sweep_start_pin, curr_step_pin=curr_step_pin, curr_step_initial_level=curr_step_initial_level, pwm_pin=pwm_pin, pwm_frequency_hz=pwm_frequency_hz, pwm_duty_cycle=float(model.pwm_duty_cycle), )