pynamicalsys 1.3.1__py3-none-any.whl → 1.4.1__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.
- pynamicalsys/__init__.py +2 -0
- pynamicalsys/__version__.py +2 -2
- pynamicalsys/common/time_series_metrics.py +85 -0
- pynamicalsys/continuous_time/chaotic_indicators.py +305 -7
- pynamicalsys/continuous_time/models.py +25 -0
- pynamicalsys/continuous_time/trajectory_analysis.py +457 -10
- pynamicalsys/core/continuous_dynamical_systems.py +933 -35
- pynamicalsys/core/discrete_dynamical_systems.py +20 -9
- pynamicalsys/core/hamiltonian_systems.py +1194 -0
- pynamicalsys/core/time_series_metrics.py +65 -0
- pynamicalsys/discrete_time/dynamical_indicators.py +5 -94
- pynamicalsys/hamiltonian_systems/__init__.py +16 -0
- pynamicalsys/hamiltonian_systems/chaotic_indicators.py +638 -0
- pynamicalsys/hamiltonian_systems/models.py +68 -0
- pynamicalsys/hamiltonian_systems/numerical_integrators.py +248 -0
- pynamicalsys/hamiltonian_systems/trajectory_analysis.py +293 -0
- pynamicalsys/hamiltonian_systems/validators.py +114 -0
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.1.dist-info}/METADATA +37 -8
- pynamicalsys-1.4.1.dist-info/RECORD +36 -0
- pynamicalsys-1.3.1.dist-info/RECORD +0 -28
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.1.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.1.dist-info}/top_level.txt +0 -0
@@ -15,11 +15,13 @@
|
|
15
15
|
# You should have received a copy of the GNU General Public License
|
16
16
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
17
|
|
18
|
-
from typing import
|
18
|
+
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union
|
19
19
|
|
20
|
+
from joblib import Parallel, delayed
|
20
21
|
import numpy as np
|
21
22
|
from numba import njit, prange
|
22
23
|
from numpy.typing import NDArray
|
24
|
+
from sklearn.cluster import DBSCAN
|
23
25
|
|
24
26
|
from pynamicalsys.continuous_time.numerical_integrators import rk4_step_wrapped
|
25
27
|
|
@@ -160,7 +162,6 @@ def generate_trajectory(
|
|
160
162
|
return trajectory
|
161
163
|
|
162
164
|
|
163
|
-
@njit(cache=True, parallel=True)
|
164
165
|
def ensemble_trajectories(
|
165
166
|
u: NDArray[np.float64],
|
166
167
|
parameters: NDArray[np.float64],
|
@@ -175,25 +176,471 @@ def ensemble_trajectories(
|
|
175
176
|
integrator=rk4_step_wrapped,
|
176
177
|
) -> NDArray[np.float64]:
|
177
178
|
|
178
|
-
|
179
|
-
|
179
|
+
def run_one(u_i, parameters, total_time, equations_of_motion, **kwargs):
|
180
|
+
result = generate_trajectory(
|
181
|
+
u_i, parameters, total_time, equations_of_motion, **kwargs
|
182
|
+
)
|
183
|
+
|
184
|
+
return np.array(result)
|
180
185
|
|
186
|
+
results = Parallel(n_jobs=-1)( # -1 = use all cores
|
187
|
+
delayed(run_one)(
|
188
|
+
u[i],
|
189
|
+
parameters,
|
190
|
+
total_time,
|
191
|
+
equations_of_motion,
|
192
|
+
transient_time=transient_time,
|
193
|
+
time_step=time_step,
|
194
|
+
atol=atol,
|
195
|
+
rtol=rtol,
|
196
|
+
integrator=integrator,
|
197
|
+
)
|
198
|
+
for i in range(len(u))
|
199
|
+
)
|
200
|
+
|
201
|
+
return results
|
202
|
+
|
203
|
+
|
204
|
+
@njit
|
205
|
+
def generate_poincare_section(
|
206
|
+
u: NDArray[np.float64],
|
207
|
+
parameters: NDArray[np.float64],
|
208
|
+
num_intersections: int,
|
209
|
+
equations_of_motion: Callable[
|
210
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
211
|
+
],
|
212
|
+
transient_time: float,
|
213
|
+
time_step: float,
|
214
|
+
atol: float,
|
215
|
+
rtol: float,
|
216
|
+
integrator,
|
217
|
+
section_index: int,
|
218
|
+
section_value: float,
|
219
|
+
crossing: int,
|
220
|
+
) -> NDArray[np.float64]:
|
221
|
+
neq = len(u)
|
222
|
+
section_points = np.zeros((num_intersections, neq + 1))
|
223
|
+
count = 0
|
224
|
+
|
225
|
+
u = u.copy()
|
226
|
+
if transient_time is not None:
|
227
|
+
u = evolve_system(
|
228
|
+
u,
|
229
|
+
parameters,
|
230
|
+
transient_time,
|
231
|
+
equations_of_motion,
|
232
|
+
time_step=time_step,
|
233
|
+
atol=atol,
|
234
|
+
rtol=rtol,
|
235
|
+
integrator=integrator,
|
236
|
+
)
|
237
|
+
time = transient_time
|
238
|
+
else:
|
239
|
+
time = 0
|
240
|
+
|
241
|
+
time_step_prev = time_step
|
242
|
+
time_prev = time
|
243
|
+
u_prev = u.copy()
|
244
|
+
while count < num_intersections:
|
245
|
+
u_new, time_new, time_step_new = step(
|
246
|
+
time_prev,
|
247
|
+
u_prev,
|
248
|
+
parameters,
|
249
|
+
equations_of_motion,
|
250
|
+
time_step=time_step_prev,
|
251
|
+
atol=atol,
|
252
|
+
rtol=rtol,
|
253
|
+
integrator=integrator,
|
254
|
+
)
|
255
|
+
|
256
|
+
# Check for crossings
|
257
|
+
if (u_prev[section_index] - section_value) * (
|
258
|
+
u_new[section_index] - section_value
|
259
|
+
) < 0.0:
|
260
|
+
lam = (section_value - u_prev[section_index]) / (
|
261
|
+
u_new[section_index] - u_prev[section_index]
|
262
|
+
)
|
263
|
+
|
264
|
+
t_cross = time_new - time_step_prev + lam * time_step_prev
|
265
|
+
u_cross = (1 - lam) * u_prev + lam * u_new
|
266
|
+
velocity = equations_of_motion(time, u_cross, parameters)[section_index]
|
267
|
+
|
268
|
+
if crossing == 0 or np.sign(velocity) == crossing:
|
269
|
+
section_points[count, 0] = t_cross
|
270
|
+
section_points[count, 1:] = u_cross
|
271
|
+
count += 1
|
272
|
+
|
273
|
+
time_prev = time_new
|
274
|
+
time_step_prev = time_step_new
|
275
|
+
u_prev = u_new
|
276
|
+
|
277
|
+
return section_points
|
278
|
+
|
279
|
+
|
280
|
+
@njit(parallel=True)
|
281
|
+
def ensemble_poincare_section(
|
282
|
+
u: NDArray[np.float64],
|
283
|
+
parameters: NDArray[np.float64],
|
284
|
+
num_intersections: int,
|
285
|
+
equations_of_motion: Callable[
|
286
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
287
|
+
],
|
288
|
+
transient_time: float,
|
289
|
+
time_step: float,
|
290
|
+
atol: float,
|
291
|
+
rtol: float,
|
292
|
+
integrator,
|
293
|
+
section_index: int,
|
294
|
+
section_value: float,
|
295
|
+
crossing: int,
|
296
|
+
) -> NDArray[np.float64]:
|
181
297
|
num_ic, neq = u.shape
|
298
|
+
section_points = np.zeros((num_ic, num_intersections, neq + 1))
|
299
|
+
for i in prange(num_ic):
|
300
|
+
section_points[i] = generate_poincare_section(
|
301
|
+
u[i],
|
302
|
+
parameters,
|
303
|
+
num_intersections,
|
304
|
+
equations_of_motion,
|
305
|
+
transient_time,
|
306
|
+
time_step,
|
307
|
+
atol,
|
308
|
+
rtol,
|
309
|
+
integrator,
|
310
|
+
section_index,
|
311
|
+
section_value,
|
312
|
+
crossing,
|
313
|
+
)
|
314
|
+
|
315
|
+
return section_points
|
316
|
+
|
317
|
+
|
318
|
+
@njit
|
319
|
+
def generate_stroboscopic_map(
|
320
|
+
u: NDArray[np.float64],
|
321
|
+
parameters: NDArray[np.float64],
|
322
|
+
num_intersections: int,
|
323
|
+
sampling_time: float,
|
324
|
+
equations_of_motion: Callable[
|
325
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
326
|
+
],
|
327
|
+
transient_time: float,
|
328
|
+
time_step: float,
|
329
|
+
atol: float,
|
330
|
+
rtol: float,
|
331
|
+
integrator,
|
332
|
+
) -> NDArray[np.float64]:
|
333
|
+
|
334
|
+
u = np.asarray(u)
|
335
|
+
neq = len(u)
|
336
|
+
strobe_points = np.zeros((num_intersections, neq + 1))
|
337
|
+
if transient_time is not None:
|
338
|
+
u_curr = evolve_system(
|
339
|
+
u,
|
340
|
+
parameters,
|
341
|
+
transient_time,
|
342
|
+
equations_of_motion,
|
343
|
+
time_step=time_step,
|
344
|
+
atol=atol,
|
345
|
+
rtol=rtol,
|
346
|
+
integrator=integrator,
|
347
|
+
)
|
348
|
+
time_curr = transient_time
|
349
|
+
else:
|
350
|
+
u_curr = u.copy()
|
351
|
+
time_curr = 0
|
352
|
+
|
353
|
+
time_target = time_curr + sampling_time
|
354
|
+
count = 0
|
355
|
+
while count < num_intersections:
|
356
|
+
u_prev = u_curr.copy()
|
357
|
+
time_prev = time_curr
|
358
|
+
# Integrate until we reach or surpass the target strobe time
|
359
|
+
while time_curr < time_target:
|
360
|
+
u_curr, time_curr, time_step = step(
|
361
|
+
time_curr,
|
362
|
+
u_curr,
|
363
|
+
parameters,
|
364
|
+
equations_of_motion,
|
365
|
+
time_step=time_step,
|
366
|
+
atol=atol,
|
367
|
+
rtol=rtol,
|
368
|
+
integrator=integrator,
|
369
|
+
)
|
182
370
|
|
183
|
-
|
371
|
+
# Linear interpolation to exactly hit time_target
|
372
|
+
lam = (time_target - time_prev) / (time_curr - time_prev)
|
373
|
+
strobe_points[count, 0] = time_target
|
374
|
+
strobe_points[count, 1:] = (1 - lam) * u_prev + lam * u_curr
|
375
|
+
# print((1 - lam), u_prev, u_curr)
|
376
|
+
count += 1
|
377
|
+
time_target += sampling_time
|
378
|
+
|
379
|
+
return strobe_points
|
380
|
+
|
381
|
+
|
382
|
+
@njit(parallel=True)
|
383
|
+
def ensemble_stroboscopic_map(
|
384
|
+
u: NDArray[np.float64],
|
385
|
+
parameters: NDArray[np.float64],
|
386
|
+
num_intersections: int,
|
387
|
+
sampling_time: float,
|
388
|
+
equations_of_motion: Callable[
|
389
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
390
|
+
],
|
391
|
+
transient_time: float,
|
392
|
+
time_step: float,
|
393
|
+
atol: float,
|
394
|
+
rtol: float,
|
395
|
+
integrator,
|
396
|
+
) -> NDArray[np.float64]:
|
397
|
+
num_ic, neq = u.shape
|
398
|
+
strobe_points = np.zeros((num_ic, num_intersections, neq + 1))
|
184
399
|
|
185
400
|
for i in prange(num_ic):
|
186
|
-
|
401
|
+
strobe_points[i] = generate_stroboscopic_map(
|
187
402
|
u[i],
|
188
403
|
parameters,
|
189
|
-
|
404
|
+
num_intersections,
|
405
|
+
sampling_time,
|
406
|
+
equations_of_motion,
|
407
|
+
transient_time,
|
408
|
+
time_step,
|
409
|
+
atol,
|
410
|
+
rtol,
|
411
|
+
integrator,
|
412
|
+
)
|
413
|
+
|
414
|
+
return strobe_points
|
415
|
+
|
416
|
+
|
417
|
+
@njit
|
418
|
+
def generate_maxima_map(
|
419
|
+
u: NDArray[np.float64],
|
420
|
+
parameters: NDArray[np.float64],
|
421
|
+
num_peaks: int,
|
422
|
+
maxima_index: int,
|
423
|
+
equations_of_motion,
|
424
|
+
transient_time: float,
|
425
|
+
time_step: float,
|
426
|
+
atol: float,
|
427
|
+
rtol: float,
|
428
|
+
integrator,
|
429
|
+
) -> NDArray[np.float64]:
|
430
|
+
"""
|
431
|
+
Generate a maxima map of a specified state variable.
|
432
|
+
|
433
|
+
Parameters
|
434
|
+
----------
|
435
|
+
u : np.ndarray
|
436
|
+
Initial state vector.
|
437
|
+
parameters : np.ndarray
|
438
|
+
Parameters for the system.
|
439
|
+
num_peaks : int
|
440
|
+
Number of maxima to collect.
|
441
|
+
maxima_index : int
|
442
|
+
Index of the variable whose maxima are to be recorded.
|
443
|
+
equations_of_motion : callable
|
444
|
+
Function f(t, u, parameters) returning du/dt.
|
445
|
+
transient_time : float
|
446
|
+
Time to integrate before starting maxima collection.
|
447
|
+
time_step : float
|
448
|
+
Initial integration time step.
|
449
|
+
atol, rtol : float
|
450
|
+
Absolute and relative tolerances for integration.
|
451
|
+
integrator : callable
|
452
|
+
Integration function or object, similar to your `step` function.
|
453
|
+
|
454
|
+
Returns
|
455
|
+
-------
|
456
|
+
maxima_points : np.ndarray
|
457
|
+
Array of shape (num_peaks, n_vars+1):
|
458
|
+
[time_of_max, u_1, u_2, ... u_n] at each maximum.
|
459
|
+
"""
|
460
|
+
neq = len(u)
|
461
|
+
maxima_points = np.zeros((num_peaks, neq + 1))
|
462
|
+
|
463
|
+
# Transient
|
464
|
+
if transient_time is not None:
|
465
|
+
u = evolve_system(
|
466
|
+
u,
|
467
|
+
parameters,
|
468
|
+
transient_time,
|
190
469
|
equations_of_motion,
|
191
|
-
transient_time=transient_time,
|
192
470
|
time_step=time_step,
|
193
471
|
atol=atol,
|
194
472
|
rtol=rtol,
|
195
473
|
integrator=integrator,
|
196
474
|
)
|
197
|
-
|
475
|
+
time = transient_time
|
476
|
+
else:
|
477
|
+
time = 0.0
|
478
|
+
|
479
|
+
# Initial step
|
480
|
+
time_step_prev = time_step
|
481
|
+
time_prev = time
|
482
|
+
u_prev = u.copy()
|
483
|
+
|
484
|
+
# We need three points to detect a local maximum
|
485
|
+
# (previous, current, next)
|
486
|
+
u_curr, time_curr, time_step_curr = step(
|
487
|
+
time_prev,
|
488
|
+
u_prev,
|
489
|
+
parameters,
|
490
|
+
equations_of_motion,
|
491
|
+
time_step=time_step_prev,
|
492
|
+
atol=atol,
|
493
|
+
rtol=rtol,
|
494
|
+
integrator=integrator,
|
495
|
+
)
|
496
|
+
|
497
|
+
count = 0
|
498
|
+
while count < num_peaks:
|
499
|
+
# Step to the next point
|
500
|
+
u_next, time_next, time_step_next = step(
|
501
|
+
time_curr,
|
502
|
+
u_curr,
|
503
|
+
parameters,
|
504
|
+
equations_of_motion,
|
505
|
+
time_step=time_step_curr,
|
506
|
+
atol=atol,
|
507
|
+
rtol=rtol,
|
508
|
+
integrator=integrator,
|
509
|
+
)
|
510
|
+
|
511
|
+
# Variable values at three times
|
512
|
+
y_prev = u_prev[maxima_index]
|
513
|
+
y_curr = u_curr[maxima_index]
|
514
|
+
y_next = u_next[maxima_index]
|
515
|
+
|
516
|
+
# Check for local maximum
|
517
|
+
if (y_curr > y_prev) and (y_curr > y_next):
|
518
|
+
# Quadratic interpolation for more precise max
|
519
|
+
# Fit parabola through (t_{i-1}, y_{i-1}), (t_i, y_i), (t_{i+1}, y_{i+1})
|
520
|
+
t1, t2, t3 = time_prev, time_curr, time_next
|
521
|
+
y1, y2, y3 = y_prev, y_curr, y_next
|
522
|
+
|
523
|
+
denom = (t1 - t2) * (t1 - t3) * (t2 - t3)
|
524
|
+
A = (t3 * (y2 - y1) + t2 * (y1 - y3) + t1 * (y3 - y2)) / denom
|
525
|
+
B = (t3**2 * (y1 - y2) + t2**2 * (y3 - y1) + t1**2 * (y2 - y3)) / denom
|
526
|
+
|
527
|
+
t_peak = -B / (2.0 * A) # vertex of the parabola
|
528
|
+
|
529
|
+
# Interpolate state vector linearly between u_curr and u_next at t_peak
|
530
|
+
lam = (t_peak - time_curr) / (time_next - time_curr)
|
531
|
+
u_peak = (1 - lam) * u_curr + lam * u_next
|
532
|
+
|
533
|
+
maxima_points[count, 0] = t_peak
|
534
|
+
maxima_points[count, 1:] = u_peak
|
535
|
+
count += 1
|
536
|
+
|
537
|
+
# Shift variables for next iteration
|
538
|
+
u_prev = u_curr
|
539
|
+
time_prev = time_curr
|
540
|
+
time_step_prev = time_step_curr
|
541
|
+
|
542
|
+
u_curr = u_next
|
543
|
+
time_curr = time_next
|
544
|
+
time_step_curr = time_step_next
|
545
|
+
|
546
|
+
return maxima_points
|
547
|
+
|
548
|
+
|
549
|
+
@njit
|
550
|
+
def ensemble_maxima_map(
|
551
|
+
u: NDArray[np.float64],
|
552
|
+
parameters: NDArray[np.float64],
|
553
|
+
num_peaks: int,
|
554
|
+
maxima_index: int,
|
555
|
+
equations_of_motion,
|
556
|
+
transient_time: float,
|
557
|
+
time_step: float,
|
558
|
+
atol: float,
|
559
|
+
rtol: float,
|
560
|
+
integrator,
|
561
|
+
) -> NDArray[np.float64]:
|
562
|
+
|
563
|
+
num_ic, neq = u.shape
|
564
|
+
maxima_points = np.zeros((num_ic, num_peaks, neq + 1))
|
565
|
+
|
566
|
+
for i in prange(num_ic):
|
567
|
+
maxima_points[i] = generate_maxima_map(
|
568
|
+
u[i],
|
569
|
+
parameters,
|
570
|
+
num_peaks,
|
571
|
+
maxima_index,
|
572
|
+
equations_of_motion,
|
573
|
+
transient_time,
|
574
|
+
time_step,
|
575
|
+
atol,
|
576
|
+
rtol,
|
577
|
+
integrator,
|
578
|
+
)
|
579
|
+
|
580
|
+
return maxima_points
|
581
|
+
|
582
|
+
|
583
|
+
def basin_of_attraction(
|
584
|
+
u: NDArray[np.float64],
|
585
|
+
parameters: NDArray[np.float64],
|
586
|
+
num_intersections: int,
|
587
|
+
equations_of_motion: Callable[
|
588
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
589
|
+
],
|
590
|
+
transient_time: float,
|
591
|
+
time_step: float,
|
592
|
+
atol: float,
|
593
|
+
rtol: float,
|
594
|
+
integrator: Callable,
|
595
|
+
select_map: str,
|
596
|
+
section_index: int = None,
|
597
|
+
section_value: float = None,
|
598
|
+
crossing: int = None,
|
599
|
+
sampling_time: float = None,
|
600
|
+
eps: float = 0.05,
|
601
|
+
min_samples: int = 1,
|
602
|
+
) -> NDArray[np.int32]:
|
603
|
+
|
604
|
+
if select_map == "PS":
|
605
|
+
if section_index is None or section_value is None or crossing is None:
|
606
|
+
raise ValueError(
|
607
|
+
"You must provide section_index, section_value, and crossing"
|
608
|
+
)
|
609
|
+
data = ensemble_poincare_section(
|
610
|
+
u,
|
611
|
+
parameters,
|
612
|
+
num_intersections,
|
613
|
+
equations_of_motion,
|
614
|
+
transient_time,
|
615
|
+
time_step,
|
616
|
+
atol,
|
617
|
+
rtol,
|
618
|
+
integrator,
|
619
|
+
section_index,
|
620
|
+
section_value,
|
621
|
+
crossing,
|
622
|
+
)
|
623
|
+
|
624
|
+
elif select_map == "SM":
|
625
|
+
if sampling_time is None:
|
626
|
+
raise ValueError("You must provide sampling_time")
|
627
|
+
|
628
|
+
data = ensemble_stroboscopic_map(
|
629
|
+
u,
|
630
|
+
parameters,
|
631
|
+
num_intersections,
|
632
|
+
sampling_time,
|
633
|
+
equations_of_motion,
|
634
|
+
transient_time,
|
635
|
+
time_step,
|
636
|
+
atol,
|
637
|
+
rtol,
|
638
|
+
integrator,
|
639
|
+
)
|
640
|
+
traj_data = data[:, :, 1:]
|
641
|
+
trajectory_centroids = traj_data.mean(axis=1) # shape (num_ic, 2)
|
642
|
+
|
643
|
+
db = DBSCAN(eps=eps, min_samples=min_samples, n_jobs=-1).fit(trajectory_centroids)
|
644
|
+
labels = db.labels_ # shape (num_ic,)
|
198
645
|
|
199
|
-
return
|
646
|
+
return labels
|