Coupled Rössler and the master stability function
Chaotic Rössler oscillators on a ring, diffusively coupled through their second variable.
- Name:
coupled_rossler - Field:
physics/synchronization - Formalism: ordinary differential equations
- References: Rössler 1976; Pecora and Carroll 1998; Datseris and Parlitz 2022; Arenas et al. 2008
Factory
@register_model(
field='physics/synchronization',
tags=('synchronization', 'chaos', 'ode', 'master stability function', 'networks'),
references=('Rössler 1976', 'Pecora and Carroll 1998', 'Datseris and Parlitz 2022', 'Arenas et al. 2008'),
defaults={'until': 100.0, 'dt': 0.1},
)
def coupled_rossler(
n_oscillators: int = 2,
*,
k: float = 0.04,
mismatch: float = 0.02,
a: float = 0.2,
b: float = 0.2,
c: float = 5.7,
seed: int = 0,
) -> System:
"""Return chaotic Rössler oscillators on a ring, diffusively coupled through their second variable.
Oscillator `i` follows `x1' = -ω_i x2 - x3`, `x2' = ω_i x1 + a x2 + k sum_j (x2_j - x2_i)`, and
`x3' = b + x3 (x1 - c)`, summed over its neighbours on the ring. Two oscillators with frequencies `1 ± mismatch`
pass from no synchronization to chaotic phase synchronization as `k` grows. Identical oscillators, with no mismatch,
synchronize completely when every nonzero eigenvalue of the ring's Laplacian times `k` falls where the master
stability function is negative.
Args:
n_oscillators:
The number of oscillators on the ring.
k:
The coupling strength.
mismatch:
The half width of the interval of natural frequencies around 1.
a:
The first Rössler parameter.
b:
The second Rössler parameter.
c:
The third Rössler parameter.
seed:
The seed of the initial states.
Returns:
system:
The model, with the states `oscillator_<i>.x1`, `oscillator_<i>.x2`, and `oscillator_<i>.x3`.
Examples:
>>> from skcomplex.models.physics.synchronization import coupled_rossler
>>> [element.omega for element in coupled_rossler().elements]
[0.98, 1.02]
"""
omegas = np.linspace(1 - mismatch, 1 + mismatch, n_oscillators)
starts = np.random.default_rng(seed).uniform(-1.0, 1.0, (n_oscillators, 2))
oscillators = [
RosslerOscillatorElement(
f'oscillator_{index}',
x1=float(starts[index, 0]),
x2=float(starts[index, 1]),
x3=0.0,
omega=round(float(omegas[index]), 12),
a=a,
b=b,
c=c,
)
for index in range(n_oscillators)
]
links = [(index, index + 1) for index in range(n_oscillators - 1)]
if n_oscillators > PAIR:
links.append((n_oscillators - 1, 0))
couplings = [
DiffusiveCouplingInteraction(
f'coupling_{source}_{target}',
source=f'oscillator_{source}',
target=f'oscillator_{target}',
k=k,
)
for first, second in links
for source, target in ((first, second), (second, first))
]
return System(elements=oscillators, interactions=couplings)
Components
class RosslerOscillatorElement(Element):
"""Rössler oscillator `x1' = -omega x2 - x3`, `x2' = omega x1 + a x2`, `x3' = b + x3 (x1 - c)`.
It is chaotic for `a = b = 0.2` and `c = 5.7`.
Args:
label:
The label of the oscillator.
visual:
How the oscillator is drawn, or `None` for `default_visual_`.
x1:
The initial first variable.
x2:
The initial second variable.
x3:
The initial third variable.
omega:
The natural frequency.
a:
The first Rössler parameter.
b:
The second Rössler parameter.
c:
The third Rössler parameter.
Attributes:
default_visual_:
The visual of the oscillator when it sets none.
Examples:
>>> from skcomplex.models.physics.synchronization import RosslerOscillatorElement
>>> RosslerOscillatorElement('oscillator').c
5.7
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x1: Annotated[float, StateSpec()] = 1.0,
x2: Annotated[float, StateSpec()] = 0.0,
x3: Annotated[float, StateSpec()] = 0.0,
omega: Annotated[float, ParameterSpec()] = 1.0,
a: Annotated[float, ParameterSpec()] = 0.2,
b: Annotated[float, ParameterSpec()] = 0.2,
c: Annotated[float, ParameterSpec()] = 5.7,
) -> None:
super().__init__(label, visual=visual)
self.x1 = x1
self.x2 = x2
self.x3 = x3
self.omega = omega
self.a = a
self.b = b
self.c = c
def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
"""Return the Rössler flow.
Args:
namespace:
The namespace of the symbols the laws can use.
Returns:
laws:
The rates of the three variables.
"""
s = namespace
return [
RateLaw(s.x1, -s.omega * s.x2 - s.x3),
RateLaw(s.x2, s.omega * s.x1 + s.a * s.x2),
RateLaw(s.x3, s.b + s.x3 * (s.x1 - s.c)),
]
class DiffusiveCouplingInteraction(RateInteraction):
"""Diffusive coupling of a state of one element to the same state of another, `x_t' += k (x_s - x_t)`.
Args:
label:
The label of the interaction, unique among the parts of its system.
source:
The path of the element whose state the coupling reads.
target:
The path of the element whose state the coupling changes.
state:
The name of the coupled state.
k:
The coupling strength.
Examples:
>>> from types import SimpleNamespace
>>> from sage.all__sagemath_symbolics import var
>>> from skcomplex.models.physics.synchronization import DiffusiveCouplingInteraction
>>> coupling = DiffusiveCouplingInteraction('coupling', source='a', target='b').check().prepare()
>>> source, target = SimpleNamespace(x2=var('u')), SimpleNamespace(x2=var('v'))
>>> [law.value for law in coupling.run(SimpleNamespace(source=source, target=target, k=var('k'))).laws_]
[k*(u - v)]
"""
def __init__(
self,
label: str,
*,
source: Annotated[str, RoleSpec()],
target: Annotated[str, RoleSpec()],
state: str = 'x2',
k: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
super().__init__(label, target=target, state=state)
self.source = source
self.k = k
def _get_rate(self, namespace: SimpleNamespace) -> Expression:
"""Return `k (x_s - x_t)`.
Args:
namespace:
The namespace of the symbols the rate can use.
Returns:
rate:
The rate of the state of the target.
"""
return namespace.k * (getattr(namespace.source, self.state) - getattr(namespace.target, self.state))
Equations
oscillator_0.x1: diff(x1_oscillator_0(t), t) == -omega_oscillator_0*x2_oscillator_0(t) - x3_oscillator_0(t)
oscillator_0.x2: diff(x2_oscillator_0(t), t) == -k_coupling_1_0*(x2_oscillator_0(t) - x2_oscillator_1(t)) + omega_oscillator_0*x1_oscillator_0(t) + a_oscillator_0*x2_oscillator_0(t)
oscillator_0.x3: diff(x3_oscillator_0(t), t) == -(c_oscillator_0 - x1_oscillator_0(t))*x3_oscillator_0(t) + b_oscillator_0
oscillator_1.x1: diff(x1_oscillator_1(t), t) == -omega_oscillator_1*x2_oscillator_1(t) - x3_oscillator_1(t)
oscillator_1.x2: diff(x2_oscillator_1(t), t) == k_coupling_0_1*(x2_oscillator_0(t) - x2_oscillator_1(t)) + omega_oscillator_1*x1_oscillator_1(t) + a_oscillator_1*x2_oscillator_1(t)
oscillator_1.x3: diff(x3_oscillator_1(t), t) == -(c_oscillator_1 - x1_oscillator_1(t))*x3_oscillator_1(t) + b_oscillator_1