"""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"]