.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "gallery/limitcycle_demo.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_gallery_limitcycle_demo.py: 2D limit cycle — nonlinear overdamped inference ================================================= Infer the force field of a 2D nonlinear system with a stable limit cycle. The radial force :math:`\dot{r} = r(1 - r^2)` drives the system to a unit circle, while an angular drift :math:`\dot{\theta} = \omega` generates rotation — a non-equilibrium steady state with non-zero probability currents. .. rubric:: Tags synthetic · overdamped · nonlinear · non-equilibrium · 2D .. GENERATED FROM PYTHON SOURCE LINES 15-17 .. code-block:: Python :dedent: 1 .. GENERATED FROM PYTHON SOURCE LINES 37-49 System definition ------------------ In Cartesian coordinates the force is: .. math:: F_x = x(1 - x^2 - y^2) - \omega\, y, \qquad F_y = y(1 - x^2 - y^2) + \omega\, x with angular frequency :math:`\omega = 2` and isotropic noise :math:`D = 0.1`. .. GENERATED FROM PYTHON SOURCE LINES 49-76 .. code-block:: Python from SFI.bases import named_scalars, ones_basis, unit_axes, x_components from SFI.langevin import OverdampedProcess # Ground-truth force written compositionally: # F = (1 - r^2) (x e_x + y e_y) + omega (x e_y - y e_x) # with r^2 = x^2 + y^2. ``named_scalars`` holds ``omega`` with a # default value so the simulator can bind parameters automatically. x, y = x_components(2) ex, ey = unit_axes(2) one = ones_basis(2) (omega_p,) = named_scalars(omega=2.0) r2 = x * x + y * y F_true = (one - r2) * (x * ex + y * ey) + omega_p * (x * ey - y * ex) D0 = 0.1 dt = 0.05 Nsteps = 4_000 seed = 0 proc = OverdampedProcess(F_true, D=jnp.eye(2) * D0) proc.initialize(jnp.array([0.5, 0.0])) key = random.PRNGKey(seed) coll = proc.simulate(dt=dt, Nsteps=Nsteps, key=key, prerun=5000, oversampling=20) .. GENERATED FROM PYTHON SOURCE LINES 77-82 Trajectory and inferred force field -------------------------------------- Build a polynomial basis, infer, sparsify with PASTIS, then overlay the inferred force field on the trajectory. .. GENERATED FROM PYTHON SOURCE LINES 82-100 .. code-block:: Python from SFI.bases import monomials_up_to from SFI import OverdampedLangevinInference from SFI.utils import plotting degree = 3 B = monomials_up_to(order=degree, dim=2, include_constant=True, rank='vector') inf = OverdampedLangevinInference(coll) inf.compute_diffusion_constant() inf.infer_force_linear(B, M_mode="Ito") inf.compute_force_error() inf.sparsify_force(criterion="PASTIS") inf.compare_to_exact(model_exact=proc, maxpoints=2000) inf.print_report() .. image-sg:: /gallery/images/sphx_glr_limitcycle_demo_001.png :alt: Limit cycle — trajectory and force field, Trajectory + force field (gold=inferred, red=exact), $r=0.9992$ MSE=0.001 :srcset: /gallery/images/sphx_glr_limitcycle_demo_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none --- StochasticForceInference Report --- Average diffusion tensor: [[ 0.1006882 -0.0014048 ] [-0.0014048 0.10216548]] Measurement noise tensor: [[-0.00485671 -0.00024895] [-0.00024895 -0.00476173]] Force estimated information: 2099.39599609375 Force: estimated normalized mean squared error (sampling only): 0.0047632734165367506 Normalized MSE (force): 0.0017 Normalized MSE (diffusion): 0.0004 Force Coefficient Table ───────────────────────────────────────────────────────────────────── # Label Coefficient Std.Err SNR Sig ───────────────────────────────────────────────────────────────────── 2 x0·e0 1.13881e+00 1.45951e-01 7.8 * 3 x0·e1 2.03314e+00 1.47018e-01 13.8 ** 4 x1·e0 -1.94517e+00 1.38036e-01 14.1 ** 5 x1·e1 9.05424e-01 1.39045e-01 6.5 * 12 x0^3·e0 -1.02404e+00 1.19558e-01 8.6 * 15 (x0^2·x1)·e1 -1.03951e+00 1.52876e-01 6.8 * 16 (x0·x1^2)·e0 -1.14151e+00 1.55510e-01 7.3 * 19 x1^3·e1 -9.12634e-01 1.12635e-01 8.1 * ───────────────────────────────────────────────────────────────────── 8/20 basis functions in support, sig: 8* / 2** / 0*** (|SNR| ≥ 2 / 10 / 100) (Std.err. reflects sampling error only; discretization bias is not included.) Zeroed (12): 1·e0, 1·e1, x0^2·e0, x0^2·e1, (x0·x1)·e0, (x0·x1)·e1, x1^2·e0, x1^2·e1, x0^3·e1, (x0^2·x1)·e0, (x0·x1^2)·e1, x1^3·e0 .. GENERATED FROM PYTHON SOURCE LINES 131-137 Force error norm ----------------- A 2D map of the force-reconstruction error :math:`\|F_{\mathrm{exact}} - F_{\mathrm{inferred}}\|` evaluated on a regular grid covering the data domain. .. GENERATED FROM PYTHON SOURCE LINES 137-138 .. code-block:: Python :dedent: 1 .. image-sg:: /gallery/images/sphx_glr_limitcycle_demo_002.png :alt: Force error norm (2D map) :srcset: /gallery/images/sphx_glr_limitcycle_demo_002.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 151-156 Bootstrapped trajectory ------------------------- A trajectory simulated from the inferred model should reproduce the limit-cycle structure and rotational dynamics. .. GENERATED FROM PYTHON SOURCE LINES 156-161 .. code-block:: Python key_boot = random.PRNGKey(seed + 123) coll_boot, _ = inf.simulate_bootstrapped_trajectory(key_boot, oversampling=20) .. image-sg:: /gallery/images/sphx_glr_limitcycle_demo_003.png :alt: Generative validation — limit cycle, Original data, Bootstrapped (inferred model) :srcset: /gallery/images/sphx_glr_limitcycle_demo_003.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 178-186 Diagnostics ----------- :func:`~SFI.diagnostics.assess` recomputes standardised residuals and runs a panel of consistency tests; a well-specified fit yields residuals consistent with i.i.d. :math:`\mathcal{N}(0, 1)` — no autocorrelation, no excess kurtosis, and a realised NMSE consistent with the predicted value. .. GENERATED FROM PYTHON SOURCE LINES 186-192 .. code-block:: Python from SFI.diagnostics import assess, plot_summary report = assess(inf, level="standard") report.print_summary() .. image-sg:: /gallery/images/sphx_glr_limitcycle_demo_004.png :alt: Limit cycle — diagnostics, Q--Q plot, Residual histogram, Residual ACF :srcset: /gallery/images/sphx_glr_limitcycle_demo_004.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none === SFI diagnostics report === backend : linear regime : OD n_obs : 3999 n_particles: 1 d: 2 level : standard -- Residuals -- mean = +0.0133 std = 0.9777 skew = +0.007 kurt-3 = +0.010 (n=7998) ✓ normality ks stat=0.0136 p=0.103 ✓ autocorr ljung_box stat=20 p=0.456 ✓ autocorr ljung_box_squared stat=22.7 p=0.303 predicted NMSE = 0.00476 realised NMSE = 0 χ² z = -2.78 (|z|>5 ⇒ bias) -- Flags -- (no issues at α = 0.01) .. GENERATED FROM PYTHON SOURCE LINES 198-202 Thumbnail --------- Compact force-error map for the gallery grid. .. GENERATED FROM PYTHON SOURCE LINES 202-205 .. code-block:: Python stamp_output() .. image-sg:: /gallery/images/sphx_glr_limitcycle_demo_005.png :alt: limitcycle demo :srcset: /gallery/images/sphx_glr_limitcycle_demo_005.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none [Generated: 2026-06-30 10:02] .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 22.243 seconds) .. rst-class:: sphx-glr-example-tags 🏷 Tags: synthetic, overdamped, nonlinear, non-equilibrium, 2D .. _sphx_glr_download_gallery_limitcycle_demo.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: limitcycle_demo.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: limitcycle_demo.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: limitcycle_demo.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_