jaxcont 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jaxcont/__init__.py +111 -0
- jaxcont/_version.py +3 -0
- jaxcont/api.py +478 -0
- jaxcont/bifurcations/__init__.py +18 -0
- jaxcont/bifurcations/detector.py +374 -0
- jaxcont/bifurcations/fold.py +108 -0
- jaxcont/bifurcations/fold_solve.py +152 -0
- jaxcont/bifurcations/hopf.py +148 -0
- jaxcont/bifurcations/period_doubling.py +99 -0
- jaxcont/bifurcations/taxonomy.py +90 -0
- jaxcont/core/__init__.py +21 -0
- jaxcont/core/continuation.py +250 -0
- jaxcont/core/natural_continuation.py +138 -0
- jaxcont/core/predictor_corrector.py +414 -0
- jaxcont/core/pseudo_arclength.py +271 -0
- jaxcont/core/scan_continuation.py +267 -0
- jaxcont/problems/__init__.py +7 -0
- jaxcont/problems/bvp.py +58 -0
- jaxcont/problems/equilibrium.py +86 -0
- jaxcont/problems/periodic.py +120 -0
- jaxcont/solvers/__init__.py +6 -0
- jaxcont/solvers/corrector.py +49 -0
- jaxcont/solvers/newton.py +172 -0
- jaxcont/stability/__init__.py +6 -0
- jaxcont/stability/eigenvalue.py +138 -0
- jaxcont/stability/floquet.py +117 -0
- jaxcont/utils/__init__.py +22 -0
- jaxcont/utils/config.py +344 -0
- jaxcont/utils/plotting.py +246 -0
- jaxcont-0.1.0.dist-info/METADATA +165 -0
- jaxcont-0.1.0.dist-info/RECORD +34 -0
- jaxcont-0.1.0.dist-info/WHEEL +5 -0
- jaxcont-0.1.0.dist-info/licenses/LICENSE +21 -0
- jaxcont-0.1.0.dist-info/top_level.txt +1 -0
jaxcont/__init__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JaxCont: High-Performance Continuation and Bifurcation Analysis in JAX
|
|
3
|
+
|
|
4
|
+
A modern Python package for numerical continuation and bifurcation analysis
|
|
5
|
+
of dynamical systems, leveraging JAX's automatic differentiation and JIT
|
|
6
|
+
compilation for exceptional performance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from jaxcont._version import __version__
|
|
10
|
+
|
|
11
|
+
__author__ = "Abolfazl Ziaeemehr"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
|
|
14
|
+
# Functional API -- the blessed public surface: bif_problem() + continuation()
|
|
15
|
+
from jaxcont.api import (
|
|
16
|
+
BifProblem,
|
|
17
|
+
bif_problem,
|
|
18
|
+
continuation,
|
|
19
|
+
ContinuationPar,
|
|
20
|
+
ContinuationAlgorithm,
|
|
21
|
+
PseudoArclength,
|
|
22
|
+
Natural,
|
|
23
|
+
Event,
|
|
24
|
+
Fold,
|
|
25
|
+
Hopf,
|
|
26
|
+
EventHit,
|
|
27
|
+
Branch,
|
|
28
|
+
ContinuationResult,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Core imports
|
|
32
|
+
from jaxcont.core.continuation import (
|
|
33
|
+
ContinuationProblem,
|
|
34
|
+
ContinuationSolution,
|
|
35
|
+
equilibrium_continuation,
|
|
36
|
+
)
|
|
37
|
+
from jaxcont.core.predictor_corrector import PredictorCorrector
|
|
38
|
+
from jaxcont.core.natural_continuation import NaturalContinuation
|
|
39
|
+
from jaxcont.core.pseudo_arclength import PseudoArclengthContinuation
|
|
40
|
+
|
|
41
|
+
# Problem definitions
|
|
42
|
+
from jaxcont.problems.equilibrium import EquilibriumProblem
|
|
43
|
+
|
|
44
|
+
# Differentiable fold solver (reverse-mode grad of a fold location via the
|
|
45
|
+
# implicit function theorem -- see examples/example_07_differentiable.py)
|
|
46
|
+
from jaxcont.bifurcations.fold_solve import fold_point, fold_parameter
|
|
47
|
+
|
|
48
|
+
# Bifurcation detection
|
|
49
|
+
from jaxcont.bifurcations.detector import BifurcationDetector
|
|
50
|
+
from jaxcont.bifurcations.fold import FoldBifurcation
|
|
51
|
+
from jaxcont.bifurcations.hopf import HopfBifurcation
|
|
52
|
+
|
|
53
|
+
# Solvers
|
|
54
|
+
from jaxcont.solvers.newton import NewtonSolver
|
|
55
|
+
from jaxcont.solvers.corrector import Corrector
|
|
56
|
+
|
|
57
|
+
# Stability analysis
|
|
58
|
+
from jaxcont.stability.eigenvalue import compute_eigenvalues, analyze_stability
|
|
59
|
+
|
|
60
|
+
# NOTE: v0.1.0 ships equilibria only. Periodic-orbit / Floquet / BVP / period-
|
|
61
|
+
# doubling APIs are experimental stubs and are intentionally NOT exported at
|
|
62
|
+
# the top level (see the project roadmap). They remain importable from their
|
|
63
|
+
# submodules for development, e.g.:
|
|
64
|
+
# from jaxcont.problems.periodic import PeriodicOrbitProblem
|
|
65
|
+
# from jaxcont.stability.floquet import compute_floquet_multipliers
|
|
66
|
+
|
|
67
|
+
# Utilities
|
|
68
|
+
from jaxcont.utils.config import Config
|
|
69
|
+
from jaxcont.utils.plotting import plot_bifurcation_diagram, plot_continuation
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
# Functional API (blessed surface)
|
|
73
|
+
"BifProblem",
|
|
74
|
+
"bif_problem",
|
|
75
|
+
"continuation",
|
|
76
|
+
"ContinuationPar",
|
|
77
|
+
"ContinuationAlgorithm",
|
|
78
|
+
"PseudoArclength",
|
|
79
|
+
"Natural",
|
|
80
|
+
"Event",
|
|
81
|
+
"Fold",
|
|
82
|
+
"Hopf",
|
|
83
|
+
"EventHit",
|
|
84
|
+
"Branch",
|
|
85
|
+
"ContinuationResult",
|
|
86
|
+
"fold_point",
|
|
87
|
+
"fold_parameter",
|
|
88
|
+
# Core
|
|
89
|
+
"ContinuationProblem",
|
|
90
|
+
"ContinuationSolution",
|
|
91
|
+
"equilibrium_continuation",
|
|
92
|
+
"PredictorCorrector",
|
|
93
|
+
"NaturalContinuation",
|
|
94
|
+
"PseudoArclengthContinuation",
|
|
95
|
+
# Problems
|
|
96
|
+
"EquilibriumProblem",
|
|
97
|
+
# Bifurcations
|
|
98
|
+
"BifurcationDetector",
|
|
99
|
+
"FoldBifurcation",
|
|
100
|
+
"HopfBifurcation",
|
|
101
|
+
# Solvers
|
|
102
|
+
"NewtonSolver",
|
|
103
|
+
"Corrector",
|
|
104
|
+
# Stability
|
|
105
|
+
"compute_eigenvalues",
|
|
106
|
+
"analyze_stability",
|
|
107
|
+
# Utilities
|
|
108
|
+
"Config",
|
|
109
|
+
"plot_bifurcation_diagram",
|
|
110
|
+
"plot_continuation",
|
|
111
|
+
]
|
jaxcont/_version.py
ADDED
jaxcont/api.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functional, diffrax-style public API for JaxCont (the "Sketch A" spine).
|
|
3
|
+
|
|
4
|
+
This is the blessed surface going forward:
|
|
5
|
+
|
|
6
|
+
prob = jaxcont.bif_problem(f, u0, p0)
|
|
7
|
+
sol = jaxcont.continuation(prob, p_span=(0.0, 1.0), events=[jaxcont.Fold()])
|
|
8
|
+
|
|
9
|
+
The default algorithm (`PseudoArclength(engine="scan")`) runs on the fully
|
|
10
|
+
JIT-compiled whole-loop engine in `core/scan_continuation.py`, which is what
|
|
11
|
+
makes `vmap`-batched continuation and `jax.grad`/`jax.jacfwd` through the
|
|
12
|
+
analysis possible; `engine="legacy"` and `NaturalContinuation` fall back to
|
|
13
|
+
the class-based Python outer loop for compatibility.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field, replace
|
|
19
|
+
from typing import Any, Callable, Literal, Optional, Sequence, Tuple
|
|
20
|
+
|
|
21
|
+
import jax
|
|
22
|
+
import jax.numpy as jnp
|
|
23
|
+
from jax import Array
|
|
24
|
+
|
|
25
|
+
from jaxcont.core.continuation import ContinuationProblem, ContinuationSolution
|
|
26
|
+
from jaxcont.core.pseudo_arclength import PseudoArclengthContinuation
|
|
27
|
+
from jaxcont.core.natural_continuation import NaturalContinuation
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BifProblem",
|
|
31
|
+
"bif_problem",
|
|
32
|
+
"continuation",
|
|
33
|
+
"ContinuationPar",
|
|
34
|
+
"ContinuationAlgorithm",
|
|
35
|
+
"PseudoArclength",
|
|
36
|
+
"Natural",
|
|
37
|
+
"Event",
|
|
38
|
+
"Fold",
|
|
39
|
+
"Hopf",
|
|
40
|
+
"EventHit",
|
|
41
|
+
"Branch",
|
|
42
|
+
"ContinuationResult",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
PyTree = Any
|
|
46
|
+
|
|
47
|
+
# Internal name for the continuation parameter inside the legacy params dict.
|
|
48
|
+
_P_KEY = "__jaxcont_p__"
|
|
49
|
+
|
|
50
|
+
# Sentinel for "leave this field unchanged" (distinct from args=None, which is
|
|
51
|
+
# a legitimate value meaning "no extra args").
|
|
52
|
+
_KEEP = object()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Problem
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class BifProblem:
|
|
61
|
+
"""
|
|
62
|
+
A continuation/bifurcation problem: find ``u`` with ``f(u, p, args) = 0``
|
|
63
|
+
and continue it in the scalar parameter ``p``.
|
|
64
|
+
|
|
65
|
+
``f`` is a pure function ``f(u, p, args) -> residual`` (same shape as ``u``).
|
|
66
|
+
Extra parameters live in ``args`` (any PyTree) — this is the axis you
|
|
67
|
+
``vmap``/``grad`` over (a second parameter, a design vector, NN weights).
|
|
68
|
+
|
|
69
|
+
Registered as a JAX PyTree: ``u0``, ``p0``, ``args`` are dynamic leaves;
|
|
70
|
+
``f`` and ``kind`` are static. This makes ``jax.vmap(..., in_axes=...)`` over
|
|
71
|
+
a batched problem structurally valid (full end-to-end vmap awaits the
|
|
72
|
+
lax.scan loop rewrite; see ARCHITECTURE.md §3.1).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
f: Callable[[Array, Array, PyTree], Array]
|
|
76
|
+
u0: Array
|
|
77
|
+
p0: Array
|
|
78
|
+
args: PyTree = None
|
|
79
|
+
kind: Literal["equilibrium", "periodic", "bvp"] = "equilibrium"
|
|
80
|
+
|
|
81
|
+
def at(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
u0: Optional[Array] = None,
|
|
85
|
+
p0: Optional[Array] = None,
|
|
86
|
+
args: PyTree = _KEEP,
|
|
87
|
+
) -> "BifProblem":
|
|
88
|
+
"""Return a copy with selected fields overridden (cheap, functional)."""
|
|
89
|
+
return replace(
|
|
90
|
+
self,
|
|
91
|
+
u0=self.u0 if u0 is None else u0,
|
|
92
|
+
p0=self.p0 if p0 is None else p0,
|
|
93
|
+
args=self.args if args is _KEEP else args,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def as_rhs(self, p) -> Callable[[Array], Array]:
|
|
97
|
+
"""
|
|
98
|
+
Return an autonomous ``rhs(u)`` frozen at parameter value ``p``.
|
|
99
|
+
|
|
100
|
+
This is the lyapax bridge (ARCHITECTURE.md §9): hand a branch point to
|
|
101
|
+
``lyapax.ode_problem(prob.as_rhs(p_star), state0=u_star, ...)``.
|
|
102
|
+
"""
|
|
103
|
+
args = self.args
|
|
104
|
+
f = self.f
|
|
105
|
+
return lambda u: f(u, p, args)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _bifproblem_flatten(prob: BifProblem):
|
|
109
|
+
children = (prob.u0, prob.p0, prob.args)
|
|
110
|
+
aux = (prob.f, prob.kind)
|
|
111
|
+
return children, aux
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _bifproblem_unflatten(aux, children):
|
|
115
|
+
f, kind = aux
|
|
116
|
+
u0, p0, args = children
|
|
117
|
+
return BifProblem(f=f, u0=u0, p0=p0, args=args, kind=kind)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
jax.tree_util.register_pytree_node(
|
|
121
|
+
BifProblem, _bifproblem_flatten, _bifproblem_unflatten
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def bif_problem(
|
|
126
|
+
f: Callable[[Array, Array, PyTree], Array],
|
|
127
|
+
u0: Array,
|
|
128
|
+
p0: float | Array,
|
|
129
|
+
*,
|
|
130
|
+
args: PyTree = None,
|
|
131
|
+
kind: str = "equilibrium",
|
|
132
|
+
) -> BifProblem:
|
|
133
|
+
"""
|
|
134
|
+
Front-door factory for a :class:`BifProblem` (mirrors lyapax's
|
|
135
|
+
``ode_problem``). Coerces ``u0``/``p0`` to JAX arrays.
|
|
136
|
+
"""
|
|
137
|
+
return BifProblem(
|
|
138
|
+
f=f,
|
|
139
|
+
u0=jnp.asarray(u0),
|
|
140
|
+
p0=jnp.asarray(p0, dtype=jnp.asarray(u0).dtype),
|
|
141
|
+
args=args,
|
|
142
|
+
kind=kind,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Algorithms & settings
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
class ContinuationAlgorithm:
|
|
151
|
+
"""Marker base for continuation algorithms."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class PseudoArclength(ContinuationAlgorithm):
|
|
156
|
+
"""
|
|
157
|
+
Pseudo-arclength continuation (default; passes fold points).
|
|
158
|
+
|
|
159
|
+
``engine`` selects the implementation.
|
|
160
|
+
|
|
161
|
+
* ``"scan"`` (default) is the fully JIT-compiled whole-loop engine
|
|
162
|
+
(``core/scan_continuation.py``): it is ``vmap``-able and structurally
|
|
163
|
+
bounded. Detection/stability are computed as a vectorized post-pass and
|
|
164
|
+
refined with the same detector.
|
|
165
|
+
* ``"legacy"`` is the class-based Python outer loop, retained for
|
|
166
|
+
compatibility.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
engine: Literal["legacy", "scan"] = "scan"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class Natural(ContinuationAlgorithm):
|
|
174
|
+
"""Natural-parameter continuation (simple; stalls at folds)."""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class ContinuationPar:
|
|
179
|
+
"""Numerical settings for a continuation run."""
|
|
180
|
+
|
|
181
|
+
ds: float = 0.01
|
|
182
|
+
ds_min: float = 1e-5
|
|
183
|
+
ds_max: float = 0.1
|
|
184
|
+
max_steps: int = 1000
|
|
185
|
+
adaptive: bool = True
|
|
186
|
+
newton_tol: float = 1e-6
|
|
187
|
+
newton_max_iter: int = 20
|
|
188
|
+
compute_stability: bool = True
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Events
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
class Event:
|
|
196
|
+
"""Marker base for a bifurcation/event detector.
|
|
197
|
+
|
|
198
|
+
Naming follows the standard abbreviations used throughout the
|
|
199
|
+
bifurcation-theory literature (see
|
|
200
|
+
``jaxcont.bifurcations.taxonomy.BIFURCATION_TYPES``) rather than
|
|
201
|
+
inventing new names -- e.g. a fold is ``jc.Fold`` (abbreviation **LP**)
|
|
202
|
+
and a Hopf point is ``jc.Hopf`` (abbreviation **H**).
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
#: legacy detector key this event maps onto ("fold" | "hopf")
|
|
206
|
+
_kind: str = ""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True)
|
|
210
|
+
class Fold(Event):
|
|
211
|
+
"""A limit point / fold bifurcation of equilibria.
|
|
212
|
+
|
|
213
|
+
Abbreviation: **LP** -- ``jaxcont.bifurcations.taxonomy.describe("LP")``.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
_kind: str = "fold"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class Hopf(Event):
|
|
221
|
+
"""A Hopf bifurcation of equilibria.
|
|
222
|
+
|
|
223
|
+
Abbreviation: **H** -- ``jaxcont.bifurcations.taxonomy.describe("H")``.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
_kind: str = "hopf"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class EventHit:
|
|
231
|
+
"""A detected event along the branch."""
|
|
232
|
+
|
|
233
|
+
kind: str
|
|
234
|
+
p: float
|
|
235
|
+
u: Array
|
|
236
|
+
index: Optional[Tuple[int, int]] = None
|
|
237
|
+
info: dict = field(default_factory=dict)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Results
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class Branch:
|
|
246
|
+
"""The computed solution branch (one connected curve)."""
|
|
247
|
+
|
|
248
|
+
params: Array # (n_valid,)
|
|
249
|
+
states: Array # (n_valid, state_dim)
|
|
250
|
+
tangents: Optional[Array] = None
|
|
251
|
+
eigenvalues: Optional[Array] = None
|
|
252
|
+
stable: Optional[Array] = None
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def n_valid(self) -> int:
|
|
256
|
+
return int(self.params.shape[0])
|
|
257
|
+
|
|
258
|
+
def at_param(self, p: float) -> Tuple[float, Array]:
|
|
259
|
+
"""Return the ``(param, state)`` on the branch closest to ``p``."""
|
|
260
|
+
idx = int(jnp.argmin(jnp.abs(self.params - p)))
|
|
261
|
+
return float(self.params[idx]), self.states[idx]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class ContinuationResult:
|
|
266
|
+
"""Return value of :func:`continuation`."""
|
|
267
|
+
|
|
268
|
+
branch: Branch
|
|
269
|
+
events: list[EventHit] = field(default_factory=list)
|
|
270
|
+
stats: dict = field(default_factory=dict)
|
|
271
|
+
_solution: Optional[ContinuationSolution] = None
|
|
272
|
+
|
|
273
|
+
def plot(self, **kwargs):
|
|
274
|
+
"""Plot the bifurcation diagram (delegates to the legacy solution)."""
|
|
275
|
+
if self._solution is None:
|
|
276
|
+
raise RuntimeError("No underlying solution to plot.")
|
|
277
|
+
return self._solution.plot(**kwargs)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Adapters (BifProblem <-> legacy ContinuationProblem/Solution)
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def _to_legacy_problem(problem: BifProblem) -> ContinuationProblem:
|
|
285
|
+
"""Wrap a BifProblem's ``f(u, p, args)`` as a legacy ``rhs(u, params)``."""
|
|
286
|
+
f = problem.f
|
|
287
|
+
args = problem.args
|
|
288
|
+
|
|
289
|
+
def rhs(u, params):
|
|
290
|
+
return f(u, params[_P_KEY], args)
|
|
291
|
+
|
|
292
|
+
return ContinuationProblem(
|
|
293
|
+
rhs=rhs,
|
|
294
|
+
u0=problem.u0,
|
|
295
|
+
params={_P_KEY: float(problem.p0)},
|
|
296
|
+
continuation_param=_P_KEY,
|
|
297
|
+
problem_type=problem.kind,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _run_scan(
|
|
302
|
+
problem: BifProblem,
|
|
303
|
+
p_span: Tuple[float, float],
|
|
304
|
+
settings: ContinuationPar,
|
|
305
|
+
events: Sequence[Event],
|
|
306
|
+
verbose: bool,
|
|
307
|
+
) -> ContinuationResult:
|
|
308
|
+
"""
|
|
309
|
+
Run the fully-JIT scan engine and reassemble a legacy-shaped
|
|
310
|
+
:class:`ContinuationSolution` so detection/plotting reuse existing code.
|
|
311
|
+
"""
|
|
312
|
+
from jaxcont.core.scan_continuation import (
|
|
313
|
+
pseudo_arclength_scan,
|
|
314
|
+
branch_eigenvalues,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
args = problem.args
|
|
318
|
+
rhs2 = lambda u, p: problem.f(u, p, args)
|
|
319
|
+
|
|
320
|
+
p_start, p_end = p_span
|
|
321
|
+
u0 = jnp.asarray(problem.u0)
|
|
322
|
+
dtype = u0.dtype
|
|
323
|
+
|
|
324
|
+
res = pseudo_arclength_scan(
|
|
325
|
+
rhs2,
|
|
326
|
+
u0,
|
|
327
|
+
jnp.asarray(p_start, dtype),
|
|
328
|
+
jnp.asarray(p_end, dtype),
|
|
329
|
+
jnp.asarray(settings.ds, dtype),
|
|
330
|
+
jnp.asarray(settings.ds_min, dtype),
|
|
331
|
+
jnp.asarray(settings.ds_max, dtype),
|
|
332
|
+
jnp.asarray(settings.newton_tol, dtype),
|
|
333
|
+
int(settings.max_steps),
|
|
334
|
+
jnp.asarray(settings.newton_max_iter),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
n = int(res.n_valid)
|
|
338
|
+
states = res.states[:n]
|
|
339
|
+
params = res.params[:n]
|
|
340
|
+
tangents = res.tangents[:n]
|
|
341
|
+
|
|
342
|
+
eigenvalues = None
|
|
343
|
+
stability = None
|
|
344
|
+
want_eigs = settings.compute_stability or len(events) > 0
|
|
345
|
+
if want_eigs and states.shape[0] > 0:
|
|
346
|
+
eigenvalues = branch_eigenvalues(rhs2, states, params)
|
|
347
|
+
stability = jnp.all(jnp.real(eigenvalues) < 0.0, axis=1)
|
|
348
|
+
|
|
349
|
+
convergence_info = [
|
|
350
|
+
{"step": i, "converged": bool(res.converged[i]), "newton_iters": 0}
|
|
351
|
+
for i in range(n)
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
sol = ContinuationSolution(
|
|
355
|
+
states=states,
|
|
356
|
+
parameters=params,
|
|
357
|
+
tangent_vectors=tangents,
|
|
358
|
+
eigenvalues=eigenvalues,
|
|
359
|
+
stability=stability,
|
|
360
|
+
convergence_info=convergence_info,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Reuse the existing detector on the reassembled solution.
|
|
364
|
+
if len(events) > 0 and eigenvalues is not None:
|
|
365
|
+
from jaxcont.bifurcations.detector import BifurcationDetector
|
|
366
|
+
|
|
367
|
+
requested = {e._kind for e in events if getattr(e, "_kind", "")}
|
|
368
|
+
detector = BifurcationDetector(
|
|
369
|
+
detect_fold="fold" in requested,
|
|
370
|
+
detect_hopf="hopf" in requested,
|
|
371
|
+
tolerance=1e-6,
|
|
372
|
+
)
|
|
373
|
+
sol.bifurcations = detector.detect_along_branch(
|
|
374
|
+
sol,
|
|
375
|
+
eigenvalues,
|
|
376
|
+
refine_location=True,
|
|
377
|
+
problem=_to_legacy_problem(problem),
|
|
378
|
+
fold_extended_system=True,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
result = _to_result(sol)
|
|
382
|
+
requested = {e._kind for e in events if getattr(e, "_kind", "")}
|
|
383
|
+
if requested:
|
|
384
|
+
result.events = [h for h in result.events if h.kind in requested]
|
|
385
|
+
return result
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _to_result(sol: ContinuationSolution) -> ContinuationResult:
|
|
389
|
+
branch = Branch(
|
|
390
|
+
params=sol.parameters,
|
|
391
|
+
states=sol.states,
|
|
392
|
+
tangents=sol.tangent_vectors,
|
|
393
|
+
eigenvalues=sol.eigenvalues,
|
|
394
|
+
stable=sol.stability,
|
|
395
|
+
)
|
|
396
|
+
events = [
|
|
397
|
+
EventHit(
|
|
398
|
+
kind=b.get("type", "unknown"),
|
|
399
|
+
p=float(b.get("parameter", 0.0)),
|
|
400
|
+
u=b.get("state"),
|
|
401
|
+
index=b.get("index"),
|
|
402
|
+
info={k: v for k, v in b.items()
|
|
403
|
+
if k not in ("type", "parameter", "state", "index")},
|
|
404
|
+
)
|
|
405
|
+
for b in (sol.bifurcations or [])
|
|
406
|
+
]
|
|
407
|
+
stats = {
|
|
408
|
+
"n_points": sol.n_points,
|
|
409
|
+
"converged_steps": sum(
|
|
410
|
+
1 for c in (sol.convergence_info or []) if c.get("converged")
|
|
411
|
+
),
|
|
412
|
+
}
|
|
413
|
+
return ContinuationResult(branch=branch, events=events, stats=stats, _solution=sol)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Entry point
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
def continuation(
|
|
421
|
+
problem: BifProblem,
|
|
422
|
+
alg: ContinuationAlgorithm = PseudoArclength(),
|
|
423
|
+
*,
|
|
424
|
+
p_span: Tuple[float, float],
|
|
425
|
+
settings: ContinuationPar = ContinuationPar(),
|
|
426
|
+
events: Sequence[Event] = (),
|
|
427
|
+
verbose: bool = False,
|
|
428
|
+
) -> ContinuationResult:
|
|
429
|
+
"""
|
|
430
|
+
Continue a solution branch of ``problem`` across ``p_span``.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
problem: the :class:`BifProblem` to continue.
|
|
434
|
+
alg: :class:`PseudoArclength` (default) or :class:`Natural`.
|
|
435
|
+
p_span: ``(p_start, p_end)`` range for the continuation parameter.
|
|
436
|
+
settings: numerical settings (:class:`ContinuationPar`).
|
|
437
|
+
events: detectors to run along the branch (e.g. ``[Fold(), Hopf()]``).
|
|
438
|
+
An empty list disables detection.
|
|
439
|
+
verbose: print a bifurcation summary.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
:class:`ContinuationResult` with ``.branch`` and ``.events``.
|
|
443
|
+
"""
|
|
444
|
+
# Fast path: the fully-JIT whole-loop engine.
|
|
445
|
+
if isinstance(alg, PseudoArclength) and alg.engine == "scan":
|
|
446
|
+
return _run_scan(problem, p_span, settings, events, verbose)
|
|
447
|
+
|
|
448
|
+
if isinstance(alg, Natural):
|
|
449
|
+
runner_cls = NaturalContinuation
|
|
450
|
+
elif isinstance(alg, PseudoArclength):
|
|
451
|
+
runner_cls = PseudoArclengthContinuation
|
|
452
|
+
else:
|
|
453
|
+
raise TypeError(f"Unknown continuation algorithm: {alg!r}")
|
|
454
|
+
|
|
455
|
+
detect = len(events) > 0
|
|
456
|
+
runner = runner_cls(
|
|
457
|
+
ds=settings.ds,
|
|
458
|
+
ds_min=settings.ds_min,
|
|
459
|
+
ds_max=settings.ds_max,
|
|
460
|
+
max_steps=settings.max_steps,
|
|
461
|
+
adaptive_stepsize=settings.adaptive,
|
|
462
|
+
newton_tol=settings.newton_tol,
|
|
463
|
+
newton_max_iter=settings.newton_max_iter,
|
|
464
|
+
detect_bifurcations=detect,
|
|
465
|
+
compute_stability=settings.compute_stability,
|
|
466
|
+
verbose=verbose,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
legacy_problem = _to_legacy_problem(problem)
|
|
470
|
+
sol = runner.run(legacy_problem, param_range=p_span)
|
|
471
|
+
result = _to_result(sol)
|
|
472
|
+
|
|
473
|
+
# Filter detected events to those requested, if the user narrowed the set.
|
|
474
|
+
requested = {e._kind for e in events if getattr(e, "_kind", "")}
|
|
475
|
+
if requested:
|
|
476
|
+
result.events = [h for h in result.events if h.kind in requested]
|
|
477
|
+
|
|
478
|
+
return result
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Bifurcation detection and analysis."""
|
|
2
|
+
|
|
3
|
+
from jaxcont.bifurcations.detector import BifurcationDetector
|
|
4
|
+
from jaxcont.bifurcations.fold import FoldBifurcation
|
|
5
|
+
from jaxcont.bifurcations.hopf import HopfBifurcation
|
|
6
|
+
from jaxcont.bifurcations.period_doubling import PeriodDoublingBifurcation
|
|
7
|
+
from jaxcont.bifurcations.taxonomy import LABELS, BIFURCATION_TYPES, BifurcationLabel, describe
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BifurcationDetector",
|
|
11
|
+
"FoldBifurcation",
|
|
12
|
+
"HopfBifurcation",
|
|
13
|
+
"PeriodDoublingBifurcation",
|
|
14
|
+
"LABELS",
|
|
15
|
+
"BIFURCATION_TYPES",
|
|
16
|
+
"BifurcationLabel",
|
|
17
|
+
"describe",
|
|
18
|
+
]
|