Source code for eclypse.policies.noise.seasonal_noise

"""Seasonal additive noise policy."""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import TYPE_CHECKING

from eclypse.policies._filters import (
    apply_numeric_transform,
    clamp,
)

if TYPE_CHECKING:
    from eclypse.graph.asset_graph import AssetGraph
    from eclypse.policies._filters import (
        EdgeFilter,
        NodeFilter,
    )
    from eclypse.utils.types import UpdatePolicy


@dataclass(slots=True)
class SeasonalNoisePolicy:
    """Apply sinusoidal additive noise to selected assets."""

    amplitude: float
    period: int
    node_assets: str | list[str] | None = None
    edge_assets: str | list[str] | None = None
    phase: float = 0.0
    lower: float | None = None
    upper: float | None = None
    node_ids: list[str] | None = None
    node_filter: NodeFilter | None = None
    edge_ids: list[tuple[str, str]] | None = None
    edge_filter: EdgeFilter | None = None
    step: int = 0

    def __post_init__(self):
        """Validate the seasonal noise configuration."""
        if self.period <= 0:
            raise ValueError("period must be strictly positive.")
        if self.node_assets is None and self.edge_assets is None:
            raise ValueError(
                "At least one of node_assets or edge_assets must be provided."
            )

    def __call__(self, graph: AssetGraph):
        """Apply one seasonal noise step."""
        delta = self.amplitude * math.sin(
            ((2 * math.pi * self.step) / self.period) + self.phase
        )
        apply_numeric_transform(
            graph,
            node_assets=self.node_assets,
            edge_assets=self.edge_assets,
            node_ids=self.node_ids,
            node_filter=self.node_filter,
            edge_ids=self.edge_ids,
            edge_filter=self.edge_filter,
            transform=lambda _key, current: clamp(
                current + delta,
                self.lower,
                self.upper,
            ),
        )
        self.step += 1
        graph.logger.trace("Applied seasonal_noise policy.")


[docs] def seasonal_noise( *, amplitude: float, period: int, node_assets: str | list[str] | None = None, edge_assets: str | list[str] | None = None, phase: float = 0.0, lower: float | None = None, upper: float | None = None, node_ids: list[str] | None = None, node_filter: NodeFilter | None = None, edge_ids: list[tuple[str, str]] | None = None, edge_filter: EdgeFilter | None = None, ) -> UpdatePolicy: """Apply sinusoidal additive noise to selected assets. Args: amplitude (float): Peak additive delta. period (int): Number of calls in one sinusoidal cycle. node_assets (str | list[str] | None): Optional node asset key selector. edge_assets (str | list[str] | None): Optional edge asset key selector. phase (float): Phase offset in radians. lower (float | None): Optional lower bound after adding noise. upper (float | None): Optional upper bound after adding noise. node_ids (list[str] | None): Optional explicit node identifiers to mutate. node_filter (NodeFilter | None): Optional predicate receiving ``(node_id, data)``. edge_ids (list[tuple[str, str]] | None): Optional explicit edge identifiers to mutate. edge_filter (EdgeFilter | None): Optional predicate receiving ``(source, target, data)``. Returns: Stateful policy that applies seasonal additive noise. """ return SeasonalNoisePolicy( amplitude=amplitude, period=period, node_assets=node_assets, edge_assets=edge_assets, phase=phase, lower=lower, upper=upper, node_ids=node_ids, node_filter=node_filter, edge_ids=edge_ids, edge_filter=edge_filter, )