
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "auto_examples/05_kuramoto_sync.py"
.. LINE NUMBERS ARE GIVEN BELOW.

.. only:: html

    .. note::
        :class: sphx-glr-download-link-note

        :ref:`Go to the end <sphx_glr_download_auto_examples_05_kuramoto_sync.py>`
        to download the full example code.

.. rst-class:: sphx-glr-example-title

.. _sphx_glr_auto_examples_05_kuramoto_sync.py:


Kuramoto network: synchronization transition
===============================================

Sweeps the global coupling strength ``G`` and tracks the top two Lyapunov
exponents of a small heterogeneous-frequency Kuramoto network. At ``G=0``
both are exactly 0 (decoupled oscillators: ``dtheta/dt = omega`` is
state-independent, so the step map's Jacobian is exactly the identity --
see ``tests/test_network.py::test_kuramoto_zero_coupling_gives_exactly_zero_spectrum``).

The Kuramoto model has an exact continuous symmetry (``theta_i -> theta_i
+ c`` for all ``i`` leaves the dynamics unchanged), which guarantees one
Lyapunov exponent stays *exactly* 0 once phase-locked -- it never goes
negative. What actually signals synchronization is ``lambda_2``: below
threshold both leading exponents are small and positive (weakly
unstable/incoherent regime); once the network locks, ``lambda_1`` snaps to
machine-precision 0 (the marginal rotation mode) and ``lambda_2`` turns
negative, growing more negative as coupling strengthens the lock. This is
exactly what the run below shows -- not a hypothesis, it's what lyapax
actually produces for this network.

Note: this sweep calls ``lyapunov_spectrum`` once per ``G`` value in a
plain Python loop; see ``examples/11_vmap_parameter_sweep.py`` for the
same sweep done as a single batched ``jax.vmap`` call instead, and
``examples/07_speed_and_accuracy.py`` for what the per-call cost is.

**The system.** 6 oscillators, all-to-all coupled (``weights`` is the
complete graph: 1 everywhere off the diagonal, 0 on it), with
heterogeneous natural frequencies ``omega`` evenly spaced over ``[-1, 1]``
so the population would never sync on its own -- staggered initial
phases start it far from the locked state. The classic Kuramoto ODE
``dtheta_i/dt = omega_i + (G/N) sum_j sin(theta_j - theta_i)`` is built
here from the same machinery as
:ref:`04_linear_network.py <sphx_glr_auto_examples_04_linear_network.py>`:
``ModelSpec``/``build_jax_dfun`` compile the bare node dynamics
``"omega + c"`` (``c`` is the coupling input) into a JAX function, and
``kuramoto_coupling`` supplies the sine-coupling term as ``c``;
``network_problem`` wires the two together (plus a ``lyapax.Network``
topology and ``state0``/``dt``) into the flat step function the Lyapunov
engine steps. Only ``k=2`` exponents are
tracked per run (not the full spectrum of 6) since only the top two are
needed to read off the transition -- see the ``k`` parameter in
``lyapunov_spectrum``'s docstring for the leading-exponents-only cost
tradeoff.

.. GENERATED FROM PYTHON SOURCE LINES 47-64

.. code-block:: Python

    import os

    os.environ["JAX_PLATFORMS"] = "cpu"
    import time

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    import numpy as np

    jax.config.update("jax_enable_x64", True)

    from lyapax.core import lyapunov_spectrum
    from lyapax.coupling import kuramoto_coupling
    from lyapax.network import Network, network_problem
    from lyapax.simulator import ModelSpec, Parameter, StateVar, build_jax_dfun








.. GENERATED FROM PYTHON SOURCE LINES 65-103

