OpenReservoirComputing 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.
@@ -0,0 +1,753 @@
1
+ """Integrators for solving ODEs and PDEs."""
2
+
3
+ import functools
4
+
5
+ import diffrax
6
+ import jax
7
+ import jax.numpy as jnp
8
+ from jaxtyping import Array, Float
9
+
10
+ jax.config.update("jax_enable_x64", True)
11
+
12
+
13
+ # TODO: typing
14
+ ######################## Basic Chaotic ODEs ########################
15
+ @jax.jit
16
+ def _lorenz63_f(t, u, args):
17
+ """Define Lorenz 63 ODE."""
18
+ rho, sigma, beta = args
19
+ u1, u2, u3 = u
20
+ du1dt = sigma * (u2 - u1)
21
+ du2dt = u1 * (rho - u3) - u2
22
+ du3dt = u1 * u2 - beta * u3
23
+ dudt = du1dt, du2dt, du3dt
24
+ return jnp.array(dudt)
25
+
26
+
27
+ def lorenz63(
28
+ tN: Float,
29
+ dt: Float,
30
+ u0: Float[Array, "3"] = (-10.0, 1.0, 10.0),
31
+ rho: Float = 28.0,
32
+ sigma: Float = 10.0,
33
+ beta: Float = 8.0 / 3.0,
34
+ **diffeqsolve_kwargs,
35
+ ) -> tuple[Float, Float]:
36
+ """Solve the Lorenz 63 system of ODEs.
37
+
38
+ Parameters
39
+ ----------
40
+ tN : float
41
+ The final time to solve the ODEs to.
42
+ dt : float
43
+ The time step size for the interpolated solution. Will be overridden if
44
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
45
+ u0 : jnp.ndarray, optional
46
+ The initial conditions for the ODEs. Default is (-10, 1, 10).
47
+ rho : float, optional
48
+ The rho parameter for the Lorenz system. Default is 28.0.
49
+ sigma : float, optional
50
+ The sigma parameter for the Lorenz system. Default is 10.0.
51
+ beta : float, optional
52
+ The beta parameter for the Lorenz system. Default is 8.0/3.0.
53
+ diffeqsolve_kwargs : dict, optional
54
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
55
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
56
+ tN with step size dt, and stepsize_controller is set to
57
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
58
+ set to None.
59
+
60
+ Returns
61
+ -------
62
+ us : jnp.ndarray
63
+ The solution array with shape (Nt, 3), where Nt is the number of time steps.
64
+ ts : jnp.ndarray
65
+ The time vector corresponding to the solution steps.
66
+ """
67
+ # set kwarg defaults
68
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
69
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
70
+ diffeqsolve_kwargs.setdefault(
71
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
72
+ )
73
+ diffeqsolve_kwargs.setdefault("dt0", dt)
74
+ diffeqsolve_kwargs.setdefault("max_steps", None)
75
+
76
+ # solve
77
+ u0 = jnp.array(u0)
78
+ term = diffrax.ODETerm(_lorenz63_f)
79
+ args = (rho, sigma, beta)
80
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
81
+ us = sol.ys
82
+ return us, sol.ts
83
+
84
+
85
+ @jax.jit
86
+ def _rossler_f(t, u, args):
87
+ """Define Rössler ODE."""
88
+ a, b, c = args
89
+ u1, u2, u3 = u
90
+ du1dt = -u2 - u3
91
+ du2dt = u1 + a * u2
92
+ du3dt = b + u3 * (u1 - c)
93
+ dudt = du1dt, du2dt, du3dt
94
+ return jnp.array(dudt)
95
+
96
+
97
+ def rossler(
98
+ tN: Float,
99
+ dt: Float,
100
+ u0: Float[Array, "3"] = (1.0, 1.0, 1.0),
101
+ a: Float = 0.1,
102
+ b: Float = 0.1,
103
+ c: Float = 14.0,
104
+ **diffeqsolve_kwargs,
105
+ ) -> tuple[Float, Float]:
106
+ """Solve the Rossler system of ODEs.
107
+
108
+ Parameters
109
+ ----------
110
+ tN : float
111
+ The final time to solve the ODEs to.
112
+ dt : float
113
+ The time step size for the interpolated solution. Will be overridden if
114
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
115
+ u0 : jnp.ndarray, optional
116
+ The initial conditions for the ODEs. Default is (1.0, 1.0, 1.0).
117
+ a : float, optional
118
+ The a parameter for the Rössler system. Default is 0.1.
119
+ b : float, optional
120
+ The b parameter for the Rössler system. Default is 0.1.
121
+ c : float, optional
122
+ The c parameter for the Rössler system. Default is 14.0.
123
+ diffeqsolve_kwargs : dict, optional
124
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
125
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
126
+ tN with step size dt, and stepsize_controller is set to
127
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
128
+ set to None.
129
+
130
+ Returns
131
+ -------
132
+ us : jnp.ndarray
133
+ The solution array with shape (Nt, 3), where Nt is the number of time steps.
134
+ ts : jnp.ndarray
135
+ The time vector corresponding to the solution steps.
136
+ """
137
+ # set kwarg defaults
138
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
139
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
140
+ diffeqsolve_kwargs.setdefault(
141
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
142
+ )
143
+ diffeqsolve_kwargs.setdefault("dt0", dt)
144
+ diffeqsolve_kwargs.setdefault("max_steps", None)
145
+
146
+ # solve
147
+ u0 = jnp.array(u0)
148
+ term = diffrax.ODETerm(_rossler_f)
149
+ args = (a, b, c)
150
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
151
+ us = sol.ys
152
+ return us, sol.ts
153
+
154
+
155
+ @jax.jit
156
+ def _sakaraya_f(t, u, args):
157
+ """Define Sakaraya ODE."""
158
+ a, b, m = args
159
+ u1, u2, u3 = u
160
+ du1dt = a * u1 + u2 + u2 * u3
161
+ du2dt = -u1 * u3 + u2 * u3
162
+ du3dt = -u3 - m * u1 * u2 + b
163
+
164
+ return jnp.array([du1dt, du2dt, du3dt])
165
+
166
+
167
+ def sakaraya(
168
+ tN: Float,
169
+ dt: Float,
170
+ u0: Float[Array, "3"] = (-2.8976045, 3.8877978, 3.07465),
171
+ a: Float = 1.0,
172
+ b: Float = 1.0,
173
+ m: Float = 1.0,
174
+ **diffeqsolve_kwargs,
175
+ ) -> tuple[Float, Float]:
176
+ """Solve the Sakaraya system of ODEs.
177
+
178
+ Parameters
179
+ ----------
180
+ tN : float
181
+ The final time to solve the ODEs to.
182
+ dt : float
183
+ The time step size for the interpolated solution. Will be overridden if
184
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
185
+ u0 : jnp.ndarray, optional
186
+ The initial conditions for the ODEs. Default is
187
+ (-2.8976045, 3.8877978, 3.07465).
188
+ a : float, optional
189
+ The a parameter for the Sakaraya system. Default is 1.0.
190
+ b : float, optional
191
+ The b parameter for the Sakaraya system. Default is 1.0.
192
+ m : float, optional
193
+ The m parameter for the Sakaraya system. Default is 1.0.
194
+ diffeqsolve_kwargs : dict, optional
195
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
196
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
197
+ tN with step size dt, and stepsize_controller is set to
198
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
199
+ set to None.
200
+
201
+ Returns
202
+ -------
203
+ us : jnp.ndarray
204
+ The solution array with shape (Nt, 3), where Nt is the number of time steps.
205
+ ts : jnp.ndarray
206
+ The time vector corresponding to the solution steps.
207
+ """
208
+ # set kwarg defaults
209
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
210
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
211
+ diffeqsolve_kwargs.setdefault(
212
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
213
+ )
214
+ diffeqsolve_kwargs.setdefault("dt0", dt)
215
+ diffeqsolve_kwargs.setdefault("max_steps", None)
216
+
217
+ # solve
218
+ u0 = jnp.array(u0)
219
+ term = diffrax.ODETerm(_sakaraya_f)
220
+ args = (a, b, m)
221
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
222
+ us = sol.ys
223
+ return us, sol.ts
224
+
225
+
226
+ @jax.jit
227
+ def _colpitts_f(t, u, args):
228
+ """Define Colpitts oscillator ODE."""
229
+ u1, u2, u3 = u
230
+ alpha, gamma, q, eta = args
231
+ du1dt = alpha * u2
232
+ du2dt = -gamma * (u1 + u3) - q * u2
233
+ du3dt = eta * (u2 + 1 - jnp.exp(-u1))
234
+ return jnp.array([du1dt, du2dt, du3dt])
235
+
236
+
237
+ def colpitts(
238
+ tN: Float,
239
+ dt: Float,
240
+ u0: Float[Array, "3"] = (1.0, -1.0, 1.0),
241
+ alpha: Float = 5.0,
242
+ gamma: Float = 0.0797,
243
+ q: Float = 0.6898,
244
+ eta: Float = 6.2723,
245
+ **diffeqsolve_kwargs,
246
+ ) -> tuple[Float, Float]:
247
+ """Solve the Colpitts oscillator system of ODEs.
248
+
249
+ Parameters
250
+ ----------
251
+ tN : float
252
+ The final time to solve the ODEs to.
253
+ dt : float
254
+ The time step size for the interpolated solution. Will be overridden if
255
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
256
+ u0 : jnp.ndarray, optional
257
+ The initial conditions for the ODEs. Default is (1.0, -1.0, 1.0).
258
+ alpha : float, optional
259
+ The alpha parameter for the Colpitts oscillator system. Default is
260
+ 5.0 (Platt, 2020).
261
+ gamma : float, optional
262
+ The gamma parameter for the Colpitts oscillator system. Default is
263
+ 0.0797 (Platt, 2020).
264
+ q : float, optional
265
+ The q parameter for the Colpitts oscillator system. Default is
266
+ 0.6898 (Platt, 2020).
267
+ eta : float, optional
268
+ The eta parameter for the Colpitts oscillator system. Default is
269
+ 6.2723 (Platt, 2020).
270
+ diffeqsolve_kwargs : dict, optional
271
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
272
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
273
+ tN with step size dt, and stepsize_controller is set to
274
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
275
+ set to None.
276
+
277
+ Returns
278
+ -------
279
+ us : jnp.ndarray
280
+ The solution array with shape (Nt, 3), where Nt is the number of time steps.
281
+ ts : jnp.ndarray
282
+ The time vector corresponding to the solution steps.
283
+ """
284
+ # set kwarg defaults
285
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
286
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
287
+ diffeqsolve_kwargs.setdefault(
288
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
289
+ )
290
+ diffeqsolve_kwargs.setdefault("dt0", dt)
291
+ diffeqsolve_kwargs.setdefault("max_steps", None)
292
+
293
+ # solve
294
+ u0 = jnp.array(u0)
295
+ term = diffrax.ODETerm(_colpitts_f)
296
+ args = (alpha, gamma, q, eta)
297
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
298
+ us = sol.ys
299
+ return us, sol.ts
300
+
301
+
302
+ ######################## Hyper-chaotic ODEs ########################
303
+ @jax.jit
304
+ def _hyper_lorenz63_f(t, u, args):
305
+ """Define Hyper-Lorenz 63 ODE."""
306
+ a, b, c, d = args
307
+ u1, u2, u3, u4 = u
308
+ du1dt = a * (u2 - u1) + u4
309
+ du2dt = u1 * (b - u3) - u2
310
+ du3dt = u1 * u2 - c * u3
311
+ du4dt = d * u4 - u2 * u3
312
+ return jnp.array([du1dt, du2dt, du3dt, du4dt])
313
+
314
+
315
+ def hyper_lorenz63(
316
+ tN: Float,
317
+ dt: Float,
318
+ u0: Float[Array, "4"] = (-10.0, 6.0, 0.0, 10.0),
319
+ a: Float = 10.0,
320
+ b: Float = 28.0,
321
+ c: Float = 8.0 / 3.0,
322
+ d: Float = -1.0,
323
+ **diffeqsolve_kwargs,
324
+ ) -> tuple[Float, Float]:
325
+ """Solve the Hyper-Lorenz 63 system of ODEs.
326
+
327
+ Parameters
328
+ ----------
329
+ tN : float
330
+ The final time to solve the ODEs to.
331
+ dt : float
332
+ The time step size for the interpolated solution. Will be overridden if
333
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
334
+ u0 : jnp.ndarray, optional
335
+ The initial conditions for the ODEs. Default is (-10.0, 6.0, 0.0, 10.0).
336
+ a : float, optional
337
+ The a parameter for the Hyper-Lorenz system. Default is 10.0.
338
+ b : float, optional
339
+ The b parameter for the Hyper-Lorenz system. Default is 28.0.
340
+ c : float, optional
341
+ The c parameter for the Hyper-Lorenz system. Default is 8.0/3.0.
342
+ d : float, optional
343
+ The d parameter for the Hyper-Lorenz system. Default is -1.0.
344
+ diffeqsolve_kwargs : dict, optional
345
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
346
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
347
+ tN with step size dt, and stepsize_controller is set to
348
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
349
+ set to None.
350
+
351
+ Returns
352
+ -------
353
+ us : jnp.ndarray
354
+ The solution array with shape (Nt, 4), where Nt is the number of time steps.
355
+ ts : jnp.ndarray
356
+ The time vector corresponding to the solution steps.
357
+ """
358
+ # set kwarg defaults
359
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
360
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
361
+ diffeqsolve_kwargs.setdefault(
362
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
363
+ )
364
+ diffeqsolve_kwargs.setdefault("dt0", dt)
365
+ diffeqsolve_kwargs.setdefault("max_steps", None)
366
+
367
+ # solve
368
+ u0 = jnp.array(u0)
369
+ term = diffrax.ODETerm(_hyper_lorenz63_f)
370
+ args = (a, b, c, d)
371
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
372
+ us = sol.ys
373
+ return us, sol.ts
374
+
375
+
376
+ @jax.jit
377
+ def _hyper_xu_f(t, u, args):
378
+ """Define Hyper-Xu ODE."""
379
+ a, b, c, d, e = args
380
+ u1, u2, u3, u4 = u
381
+ du1dt = a * (u2 - u1) + u4
382
+ du2dt = b * u1 + e * u1 * u3
383
+ du3dt = -c * u3 - u1 * u2
384
+ du4dt = u1 * u3 - d * u2
385
+ return jnp.array([du1dt, du2dt, du3dt, du4dt])
386
+
387
+
388
+ def hyper_xu(
389
+ tN: Float,
390
+ dt: Float,
391
+ u0: Float[Array, "4"] = (-2.0, -1.0, -2.0, -10.0),
392
+ a: Float = 10.0,
393
+ b: Float = 40.0,
394
+ c: Float = 2.5,
395
+ d: Float = 2.0,
396
+ e: Float = 16.0,
397
+ **diffeqsolve_kwargs,
398
+ ) -> tuple[Float, Float]:
399
+ """Solve the Hyper-Xu system of ODEs.
400
+
401
+ Parameters
402
+ ----------
403
+ tN : float
404
+ The final time to solve the ODEs to.
405
+ dt : float
406
+ The time step size for the interpolated solution. Will be overridden if
407
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
408
+ u0 : jnp.ndarray, optional
409
+ The initial conditions for the ODEs. Default is (-2.0, -1.0, -2.0, -10.0).
410
+ a : float, optional
411
+ The a parameter for the Hyper-Xu system. Default is 10.0.
412
+ b : float, optional
413
+ The b parameter for the Hyper-Xu system. Default is 40.0.
414
+ c : float, optional
415
+ The c parameter for the Hyper-Xu system. Default is 2.5.
416
+ d : float, optional
417
+ The d parameter for the Hyper-Xu system. Default is 2.0.
418
+ e : float, optional
419
+ The e parameter for the Hyper-Xu system. Default is 16.0.
420
+ diffeqsolve_kwargs : dict, optional
421
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
422
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
423
+ tN with step size dt, and stepsize_controller is set to
424
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
425
+ set to None.
426
+
427
+ Returns
428
+ -------
429
+ us : jnp.ndarray
430
+ The solution array with shape (Nt, 4), where Nt is the number of time steps.
431
+ ts : jnp.ndarray
432
+ The time vector corresponding to the solution steps.
433
+ """
434
+ # set kwarg defaults
435
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
436
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
437
+ diffeqsolve_kwargs.setdefault(
438
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
439
+ )
440
+ diffeqsolve_kwargs.setdefault("dt0", dt)
441
+ diffeqsolve_kwargs.setdefault("max_steps", None)
442
+
443
+ # solve
444
+ u0 = jnp.array(u0)
445
+ term = diffrax.ODETerm(_hyper_xu_f)
446
+ args = (a, b, c, d, e)
447
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
448
+ us = sol.ys
449
+ return us, sol.ts
450
+
451
+
452
+ ####################### Hamiltonian Systems #######################
453
+ @jax.jit
454
+ def _double_pendulum_f(t, u, args):
455
+ """Define the equations of motion for double pendulum."""
456
+ m1, m2, L1, L2, g, damping = args
457
+ theta1, omega1, theta2, omega2 = u
458
+
459
+ # define some vars to shorten the expressions
460
+ delta_theta = theta1 - theta2
461
+ M_tot = m1 + m2
462
+ alpha = m1 + m2 * jnp.sin(delta_theta) ** 2
463
+
464
+ # compute derivs
465
+ dtheta1_dt = omega1
466
+ dtheta2_dt = omega2
467
+ domega1_dt_num = -jnp.sin(delta_theta) * (
468
+ m2 * L1 * omega1**2 * jnp.cos(delta_theta) + m2 * L2 * omega2**2
469
+ ) - g * (M_tot * jnp.sin(theta1) - m2 * jnp.sin(theta2) * jnp.cos(delta_theta))
470
+ domega1_dt_denom = L1 * alpha
471
+ domega1_dt_damp = damping * (omega1 - omega2) + damping * omega1
472
+ domega1_dt = domega1_dt_num / domega1_dt_denom - domega1_dt_damp
473
+ domega2_dt_num = jnp.sin(delta_theta) * (
474
+ M_tot * L1 * omega1**2 + m2 * L2 * omega2**2 * jnp.cos(delta_theta)
475
+ ) + g * (M_tot * jnp.sin(theta1) * jnp.cos(delta_theta) - M_tot * jnp.sin(theta2))
476
+ domega2_dt_denom = L2 * alpha
477
+ domega2_dt_damp = damping * (omega2 - omega1)
478
+ domega2_dt = domega2_dt_num / domega2_dt_denom - domega2_dt_damp
479
+
480
+ return jnp.array([dtheta1_dt, domega1_dt, dtheta2_dt, domega2_dt])
481
+
482
+
483
+ def double_pendulum(
484
+ tN: Float,
485
+ dt: Float,
486
+ u0: Float[Array, "4"] = (jnp.pi / 4, -1.0, jnp.pi / 2, 1.0),
487
+ m1: Float = 1.0,
488
+ m2: Float = 1.0,
489
+ L1: Float = 1.0,
490
+ L2: Float = 1.0,
491
+ g: Float = 9.81,
492
+ damping: Float = 0.0,
493
+ **diffeqsolve_kwargs,
494
+ ) -> tuple[Float, Float]:
495
+ """Solve the equations of motion for a damped double pendulum.
496
+
497
+ The state u is represented as a 4-tuple (theta1, omega1, theta2, omega2) where:
498
+ - theta1 is the angle of the first pendulum from vertical (in radians).
499
+ - omega1 is the angular velocity of the first pendulum (in radians/s).
500
+ - theta2 is the angle of the second pendulum from vertical (in radians).
501
+ - omega2 is the angular velocity of the second pendulum (in radians/s).
502
+
503
+ Parameters
504
+ ----------
505
+ tN : float
506
+ The final time to solve the ODEs to.
507
+ dt : float
508
+ The time step size for the interpolated solution. Will be overridden if
509
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
510
+ u0 : jnp.ndarray, optional
511
+ The initial conditions for the ODEs. Default is (jnp.pi/4, -1.0, jnp.pi/2, 1.0).
512
+ m1 : float, optional
513
+ The mass of the first pendulum bob. Default is 1.0.
514
+ m2 : float, optional
515
+ The mass of the second pendulum bob. Default is 1.0.
516
+ L1 : float, optional
517
+ The length of the first pendulum rod. Default is 1.0.
518
+ L2 : float, optional
519
+ The length of the second pendulum rod. Default is 1.0.
520
+ g : float, optional
521
+ The acceleration due to gravity. Default is 9.81.
522
+ damping : float, optional
523
+ The damping coefficient for the pendulum system. Default is 0.0 (no damping).
524
+ diffeqsolve_kwargs : dict, optional #TODO test which sovler best conserves energy
525
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
526
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
527
+ tN with step size dt, and stepsize_controller is set to
528
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
529
+ set to None.
530
+
531
+ Returns
532
+ -------
533
+ us : jnp.ndarray
534
+ The solution array with shape (Nt, 4), where Nt is the number of time steps.
535
+ ts : jnp.ndarray
536
+ The time vector corresponding to the solution steps.
537
+ """
538
+ # set kwarg defaults
539
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
540
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
541
+ diffeqsolve_kwargs.setdefault(
542
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
543
+ )
544
+ diffeqsolve_kwargs.setdefault("dt0", dt)
545
+ diffeqsolve_kwargs.setdefault("max_steps", None)
546
+
547
+ # solve
548
+ u0 = jnp.array(u0)
549
+ term = diffrax.ODETerm(_double_pendulum_f)
550
+ args = (m1, m2, L1, L2, g, damping)
551
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
552
+ us = sol.ys
553
+ return us, sol.ts
554
+
555
+
556
+ ###################### High Dimensional ODEs ######################
557
+ @jax.jit
558
+ def _lorenz96_interior(i, u, F):
559
+ """Define the interior points of the Lorenz 96 ODE."""
560
+ return (u[i + 1] - u[i - 2]) * u[i - 1] - u[i] + F
561
+
562
+
563
+ @functools.partial(jax.jit, static_argnames=["args"])
564
+ def _lorenz96_f(t, u, args):
565
+ """Define the Lorenz 96 ODE."""
566
+ N, F = args
567
+
568
+ # boundary at N
569
+ dudt_N = (u[0] - u[N - 3]) * u[N - 2] - u[N - 1] + F
570
+
571
+ # calculate all other points (interior plus boundary at 0)
572
+ dudt_func = jax.vmap(_lorenz96_interior, in_axes=(0, None, None))
573
+ interior_idxs = jnp.arange(N - 1)
574
+ dudt_interior = dudt_func(interior_idxs, u, F)
575
+
576
+ return jnp.append(dudt_interior, dudt_N)
577
+
578
+
579
+ def lorenz96(
580
+ tN: Float,
581
+ dt: Float,
582
+ u0: Array = None,
583
+ N: Float = 10,
584
+ F: Float = 8.0,
585
+ **diffeqsolve_kwargs,
586
+ ) -> tuple[Float, Float]:
587
+ """Solve the Lorenz 96 system of ODEs.
588
+
589
+ Parameters
590
+ ----------
591
+ tN : float
592
+ The final time to solve the ODEs to.
593
+ dt : float
594
+ The time step size for the interpolated solution. Will be overridden if
595
+ `diffeqsolve_kwargs` contains a saveat argument with a different time grid.
596
+ u0 : jnp.ndarray, optional
597
+ The initial conditions for the ODEs. Default is None, which initializes y0
598
+ to `jnp.sin(jnp.arange(N))`.
599
+ N : int, optional
600
+ The number of variables in the Lorenz 96 system. Default is 10.
601
+ F : float, optional
602
+ The forcing parameter for the Lorenz 96 system. Default is 8.0.
603
+ diffeqsolve_kwargs : dict, optional
604
+ Additional keyword arguments to pass to the `diffrax.diffeqsolve` function.
605
+ Default solver is `diffrax.Tsit5()`, saveat is set to a grid of times from 0 to
606
+ tN with step size dt, and stepsize_controller is set to
607
+ `diffrax.PIDController(rtol=1e-7, atol=1e-9)`. dt0 is set to dt. max_steps is
608
+ set to None.
609
+
610
+ Returns
611
+ -------
612
+ us : jnp.ndarray
613
+ The solution array with shape (Nt, N), where Nt is the number of time steps.
614
+ ts : jnp.ndarray
615
+ The time vector corresponding to the solution steps.
616
+ """
617
+ # set kwarg defaults
618
+ diffeqsolve_kwargs.setdefault("solver", diffrax.Tsit5())
619
+ diffeqsolve_kwargs.setdefault("saveat", diffrax.SaveAt(ts=jnp.arange(0, tN, dt)))
620
+ diffeqsolve_kwargs.setdefault(
621
+ "stepsize_controller", diffrax.PIDController(rtol=1e-7, atol=1e-9)
622
+ )
623
+ diffeqsolve_kwargs.setdefault("dt0", dt)
624
+ diffeqsolve_kwargs.setdefault("max_steps", None)
625
+
626
+ # solve
627
+ if u0 is None:
628
+ u0 = jnp.sin(jnp.arange(N))
629
+ u0 = jnp.array(u0)
630
+ term = diffrax.ODETerm(_lorenz96_f)
631
+ args = (N, F)
632
+ sol = diffrax.diffeqsolve(term, t0=0, t1=tN, y0=u0, args=args, **diffeqsolve_kwargs)
633
+ us = sol.ys
634
+ return us, sol.ts
635
+
636
+
637
+ ########################### Chaotic PDEs ###########################
638
+ @functools.partial(jax.jit, static_argnames=["tN", "dt", "Nx"])
639
+ def KS_1D(
640
+ tN: Float,
641
+ u0: Array = None,
642
+ dt: Float = 0.25,
643
+ domain: tuple[Float, Float] = (0, 22),
644
+ Nx: int = 64,
645
+ ) -> tuple[Float, Float]:
646
+ """Solve the Kuramoto-Sivashinsky equation in 1D with periodic boundary conditions.
647
+
648
+ The KS PDE solved is:
649
+ u_t + u*u_x + u_xx + u_xxxx = 0
650
+
651
+ The solver uses a fixed time-step ETDRK4 (Kassam & Trefethen 2005) method
652
+ for handling the stiffness of the PDE. Dealiasing (2/3 rule) is applied.
653
+
654
+ Parameters
655
+ ----------
656
+ tN : float
657
+ The final time to solve the PDE to.
658
+ u0 : jnp.ndarray, optional
659
+ The initial condition for the PDE (shape (Nx,)). Default is None, which
660
+ initializes u0 to `sin((32/domain[1])*pi*x)`.
661
+ dt : float, optional
662
+ The time step size for the solution. Default is 0.25.
663
+ domain : tuple[float, float], optional
664
+ The spatial domain (x_min, x_max). Default is (0, 22).
665
+ Nx : int, optional
666
+ The number of spatial grid points. Default is 64.
667
+
668
+ Returns
669
+ -------
670
+ U : jnp.ndarray
671
+ The solution array with shape (Nt, Nx+1), where Nt is the number of time steps.
672
+ Includes the periodic boundary point.
673
+ t : jnp.ndarray
674
+ The time vector corresponding to the solution steps.
675
+ """
676
+ # Setup spatial grid
677
+ if u0 is None:
678
+ x0 = jnp.linspace(domain[0], domain[1], Nx, endpoint=True)
679
+ u0 = jnp.sin((32 / domain[1]) * jnp.pi * x0) # TODO find a better default
680
+ u0 = u0[:-1]
681
+ Nx = Nx - 1 # remove duplicate periodic point
682
+ x = jnp.linspace(domain[0], domain[1], Nx, endpoint=False)
683
+ dx = x[1] - x[0]
684
+
685
+ Nt = int(tN / dt)
686
+ U = jnp.zeros((Nx, Nt))
687
+ U = U.at[:, 0].set(u0)
688
+
689
+ # Wavenumbers
690
+ k = jnp.fft.fftfreq(Nx, d=dx) * 2 * jnp.pi
691
+ k2 = k**2
692
+ k4 = k**4
693
+ L_op = k2 - k4
694
+
695
+ # Dealiasing (2/3 rule)
696
+ def dealias(f_hat):
697
+ cutoff = Nx // 3
698
+ f_hat = f_hat.at[cutoff:-cutoff].set(0)
699
+ return f_hat
700
+
701
+ # nonlinear operators on u and u_hat
702
+ def N_op_u(u):
703
+ return dealias(1j * k * jnp.fft.fft(-0.5 * u**2))
704
+
705
+ def N_op_uhat(u_hat):
706
+ return dealias(1j * k * jnp.fft.fft(-0.5 * jnp.real(jnp.fft.ifft(u_hat)) ** 2))
707
+
708
+ # ETDRK4 coefficients (Kassam & Trefethen 2005)
709
+ E1 = jnp.exp(L_op * dt)
710
+ E2 = jnp.exp(L_op * dt / 2)
711
+ M = 16
712
+ r = jnp.exp(1j * jnp.pi * (jnp.arange(1, M + 1) - 0.5) / M)
713
+ LR = dt * jnp.column_stack([L_op] * M) + jnp.vstack([r] * Nx)
714
+ Q = dt * jnp.mean((jnp.exp(LR / 2) - 1) / LR, axis=1)
715
+ f1 = dt * jnp.mean((-4 - LR + jnp.exp(LR) * (4 - 3 * LR + LR**2)) / LR**3, axis=1)
716
+ f2 = dt * jnp.mean((2 + LR + jnp.exp(LR) * (-2 + LR)) / LR**3, axis=1)
717
+ f3 = dt * jnp.mean((-4 - 3 * LR - LR**2 + jnp.exp(LR) * (4 - LR)) / LR**3, axis=1)
718
+
719
+ def _KS_ETDRK4_step(carry, _):
720
+ u, E1, E2, Q, f1, f2, f3 = carry
721
+
722
+ u_hat = jnp.fft.fft(u)
723
+
724
+ a = E2 * u_hat + Q * N_op_u(u)
725
+ b = E2 * u_hat + Q * N_op_uhat(a)
726
+ c = E2 * a + Q * (2 * N_op_uhat(b) - N_op_u(u))
727
+
728
+ u_hat = (
729
+ E1 * u_hat
730
+ + f1 * N_op_u(u)
731
+ + f2 * (N_op_uhat(a) + N_op_uhat(b))
732
+ + f3 * N_op_uhat(c)
733
+ )
734
+
735
+ # Enforce conservation by zeroing the mean mode
736
+ u_hat = u_hat.at[0].set(0.0)
737
+
738
+ u_next = jnp.real(jnp.fft.ifft(u_hat, n=Nx))
739
+ carry_next = (u_next, E1, E2, Q, f1, f2, f3)
740
+ return carry_next, u_next
741
+
742
+ _, u_vals = jax.lax.scan(
743
+ _KS_ETDRK4_step, (u0, E1, E2, Q, f1, f2, f3), length=Nt - 1
744
+ )
745
+
746
+ # add back in the initial point and boundary points
747
+ U = jnp.concatenate([u0[None, :], u_vals], axis=0)
748
+ U = jnp.concatenate((U, U[:, 0:1]), axis=1)
749
+
750
+ # create time vector for output
751
+ t = jnp.arange(0, tN, dt)
752
+
753
+ return U, t