dynamodels 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {dynamodels-0.3.2/dynamodels.egg-info → dynamodels-0.3.3}/PKG-INFO +1 -1
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/__init__.py +1 -1
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/kuramoto_sivashinsky.py +46 -13
- {dynamodels-0.3.2 → dynamodels-0.3.3/dynamodels.egg-info}/PKG-INFO +1 -1
- {dynamodels-0.3.2 → dynamodels-0.3.3}/pyproject.toml +1 -1
- {dynamodels-0.3.2 → dynamodels-0.3.3}/tests/test_physical.py +59 -2
- {dynamodels-0.3.2 → dynamodels-0.3.3}/LICENSE +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/MANIFEST.in +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/README.md +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/history.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/integrator.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/model.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/__init__.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/annular.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/kuramoto_sivashinsky_2d.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/lorenz63.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/lorenz96.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/rijke.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/van_der_pol.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/utils.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/SOURCES.txt +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/dependency_links.txt +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/requires.txt +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/top_level.txt +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/setup.cfg +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/tests/conftest.py +0 -0
- {dynamodels-0.3.2 → dynamodels-0.3.3}/tests/test_models_history.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamodels
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Dynamical-system models with history tracking and pluggable integrators (Lorenz, Van der Pol, Rijke, annular combustor, KS)
|
|
5
5
|
Author-email: Andrea Nóvoa <a.novoa@imperial.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,9 +20,8 @@ class KS(Model):
|
|
|
20
20
|
u_t + u_{xx} + \nu\, u_{xxxx} + u\,u_x = 0, \qquad x \in (0, L],
|
|
21
21
|
$$
|
|
22
22
|
|
|
23
|
-
with periodic boundary conditions $u(t, 0) = u(t, L)$, $u_x(t, 0) = u_x(t, L)
|
|
24
|
-
|
|
25
|
-
Fourier transform pair is
|
|
23
|
+
with periodic boundary conditions $u(t, 0) = u(t, L)$, $u_x(t, 0) = u_x(t, L)$.
|
|
24
|
+
Solved with the ETDRK4 scheme in Fourier space, where the Fourier transform pair is
|
|
26
25
|
|
|
27
26
|
$$
|
|
28
27
|
\hat{u}(k) = \mathcal{F}[u(x)] = \frac{1}{L}\int_0^L u(x)\,e^{-ikx}\,\mathrm{d}x,
|
|
@@ -30,8 +29,27 @@ class KS(Model):
|
|
|
30
29
|
u(x) = \mathcal{F}^{-1}[\hat{u}(k)] = \sum_k \hat{u}(k)\,e^{ikx}.
|
|
31
30
|
$$
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
On the $N_x$-point grid the wavenumbers are $\alpha_j = 2\pi j / L$, so the
|
|
33
|
+
(diagonal) linear operator is $\alpha_j^2 - \nu\,\alpha_j^4$ and the nonlinear
|
|
34
|
+
term $u\,u_x$ is computed in physical space and transformed back to Fourier
|
|
35
|
+
space at every stage.
|
|
36
|
+
|
|
37
|
+
**Parametrization ($\nu$, $L$).** The pair is independent: whichever of the two
|
|
38
|
+
is given fixes that side of the operator.
|
|
39
|
+
|
|
40
|
+
- `nu` only (the default): the domain follows the standard nondimensionalization
|
|
41
|
+
$L = 2\pi/\sqrt{\nu}$, and the equation is then integrated in its $\nu = 1$
|
|
42
|
+
form on that domain -- so `self.nu` is **1** afterwards and the stored
|
|
43
|
+
$(N_x, \nu, L)$ always describes the operator that was actually integrated
|
|
44
|
+
(this is what makes `ntsa.respawn`, which rebuilds from `fixed_params`,
|
|
45
|
+
bit-faithful).
|
|
46
|
+
- `L` only: $\nu = 1$ on the given domain.
|
|
47
|
+
- **both**: both are honoured as given, i.e. the genuine two-parameter system
|
|
48
|
+
$u_t + u_{xx} + \nu u_{xxxx} + u u_x = 0$ on $(0, L]$.
|
|
49
|
+
|
|
50
|
+
The one- and two-parameter forms are related by $v(x', t') = \sqrt{\nu}\,u(x, t)$
|
|
51
|
+
with $x = \sqrt{\nu}\,x'$ and $t = \nu\,t'$: `KS(Nx, L=Lx, nu=visc, dt=dt)` and
|
|
52
|
+
`KS(Nx, L=Lx/sqrt(visc), dt=dt/visc)` describe the same physical system.
|
|
35
53
|
"""
|
|
36
54
|
|
|
37
55
|
# name: str = 'KS'
|
|
@@ -42,6 +60,7 @@ class KS(Model):
|
|
|
42
60
|
Nx = 256 # Spatial discretization
|
|
43
61
|
nu = 0.08 # 'Viscosity' parameter of the KS equation.
|
|
44
62
|
L = -1 # Domain length (0, L] NB. Will be set to 2*pi/sqrt(nu) if not specified.
|
|
63
|
+
# (nu, L) resolution rules: see the class docstring. Passing BOTH keeps both.
|
|
45
64
|
|
|
46
65
|
initial_amplitude = 0.01
|
|
47
66
|
|
|
@@ -63,6 +82,9 @@ class KS(Model):
|
|
|
63
82
|
|
|
64
83
|
- ``Nx`` : int, number of spatial grid points (must be even).
|
|
65
84
|
- ``nu`` : float, viscosity parameter.
|
|
85
|
+
- ``L`` : float, domain length, domain is (0, L].
|
|
86
|
+
``nu`` and ``L`` are independent -- see the class docstring for the
|
|
87
|
+
resolution rules when only one of them is given.
|
|
66
88
|
- ``dt`` : float, time step size.
|
|
67
89
|
- ``initial_amplitude`` : float, amplitude of the initial condition.
|
|
68
90
|
- ``Nq`` : int, number of sensors.
|
|
@@ -72,6 +94,10 @@ class KS(Model):
|
|
|
72
94
|
"""
|
|
73
95
|
|
|
74
96
|
|
|
97
|
+
# 'nu' has a non-sentinel class default, so an EXPLICIT nu is what marks the
|
|
98
|
+
# two-parameter form; 'L' uses its non-positive class default as the sentinel.
|
|
99
|
+
nu_given = model_dict.get('nu') is not None
|
|
100
|
+
|
|
75
101
|
for key in list(model_dict.keys()):
|
|
76
102
|
if key in vars(KS):
|
|
77
103
|
setattr(self, key, model_dict.pop(key))
|
|
@@ -80,17 +106,24 @@ class KS(Model):
|
|
|
80
106
|
if self.Nx % 2 != 0:
|
|
81
107
|
raise ValueError("Nx must be even.")
|
|
82
108
|
|
|
83
|
-
|
|
109
|
+
L_given = self.L is not None and self.L > 0
|
|
110
|
+
|
|
111
|
+
if not L_given and not nu_given and self.nu is None:
|
|
84
112
|
raise ValueError("Either L or nu must be specified.")
|
|
85
|
-
elif
|
|
113
|
+
elif not L_given:
|
|
114
|
+
# nu alone: standard nondimensionalization. The domain absorbs nu and the
|
|
115
|
+
# equation is integrated in its nu = 1 form, so (Nx, nu, L) stays a faithful
|
|
116
|
+
# description of the operator (respawn / filename keying).
|
|
86
117
|
self.L = 2 * np.pi / np.sqrt(self.nu)
|
|
87
|
-
else:
|
|
88
118
|
self.nu = 1.
|
|
119
|
+
elif not nu_given:
|
|
120
|
+
self.nu = 1.
|
|
121
|
+
# else: both given -- honour both (general two-parameter form).
|
|
89
122
|
|
|
90
123
|
assert self.L is not None and self.L > 0, "L must be positive."
|
|
91
124
|
assert self.nu is not None and self.nu > 0, "nu must be positive."
|
|
92
125
|
|
|
93
|
-
#
|
|
126
|
+
# Fourier wavenumbers alpha_j = 2 pi j / L on the domain (0, L]
|
|
94
127
|
self.k = 2 * np.pi * np.fft.rfftfreq(self.Nx, d=self.L / self.Nx)
|
|
95
128
|
|
|
96
129
|
dt_requested = model_dict.pop('dt', 0.25)
|
|
@@ -238,10 +271,10 @@ class KS(Model):
|
|
|
238
271
|
|
|
239
272
|
@property
|
|
240
273
|
def __linear_operator(self):
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
return (self.k**2 - self.k**4)[:, None]
|
|
274
|
+
# Fourier multipliers of the linear term: alpha^2 - nu alpha^4, with
|
|
275
|
+
# alpha = self.k = 2 pi j / L. The single-parameter forms resolve nu to 1
|
|
276
|
+
# in __init__, so this reduces to alpha^2 - alpha^4 for them.
|
|
277
|
+
return (self.k**2 - self.nu * self.k**4)[:, None]
|
|
245
278
|
|
|
246
279
|
|
|
247
280
|
@property
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamodels
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Dynamical-system models with history tracking and pluggable integrators (Lorenz, Van der Pol, Rijke, annular combustor, KS)
|
|
5
5
|
Author-email: Andrea Nóvoa <a.novoa@imperial.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dynamodels"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Dynamical-system models with history tracking and pluggable integrators (Lorenz, Van der Pol, Rijke, annular combustor, KS)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
+
import pytest
|
|
5
6
|
|
|
6
7
|
import dynamodels
|
|
7
8
|
from dynamodels.physical import KS, KS2D, Annular, Lorenz63, Lorenz96, Rijke, VdP
|
|
@@ -134,9 +135,65 @@ def test_ks_dt_honored_and_param_roundtrip():
|
|
|
134
135
|
assert m.dt == 0.1
|
|
135
136
|
_, t = m.time_integrate(Nt=5)
|
|
136
137
|
assert np.isclose(t[1] - t[0], 0.1)
|
|
137
|
-
#
|
|
138
|
-
#
|
|
138
|
+
# nu alone nondimensionalizes: the domain absorbs nu and the stored operator
|
|
139
|
+
# is the nu = 1 one, so (Nx, nu, L) keeps describing what was integrated.
|
|
140
|
+
assert m.nu == 1.0 and m.L == pytest.approx(2 * np.pi / np.sqrt(0.08))
|
|
141
|
+
# (Nx, nu, L) are fixed_params so a respawn-style rebuild is bit-faithful
|
|
139
142
|
m2 = KS(Nx=64, dt=0.1, nu=m.nu, L=m.L, psi0=m.psi0.copy())
|
|
140
143
|
pa, _ = m.time_step(Nt=50)
|
|
141
144
|
pb, _ = m2.time_step(Nt=50)
|
|
142
145
|
assert np.allclose(pa, pb)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_ks_two_parameter_form_honors_both_nu_and_L():
|
|
149
|
+
# Regression: nu used to be silently forced to 1 whenever L was given, so an
|
|
150
|
+
# independent (visc, Lx) pair was inexpressible. Both must now survive, and the
|
|
151
|
+
# linear operator must be the general alpha^2 - nu alpha^4.
|
|
152
|
+
Nx, Lx, visc, dt = 64, 20 * np.pi, 0.37, 0.01
|
|
153
|
+
m = KS(Nx=Nx, dt=dt, L=Lx, nu=visc)
|
|
154
|
+
assert m.nu == visc and m.L == Lx
|
|
155
|
+
|
|
156
|
+
alpha = 2 * np.pi * np.arange(Nx // 2 + 1) / Lx
|
|
157
|
+
np.testing.assert_allclose(m.k, alpha, rtol=0, atol=1e-12)
|
|
158
|
+
# E = exp(dt * Lhat) is the exact exponential of the linear operator, so it
|
|
159
|
+
# exposes the operator the ETDRK4 coefficients were rebuilt from.
|
|
160
|
+
expected = np.exp(dt * (alpha**2 - visc * alpha**4))[:, None]
|
|
161
|
+
np.testing.assert_allclose(m.ETDRK4_f_terms['E'], expected, rtol=1e-14, atol=0)
|
|
162
|
+
|
|
163
|
+
# The single-parameter defaults keep their nu = 1 operator
|
|
164
|
+
m_L = KS(Nx=Nx, dt=dt, L=Lx)
|
|
165
|
+
assert m_L.nu == 1.0
|
|
166
|
+
np.testing.assert_allclose(m_L.ETDRK4_f_terms['E'],
|
|
167
|
+
np.exp(dt * (alpha**2 - alpha**4))[:, None], rtol=1e-14, atol=0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_ks_two_parameter_form_matches_rescaled_construction():
|
|
171
|
+
# (visc, Lx) and the old nu = 1 rescaling describe the SAME physical system:
|
|
172
|
+
# v(x', t') = sqrt(visc) u(x, t), x = sqrt(visc) x', t = visc t'
|
|
173
|
+
# so KS(Nx, L=Lx, nu=visc, dt=dt) and KS(Nx, L=Lx/sqrt(visc), dt=dt/visc) must
|
|
174
|
+
# agree after u = v / sqrt(visc) (and t = visc t').
|
|
175
|
+
# The qlROM ks1d quasi-periodic parameters. dt is finer than the case's own 0.1
|
|
176
|
+
# on purpose: the two constructions differ only by floating-point roundoff, and
|
|
177
|
+
# this system amplifies a 1e-15 perturbation to O(1) within t ~ 30 (verified with
|
|
178
|
+
# a perturbed IC on a SINGLE construction), so the comparison horizon has to stay
|
|
179
|
+
# inside the roundoff-dominated regime for the equivalence itself to be visible.
|
|
180
|
+
Nx, Lx, visc, dt, Nt = 128, 2 * np.pi, 16 / 71, 0.02, 300
|
|
181
|
+
s = np.sqrt(visc)
|
|
182
|
+
u0 = np.cos(Lx * np.arange(Nx) / Nx)
|
|
183
|
+
|
|
184
|
+
direct = KS(Nx=Nx, dt=dt, L=Lx, nu=visc, psi0=np.fft.rfft(u0)[:, None])
|
|
185
|
+
rescaled = KS(Nx=Nx, dt=dt / visc, L=Lx / s, psi0=np.fft.rfft(s * u0)[:, None])
|
|
186
|
+
|
|
187
|
+
assert direct.dt == dt and rescaled.dt == dt / visc
|
|
188
|
+
|
|
189
|
+
pa, ta = direct.time_step(Nt=Nt)
|
|
190
|
+
pb, tb = rescaled.time_step(Nt=Nt)
|
|
191
|
+
|
|
192
|
+
ua = KS.fourier_to_physical(pa)[:, :, 0]
|
|
193
|
+
ub = KS.fourier_to_physical(pb)[:, :, 0] / s
|
|
194
|
+
err = np.max(np.abs(ua - ub)) / np.max(np.abs(ua))
|
|
195
|
+
assert err < 1e-10, f'rescaling equivalence broken: rel err {err:.2e}'
|
|
196
|
+
# time also maps: t = visc * t' (the stamps themselves are rounded to
|
|
197
|
+
# precision_t by Model, so compare the exact steps they are built from)
|
|
198
|
+
assert visc * rescaled.dt == pytest.approx(direct.dt, rel=1e-14)
|
|
199
|
+
assert len(ta) == len(tb) == Nt + 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|