
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "auto_examples/12_public_api_overview.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_12_public_api_overview.py>`
        to download the full example code.

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

.. _sphx_glr_auto_examples_12_public_api_overview.py:


The lyapax front door: problem objects for ODEs, networks, and DDEs
=======================================================================

Every example so far reaches the Lyapunov engine through a different door:
plain systems build a step with ``rk4_step`` and pass ``state0``/``dt`` a
second time into ``lyapunov_spectrum``, networks assemble a step through
``make_network_step_fn`` with topology/coupling/integrator all passed as
separate positional arguments (and, again, a second ``dt``), and delayed
systems hand ``lyapunov_spectrum_dde`` a raw ``(state0, buf0, params, dt)``
tuple that exposes the ring buffer directly. Passing ``dt`` twice is not
just repetitive -- nothing checks that the two copies agree, so a typo in
either one silently computes the spectrum for the wrong step size.

This demo shows the layer that gives all three cases the *same* four-step
recipe, with each piece of data given exactly once:

1. define the dynamics (``rhs`` or ``dfun``),
2. optionally define a network (``Network``) and coupling rule,
3. pick an integrator by name (``"euler"``, ``"heun"``, ``"rk4"``),
4. bundle everything (including ``state0`` and ``dt``) into a problem
   object, and hand *that* to the spectrum function together with
   ``n_steps``.

``ode_problem``, ``network_problem``, and ``network_dde_problem`` are thin
wrappers around the exact same engine used in the earlier examples --
nothing about the numerics changes, only how much plumbing the caller has
to see and keep in sync by hand. Each section below re-derives one of the
earlier examples' known-exact answers through the new front door, so "does
this actually compute the same thing" is checked, not just asserted.

.. GENERATED FROM PYTHON SOURCE LINES 33-49

.. code-block:: Python

    import os

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

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    import numpy as np
    from scipy.special import lambertw

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

    import lyapax
    from lyapax import coupling as lc
    from lyapax.simulator import ModelSpec, Parameter, StateVar, build_jax_dfun








.. GENERATED FROM PYTHON SOURCE LINES 50-59

