Skip to content

Bernoulli doubling map

An ensemble of the doubling map x' = 2x (mod 1), iterated exactly on fractions with an odd denominator.

  • Name: doubling_map
  • Field: mathematics/dynamical_systems
  • Formalism: discrete map
  • References: Bertin 2021

Factory

@register_model(
    field='mathematics/dynamical_systems',
    tags=('chaos', 'discrete map', 'bernoulli shift', 'invariant density'),
    references=('Bertin 2021',),
    defaults={'until': 50, 'dt': 1},
)
def doubling_map(
    n_points: int = 100,
    *,
    x: float = 0.4,
    spread: float = 0.01,
    q: float = DOUBLING_DENOMINATOR,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return an ensemble of the doubling map `x' = 2x (mod 1)`, iterated exactly on fractions with an odd denominator.

    In floating point `2x (mod 1)` shifts out one binary digit per step, and every orbit reaches 0 within 53 steps.
    The model keeps every point on the fractions `k / q` of an odd denominator `q` and applies the map to them exactly,
    `k' = 2k (mod q)`, through the update `x' = (2 round(q x) mod q) / q`. The orbits are the exact orbits of these
    fractions. The default `q` is a prime below `2^50` of which 2 is a primitive root, so every orbit except that of 0
    has the period `q - 1`, about `10^15`. The points start in a narrow interval and spread until their histogram
    matches the uniform invariant density.

    Args:
        n_points:
            The number of points of the ensemble.

        x:
            The left end of the interval of initial points, which are rounded to the nearest fraction `k / q`.

        spread:
            The width of the interval of initial points.

        q:
            The odd denominator of the fractions the points stay on, an integer below `2^51`.

        random_state:
            The seed or generator of the initial points.

    Returns:
        system:
            The model, with the states `points.x`.

    Examples:
        >>> from skcomplex.models.mathematics.dynamical_systems import doubling_map
        >>> float(doubling_map(10, q=101.0).elements[0].values['x'][0] * 101)
        41.0
    """
    start = np.floor(q * _draw_cloud(n_points, x, spread, random_state) + 1 / 2) / q
    return System(
        elements=[GroupElement('points', element=DoublingMapElement, size=n_points, values={'x': start, 'q': q})],
    )

Components

class DoublingMapElement(Element):
    """Doubling map `x' = 2 x (mod 1)` on the grid of the fractions of denominator `q`.

    Args:
        label:
            The label of the element.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        x:
            The initial value, between 0 and 1.

        q:
            The denominator of the grid.

    Examples:
        >>> from skcomplex.models.mathematics.dynamical_systems import DoublingMapElement
        >>> DoublingMapElement('point', q=1024.0).q
        1024.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[object, StateSpec()] = 0.4,
        q: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = DOUBLING_DENOMINATOR,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.q = q

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the next value, rounded to the grid.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the element.
        """
        return [UpdateLaw(s.x, mod(2 * floor(s.q * s.x + 1 / 2), s.q) / s.q)]

Equations

points.x[i]: x_points(t + 1) == mod(2*floor(q_points*x_points(t) + 0.5), q_points)/q_points