Source code for axsdb.error

from __future__ import annotations

import enum
import warnings
from collections.abc import Mapping

import attrs

# ------------------------------------------------------------------------------
#                                   Exceptions
# ------------------------------------------------------------------------------


class DataError(Exception):
    """Raised when encountering issues with data."""

    pass


class InterpolationError(Exception):
    """Raised when encountering errors during interpolation."""

    pass


# ------------------------------------------------------------------------------
#                           Error handling components
# ------------------------------------------------------------------------------


[docs] class ErrorHandlingAction(enum.Enum): """ Error handling action descriptors. """ IGNORE = "ignore" #: Ignore the error. RAISE = "raise" #: Raise the error. WARN = "warn" #: Emit a warning.
[docs] class BoundsMode(enum.Enum): """ Bounds policy mode descriptors. """ FILL = "fill" #: Use fill_value for out-of-bounds points. CLAMP = "clamp" #: Clamp to nearest boundary value.
[docs] @attrs.define class BoundsPolicy: """ Policy for handling out-of-bounds values on one side (lower or upper). Parameters ---------- action : ErrorHandlingAction, default: IGNORE Whether to check/warn/raise on out-of-bounds values. mode : BoundsPolicyMode, default: FILL How to handle out-of-bounds values during interpolation. * FILL: Use fill_value for out-of-bounds points. * CLAMP: Clamp to nearest boundary value. fill_value : float or None, default: 0.0 Value to use when mode=FILL. None means NaN. """ action: ErrorHandlingAction = attrs.field( default=ErrorHandlingAction.IGNORE, repr=lambda x: f"<{x.name}>" ) mode: BoundsMode = attrs.field( default=BoundsMode.FILL, repr=lambda x: f"<{x.name}>" ) fill_value: float | None = 0.0
[docs] @classmethod def convert(cls, value): """ Convert various inputs to :class:`.BoundsPolicy`. Parameters ---------- value Value to convert. Can be: * a :class:`.BoundsPolicy` instance (returned as-is). * a string ``"ignore"``, ``"warn"``, or ``"raise"`` (sets action). * a string ``"fill"`` or ``"clamp"`` (sets mode). * a numeric value (sets ``fill_value``). * ``None`` (returns default policy). * a dict with keys: ``action``, ``mode``, ``fill_value``. Returns ------- BoundsPolicy Raises ------ ValueError If the value cannot be converted to a :class:`.BoundsPolicy` instance. Examples -------- >>> BoundsPolicy.convert("ignore") BoundsPolicy(action=<IGNORE>, mode=<FILL>, fill_value=0.0) >>> BoundsPolicy.convert("clamp") BoundsPolicy(action=<IGNORE>, mode=<CLAMP>, fill_value=0.0) >>> BoundsPolicy.convert(0.5) BoundsPolicy(action=<IGNORE>, mode=<FILL>, fill_value=0.5) >>> BoundsPolicy.convert({"action": "warn", "fill_value": 1.0}) BoundsPolicy(action=<WARN>, mode=<FILL>, fill_value=1.0) >>> BoundsPolicy.convert(None) BoundsPolicy(action=<IGNORE>, mode=<FILL>, fill_value=0.0) """ if isinstance(value, cls): return value if value is None: # default return cls() if isinstance(value, str): # Check if it's an action if value in ("ignore", "warn", "raise"): return cls(action=ErrorHandlingAction(value)) # Otherwise assume it's a mode elif value in ("fill", "clamp"): return cls(mode=BoundsMode(value)) else: raise ValueError( f"String value '{value}' must be an action " "('ignore', 'warn', 'raise') or mode ('fill', 'clamp')" ) if isinstance(value, (int, float)): # fill_value return cls(fill_value=float(value)) if isinstance(value, Mapping): # full control kwargs = {} if "action" in value: kwargs["action"] = ErrorHandlingAction(value["action"]) if "mode" in value: mode_val = value["mode"] if isinstance(mode_val, str): kwargs["mode"] = BoundsMode(mode_val) else: kwargs["mode"] = mode_val if "fill_value" in value: kwargs["fill_value"] = value["fill_value"] return cls(**kwargs) raise ValueError(f"could not convert value to BoundsPolicy (got {value!r})")
def _convert_bounds(value) -> tuple[BoundsPolicy, BoundsPolicy]: """ Convert various inputs to a tuple of two BoundsPolicy instances. Parameters ---------- value Value to convert. Can be: * tuple of 2 :class:`.BoundsPolicy` instances (returned as-is); * tuple of 2 numbers (interpreted as fill values for lower/upper); * dict with "lower" and/or "upper" keys; * single value (string, number, dict, :class:`.BoundsPolicy`): symmetric. Returns ------- tuple of BoundsPolicy (lower_policy, upper_policy) """ if isinstance(value, tuple) and len(value) == 2: if all(isinstance(v, BoundsPolicy) for v in value): return value # Tuple of 2 numbers = fill values if all(isinstance(v, (int, float)) for v in value): return ( BoundsPolicy(fill_value=float(value[0])), BoundsPolicy(fill_value=float(value[1])), ) # Tuple of 2 convertible values (e.g., strings, dicts) return ( BoundsPolicy.convert(value[0]), BoundsPolicy.convert(value[1]), ) # Dict with "lower" and/or "upper" keys if isinstance(value, Mapping): if "lower" in value or "upper" in value: return ( BoundsPolicy.convert(value.get("lower", {})), BoundsPolicy.convert(value.get("upper", {})), ) # Single value (string, number, dict, BoundsPolicy): symmetric policy = BoundsPolicy.convert(value) return (policy, policy)
[docs] @attrs.define class ErrorHandlingPolicy: """ Error handling policy for a single coordinate. Parameters ---------- missing : ErrorHandlingAction Action when coordinate is missing from dataset. scalar : ErrorHandlingAction Action when coordinate dimension is scalar. bounds : tuple[BoundsPolicy, BoundsPolicy] Policies for (``lower_bound``, ``upper_bound``). Values are automatically converted by :meth:`.BoundsPolicy.convert`. A single value can be passed and will be interpreted as a symmetric case. """ missing: ErrorHandlingAction = attrs.field(repr=lambda x: f"<{x.name}>") scalar: ErrorHandlingAction = attrs.field(repr=lambda x: f"<{x.name}>") bounds: tuple[BoundsPolicy, BoundsPolicy] = attrs.field(converter=_convert_bounds)
[docs] @classmethod def convert(cls, value): """ Convert a value to an :class:`.ErrorHandlingPolicy`. Parameters ---------- value Value to convert. Can be: * an :class:`.ErrorHandlingPolicy` instance (returned as-is). * a dict with keys ``missing``, ``scalar``, and ``bounds``. The ``bounds`` value is passed to :func:`._convert_bounds` and can be: * a string/number/dict/:class:`.BoundsPolicy` (symmetric) * a tuple of 2 :class:`.BoundsPolicy` instances * a tuple of 2 numbers (fill values) * a dict with ``lower`` and/or ``upper`` keys Returns ------- ErrorHandlingPolicy Raises ------ ValueError If the value cannot be converted to an :class:`.ErrorHandlingPolicy` instance. """ if isinstance(value, cls): return value if isinstance(value, Mapping): kwargs = {} for k, v in value.items(): if k == "missing": kwargs["missing"] = ErrorHandlingAction(v) elif k == "scalar": kwargs["scalar"] = ErrorHandlingAction(v) elif k == "bounds": kwargs["bounds"] = v else: # Unknown key, pass through kwargs[k] = v return cls(**kwargs) else: raise ValueError( f"could not convert value to ErrorHandlingPolicy (got {value!r})" )
def _merge_policy( default_policy: ErrorHandlingPolicy, user_value: Mapping ) -> ErrorHandlingPolicy: """ Merge a partial user policy dict with a complete default policy. Parameters ---------- default_policy : ErrorHandlingPolicy The complete default policy to use as a base. user_value : Mapping A partial dict with one or more of: missing, scalar, bounds. Returns ------- ErrorHandlingPolicy A complete policy with user values overriding defaults. """ kwargs = { "missing": default_policy.missing, "scalar": default_policy.scalar, "bounds": default_policy.bounds, } # Override with user-provided values if "missing" in user_value: kwargs["missing"] = ErrorHandlingAction(user_value["missing"]) if "scalar" in user_value: kwargs["scalar"] = ErrorHandlingAction(user_value["scalar"]) if "bounds" in user_value: kwargs["bounds"] = user_value["bounds"] return ErrorHandlingPolicy(**kwargs) def _get_default_policy(dim: str): if dim in {"x", "p", "t"}: config = get_error_handling_config() return getattr(config, dim) raise ValueError(f"unhandled dimension {dim!r}") def _convert_error_handling_policy_with_default(dim: str): """ Create a converter for ErrorHandlingPolicy that supports partial configs. Parameters ---------- dim : str The dimension name ('x', 'p', or 't') to fetch the default for. Returns ------- callable A converter function for use with attrs.field(converter=...). """ def converter(value): if value is None: # None means use default - will be handled by factory return None # If it's a complete policy or string, convert normally if not isinstance(value, Mapping): return ErrorHandlingPolicy.convert(value) # Check if it's a partial policy dict (missing some keys) if set(value.keys()) < {"missing", "scalar", "bounds"}: # Partial policy - merge with default default_policy = _get_default_policy(dim) return _merge_policy(default_policy, value) else: # Complete policy dict return ErrorHandlingPolicy.convert(value) return converter
[docs] @attrs.define class ErrorHandlingConfiguration: """ Error handling configuration. Parameters ---------- x : ErrorHandlingPolicy, optional Error handling policy for species concentrations. If not provided, uses the global default. p : ErrorHandlingPolicy, optional Error handling policy for pressure. If not provided, uses the global default. t : ErrorHandlingPolicy, optional Error handling policy for temperature. If not provided, uses the global default. """ x: ErrorHandlingPolicy = attrs.field( factory=lambda: _get_default_policy("x"), converter=_convert_error_handling_policy_with_default("x"), ) p: ErrorHandlingPolicy = attrs.field( factory=lambda: _get_default_policy("p"), converter=_convert_error_handling_policy_with_default("p"), ) t: ErrorHandlingPolicy = attrs.field( factory=lambda: _get_default_policy("t"), converter=_convert_error_handling_policy_with_default("t"), )
[docs] @classmethod def convert(cls, value): """ Convert a value to an :class:`.ErrorHandlingConfiguration`. Parameters ---------- value Value to convert. Dictionaries values are passed as keyword arguments to the constructor. Returns ------- ErrorHandlingConfiguration """ if isinstance(value, Mapping): return cls(**value) else: return value
def handle_error(error: InterpolationError, action: ErrorHandlingAction): """ Apply an error handling policy. Parameters ---------- error : .InterpolationError The error that is handled. action : ErrorHandlingAction If ``IGNORE``, do nothing; if ``WARN``, emit a warning; if ``RAISE``, raise the error. """ if action is ErrorHandlingAction.IGNORE: return if action is ErrorHandlingAction.WARN: warnings.warn(str(error), UserWarning) return if action is ErrorHandlingAction.RAISE: raise error raise NotImplementedError #: Global default error handling configuration _DEFAULT_ERROR_HANDLING_CONFIG: ErrorHandlingConfiguration | None = None
[docs] def set_error_handling_config(value: Mapping | ErrorHandlingConfiguration) -> None: """ Set the global default error handling configuration. Parameters ---------- value : Mapping | ErrorHandlingConfiguration Error handling configuration. Raises ------ ValueError If ``value`` cannot be converted to an :class:`.ErrorHandlingConfiguration`. """ global _DEFAULT_ERROR_HANDLING_CONFIG value = ErrorHandlingConfiguration.convert(value) if not isinstance(value, ErrorHandlingConfiguration): raise ValueError("could not convert value to ErrorHandlingConfiguration") _DEFAULT_ERROR_HANDLING_CONFIG = value
[docs] def get_error_handling_config() -> ErrorHandlingConfiguration: """ Retrieve the current global default error handling configuration. Returns ------- ErrorHandlingConfiguration """ global _DEFAULT_ERROR_HANDLING_CONFIG if _DEFAULT_ERROR_HANDLING_CONFIG is None: # No config yet: assign a default set_error_handling_config( { # This default configuration ignores bound errors on pressure and temperature # variables because this usually occurs at high altitude, where the absorption # coefficient is very low and can be safely forced to 0. "p": {"missing": "raise", "scalar": "raise", "bounds": "ignore"}, "t": {"missing": "raise", "scalar": "raise", "bounds": "ignore"}, # Ignore missing molecule coordinates, raise on bound error. "x": {"missing": "ignore", "scalar": "ignore", "bounds": "raise"}, } ) return _DEFAULT_ERROR_HANDLING_CONFIG