1. Plain ODE: ``ode_problem(rhs, state0, dt, integrator=...)``
-----------------------------------------------------------------
Same linear system as
:ref:`01_linear_ode.py <sphx_glr_auto_examples_01_linear_ode.py>` (diagonal
``A``, so the
exact Lyapunov spectrum is just ``A``'s eigenvalues). ``ode_problem``
builds the step (via ``ode_step`` internally) and carries ``state0``/``dt``
alongside it, so ``lyapunov_spectrum`` only needs the problem and
``n_steps`` -- ``dt`` is given once, not once per function call.

.. GENERATED FROM PYTHON SOURCE LINES 59-78

.. code-block:: Python

    A = jnp.diag(jnp.array([-1.0, -2.0, -5.0]))


    def linear_rhs(state):
        return A @ state


    problem_ode = lyapax.ode_problem(
        linear_rhs, state0=jnp.array([0.3, -0.2, 0.5]), dt=1e-3, integrator="rk4",
    )
    result_ode = lyapax.lyapunov_spectrum(
        problem_ode, n_steps=20_000, renorm_every=10, t_transient=5.0,
    )

    expected_ode = np.array([-1.0, -2.0, -5.0])
    print("plain ODE")
    print(f"  exact eigenvalues:  {expected_ode}")
    print(f"  ode_problem estimate: {np.array(result_ode.exponents)}")





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

 .. code-block:: none

    plain ODE
      exact eigenvalues:  [-1. -2. -5.]
      ode_problem estimate: [-1.00000095 -1.99999905 -5.        ]




.. GENERATED FROM PYTHON SOURCE LINES 79-89

2. Coupled network: ``Network`` + ``network_problem(..., integrator=...)``
--------------------------------------------------------------------------
Same 4-node cycle network as
:ref:`04_linear_network.py <sphx_glr_auto_examples_04_linear_network.py>`:
identical
scalar node dynamics ``x_i' = gamma*x_i + c_i`` coupled linearly through
adjacency matrix ``W``, so the exact spectrum is ``eig(gamma*I + G*W)``.
``Network`` names ``weights``/``cvar_indices`` as one object instead of two
parallel positional arguments; ``network_problem`` reads them off it and,
like ``ode_problem`` above, folds in ``state0``/``dt`` too.

.. GENERATED FROM PYTHON SOURCE LINES 89-121

.. code-block:: Python

    weights = jnp.array([
        [0., 1., 0., 1.],
        [1., 0., 1., 0.],
        [0., 1., 0., 1.],
        [1., 0., 1., 0.],
    ])
    gamma, G = -2.0, 0.5
    expected_net = np.sort(np.linalg.eigvalsh(gamma * np.eye(4) + G * np.array(weights)))[::-1]

    model = ModelSpec(
        name="linear_node",
        state_variables=(StateVar("x", default_init=0.0),),
        parameters=(Parameter("gamma", gamma),),
        cvar=("x",),
        dfun_str={"x": "gamma * x + c"},
    )
    dfun = build_jax_dfun(model)
    network = lyapax.Network(weights=weights, cvar_indices=model.cvar_indices)

    problem_net = lyapax.network_problem(
        dfun, network, lc.linear_coupling(a=1.0, b=0.0),
        params={"gamma": gamma, "G": G}, state0=jnp.array([0.3, -0.1, 0.2, -0.4]),
        dt=1e-3, integrator="heun",
    )
    result_net = lyapax.lyapunov_spectrum(
        problem_net, n_steps=20_000, renorm_every=10, t_transient=5.0,
    )

    print("coupled network")
    print(f"  exact eigenvalues:      {expected_net}")
    print(f"  network_problem estimate: {np.array(result_net.exponents)}")





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

 .. code-block:: none

    coupled network
      exact eigenvalues:      [-1. -2. -2. -3.]
      network_problem estimate: [-1.00000336 -1.99999657 -1.99999842 -2.9999943 ]




.. GENERATED FROM PYTHON SOURCE LINES 122-134

3. Delayed coupled network: ``network_dde_problem``
------------------------------------------------------
Same 2-node delayed linear network as
:ref:`08_delayed_coupling.py <sphx_glr_auto_examples_08_delayed_coupling.py>`
(``x1' = gamma*x1 + G*x2(t-tau)``, ``x2' = gamma*x2 + G*x1(t-tau)``), whose
symmetric/antisymmetric modes have closed-form Lambert-W exponents. There,
building this required ``Connectivity`` for the delay matrix and a raw
``(state0, buf0, params, dt)`` call into ``lyapunov_spectrum_dde``.
``network_dde_problem`` collapses that into the same
dynamics/network/coupling/integrator recipe as the zero-delay case above,
plus ``tau`` -- the ring buffer (``buf0``) and its initial constant
history are built for you.

.. GENERATED FROM PYTHON SOURCE LINES 134-162

.. code-block:: Python

    weights_2 = jnp.array([[0., 1.], [1., 0.]])
    network_2 = lyapax.Network(weights=weights_2, cvar_indices=model.cvar_indices)
    tau = 0.2
    dt_dde = 1e-3

    problem = lyapax.network_dde_problem(
        dfun, network_2, lc.linear_coupling(a=1.0, b=0.0),
        params={"gamma": gamma, "G": G}, state0=jnp.array([[0.3, -0.2]]),
        dt=dt_dde, tau=tau, integrator="heun",
    )
    result_dde = lyapax.lyapunov_spectrum_dde(
        problem, n_steps=20_000, k=2, renorm_every=10, t_transient=5.0,
    )


    def lambda_sym(tau):
        return gamma + float((lambertw(G * tau * np.exp(-gamma * tau), k=0) / tau).real)


    def lambda_antisym(tau):
        return gamma + float((lambertw(-G * tau * np.exp(-gamma * tau), k=0) / tau).real)


    expected_dde = np.array([lambda_sym(tau), lambda_antisym(tau)])
    print("delayed coupled network")
    print(f"  exact (Lambert W):            {expected_dde}")
    print(f"  network_dde_problem estimate: {np.array(result_dde.exponents)}")





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

 .. code-block:: none

    delayed coupled network
      exact (Lambert W):            [-1.34559461 -2.89150289]
      network_dde_problem estimate: [-1.34520443 -2.89307176]




.. GENERATED FROM PYTHON SOURCE LINES 163-164

All three estimates converging to their known-exact answers, side by side.

.. GENERATED FROM PYTHON SOURCE LINES 164-182

.. code-block:: Python

    fig, axes = plt.subplots(1, 3, figsize=(13, 4), sharey=False)

    for ax, result, expected, title in zip(
        axes,
        [result_ode, result_net, result_dde],
        [expected_ode, expected_net, expected_dde],
        ["ode_problem", "network_problem", "network_dde_problem"],
    ):
        h, t = np.array(result.history), np.array(result.times)
        for i, lam in enumerate(expected):
            ax.plot(t, h[:, i], color=f"C{i}")
            ax.axhline(lam, color=f"C{i}", linestyle="--", alpha=0.5)
        ax.set_xlabel("time (post-transient)")
        ax.set_title(title)
    axes[0].set_ylabel("running Lyapunov exponent estimate")
    fig.suptitle("New front door, same engine: convergence to known-exact spectra")
    fig.tight_layout()
    plt.show()



.. image-sg:: /auto_examples/images/sphx_glr_12_public_api_overview_001.png
   :alt: New front door, same engine: convergence to known-exact spectra, ode_problem, network_problem, network_dde_problem
   :srcset: /auto_examples/images/sphx_glr_12_public_api_overview_001.png
   :class: sphx-glr-single-img






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

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


.. _sphx_glr_download_auto_examples_12_public_api_overview.py:

.. only:: html

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

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

      :download:`Download Jupyter notebook: 12_public_api_overview.ipynb <12_public_api_overview.ipynb>`

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

      :download:`Download Python source code: 12_public_api_overview.py <12_public_api_overview.py>`

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

      :download:`Download zipped: 12_public_api_overview.zip <12_public_api_overview.zip>`


.. only:: html

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

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