Source code for gunz_cm.reconstructions.objectives._loss_config

"""Frozen dataclass configs for reconstruction objective loss functions.

This module replaces the per-loss ``__init__`` validation logic that
previously lived on ``MultiDimensionalScaling`` (mds.py), 
``WeightedMultiDimensionalScaling`` (mds.py), and ``GeneralError``
(general_error.py). The configs are DOP-4-compliant value types:
validation happens at construction (frozen=True makes illegal
combinations unrepresentable after creation), and the same config can
be used by both the OOP class wrappers (DOP Phase 4 shims) and the
DOP-style free functions in ``_loss_dispatch.py``.

DOP alignment
-------------
- Principle 1 (separate code from data): the configs ARE data, with
  no methods beyond the auto-generated dataclass protocol.
- Principle 2 (data is immutable): frozen=True makes every config
  instance hashable and prevents post-construction mutation.
- Principle 4 (illegal states unrepresentable): the ``__post_init__``
  validation runs once at construction; an invalid ``term``,
  ``reduction``, or ``weight_exp`` cannot survive past instantiation.
"""

from __future__ import annotations

import typing as t
from dataclasses import dataclass, field

from gunz_cm.exceptions import ReconstructionError

from .consts import VALID_REDUCTION, VALID_TERM_GENERAL, VALID_TERM_MDS


[docs]@dataclass(frozen=True) class MDSConfig: """Immutable configuration for the Multi-Dimensional Scaling loss. Parameters ---------- term : str, default='abs' Element-wise function applied to the error and target. Must be one of VALID_TERM_MDS = {'abs', 'square'}. reduction : str, default='mean' Method for reducing element-wise losses to a single value. Must be one of VALID_REDUCTION = {'mean', 'sum'}. eps : float, default=1e-8 Small epsilon added to the denominator for numerical stability. Raises ------ ReconstructionError If ``term`` or ``reduction`` is not in the allowed set. Examples -------- >>> cfg = MDSConfig(term='abs', reduction='mean') >>> cfg.term 'abs' """ term: str = 'abs' reduction: str = 'mean' eps: float = 1e-8 def __post_init__(self) -> None: """Validate fields at construction (frozen=True allows this only here).""" if self.term not in VALID_TERM_MDS: raise ReconstructionError( f"Invalid term function: '{self.term}'. " f"Must be one of {VALID_TERM_MDS}." ) if self.reduction not in VALID_REDUCTION: raise ReconstructionError( f"Invalid reduction function: '{self.reduction}'. " f"Must be one of {VALID_REDUCTION}." )
[docs]@dataclass(frozen=True) class WMDSConfig: """Immutable configuration for the Weighted Multi-Dimensional Scaling loss. Parameters ---------- term : str, default='square' Element-wise function. Must be in VALID_TERM_MDS. reduction : str, default='mean' Reduction method. Must be in VALID_REDUCTION. weight_exp : float, default=1.0 Exponent applied to target to compute per-element weights. eps : float, default=1e-8 Small epsilon added to the denominator of the weighted mean. Raises ------ ReconstructionError If ``term`` or ``reduction`` is invalid. Examples -------- >>> cfg = WMDSConfig(term='square', reduction='mean', weight_exp=1.0) >>> cfg.weight_exp 1.0 """ term: str = 'square' reduction: str = 'mean' weight_exp: float = 1.0 eps: float = 1e-8 def __post_init__(self) -> None: if self.term not in VALID_TERM_MDS: raise ReconstructionError( f"Invalid term function: '{self.term}'. " f"Must be one of {VALID_TERM_MDS}." ) if self.reduction not in VALID_REDUCTION: raise ReconstructionError( f"Invalid reduction function: '{self.reduction}'. " f"Must be one of {VALID_REDUCTION}." )
[docs]@dataclass(frozen=True) class GeneralErrorConfig: """Immutable configuration for the general-purpose error loss. Parameters ---------- term : str, default='square' Term function. Must be in VALID_TERM_GENERAL. The values ``'abs'`` and ``'square'`` are normalised to ``'l1'`` / ``'l2'`` at construction. reduction : str, default='mean' Reduction method. Must be in VALID_REDUCTION. Raises ------ ReconstructionError If ``term`` or ``reduction`` is invalid. Examples -------- >>> cfg = GeneralErrorConfig(term='square', reduction='mean') >>> cfg.term 'l2' """ term: str = 'square' reduction: str = 'mean' def __post_init__(self) -> None: if self.term not in VALID_TERM_GENERAL: raise ReconstructionError( f"Invalid term function: '{self.term}'. " f"Must be one of {VALID_TERM_GENERAL}." ) if self.reduction not in VALID_REDUCTION: raise ReconstructionError( f"Invalid reduction function: '{self.reduction}'. " f"Must be one of {VALID_REDUCTION}." ) # Normalize term names for internal consistency (object.__setattr__ # because the dataclass is frozen=True). object.__setattr__(self, 'term', self._normalize_term(self.term)) @staticmethod def _normalize_term(term: str) -> str: """Map ``'abs'`` -> ``'l1'`` and ``'square'`` -> ``'l2'``.""" if term == 'abs': return 'l1' if term == 'square': return 'l2' return term
# Workaround: the objectives modules historically did NOT define a separate # VALID_TERM_GENERAL constant; they hardcoded the list inside the class. # Provide it here as a re-export for the validation logic above. __all__ = ["MDSConfig", "WMDSConfig", "GeneralErrorConfig"]