""" Parameter validation for laser control module. Validates all input parameters against physical constraints and protocol limits before sending to device. """ import math from typing import Dict, Any, Tuple from .constants import ( TEMP_MIN_C, TEMP_MAX_C, CURRENT_MIN_MA, CURRENT_MAX_MA, CURRENT_STEP_MIN_MA, CURRENT_STEP_MAX_MA, TEMP_STEP_MIN_C, TEMP_STEP_MAX_C, TIME_STEP_MIN_US, TIME_STEP_MAX_US, DELAY_TIME_MIN_MS, DELAY_TIME_MAX_MS, ) from .exceptions import ( ValidationError, TemperatureOutOfRangeError, CurrentOutOfRangeError, InvalidParameterError, ) from .models import VariationType class ParameterValidator: """Validates all input parameters for the laser controller.""" @staticmethod def _check_numeric(value: Any, param_name: str) -> float: """Check that value is a valid finite number. Returns float.""" if value is None: raise InvalidParameterError(param_name, "Value must not be None") if not isinstance(value, (int, float)): raise InvalidParameterError(param_name, "Value must be a number") if math.isnan(value): raise InvalidParameterError(param_name, "Value must not be NaN") if math.isinf(value): raise InvalidParameterError(param_name, "Value must not be infinite") return float(value) @staticmethod def validate_temperature(value: Any, param_name: str) -> float: """ Validate a laser temperature value. Args: value: Temperature in °C. param_name: Parameter name for error messages. Returns: Validated temperature as float. Raises: InvalidParameterError: If value is not a valid number. TemperatureOutOfRangeError: If value is outside [TEMP_MIN_C, TEMP_MAX_C]. """ value = ParameterValidator._check_numeric(value, param_name) if value < TEMP_MIN_C or value > TEMP_MAX_C: raise TemperatureOutOfRangeError( param_name, value, TEMP_MIN_C, TEMP_MAX_C ) return value @staticmethod def validate_current(value: Any, param_name: str) -> float: """ Validate a laser drive current value. Args: value: Current in mA. param_name: Parameter name for error messages. Returns: Validated current as float. Raises: InvalidParameterError: If value is not a valid number. CurrentOutOfRangeError: If value is outside [CURRENT_MIN_MA, CURRENT_MAX_MA]. """ value = ParameterValidator._check_numeric(value, param_name) if value < CURRENT_MIN_MA or value > CURRENT_MAX_MA: raise CurrentOutOfRangeError( param_name, value, CURRENT_MIN_MA, CURRENT_MAX_MA ) return value @staticmethod def validate_time_params(time_step: Any, delay_time: Any) -> Tuple[int, int]: """ Validate time parameters for variation mode. Args: time_step: Discretisation time step in microseconds. delay_time: Delay between pulses in milliseconds. Returns: Tuple (time_step, delay_time) as integers. Raises: InvalidParameterError: If values are not numeric. ValidationError: If values are outside allowed ranges. """ if not isinstance(time_step, (int, float)): raise InvalidParameterError("time_step", "Value must be a number") if not isinstance(delay_time, (int, float)): raise InvalidParameterError("delay_time", "Value must be a number") time_step_int = int(time_step) delay_time_int = int(delay_time) if time_step_int < TIME_STEP_MIN_US or time_step_int > TIME_STEP_MAX_US: raise ValidationError( f"time step {time_step_int} µs is out of range " f"[{TIME_STEP_MIN_US} - {TIME_STEP_MAX_US}] µs" ) if delay_time_int < DELAY_TIME_MIN_MS or delay_time_int > DELAY_TIME_MAX_MS: raise ValidationError( f"delay time {delay_time_int} ms is out of range " f"[{DELAY_TIME_MIN_MS} - {DELAY_TIME_MAX_MS}] ms" ) return time_step_int, delay_time_int @staticmethod def validate_variation_params( params: Dict[str, Any], variation_type: Any ) -> Dict[str, Any]: """ Validate parameters for variation mode. Args: params: Dictionary with keys: min_value, max_value, step, time_step, delay_time. variation_type: A VariationType enum value. Returns: Dictionary with validated and type-coerced values. Raises: ValidationError: For any constraint violation. InvalidParameterError: For wrong types. """ # Validate variation type if not isinstance(variation_type, VariationType): try: variation_type = VariationType(variation_type) except (ValueError, KeyError): raise ValidationError( f"Invalid variation type '{variation_type}'. " f"Must be one of {[e.name for e in VariationType]}" ) # Check required keys required_keys = {'min_value', 'max_value', 'step', 'time_step', 'delay_time'} missing = required_keys - params.keys() if missing: raise ValidationError( f"Missing required parameters: {sorted(missing)}" ) # Validate min/max min_val = ParameterValidator._check_numeric(params['min_value'], 'min_value') max_val = ParameterValidator._check_numeric(params['max_value'], 'max_value') if min_val >= max_val: raise ValidationError( f"min_value ({min_val}) must be less than max_value ({max_val})" ) # Validate step based on variation type step = ParameterValidator._check_numeric(params['step'], 'step') is_current_variation = variation_type in ( VariationType.CHANGE_CURRENT_LD1, VariationType.CHANGE_CURRENT_LD2 ) is_temp_variation = variation_type in ( VariationType.CHANGE_TEMPERATURE_LD1, VariationType.CHANGE_TEMPERATURE_LD2 ) if is_current_variation: step_min, step_max = CURRENT_STEP_MIN_MA, CURRENT_STEP_MAX_MA unit = "mA" # Also validate range against current limits ParameterValidator.validate_current(min_val, 'min_value') ParameterValidator.validate_current(max_val, 'max_value') elif is_temp_variation: step_min, step_max = TEMP_STEP_MIN_C, TEMP_STEP_MAX_C unit = "°C" # Also validate range against temperature limits ParameterValidator.validate_temperature(min_val, 'min_value') ParameterValidator.validate_temperature(max_val, 'max_value') else: raise ValidationError( f"Variation type {variation_type.name} cannot be used in variation mode" ) if step <= 0: raise ValidationError( f"step must be positive, got {step} {unit}" ) if step < step_min: raise ValidationError( f"step {step} {unit} is too small (minimum {step_min} {unit})" ) if step > step_max: raise ValidationError( f"step {step} {unit} is too large (maximum {step_max} {unit})" ) # Validate time parameters time_step, delay_time = ParameterValidator.validate_time_params( params['time_step'], params['delay_time'] ) return { 'variation_type': variation_type, 'min_value': min_val, 'max_value': max_val, 'step': step, 'time_step': time_step, 'delay_time': delay_time, } @staticmethod def validate_manual_mode_params( temp1: Any, temp2: Any, current1: Any, current2: Any, ) -> Dict[str, float]: """ Validate all four manual mode parameters. Args: temp1: Laser 1 temperature, °C. temp2: Laser 2 temperature, °C. current1: Laser 1 current, mA. current2: Laser 2 current, mA. Returns: Dict with validated floats: temp1, temp2, current1, current2. Raises: ValidationError: For any out-of-range value. InvalidParameterError: For wrong types. """ return { 'temp1': ParameterValidator.validate_temperature(temp1, 'temp1'), 'temp2': ParameterValidator.validate_temperature(temp2, 'temp2'), 'current1': ParameterValidator.validate_current(current1, 'current1'), 'current2': ParameterValidator.validate_current(current2, 'current2'), }