Skip to content

Swarmalators

Swarmalators, identical oscillators whose phases set their attraction and whose positions their coupling.

  • Name: swarmalators
  • Field: physics/synchronization
  • Formalism: ordinary differential equations
  • References: O'Keeffe, Ceron, and Petersen 2017; te Vrugt and Wittkowski 2024

Factory

@register_model(
    field='physics/synchronization',
    tags=('swarmalators', 'synchronization', 'swarming', 'pair interaction', 'ode'),
    references=("O'Keeffe, Ceron, and Petersen 2017", 'te Vrugt and Wittkowski 2024'),
    defaults={'until': 100.0, 'dt': 1.0, 'solver': RK4Solver(dt=0.1)},
)
def swarmalators(
    n_units: int = 100,
    *,
    J: float = 1.0,
    K: float = 0.0,
    A: float = 1.0,
    B: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return swarmalators, identical oscillators whose phases set their attraction and whose positions their coupling.

    Every swarmalator `i` moves by `(1 / N) sum_j [(x_j - x_i) / |x_j - x_i| (A + J cos(theta_j - theta_i)) -
    B (x_j - x_i) / |x_j - x_i|^2]` and turns by `(K / N) sum_j sin(theta_j - theta_i) / |x_j - x_i|`. With `J > 0`
    like phases attract. With `K = 0` and `J = 1` the swarm settles into a static phase wave, a disk whose phase winds
    once around its center, so one of the order parameters `S± = |mean(exp(i (phi ± theta)))|` of the spatial angle
    `phi` and the phase approaches 1. With `K > 0` the phases synchronize into a uniform disk, and with `K < 0` and
    small `J` they stay asynchronous.

    Args:
        n_units:
            The number of swarmalators `N`.

        J:
            The influence of the phase similarity on the attraction.

        K:
            The phase coupling.

        A:
            The strength of the attraction.

        B:
            The strength of the repulsion.

        random_state:
            The seed or generator of the initial positions in the square of side 2 and of the initial phases.

    Returns:
        system:
            The model, with the positions `units.x` and the phases `units.theta`.

    Examples:
        >>> from skcomplex.models.physics.synchronization import swarmalators
        >>> swarmalators(10, K=-0.1).interactions[0].K
        -0.1
    """
    rng = np.random.default_rng(random_state)
    units = GroupElement(
        'units',
        element=SwarmalatorElement,
        size=n_units,
        values={'x': rng.uniform(-1.0, 1.0, (n_units, 2)), 'theta': rng.uniform(-np.pi, np.pi, n_units), 'omega': 0.0},
        visual=VisualSpec('points', {'position': 'x', 'color_by': 'theta', 'radius': 0.03}),
    )
    coupling = SwarmalatorCouplingInteraction(
        'coupling',
        first='units',
        second='units',
        A=A,
        B=B,
        J=J,
        K=K,
        n_units=n_units,
    )
    return System(elements=[units], interactions=[coupling], space=ContinuousSpace(n_dim=2))

Components

class SwarmalatorElement(SitedOscillatorElement):
    """Phase oscillator whose place `x` is a position that moves by the rates of interactions.

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

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

        theta:
            The initial phase.

        omega:
            The natural frequency.

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

    Examples:
        >>> from skcomplex.models.physics.synchronization import SwarmalatorElement
        >>> SwarmalatorElement('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='position')] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual, theta=theta, omega=omega, x=x)

class SwarmalatorCouplingInteraction(PairCouplingInteraction):
    """Coupling of two swarmalators through their separation `r` and their phase difference `dtheta`.

    Each swarmalator moves toward the other at the speed `(A + J cos(dtheta)) / N`, away from it at `B / (N |r|)`, and
    turns its phase toward the other's at the rate `K sin(dtheta) / (N |r|)`, as in O'Keeffe, Ceron, and Petersen. It
    is a pair coupling of the positions and the phases jointly.

    Args:
        label:
            The label of the interaction.

        first:
            The path of the group of the first members.

        second:
            The path of the group of the second members.

        cutoff:
            The distance beyond which pairs do not interact, large for the all-to-all model.

        position:
            The name of the vector state that holds the positions of the members.

        A:
            The strength of the attraction.

        B:
            The strength of the repulsion.

        J:
            The influence of the phase similarity on the attraction.

        K:
            The phase coupling, synchronizing when positive.

        period:
            The side of a periodic box, for minimum image distances, or `None` for an open space.

        n_units:
            The number of swarmalators `N` that normalizes the coupling.

    Examples:
        >>> from skcomplex.models.physics.synchronization import SwarmalatorCouplingInteraction
        >>> SwarmalatorCouplingInteraction('coupling', first='units', second='units', J=0.5).J
        0.5
    """

    def __init__(
        self,
        label: str,
        first: Annotated[str, RoleSpec()],
        second: Annotated[str, RoleSpec()],
        cutoff: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1000000.0,
        position: str = 'x',
        *,
        A: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        B: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        J: Annotated[float, ParameterSpec()] = 1.0,
        K: Annotated[float, ParameterSpec()] = 0.0,
        period: float | None = None,
        n_units: int = 1,
    ) -> None:
        super().__init__(
            label,
            first,
            second,
            cutoff=cutoff,
            position=position,
            write=(position, 'theta'),
            function=self._get_terms,
            period=period,
        )
        self.A = A
        self.B = B
        self.J = J
        self.K = K
        self.n_units = n_units

    def _get_terms(self, s: SimpleNamespace) -> tuple[Target, Target]:
        """Return the move and the phase turn of the first member of a pair toward the second."""
        separation = getattr(s.second, self.position) - getattr(s.first, self.position)
        r = sqrt(separation.dot_product(separation))
        difference = s.second.theta - s.first.theta
        move = ((s.A + s.J * cos(difference)) / r - s.B / r**2) * separation / self.n_units
        return move, s.K * sin(difference) / (self.n_units * r)

Equations