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
@@ -0,0 +1,248 @@
|
|
1
|
+
# numerical_integrators.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from typing import Callable, Tuple
|
19
|
+
from numpy.typing import NDArray
|
20
|
+
from numba import njit
|
21
|
+
import numpy as np
|
22
|
+
|
23
|
+
# Yoshida 4th-order symplectic integrator coefficients
|
24
|
+
ALPHA: float = 1.0 / (2.0 - 2.0 ** (1.0 / 3.0))
|
25
|
+
BETA: float = -(2.0 ** (1.0 / 3.0)) / (2.0 - 2.0 ** (1.0 / 3.0))
|
26
|
+
|
27
|
+
|
28
|
+
@njit
|
29
|
+
def velocity_verlet_2nd_step(
|
30
|
+
q: NDArray[np.float64],
|
31
|
+
p: NDArray[np.float64],
|
32
|
+
time_step: float,
|
33
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
34
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
35
|
+
parameters: NDArray[np.float64],
|
36
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
37
|
+
"""
|
38
|
+
Perform one step of the velocity Verlet integrator (second-order, symplectic).
|
39
|
+
|
40
|
+
Parameters
|
41
|
+
----------
|
42
|
+
q : NDArray[np.float64]
|
43
|
+
Current generalized coordinates.
|
44
|
+
p : NDArray[np.float64]
|
45
|
+
Current generalized momenta.
|
46
|
+
time_step : float
|
47
|
+
Integration time step.
|
48
|
+
grad_T : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
49
|
+
Function returning the gradient of the kinetic energy with respect to `p`.
|
50
|
+
grad_V : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
51
|
+
Function returning the gradient of the potential energy with respect to `q`.
|
52
|
+
parameters : NDArray[np.float64]
|
53
|
+
Additional parameters passed to `grad_T` and `grad_V`.
|
54
|
+
|
55
|
+
Returns
|
56
|
+
-------
|
57
|
+
q_new : NDArray[np.float64]
|
58
|
+
Updated generalized coordinates after one step.
|
59
|
+
p_new : NDArray[np.float64]
|
60
|
+
Updated generalized momenta after one step.
|
61
|
+
"""
|
62
|
+
q_new = q.copy()
|
63
|
+
p_new = p.copy()
|
64
|
+
|
65
|
+
# Half kick
|
66
|
+
gradV = grad_V(q, parameters)
|
67
|
+
p_new -= 0.5 * time_step * gradV
|
68
|
+
|
69
|
+
# Drift
|
70
|
+
gradT = grad_T(p_new, parameters)
|
71
|
+
q_new += time_step * gradT
|
72
|
+
|
73
|
+
# Half kick
|
74
|
+
gradV = grad_V(q_new, parameters)
|
75
|
+
p_new -= 0.5 * time_step * gradV
|
76
|
+
|
77
|
+
return q_new, p_new
|
78
|
+
|
79
|
+
|
80
|
+
@njit
|
81
|
+
def velocity_verlet_2nd_step_tangent(
|
82
|
+
q: NDArray[np.float64],
|
83
|
+
p: NDArray[np.float64],
|
84
|
+
dq: NDArray[np.float64],
|
85
|
+
dp: NDArray[np.float64],
|
86
|
+
time_step: float,
|
87
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
88
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
89
|
+
parameters: NDArray[np.float64],
|
90
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
91
|
+
"""
|
92
|
+
Perform one step of the tangent map associated with the velocity Verlet integrator.
|
93
|
+
|
94
|
+
This evolves deviation (tangent) vectors `(dq, dp)` along the flow of the system,
|
95
|
+
which is necessary for computing Lyapunov exponents and stability analysis.
|
96
|
+
|
97
|
+
Parameters
|
98
|
+
----------
|
99
|
+
q : NDArray[np.float64]
|
100
|
+
Current generalized coordinates.
|
101
|
+
p : NDArray[np.float64]
|
102
|
+
Current generalized momenta.
|
103
|
+
dq : NDArray[np.float64]
|
104
|
+
Deviation in coordinates.
|
105
|
+
dp : NDArray[np.float64]
|
106
|
+
Deviation in momenta.
|
107
|
+
time_step : float
|
108
|
+
Integration time step.
|
109
|
+
hess_T : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
110
|
+
Function returning the Hessian of the kinetic energy with respect to `p`.
|
111
|
+
hess_V : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
112
|
+
Function returning the Hessian of the potential energy with respect to `q`.
|
113
|
+
parameters : NDArray[np.float64]
|
114
|
+
Additional parameters passed to `hess_T` and `hess_V`.
|
115
|
+
|
116
|
+
Returns
|
117
|
+
-------
|
118
|
+
dq_new : NDArray[np.float64]
|
119
|
+
Updated deviation in coordinates after one step.
|
120
|
+
dp_new : NDArray[np.float64]
|
121
|
+
Updated deviation in momenta after one step.
|
122
|
+
"""
|
123
|
+
dq_new = dq.copy()
|
124
|
+
dp_new = dp.copy()
|
125
|
+
|
126
|
+
# Compute Hessians
|
127
|
+
HT = hess_T(p, parameters)
|
128
|
+
HV = hess_V(q, parameters)
|
129
|
+
|
130
|
+
# Half kick
|
131
|
+
HV_dot_dq = HV @ dq
|
132
|
+
dp_new -= 0.5 * time_step * HV_dot_dq
|
133
|
+
|
134
|
+
# Drift
|
135
|
+
HT_dot_dp = HT @ dp_new
|
136
|
+
dq_new += time_step * HT_dot_dp
|
137
|
+
|
138
|
+
# Half kick
|
139
|
+
HV_dot_dq = HV @ dq_new
|
140
|
+
dp_new -= 0.5 * time_step * HV_dot_dq
|
141
|
+
|
142
|
+
return dq_new, dp_new
|
143
|
+
|
144
|
+
|
145
|
+
@njit
|
146
|
+
def yoshida_4th_step(
|
147
|
+
q: NDArray[np.float64],
|
148
|
+
p: NDArray[np.float64],
|
149
|
+
time_step: float,
|
150
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
151
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
152
|
+
parameters: NDArray[np.float64],
|
153
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
154
|
+
"""
|
155
|
+
Perform one step of the 4th-order Yoshida symplectic integrator.
|
156
|
+
|
157
|
+
This is constructed by composing three velocity Verlet steps with
|
158
|
+
appropriately chosen coefficients (`ALPHA`, `BETA`).
|
159
|
+
|
160
|
+
Parameters
|
161
|
+
----------
|
162
|
+
q : NDArray[np.float64]
|
163
|
+
Current generalized coordinates.
|
164
|
+
p : NDArray[np.float64]
|
165
|
+
Current generalized momenta.
|
166
|
+
time_step : float
|
167
|
+
Integration time step.
|
168
|
+
grad_T : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
169
|
+
Function returning the gradient of the kinetic energy with respect to `p`.
|
170
|
+
grad_V : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
171
|
+
Function returning the gradient of the potential energy with respect to `q`.
|
172
|
+
parameters : NDArray[np.float64]
|
173
|
+
Additional parameters passed to `grad_T` and `grad_V`.
|
174
|
+
|
175
|
+
Returns
|
176
|
+
-------
|
177
|
+
q_new : NDArray[np.float64]
|
178
|
+
Updated generalized coordinates after one Yoshida step.
|
179
|
+
p_new : NDArray[np.float64]
|
180
|
+
Updated generalized momenta after one Yoshida step.
|
181
|
+
"""
|
182
|
+
q_new, p_new = velocity_verlet_2nd_step(
|
183
|
+
q, p, ALPHA * time_step, grad_T, grad_V, parameters
|
184
|
+
)
|
185
|
+
q_new, p_new = velocity_verlet_2nd_step(
|
186
|
+
q_new, p_new, BETA * time_step, grad_T, grad_V, parameters
|
187
|
+
)
|
188
|
+
q_new, p_new = velocity_verlet_2nd_step(
|
189
|
+
q_new, p_new, ALPHA * time_step, grad_T, grad_V, parameters
|
190
|
+
)
|
191
|
+
|
192
|
+
return q_new, p_new
|
193
|
+
|
194
|
+
|
195
|
+
@njit
|
196
|
+
def yoshida_4th_step_tangent(
|
197
|
+
q: NDArray[np.float64],
|
198
|
+
p: NDArray[np.float64],
|
199
|
+
dq: NDArray[np.float64],
|
200
|
+
dp: NDArray[np.float64],
|
201
|
+
time_step: float,
|
202
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
203
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
204
|
+
parameters: NDArray[np.float64],
|
205
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
206
|
+
"""
|
207
|
+
Perform one step of the tangent map associated with the Yoshida 4th-order integrator.
|
208
|
+
|
209
|
+
This evolves deviation vectors `(dq, dp)` along the flow of the Yoshida integrator,
|
210
|
+
which is useful for stability analysis and Lyapunov exponent computation.
|
211
|
+
|
212
|
+
Parameters
|
213
|
+
----------
|
214
|
+
q : NDArray[np.float64]
|
215
|
+
Current generalized coordinates.
|
216
|
+
p : NDArray[np.float64]
|
217
|
+
Current generalized momenta.
|
218
|
+
dq : NDArray[np.float64]
|
219
|
+
Deviation in coordinates.
|
220
|
+
dp : NDArray[np.float64]
|
221
|
+
Deviation in momenta.
|
222
|
+
time_step : float
|
223
|
+
Integration time step.
|
224
|
+
hess_T : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
225
|
+
Function returning the Hessian of the kinetic energy with respect to `p`.
|
226
|
+
hess_V : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
227
|
+
Function returning the Hessian of the potential energy with respect to `q`.
|
228
|
+
parameters : NDArray[np.float64]
|
229
|
+
Additional parameters passed to `hess_T` and `hess_V`.
|
230
|
+
|
231
|
+
Returns
|
232
|
+
-------
|
233
|
+
dq_new : NDArray[np.float64]
|
234
|
+
Updated deviation in coordinates after one Yoshida step.
|
235
|
+
dp_new : NDArray[np.float64]
|
236
|
+
Updated deviation in momenta after one Yoshida step.
|
237
|
+
"""
|
238
|
+
dq_new, dp_new = velocity_verlet_2nd_step_tangent(
|
239
|
+
q, p, dq, dp, ALPHA * time_step, hess_T, hess_V, parameters
|
240
|
+
)
|
241
|
+
dq_new, dp_new = velocity_verlet_2nd_step_tangent(
|
242
|
+
q, p, dq_new, dp_new, BETA * time_step, hess_T, hess_V, parameters
|
243
|
+
)
|
244
|
+
dq_new, dp_new = velocity_verlet_2nd_step_tangent(
|
245
|
+
q, p, dq_new, dp_new, ALPHA * time_step, hess_T, hess_V, parameters
|
246
|
+
)
|
247
|
+
|
248
|
+
return dq_new, dp_new
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# trajectory_analysis.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from typing import Callable
|
19
|
+
import numpy as np
|
20
|
+
from numba import njit, prange
|
21
|
+
from numpy.typing import NDArray
|
22
|
+
|
23
|
+
|
24
|
+
@njit
|
25
|
+
def generate_trajectory(
|
26
|
+
q: NDArray[np.float64],
|
27
|
+
p: NDArray[np.float64],
|
28
|
+
total_time: np.float64,
|
29
|
+
parameters: NDArray[np.float64],
|
30
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
31
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
32
|
+
time_step: np.float64,
|
33
|
+
integrator: Callable[
|
34
|
+
[
|
35
|
+
NDArray[np.float64],
|
36
|
+
NDArray[np.float64],
|
37
|
+
float,
|
38
|
+
Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
39
|
+
Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
40
|
+
NDArray[np.float64],
|
41
|
+
],
|
42
|
+
tuple[NDArray[np.float64], NDArray[np.float64]],
|
43
|
+
],
|
44
|
+
) -> NDArray[np.float64]:
|
45
|
+
"""
|
46
|
+
Generate a single trajectory of a Hamiltonian system using a symplectic integrator.
|
47
|
+
|
48
|
+
Parameters
|
49
|
+
----------
|
50
|
+
q : NDArray[np.float64], shape (dof,)
|
51
|
+
Initial generalized coordinates.
|
52
|
+
p : NDArray[np.float64], shape (dof,)
|
53
|
+
Initial generalized momenta.
|
54
|
+
total_time : float
|
55
|
+
Total integration time.
|
56
|
+
parameters : NDArray[np.float64]
|
57
|
+
Additional system parameters passed to `grad_T` and `grad_V`.
|
58
|
+
grad_T : Callable
|
59
|
+
Function returning the gradient of the kinetic energy with respect to `p`.
|
60
|
+
grad_V : Callable
|
61
|
+
Function returning the gradient of the potential energy with respect to `q`.
|
62
|
+
time_step : float
|
63
|
+
Integration step size.
|
64
|
+
integrator : Callable
|
65
|
+
Symplectic integrator function (e.g. `velocity_verlet_2nd_step`,
|
66
|
+
`yoshida_4th_step`).
|
67
|
+
|
68
|
+
Returns
|
69
|
+
-------
|
70
|
+
result : NDArray[np.float64], shape (num_steps+1, 2*dof+1)
|
71
|
+
Trajectory array:
|
72
|
+
- Column 0: time values.
|
73
|
+
- Columns 1..dof: generalized coordinates `q`.
|
74
|
+
- Columns dof+1..2*dof: generalized momenta `p`.
|
75
|
+
"""
|
76
|
+
num_steps = round(total_time / time_step)
|
77
|
+
dof = len(q)
|
78
|
+
result = np.zeros((num_steps + 1, 2 * dof + 1))
|
79
|
+
|
80
|
+
result[0, 1 : dof + 1] = q
|
81
|
+
result[0, dof + 1 :] = p
|
82
|
+
for i in range(1, num_steps + 1):
|
83
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
84
|
+
result[i, 0] = i * time_step
|
85
|
+
result[i, 1 : dof + 1] = q
|
86
|
+
result[i, dof + 1 :] = p
|
87
|
+
|
88
|
+
return result
|
89
|
+
|
90
|
+
|
91
|
+
@njit(parallel=True)
|
92
|
+
def ensemble_trajectories(
|
93
|
+
q: NDArray[np.float64],
|
94
|
+
p: NDArray[np.float64],
|
95
|
+
total_time: np.float64,
|
96
|
+
parameters: NDArray[np.float64],
|
97
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
98
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
99
|
+
time_step: np.float64,
|
100
|
+
integrator: Callable,
|
101
|
+
) -> NDArray[np.float64]:
|
102
|
+
"""
|
103
|
+
Generate an ensemble of trajectories from multiple initial conditions.
|
104
|
+
|
105
|
+
Parameters
|
106
|
+
----------
|
107
|
+
q : NDArray[np.float64], shape (num_ic, dof)
|
108
|
+
Initial coordinates for each trajectory.
|
109
|
+
p : NDArray[np.float64], shape (num_ic, dof)
|
110
|
+
Initial momenta for each trajectory.
|
111
|
+
total_time : float
|
112
|
+
Total integration time.
|
113
|
+
parameters : NDArray[np.float64]
|
114
|
+
Additional system parameters passed to `grad_T` and `grad_V`.
|
115
|
+
grad_T : Callable
|
116
|
+
Function returning the gradient of the kinetic energy with respect to `p`.
|
117
|
+
grad_V : Callable
|
118
|
+
Function returning the gradient of the potential energy with respect to `q`.
|
119
|
+
time_step : float
|
120
|
+
Integration step size.
|
121
|
+
integrator : Callable
|
122
|
+
Symplectic integrator function.
|
123
|
+
|
124
|
+
Returns
|
125
|
+
-------
|
126
|
+
trajectories : NDArray[np.float64], shape (num_ic, num_steps+1, 2*dof+1)
|
127
|
+
Array of trajectories for all initial conditions.
|
128
|
+
"""
|
129
|
+
num_steps = round(total_time / time_step)
|
130
|
+
num_ic, dof = q.shape
|
131
|
+
|
132
|
+
trajectories = np.zeros((num_ic, num_steps + 1, 2 * dof + 1), dtype=np.float64)
|
133
|
+
|
134
|
+
for i in prange(num_ic):
|
135
|
+
trajectory = generate_trajectory(
|
136
|
+
q[i], p[i], total_time, parameters, grad_T, grad_V, time_step, integrator
|
137
|
+
)
|
138
|
+
trajectories[i] = trajectory
|
139
|
+
|
140
|
+
return trajectories
|
141
|
+
|
142
|
+
|
143
|
+
@njit
|
144
|
+
def generate_poincare_section(
|
145
|
+
q: NDArray[np.float64],
|
146
|
+
p: NDArray[np.float64],
|
147
|
+
num_intersections: np.int32,
|
148
|
+
parameters: NDArray[np.float64],
|
149
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
150
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
151
|
+
time_step: np.float64,
|
152
|
+
integrator: Callable,
|
153
|
+
section_index: int = 0,
|
154
|
+
section_value: float = 0.0,
|
155
|
+
crossing: int = 1,
|
156
|
+
) -> NDArray[np.float64]:
|
157
|
+
"""
|
158
|
+
Generate a Poincaré section for a Hamiltonian system.
|
159
|
+
|
160
|
+
Parameters
|
161
|
+
----------
|
162
|
+
q : NDArray[np.float64], shape (dof,)
|
163
|
+
Initial generalized coordinates.
|
164
|
+
p : NDArray[np.float64], shape (dof,)
|
165
|
+
Initial generalized momenta.
|
166
|
+
num_intersections : int
|
167
|
+
Total number of section crossings to record.
|
168
|
+
parameters : NDArray[np.float64]
|
169
|
+
Additional system parameters passed to `grad_T` and `grad_V`.
|
170
|
+
grad_T : Callable
|
171
|
+
Function returning the gradient of the kinetic energy with respect to `p`.
|
172
|
+
grad_V : Callable
|
173
|
+
Function returning the gradient of the potential energy with respect to `q`.
|
174
|
+
time_step : float
|
175
|
+
Integration step size.
|
176
|
+
integrator : Callable
|
177
|
+
Symplectic integrator function.
|
178
|
+
section_index : int, optional
|
179
|
+
Index of coordinate `q[i]` used to define the section (default 0).
|
180
|
+
section_value : float, optional
|
181
|
+
Value of `q[section_index]` defining the section (default 0.0).
|
182
|
+
crossing : int, optional
|
183
|
+
Crossing rule:
|
184
|
+
- +1 : only upward crossings (dq/dt > 0),
|
185
|
+
- -1 : only downward crossings (dq/dt < 0),
|
186
|
+
- 0 : count all crossings.
|
187
|
+
|
188
|
+
Returns
|
189
|
+
-------
|
190
|
+
section_points : NDArray[np.float64], shape (num_intersections, 2*dof)
|
191
|
+
Phase-space points `(q, p)` recorded at section crossings.
|
192
|
+
"""
|
193
|
+
dof = len(q)
|
194
|
+
section_points = np.zeros((num_intersections, 2 * dof + 1))
|
195
|
+
count = 0
|
196
|
+
n_steps = 0
|
197
|
+
q_prev, p_prev = q.copy(), p.copy()
|
198
|
+
while count < num_intersections:
|
199
|
+
q_new, p_new = integrator(q_prev, p_prev, time_step, grad_T, grad_V, parameters)
|
200
|
+
|
201
|
+
# Check if crossing occurred
|
202
|
+
if (q_prev[section_index] - section_value) * (
|
203
|
+
q_new[section_index] - section_value
|
204
|
+
) < 0.0:
|
205
|
+
lam = (section_value - q_prev[section_index]) / (
|
206
|
+
q_new[section_index] - q_prev[section_index]
|
207
|
+
)
|
208
|
+
q_cross = (1 - lam) * q_prev + lam * q_new
|
209
|
+
p_cross = (1 - lam) * p_prev + lam * p_new
|
210
|
+
t_cross = n_steps * time_step + lam * time_step
|
211
|
+
|
212
|
+
velocity = grad_T(p_cross, parameters)[section_index]
|
213
|
+
|
214
|
+
if crossing == 0 or np.sign(velocity) == crossing:
|
215
|
+
section_points[count, 0] = t_cross
|
216
|
+
section_points[count, 1 : dof + 1] = q_cross
|
217
|
+
section_points[count, dof + 1 :] = p_cross
|
218
|
+
count += 1
|
219
|
+
|
220
|
+
q_prev, p_prev = q_new, p_new
|
221
|
+
n_steps += 1
|
222
|
+
|
223
|
+
return section_points
|
224
|
+
|
225
|
+
|
226
|
+
@njit(parallel=True)
|
227
|
+
def ensemble_poincare_section(
|
228
|
+
q: NDArray[np.float64],
|
229
|
+
p: NDArray[np.float64],
|
230
|
+
num_intersections: int,
|
231
|
+
parameters: NDArray[np.float64],
|
232
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
233
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
234
|
+
time_step: np.float64,
|
235
|
+
integrator: Callable,
|
236
|
+
section_index: int = 0,
|
237
|
+
section_value: float = 0.0,
|
238
|
+
crossing: int = 1,
|
239
|
+
) -> NDArray[np.float64]:
|
240
|
+
"""
|
241
|
+
Generate Poincaré sections for an ensemble of initial conditions.
|
242
|
+
|
243
|
+
Parameters
|
244
|
+
----------
|
245
|
+
q : NDArray[np.float64], shape (num_ic, dof)
|
246
|
+
Initial coordinates for each trajectory.
|
247
|
+
p : NDArray[np.float64], shape (num_ic, dof)
|
248
|
+
Initial momenta for each trajectory.
|
249
|
+
num_intersections : int
|
250
|
+
Number of section crossings to record per trajectory.
|
251
|
+
parameters : NDArray[np.float64]
|
252
|
+
Additional system parameters.
|
253
|
+
grad_T : Callable
|
254
|
+
Gradient of kinetic energy with respect to `p`.
|
255
|
+
grad_V : Callable
|
256
|
+
Gradient of potential energy with respect to `q`.
|
257
|
+
time_step : float
|
258
|
+
Integration step size.
|
259
|
+
integrator : Callable
|
260
|
+
Symplectic integrator function.
|
261
|
+
section_index : int, optional
|
262
|
+
Index of coordinate `q[i]` used for the section (default 0).
|
263
|
+
section_value : float, optional
|
264
|
+
Value of `q[section_index]` defining the section (default 0.0).
|
265
|
+
crossing : int, optional
|
266
|
+
Crossing rule:
|
267
|
+
- +1 : upward crossings,
|
268
|
+
- -1 : downward crossings,
|
269
|
+
- 0 : all crossings.
|
270
|
+
|
271
|
+
Returns
|
272
|
+
-------
|
273
|
+
section_points : NDArray[np.float64], shape (num_ic, num_intersections, 2*dof + 1)
|
274
|
+
Poincaré section points for each initial condition with the first column being the time at each crossing.
|
275
|
+
"""
|
276
|
+
num_ic, dof = q.shape
|
277
|
+
section_points = np.zeros((num_ic, num_intersections, 2 * dof + 1))
|
278
|
+
for i in prange(num_ic):
|
279
|
+
section_points[i] = generate_poincare_section(
|
280
|
+
q[i],
|
281
|
+
p[i],
|
282
|
+
num_intersections,
|
283
|
+
parameters,
|
284
|
+
grad_T,
|
285
|
+
grad_V,
|
286
|
+
time_step,
|
287
|
+
integrator,
|
288
|
+
section_index,
|
289
|
+
section_value,
|
290
|
+
crossing,
|
291
|
+
)
|
292
|
+
|
293
|
+
return section_points
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# validators.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from numbers import Integral, Real
|
19
|
+
from typing import Type, Union
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
|
25
|
+
def validate_non_negative(
|
26
|
+
value: Union[Integral, int, Real, float],
|
27
|
+
name: str,
|
28
|
+
type_: Type[Union[Integral, int, Real, float]] = Integral,
|
29
|
+
) -> None:
|
30
|
+
if not isinstance(value, type_):
|
31
|
+
raise TypeError(f"{name} must be of type {type_.__name__}")
|
32
|
+
if value < 0:
|
33
|
+
raise ValueError(f"{name} must be non-negative")
|
34
|
+
|
35
|
+
|
36
|
+
def validate_times(transient_time, total_time) -> tuple[float, float]:
|
37
|
+
|
38
|
+
if isinstance(total_time, (Integral, Real)):
|
39
|
+
total_time = float(total_time)
|
40
|
+
else:
|
41
|
+
raise ValueError("total_time must be a valid number")
|
42
|
+
if total_time < 0:
|
43
|
+
raise ValueError("total_time must be non-negative")
|
44
|
+
|
45
|
+
if transient_time is not None:
|
46
|
+
|
47
|
+
if isinstance(transient_time, (Integral, Real)):
|
48
|
+
transient_time = float(transient_time)
|
49
|
+
else:
|
50
|
+
raise ValueError("transient_time must be a valid number")
|
51
|
+
if transient_time < 0:
|
52
|
+
raise ValueError("transient_time must be non-negative")
|
53
|
+
|
54
|
+
if transient_time >= total_time:
|
55
|
+
raise ValueError("transient_time must be less than total_time")
|
56
|
+
|
57
|
+
return transient_time, total_time
|
58
|
+
|
59
|
+
|
60
|
+
def validate_initial_conditions(
|
61
|
+
u, degrees_of_freedom, allow_ensemble=True
|
62
|
+
) -> NDArray[np.float64]:
|
63
|
+
if np.isscalar(u):
|
64
|
+
u = np.array([u], dtype=np.float64)
|
65
|
+
else:
|
66
|
+
u = np.asarray(u, dtype=np.float64)
|
67
|
+
if u.ndim not in (1, 2):
|
68
|
+
raise ValueError("Initial condition must be 1D or 2D array")
|
69
|
+
|
70
|
+
u = np.ascontiguousarray(u).copy()
|
71
|
+
|
72
|
+
if u.ndim == 1:
|
73
|
+
if len(u) != degrees_of_freedom:
|
74
|
+
raise ValueError(
|
75
|
+
f"1D initial condition must have length {degrees_of_freedom}"
|
76
|
+
)
|
77
|
+
elif u.ndim == 2:
|
78
|
+
if not allow_ensemble:
|
79
|
+
raise ValueError(
|
80
|
+
"Ensemble of initial conditions not allowed in this context"
|
81
|
+
)
|
82
|
+
if u.shape[1] != degrees_of_freedom:
|
83
|
+
raise ValueError(
|
84
|
+
f"Each initial condition must have length {degrees_of_freedom}"
|
85
|
+
)
|
86
|
+
return u
|
87
|
+
|
88
|
+
|
89
|
+
def validate_parameters(parameters, number_of_parameters) -> NDArray[np.float64]:
|
90
|
+
if number_of_parameters == 0:
|
91
|
+
if parameters is not None:
|
92
|
+
raise ValueError("This system does not expect any parameters.")
|
93
|
+
return np.array([0], dtype=np.float64)
|
94
|
+
|
95
|
+
if parameters is None:
|
96
|
+
raise ValueError(
|
97
|
+
f"This system expects {number_of_parameters} parameter(s), but got None."
|
98
|
+
)
|
99
|
+
|
100
|
+
if np.isscalar(parameters):
|
101
|
+
parameters = np.array([parameters], dtype=np.float64)
|
102
|
+
else:
|
103
|
+
parameters = np.asarray(parameters, dtype=np.float64)
|
104
|
+
if parameters.ndim != 1:
|
105
|
+
raise ValueError(
|
106
|
+
f"`parameters` must be a 1D array or scalar. Got shape {parameters.shape}."
|
107
|
+
)
|
108
|
+
|
109
|
+
if parameters.size != number_of_parameters:
|
110
|
+
raise ValueError(
|
111
|
+
f"Expected {number_of_parameters} parameter(s), but got {parameters.size}."
|
112
|
+
)
|
113
|
+
|
114
|
+
return parameters
|