"""Boundary handling helpers for bounded parameter spaces.

These are used by examples and can be useful for downstream optimizers.

Why this exists:
  * naive clipping creates *sticky* boundary mass at 0/1
  * reflection keeps a smoother stationary distribution inside the box
"""

from __future__ import annotations

import math


def clip01(x: float) -> float:
    """Clip into [0,1].

    Cheap, but creates *absorbing/sticky* boundaries: whenever the mutated value
    crosses the boundary, the result becomes exactly 0.0 or 1.0.
    """

    x = float(x)
    if x <= 0.0:
        return 0.0
    if x >= 1.0:
        return 1.0
    return x


def reflect01(x: float) -> float:
    """Reflect into [0,1] without 'sticking' to the boundaries.

    Implemented by folding the line into a [0,2] saw and mirroring the upper
    half. This keeps the distribution continuous at the boundaries.
    """

    x = abs(float(x))
    x = math.fmod(x, 2.0)
    return float(2.0 - x if x > 1.0 else x)
