Source code for heterodyne.core.physics

"""Physical constants, parameter bounds, and validation for heterodyne model."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import ClassVar

import numpy as np


[docs] @dataclass class ValidationResult: """Result of parameter validation with detailed error reporting. Attributes: valid: True if all parameters are within bounds. violations: List of human-readable violation messages. parameters_checked: Number of parameters validated. message: Summary message about validation result. """ valid: bool violations: list[str] = field(default_factory=list) parameters_checked: int = 0 message: str = "" def __str__(self) -> str: if self.valid: return f"OK {self.message}" violations_str = "\n - ".join(self.violations) return f"FAIL {self.message}\n - {violations_str}"
[docs] @dataclass(frozen=True) class PhysicsConstants: """Physical constants for XPCS scattering analysis. All values in SI base units unless otherwise noted. """ # Boltzmann constant (J/K) k_B: ClassVar[float] = 1.380649e-23 # Planck constant (J·s) h: ClassVar[float] = 6.62607015e-34 # Speed of light (m/s) c: ClassVar[float] = 299792458.0 # X-ray wavelengths (Å) - common energies WAVELENGTH_8KEV: ClassVar[float] = 1.55 # Å WAVELENGTH_10KEV: ClassVar[float] = 1.24 # Å WAVELENGTH_12KEV: ClassVar[float] = 1.0332 # Å # Typical q-ranges (inverse Angstroms) Q_MIN_TYPICAL: ClassVar[float] = 1e-4 Q_MAX_TYPICAL: ClassVar[float] = 1.0 # Time scales (seconds) TIME_MIN_XPCS: ClassVar[float] = 1e-6 # Microsecond resolution TIME_MAX_XPCS: ClassVar[float] = 1e3 # Kilosecond measurements # Velocity ranges (Å/s) — heterodyne equivalent of homodyne shear rate VELOCITY_MIN: ClassVar[float] = 1e-6 # Quasi-static limit VELOCITY_MAX: ClassVar[float] = 1e4 # Upper bound for directed flow # Fraction parameter ranges FRACTION_MIN: ClassVar[float] = 0.0 FRACTION_MAX: ClassVar[float] = 1.0 # Numerical stability constants EPS: ClassVar[float] = 1e-12 # Avoid division by zero MAX_EXP_ARG: ClassVar[float] = 700.0 # Prevent exponential overflow MIN_POSITIVE: ClassVar[float] = 1e-100 # Minimum positive value
# Default parameter bounds for heterodyne model PARAMETER_BOUNDS: dict[str, tuple[float, float]] = { # Reference transport "D0_ref": (100.0, 1e6), "alpha_ref": (-2.0, 2.0), "D_offset_ref": (-1e5, 1e5), # Sample transport "D0_sample": (100.0, 1e6), "alpha_sample": (-2.0, 2.0), "D_offset_sample": (-1e5, 1e5), # Velocity "v0": (1e-6, 1e4), "beta": (-2.0, 2.0), "v_offset": (-100.0, 100.0), # Fraction "f0": (0.0, 1.0), "f1": (-10.0, 10.0), "f2": (-1e4, 1e4), "f3": (0.0, 1.0), # Angle "phi0": (-10.0, 10.0), }
[docs] def get_default_bounds_array() -> tuple[np.ndarray, np.ndarray]: """Get default bounds as arrays in canonical parameter order. Returns: (lower_bounds, upper_bounds) each of shape (14,) """ from heterodyne.config.parameter_names import ALL_PARAM_NAMES lower = np.array([PARAMETER_BOUNDS[name][0] for name in ALL_PARAM_NAMES]) upper = np.array([PARAMETER_BOUNDS[name][1] for name in ALL_PARAM_NAMES]) return lower, upper
[docs] @dataclass(frozen=True) class TransportPhysics: """Physical interpretation of transport parameters. Transport coefficient: J(t) = D0 * t^alpha + offset Physical regimes based on alpha: - alpha = 1.0: Normal (Brownian) diffusion - alpha < 1.0: Subdiffusion (crowded/constrained) - alpha > 1.0: Superdiffusion (active/directed) - alpha = 2.0: Ballistic motion """ # Alpha value regimes NORMAL_DIFFUSION: ClassVar[float] = 1.0 BALLISTIC: ClassVar[float] = 2.0
[docs] @staticmethod def interpret_alpha(alpha: float) -> str: """Interpret alpha value physically. For J(t) = D0 * t^alpha + offset: - alpha ≈ 0: constant transport rate (equilibrium) - alpha ≈ 1: linearly growing transport (normal diffusion) - alpha < 1: sub-linear (subdiffusive) - alpha > 1: super-linear (superdiffusive) - alpha ≈ 2: quadratic (ballistic) Args: alpha: Transport rate exponent Returns: Physical interpretation string """ if abs(alpha) < 0.05: return "constant transport (equilibrium)" elif alpha < 0: return "decelerating transport" elif alpha < 0.5: return "strongly subdiffusive" elif abs(alpha - 1.0) < 0.05: return "normal diffusion" elif alpha < 1.0: return "subdiffusive" elif alpha < 1.5: return "weakly superdiffusive" elif alpha < 2.0: return "superdiffusive" else: return "ballistic/directed"
[docs] @staticmethod def diffusion_coefficient(D0: float, alpha: float, t: float = 1.0) -> float: """Compute effective diffusion coefficient at time t. For J(t) = D0 * t^alpha, the effective D is: D_eff = dJ/dt = D0 * alpha * t^(alpha-1) Args: D0: Transport prefactor alpha: Transport exponent t: Time point (default 1.0) Returns: Effective diffusion coefficient """ if t <= 0: return 0.0 return float(D0 * alpha * (t ** (alpha - 1)))
[docs] def validate_parameters( params: dict[str, float], bounds: dict[str, tuple[float, float]] | None = None, ) -> ValidationResult: """Validate parameter values against physical bounds. Args: params: Dictionary mapping parameter names to values. bounds: Optional custom bounds. Defaults to PARAMETER_BOUNDS. Returns: ValidationResult with violations list if any bounds are exceeded. """ if bounds is None: bounds = PARAMETER_BOUNDS violations: list[str] = [] checked = 0 for name, value in params.items(): if name not in bounds: continue checked += 1 lo, hi = bounds[name] if not np.isfinite(value): violations.append(f"{name}={value} is not finite") elif value < lo: violations.append(f"{name}={value:.6g} < lower bound {lo:.6g}") elif value > hi: violations.append(f"{name}={value:.6g} > upper bound {hi:.6g}") valid = len(violations) == 0 n_violations = len(violations) message = ( f"All {checked} parameters within bounds" if valid else f"{n_violations} violation(s) in {checked} parameters" ) return ValidationResult( valid=valid, violations=violations, parameters_checked=checked, message=message, )