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