257 lines
8.9 KiB
Python
257 lines
8.9 KiB
Python
"""
|
|
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'),
|
|
} |