Skip to content

Nonlocal ring chimera

Identical phase oscillators on a ring, each coupled to its R nearest neighbours on both sides.

  • Name: nonlocal_ring_chimera
  • Field: physics/synchronization
  • Formalism: ordinary differential equations
  • References: Kuramoto and Battogtokh 2002; Datseris and Parlitz 2022

Factory

@register_model(
    field='physics/synchronization',
    tags=('synchronization', 'oscillators', 'phase', 'chimera', 'nonlocal coupling'),
    references=('Kuramoto and Battogtokh 2002', 'Datseris and Parlitz 2022'),
    defaults={'until': 100.0, 'dt': 1.0},
    observables=lambda system: _order_parameter('order', 'ring'),
)
def nonlocal_ring_chimera(
    n_oscillators: int = 100,
    *,
    R: int = 14,
    alpha: float = 1.46,
    omega: float = 0.0,
    seed: int = 0,
) -> System:
    """Return identical phase oscillators on a ring, each coupled to its `R` nearest neighbours on both sides.

    Oscillator `i` follows `phi_i' = omega - (1 / 2R) sum_{j = i - R}^{i + R} sin(phi_i - phi_j + alpha)`, where the sum
    includes the oscillator itself. Near `alpha = pi / 2`, coherent and incoherent domains coexist on the ring as a
    chimera state, which is a long transient on small rings.

    Args:
        n_oscillators:
            The number of oscillators on the ring.

        R:
            The range of the coupling.

        alpha:
            The phase lag.

        omega:
            The natural frequency.

        seed:
            The seed of the initial phases.

    Returns:
        system:
            The model, with the phases `ring.theta` and the places of the oscillators `ring.x`.

    Examples:
        >>> from skcomplex.models.physics.synchronization import nonlocal_ring_chimera
        >>> nonlocal_ring_chimera(20, R=3).interactions[0].neighborhood.radius
        3.5
    """
    weight = 1 / (2 * R)
    ring = GroupElement(
        'ring',
        element=SitedOscillatorElement,
        size=n_oscillators,
        values={
            'theta': _spread_phases(n_oscillators, seed),
            'omega': omega,
            'x': np.arange(n_oscillators, dtype=float).reshape(-1, 1),
        },
        visual=VisualSpec('phase_ring', {'phase': 'theta', 'position': 'x'}),
    )
    return System(
        elements=[ring],
        interactions=[
            KuramotoCouplingInteraction(
                'coupling',
                target='ring',
                K=weight,
                alpha=alpha,
                include_self=True,
                neighborhood=RadiusNeighborhood(R + 0.5),
            ),
        ],
        space=ContinuousSpace((float(n_oscillators),), boundary='periodic'),
    )

Components

