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.
Files changed (27) hide show
  1. {dynamodels-0.3.2/dynamodels.egg-info → dynamodels-0.3.3}/PKG-INFO +1 -1
  2. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/__init__.py +1 -1
  3. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/kuramoto_sivashinsky.py +46 -13
  4. {dynamodels-0.3.2 → dynamodels-0.3.3/dynamodels.egg-info}/PKG-INFO +1 -1
  5. {dynamodels-0.3.2 → dynamodels-0.3.3}/pyproject.toml +1 -1
  6. {dynamodels-0.3.2 → dynamodels-0.3.3}/tests/test_physical.py +59 -2
  7. {dynamodels-0.3.2 → dynamodels-0.3.3}/LICENSE +0 -0
  8. {dynamodels-0.3.2 → dynamodels-0.3.3}/MANIFEST.in +0 -0
  9. {dynamodels-0.3.2 → dynamodels-0.3.3}/README.md +0 -0
  10. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/history.py +0 -0
  11. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/integrator.py +0 -0
  12. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/model.py +0 -0
  13. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/__init__.py +0 -0
  14. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/annular.py +0 -0
  15. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/kuramoto_sivashinsky_2d.py +0 -0
  16. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/lorenz63.py +0 -0
  17. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/lorenz96.py +0 -0
  18. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/rijke.py +0 -0
  19. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/physical/van_der_pol.py +0 -0
  20. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels/utils.py +0 -0
  21. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/SOURCES.txt +0 -0
  22. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/dependency_links.txt +0 -0
  23. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/requires.txt +0 -0
  24. {dynamodels-0.3.2 → dynamodels-0.3.3}/dynamodels.egg-info/top_level.txt +0 -0
  25. {dynamodels-0.3.2 → dynamodels-0.3.3}/setup.cfg +0 -0
  26. {dynamodels-0.3.2 → dynamodels-0.3.3}/tests/conftest.py +0 -0
  27. {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.2
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
@@ -17,7 +17,7 @@ from .history import HistoryTracker
17
17
  from .integrator import ConstantIntegrator, DiscreteIntegrator, Integrator, IVPIntegrator
18
18
  from .model import Model
19
19
 
20
- __version__ = "0.3.2"
20
+ __version__ = "0.3.3"
21
21
 
22
22
  __all__ = [
23
23
  "Model",
@@ -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
- and $\nu = (2\pi/L)^2$. Solved with the ETDRK4 scheme in Fourier space, where the
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
- The nonlinear term $u\,u_x$ is computed in physical space and transformed back to
34
- Fourier space at every stage.
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
- if self.L is None or self.L <= 0 and self.nu is None:
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 self.L is None or self.L <= 0:
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
- # Define Fourier wavenumbers k on the nondimensional domain
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
- # ÷ Fourier multipliers for linear term
242
- # return (self.k**2 - self.nu * self.k**4)[:, None]
243
- # else:
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.2
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.2"
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
- # (Nx, nu, L) are fixed_params so a respawn-style rebuild is bit-faithful,
138
- # including the L-given branch that normalizes nu to 1
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