Source code for zephon.work.mixture

# Copyright 2025 DatologyAI
# SPDX-License-Identifier: Apache-2.0

"""Utilities for validating and normalising mixture specifications."""

from dataclasses import dataclass
from functools import cached_property
from typing import Iterable, Mapping


[docs] @dataclass(frozen=True) class MixtureSpec: """Mixture weight specification with shared validation/normalisation logic.""" weights: Mapping[str, float] def __post_init__(self) -> None: # Eagerly validate so bad specs fail on construction, not on first use. # This also populates the cached_property once. _ = self.normalized
[docs] def validate_for(self, components: Iterable[str]) -> None: """Ensure this mixture matches the provided component names exactly.""" ordered = list(components) normalized = self.normalized missing = [name for name in ordered if name not in normalized] if missing: raise ValueError( "MixtureSpec missing weights for components: " + ", ".join(missing) ) component_set = set(ordered) extra = [name for name in normalized if name not in component_set] if extra: raise ValueError( "MixtureSpec contains unknown components: " + ", ".join(extra) )
[docs] def normalized_for(self, components: Iterable[str]) -> dict[str, float]: """Return normalised weights ordered according to ``components``.""" normalized = self.normalized ordered = list(components) missing = [name for name in ordered if name not in normalized] if missing: raise ValueError( "MixtureSpec missing weights for components: " + ", ".join(missing) ) return {name: normalized[name] for name in ordered}
@cached_property def normalized(self) -> dict[str, float]: ordered = dict(self.weights) # preserves insertion order if not ordered: raise ValueError("MixtureSpec must contain at least one component") total = 0.0 normalized: dict[str, float] = {} for name, raw_value in ordered.items(): value = float(raw_value) if value <= 0.0: raise ValueError( f"MixtureSpec weight for component '{name}' must be positive" ) total += value normalized[str(name)] = value if total <= 0.0: raise ValueError("MixtureSpec weights must sum to more than zero") return {name: value / total for name, value in normalized.items()}
__all__ = ["MixtureSpec"]