class SitedOscillatorElement(PhaseOscillatorElement):
    """Phase oscillator with a place `x` in a continuous space, which couplings within a distance or swarming read.

    The phase turns at the natural frequency, and the place changes only by the rates of interactions.

    Args:
        label:
            The label of the oscillator or the group.

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

        theta:
            The initial phase.

        omega:
            The natural frequency.

        x:
            The initial place. A number applies to every component.

    Attributes:
        default_visual_:
            The visual of the oscillator when it sets none.

    Examples:
        >>> from skcomplex.models.physics.common.oscillators import SitedOscillatorElement
        >>> SitedOscillatorElement('unit', x=[0.0, 1.0]).x
        [0.0, 1.0]
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        theta: float = 0.0,
        omega: float = 1.0,
        x: Annotated[npt.ArrayLike, StateSpec(vector=True, role='site')] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual, theta=theta, omega=omega)
        self.x = x

class KuramotoCouplingInteraction(CouplingInteraction):
    """Kuramoto coupling `theta_i' += K sum_j sin(theta_j - theta_i - alpha)` of the phases of a group.

    The sum, or the mean, runs over the members `j` that the mode selects: the neighbours along the edges of a lattice
    or network, or within a neighbourhood such as `RadiusNeighborhood(r)`, every other member of the group, every member
    of a separate source group, or the next member. With `include_self` it also runs over `j = i`, whose term
    `-K sin(alpha)` slows every phase. Over the whole group without weights or delay, the coupling reads the group sums
    of `sin theta` and `cos theta` once, so it costs time linear in the size of the group.

    Args:
        label:
            The label of the interaction, unique among the parts of its system.

        target:
            The path of the group of oscillators.

        source:
            The path of the group of oscillators the target couples to in the mode `'group'`, or `None` for the target.

        K:
            The coupling strength of one pair.

        alpha:
            The phase lag.

        state:
            The name of the state that holds the phases.

        reduce:
            `'sum'` or `'mean'`.

        mode:
            `'edges'`, `'group'`, or `'next'`.

        weights:
            The name of the state of the edge group in the mode `'edges'`, a matrix in the mode `'group'`, or `None`.

        include_self:
            Whether the sum also runs over the member itself.

        delay:
            The delay of the phases of the other members, 0 for none.

        neighborhood:
            The neighbourhood of the mode `'edges'`, or `None` for the edges of the lattice or network.

        layer:
            The layer of the network whose edges the interaction runs on, or `None` for the graph of the network.

    Examples:
        >>> import numpy as np
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.physics.common.oscillators import (
        ...     KuramotoCouplingInteraction,
        ...     PhaseOscillatorElement,
        ... )
        >>> phases = GroupElement('phases', element=PhaseOscillatorElement, size=4, values={'theta': [0, 1, 2, 3]})
        >>> coupling = KuramotoCouplingInteraction('coupling', target='phases', K=2.0, mode='group', reduce='mean')
        >>> system = System(elements=[phases], interactions=[coupling])
        >>> theta = Simulation(system, dt=1.0).check().prepare().run(until=20.0).trajectory_.values('phases.theta')
        >>> round(float(np.ptp(theta[-1])), 6)
        0.0
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        source: Annotated[str | None, RoleSpec(optional=True)] = None,
        K: Annotated[float, ParameterSpec()] = 1.0,
        alpha: Annotated[float, ParameterSpec()] = 0.0,
        state: str = 'theta',
        reduce: str = 'sum',
        mode: str = 'edges',
        weights: str | npt.ArrayLike | scipy.sparse.sparray | None = None,
        include_self: bool = False,
        delay: float = 0.0,
        neighborhood: BaseNeighborhood | None = None,
        layer: str | None = None,
    ) -> None:
        super().__init__(
            label,
            target=target,
            source=source,
            read=state,
            write=state,
            function=self._sine,
            reduce=reduce,
            mode=mode,
            weights=weights,
            include_self=include_self,
            delay=delay,
            neighborhood=neighborhood,
            layer=layer,
        )
        self.K = K
        self.alpha = alpha
        self.state = state

    def _sine(self, namespace: SimpleNamespace) -> Expression:
        """Return the term `K sin(theta_j - theta_i - alpha)` of one other member."""
        member, other = getattr(namespace.member, self.state), getattr(namespace.other, self.state)
        return namespace.K * sin(other - member - namespace.alpha)

    def _get_group_reduction(self, namespace: SimpleNamespace) -> Target:
        """Return the reduction over the group, from the group sums of `sin theta` and `cos theta` when it can."""
        if self.weights is not None or self.delay != 0:
            return super()._get_group_reduction(namespace)
        source, target = namespace.source, namespace.target
        shifted = getattr(target, self.state) + namespace.alpha
        phases = getattr(source, self.state)
        sines, cosines = total(sin(phases), over=source), total(cos(phases), over=source)
        reduction = namespace.K * (sines * cos(shifted) - cosines * sin(shifted))
        if self._excludes_self():
            reduction = reduction + namespace.K * sin(namespace.alpha)
        if self.reduce == 'mean':
            reduction = reduction / (count(over=source) - (1 if self._excludes_self() else 0))
        return reduction

Equations

ring.theta[i]: diff(theta_ring(t), t) == K_coupling*sin(-alpha_coupling) + _neighbours_0 + omega_ring
ring.x[i, 0]: diff(x_ring_0(t), t) == 0