.. code-block:: Python

    n_nodes = 6
    omega = jnp.linspace(-1.0, 1.0, n_nodes)
    weights = jnp.ones((n_nodes, n_nodes)) - jnp.eye(n_nodes)

    model = ModelSpec(
        name="kuramoto",
        state_variables=(StateVar("theta", default_init=0.0),),
        parameters=(Parameter("omega", 0.0),),
        cvar=("theta",),
        dfun_str={"theta": "omega + c"},
    )
    dfun = build_jax_dfun(model)
    network = Network(weights=weights, cvar_indices=model.cvar_indices)

    dt = 1e-2
    state0 = jnp.linspace(0.0, 2 * jnp.pi, n_nodes, endpoint=False)

    G_values = np.linspace(0.0, 4.0, 13)
    lambda1, lambda2 = [], []

    t0 = time.perf_counter()
    for G in G_values:
        params = {"omega": omega, "G": float(G)}
        problem = network_problem(
            dfun, network, kuramoto_coupling(alpha=0.0),
            params=params, state0=state0, dt=dt,
        )
        result = lyapunov_spectrum(
            problem, n_steps=5_000, renorm_every=10, k=2, t_transient=50.0,
        )
        lambda1.append(float(result.exponents[0]))
        lambda2.append(float(result.exponents[1]))
    elapsed = time.perf_counter() - t0
    print(f"swept {len(G_values)} values of G in {elapsed:.1f}s "
          f"({elapsed / len(G_values):.2f}s/run)")
    for G, l1, l2 in zip(G_values, lambda1, lambda2):
        print(f"  G={G:.2f}  lambda1={l1:+.4f}  lambda2={l2:+.4f}")





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    swept 13 values of G in 5.8s (0.45s/run)
      G=0.00  lambda1=+0.0000  lambda2=+0.0000
      G=0.33  lambda1=+0.0040  lambda2=+0.0034
      G=0.67  lambda1=+0.0126  lambda2=+0.0070
      G=1.00  lambda1=+0.0289  lambda2=+0.0121
      G=1.33  lambda1=+0.0243  lambda2=+0.0116
      G=1.67  lambda1=+0.0000  lambda2=-0.7518
      G=2.00  lambda1=+0.0000  lambda2=-1.3269
      G=2.33  lambda1=+0.0000  lambda2=-1.7867
      G=2.67  lambda1=+0.0000  lambda2=-2.2025
      G=3.00  lambda1=+0.0000  lambda2=-2.5951
      G=3.33  lambda1=+0.0000  lambda2=-2.9734
      G=3.67  lambda1=+0.0000  lambda2=-3.3423
      G=4.00  lambda1=+0.0000  lambda2=-3.7044




.. GENERATED FROM PYTHON SOURCE LINES 104-114

.. code-block:: Python

    fig, ax = plt.subplots(figsize=(6, 4))
    ax.plot(G_values, lambda1, "o-", label=r"$\lambda_1$ (always -> 0: symmetry mode)")
    ax.plot(G_values, lambda2, "o-", label=r"$\lambda_2$ (synchronization strength)")
    ax.axhline(0.0, color="gray", lw=0.5)
    ax.set_xlabel("coupling strength G")
    ax.set_ylabel("Lyapunov exponent")
    ax.set_title("Kuramoto network: synchronization transition")
    ax.legend()
    fig.tight_layout()
    plt.show()



.. image-sg:: /auto_examples/images/sphx_glr_05_kuramoto_sync_001.png
   :alt: Kuramoto network: synchronization transition
   :srcset: /auto_examples/images/sphx_glr_05_kuramoto_sync_001.png
   :class: sphx-glr-single-img






.. rst-class:: sphx-glr-timing

   **Total running time of the script:** (0 minutes 6.249 seconds)


.. _sphx_glr_download_auto_examples_05_kuramoto_sync.py:

.. only:: html

  .. container:: sphx-glr-footer sphx-glr-footer-example

    .. container:: sphx-glr-download sphx-glr-download-jupyter

      :download:`Download Jupyter notebook: 05_kuramoto_sync.ipynb <05_kuramoto_sync.ipynb>`

    .. container:: sphx-glr-download sphx-glr-download-python

      :download:`Download Python source code: 05_kuramoto_sync.py <05_kuramoto_sync.py>`

    .. container:: sphx-glr-download sphx-glr-download-zip

      :download:`Download zipped: 05_kuramoto_sync.zip <05_kuramoto_sync.zip>`


.. only:: html

 .. rst-class:: sphx-glr-signature

    `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_
