qg-python 1.0.0__tar.gz → 1.1.0__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.
@@ -1,358 +1,418 @@
1
- Metadata-Version: 2.4
2
- Name: qg-python
3
- Version: 1.0.0
4
- Summary: Reproducible 1.5-layer quasi-geostrophic model (port of QG-C) with a direct sparse Helmholtz solver, RK4 / dp5 / Leapfrog (RA, RAW) integrators and stability diagnostics
5
- Author-email: "Elias D. Nino-Ruiz" <enino@uninorte.edu.co>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://aml-cs.github.io/software/qg-python/
8
- Project-URL: Repository, https://github.com/enino84/qg-leapfrog-stability
9
- Project-URL: Paper, https://doi.org/10.1002/gdj3.70107
10
- Project-URL: Dataset, https://doi.org/10.5281/zenodo.22168803
11
- Keywords: quasi-geostrophic,data assimilation,leapfrog,robert-asselin-williams,geophysical fluid dynamics
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Intended Audience :: Science/Research
14
- Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
15
- Classifier: Operating System :: OS Independent
16
- Requires-Python: >=3.9
17
- Description-Content-Type: text/markdown
18
- License-File: LICENSE
19
- Requires-Dist: numpy>=1.24
20
- Requires-Dist: scipy>=1.10
21
- Provides-Extra: plots
22
- Requires-Dist: matplotlib>=3.7; extra == "plots"
23
- Provides-Extra: netcdf
24
- Requires-Dist: netCDF4; extra == "netcdf"
25
- Provides-Extra: all
26
- Requires-Dist: matplotlib>=3.7; extra == "all"
27
- Requires-Dist: netCDF4; extra == "all"
28
- Dynamic: license-file
29
-
30
- # qg-leapfrog-stability
31
-
32
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22168803.svg)](https://doi.org/10.5281/zenodo.22168803)
33
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
-
35
- **QG-Python: a reproducible 1.5-layer quasi-geostrophic modelling service
36
- with spectral stability and filter sensitivity diagnostics**
37
-
38
- This repository contains the QG-Python model, the analysis scripts, and the
39
- shell drivers associated with the Data Services Article published in
40
- *Geoscience Data Journal* (Royal Meteorological Society / Wiley):
41
-
42
- > Nino-Ruiz, E. D. (2026). **QG-Python: A Reproducible 1.5-Layer
43
- > Quasi-Geostrophic Modelling Service with Direct Sparse Solvers, Spectral
44
- > Stability Diagnostics, and Filter Sensitivity Data**.
45
- > *Geoscience Data Journal*. doi:[10.1002/gdj3.70107](https://doi.org/10.1002/gdj3.70107)
46
-
47
- The archived snapshot of this repository **together with the simulation
48
- outputs** is the citable version of record:
49
- doi:[10.5281/zenodo.22168803](https://doi.org/10.5281/zenodo.22168803)
50
-
51
- ---
52
-
53
- ## Install
54
-
55
- ```bash
56
- pip install qg-python # model only (numpy, scipy)
57
- pip install "qg-python[all]" # + matplotlib and netCDF4 for the viewer and .nc output
58
- ```
59
-
60
- ```python
61
- from qg_model import QGModel, QGParams
62
-
63
- prm = QGParams(mrefin=6, tend=1000, dtout=20, scheme='leapfrog',
64
- ra_alpha=0.1, raw_filter=True, outfname='qg_run.npz')
65
- psi, q = QGModel(prm).run('fourier', seed=42)
66
- ```
67
-
68
- The same model is available on the command line as `qg-python` (run `qg-python --help`).
69
- To reproduce the experiments of the paper, clone this repository and run `bash run_all.sh`.
70
-
71
- ---
72
-
73
- ## Overview
74
-
75
- We implement the Leapfrog scheme with Robert-Asselin (RA) and
76
- Robert-Asselin-Williams (RAW) filters in a 1.5-layer quasi-geostrophic (QG)
77
- model, and carry out a systematic numerical analysis covering:
78
-
79
- - Replacement of the original Fortran multigrid Helmholtz solver with a direct
80
- sparse LU factorization (SuperLU via SciPy), enabling unambiguous stability analysis
81
- - Eigenspectrum of the linearized operator $J_\mathcal{F}(q_\mathrm{eq})$ at two
82
- grid resolutions (49×49 and 97×97), revealing resolution-dependent deviation
83
- from the classical von Neumann bound
84
- - Maximum stable time step $\Delta t_{\max}$ as a function of grid spacing $h$
85
- - First estimate of the maximum Lyapunov exponent for this model
86
- ($\sigma_1 = 0.0318$, $\tau_L = 31.5$ time units)
87
- - Statistical uncertainty propagation: kinetic energy and enstrophy PDFs,
88
- energy spectra, and KL divergences across schemes
89
- - Sensitivity of filter-induced dissipation to the RA parameter
90
- $\alpha \in \{0.05, 0.10, 0.20, 0.40\}$
91
-
92
- The QG model is a faithful Python port of the QG-C model by
93
- [Pavel Sakov (Bureau of Meteorology)](https://github.com/sakov/qg-c),
94
- extended with the Leapfrog family of integrators.
95
-
96
- ---
97
-
98
- ## Repository structure
99
-
100
- ```
101
- qg-leapfrog-stability/
102
- │
103
- ├── qg_model.py # QG model — all time-stepping schemes
104
- ├── qg_viewer.py # Interactive field viewer (.npz files)
105
- ├── run_all.sh # Single script to reproduce all results
106
- │
107
- ├── analysis/
108
- │ ├── plot_style.py # Shared publication style settings
109
- │ ├── jacobian_spectrum.py # Build J_F and compute eigenspectrum
110
- │ ├── stability_regions.py # Plot eigenspectrum + stability regions
111
- │ ├── lyapunov.py # Benettin algorithm for Lyapunov exponent
112
- │ ├── uncertainty.py # PDFs, power spectra, KL divergence
113
- │ ├── plot_fields.py # Spatial snapshots of streamfunction
114
- │ └── plot_alpha_sensitivity.py # RA filter sensitivity figures
115
- │
116
- ├── experiments/
117
- │ ├── run_reference.sh # Spin-up + RK4 reference run
118
- │ ├── run_leapfrog_long.sh # LF+RA and LF+RAW main runs
119
- │ ├── run_leapfrog_sweep.sh # RA filter alpha sweep
120
- │ └── run_analysis.sh # Eigenspectrum + Lyapunov diagnostics
121
- │
122
- ├── data/
123
- │ ├── README.md # Exact command line behind every run
124
- │ ├── blowup_times.txt # dt=1.0 instability onset, LF+RA / LF+RAW
125
- │ ├── lyapunov_results.txt # Benettin MLE summary
126
- │ └── uncertainty_results.txt # KE / enstrophy / KL divergence summary
127
- │
128
- ├── paper/
129
- │ └── README.md # Pointer to the published article
130
- │
131
- ├── figures/ # Generated locally — not tracked by git
132
- ├── requirements.txt
133
- ├── CITATION.cff
134
- ├── LICENSE
135
- └── README.md
136
- ```
137
-
138
- ---
139
-
140
- ## Dependencies
141
-
142
- ```
143
- numpy >= 1.24
144
- scipy >= 1.10
145
- matplotlib >= 3.7
146
- ```
147
-
148
- Install with:
149
-
150
- ```bash
151
- pip install -r requirements.txt
152
- ```
153
-
154
- No additional compiled extensions are required. The Helmholtz solver uses
155
- `scipy.sparse.linalg.factorized` (SuperLU backend).
156
-
157
- ---
158
-
159
- ## Reproducing the results
160
-
161
- All steps are collected in a single script. Run from the root of the repository:
162
-
163
- ```bash
164
- bash run_all.sh
165
- ```
166
-
167
- The script executes the following steps in order:
168
-
169
- | Step | Description | Runtime (approx.) |
170
- |------|-------------|-------------------|
171
- | 1 | Spin-up from rest (mrefin=6, tend=100000, RK4, dt=1) | ~45 min |
172
- | 2 | RK4 reference run (dt=1, t=100000→110000) | ~25 min |
173
- | 3 | LF+RA main run (α=0.10, dt=0.5) | ~12 min |
174
- | 4 | LF+RAW main run (α=0.10, dt=0.5) | ~12 min |
175
- | 5 | Alpha sensitivity sweep (α=0.05, 0.20, 0.40) | ~36 min |
176
- | 6 | Jacobian eigenspectrum, mrefin=6 (97×97) | ~2 min |
177
- | 7a | Spin-up from rest, mrefin=5 (dt=0.1) | ~45 min |
178
- | 7b | Jacobian eigenspectrum, mrefin=5 (49×49) | ~1 min |
179
- | 8 | Lyapunov exponent (Benettin, 50000 steps) | ~90 min |
180
- | 9 | All figures | ~1 min |
181
-
182
- Total runtime on a modern laptop: approximately 4–5 hours.
183
-
184
- > **Note:** The mrefin=5 spin-up (Step 7a) requires `dt=0.1` — larger time steps
185
- > cause blow-up during spin-up at this resolution.
186
-
187
- ### Running individual steps
188
-
189
- The pipeline is also split into four self-contained drivers under
190
- `experiments/`, which can be run in isolation for partial reproduction:
191
-
192
- ```bash
193
- bash experiments/run_reference.sh # spin-up + RK4 reference
194
- bash experiments/run_leapfrog_long.sh # LF+RA and LF+RAW main runs
195
- bash experiments/run_leapfrog_sweep.sh # alpha sensitivity sweep
196
- bash experiments/run_analysis.sh # eigenspectrum + Lyapunov
197
- ```
198
-
199
- Each command in `run_all.sh` is likewise self-contained. For example, to
200
- recompute only the eigenspectrum:
201
-
202
- ```bash
203
- python analysis/jacobian_spectrum.py --restart qg_zero.npz --neigs 200 --no-matrix --out jacobian_spectrum.npz
204
- ```
205
-
206
- ---
207
-
208
- ## Key results
209
-
210
- | Quantity | Value |
211
- |----------|-------|
212
- | Grid (main experiments) | 97×97 (mrefin=6), $h=0.01042$ |
213
- | Analytical $\Delta t_{\max}^{LF}$ (mrefin=6) | 0.090 |
214
- | Numerical $\Delta t_{\max}^{LF}$ (mrefin=6) | 1.236 (factor **13.7×** larger) |
215
- | Analytical $\Delta t_{\max}^{LF}$ (mrefin=5) | 0.362 |
216
- | Numerical $\Delta t_{\max}^{LF}$ (mrefin=5) | 0.235 (factor **1.54×** smaller) |
217
- | Maximum Lyapunov exponent $\sigma_1$ | 0.0318 |
218
- | Lyapunov time $\tau_L$ | 31.5 time units |
219
- | LF+RA kinetic energy bias (α=0.10) | −21% |
220
- | LF+RA enstrophy bias (α=0.10) | −46% |
221
- | LF+RAW kinetic energy bias | −2% |
222
- | LF+RAW enstrophy bias | +4% |
223
-
224
- ---
225
-
226
- ## Time-stepping schemes
227
-
228
- | Scheme | Order | RHS evals / step | Notes |
229
- |--------|-------|-------------------|-------|
230
- | `order1` | 1 | 1 | Explicit Euler |
231
- | `order2` | 2 | 2 | Midpoint RK |
232
- | `rk4` | 4 | 4 | Default in QG-C |
233
- | `dp5` | 5 | 6 | Dormand-Prince |
234
- | `leapfrog` | 2 | 1 | + RA or RAW filter |
235
-
236
- The first Leapfrog step uses a single RK4 step as bootstrap to avoid
237
- exciting the computational mode from the start.
238
-
239
- ### Leapfrog filter options
240
-
241
- ```bash
242
- # Robert-Asselin filter (classic, Asselin 1972)
243
- python qg_model.py --scheme leapfrog --ra-alpha 0.1 ...
244
-
245
- # Robert-Asselin-Williams filter (less dissipative, Williams 2009)
246
- python qg_model.py --scheme leapfrog --ra-alpha 0.1 --raw ...
247
- ```
248
-
249
- ---
250
-
251
- ## Data availability
252
-
253
- The simulation output files (`.npz`) are several hundred megabytes and are
254
- therefore not tracked by git. They are archived, together with a snapshot of
255
- this repository, on Zenodo:
256
-
257
- > **DOI**: [10.5281/zenodo.22168803](https://doi.org/10.5281/zenodo.22168803)
258
- > **Record**: <https://doi.org/10.5281/zenodo.22168803>
259
- > **Development version**: <https://github.com/enino84/qg-leapfrog-stability>
260
-
261
- The Zenodo record is the authoritative citation target: it holds both the
262
- tagged source snapshot (`qg-python-v1.0.zip`) and the simulation outputs. This
263
- GitHub repository is the development mirror and tracks the source only.
264
-
265
- The archive contains the equilibrium restart fields at `t = 100000` for both
266
- resolutions, the eigenvalue clouds of the linearised operator, the Benettin
267
- trace of the maximum Lyapunov exponent, and the time-resolved kinetic energy,
268
- enstrophy and isotropic spectra for RK4, LF+RA and LF+RAW across
269
- alpha in {0.05, 0.10, 0.20, 0.40}. `data/README.md` documents the exact
270
- command line behind each file, so every output can also be regenerated from
271
- scratch.
272
-
273
- ---
274
-
275
- ## Reference model
276
-
277
- This work builds on:
278
-
279
- > Sakov, P. (2024). *QG-C: A quasi-geostrophic model in C*.
280
- > Bureau of Meteorology, Australia.
281
- > https://github.com/sakov/qg-c
282
-
283
- Key numerical methods:
284
-
285
- > Arakawa, A. (1966). Computational design for long-term numerical integration
286
- > of the equations of fluid motion.
287
- > *Journal of Computational Physics*, 1(1), 119–143.
288
-
289
- > Asselin, R. (1972). Frequency filter for time integrations.
290
- > *Monthly Weather Review*, 100(6), 487–490.
291
-
292
- > Williams, P. D. (2009). A proposed modification to the Robert-Asselin
293
- > time filter. *Monthly Weather Review*, 137(8), 2538–2546.
294
-
295
- > Li, X. S., & Demmel, J. W. (2003). SuperLU_DIST: A scalable distributed-memory
296
- > sparse direct solver for unsymmetric linear systems.
297
- > *ACM Transactions on Mathematical Software*, 29(2), 110–140.
298
-
299
- ---
300
-
301
- ## License
302
-
303
- The source code in this repository is released under the MIT License — see
304
- [LICENSE](LICENSE). The simulation output files distributed through the Zenodo
305
- archive are released under
306
- [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/).
307
-
308
- ---
309
-
310
- ## Citation
311
-
312
- Please cite both the article and the archived software release. Machine-readable
313
- metadata is in [CITATION.cff](CITATION.cff).
314
-
315
- ```bibtex
316
- @article{NinoRuiz2026qgpython,
317
- author = {Nino-Ruiz, Elias D.},
318
- title = {{QG-Python}: A Reproducible 1.5-Layer Quasi-Geostrophic Modelling
319
- Service with Direct Sparse Solvers, Spectral Stability Diagnostics,
320
- and Filter Sensitivity Data},
321
- journal = {Geoscience Data Journal},
322
- year = {2026},
323
- doi = {10.1002/gdj3.70107}
324
- }
325
-
326
- @software{NinoRuiz2026qgpythonsoftware,
327
- author = {Nino-Ruiz, Elias D.},
328
- title = {{QG-Python}: a reproducible 1.5-layer quasi-geostrophic modelling
329
- service with spectral stability and filter sensitivity diagnostics},
330
- version = {1.0},
331
- year = {2026},
332
- publisher = {Zenodo},
333
- doi = {10.5281/zenodo.22168803}
334
- }
335
- ```
336
-
337
- ---
338
-
339
- ## A note on the enstrophy diagnostic
340
-
341
- Enstrophy is reported throughout as the domain average of
342
- `(nabla^2 psi)^2`, evaluated with the standard 5-point Laplacian on the
343
- **interior nodes only**. This is the definition in
344
- `analysis/uncertainty.py::enstrophy`, and it is the one every other script
345
- imports.
346
-
347
- The wall ring must be excluded from the average. `qg_model.py` imposes
348
- homogeneous Dirichlet conditions `psi = 0` on all four walls, so at a wall node
349
- the 5-point stencil collapses to `psi_adjacent / h^2`; with `h ~ 0.01` that is
350
- `O(1e5)`, and squaring it lets a one-node ring — about 4% of the points —
351
- dominate the domain average and inflate its variance by considerably more,
352
- while leaving the *relative* biases between time-stepping schemes almost
353
- unchanged.
354
-
355
- `analysis/make_tables.py` regenerates the LaTeX rows of both tables in the
356
- article directly from the `.npz` runs, so the statistical summary and the alpha
357
- sweep cannot drift apart. Pass `--compare` to also print the wall-inclusive
358
- average.
1
+ Metadata-Version: 2.4
2
+ Name: qg-python
3
+ Version: 1.1.0
4
+ Summary: Reproducible 1.5-layer quasi-geostrophic model (port of QG-C) with a direct sparse Helmholtz solver, RK4 / dp5 / Leapfrog (RA, RAW) integrators, stability diagnostics, and a stationary preset with a state-vector API for data assimilation
5
+ Author-email: "Elias D. Nino-Ruiz" <enino@uninorte.edu.co>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://aml-cs.github.io/software/qg-python/
8
+ Project-URL: Repository, https://github.com/enino84/qg-leapfrog-stability
9
+ Project-URL: Paper, https://doi.org/10.1002/gdj3.70107
10
+ Project-URL: Dataset, https://doi.org/10.5281/zenodo.22168803
11
+ Keywords: quasi-geostrophic,data assimilation,leapfrog,robert-asselin-williams,geophysical fluid dynamics
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: scipy>=1.10
21
+ Provides-Extra: plots
22
+ Requires-Dist: matplotlib>=3.7; extra == "plots"
23
+ Provides-Extra: netcdf
24
+ Requires-Dist: netCDF4; extra == "netcdf"
25
+ Provides-Extra: all
26
+ Requires-Dist: matplotlib>=3.7; extra == "all"
27
+ Requires-Dist: netCDF4; extra == "all"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # qg-leapfrog-stability
33
+
34
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22168803.svg)](https://doi.org/10.5281/zenodo.22168803)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+
37
+ **QG-Python: a reproducible 1.5-layer quasi-geostrophic modelling service
38
+ with spectral stability and filter sensitivity diagnostics**
39
+
40
+ This repository contains the QG-Python model, the analysis scripts, and the
41
+ shell drivers associated with the Data Services Article published in
42
+ *Geoscience Data Journal* (Royal Meteorological Society / Wiley):
43
+
44
+ > Nino-Ruiz, E. D. (2026). **QG-Python: A Reproducible 1.5-Layer
45
+ > Quasi-Geostrophic Modelling Service with Direct Sparse Solvers, Spectral
46
+ > Stability Diagnostics, and Filter Sensitivity Data**.
47
+ > *Geoscience Data Journal*. doi:[10.1002/gdj3.70107](https://doi.org/10.1002/gdj3.70107)
48
+
49
+ The archived snapshot of this repository **together with the simulation
50
+ outputs** is the citable version of record:
51
+ doi:[10.5281/zenodo.22168803](https://doi.org/10.5281/zenodo.22168803)
52
+
53
+ ---
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install qg-python # model only (numpy, scipy)
59
+ pip install "qg-python[all]" # + matplotlib and netCDF4 for the viewer and .nc output
60
+ ```
61
+
62
+ ```python
63
+ from qg_model import QGModel, QGParams
64
+
65
+ prm = QGParams(mrefin=6, tend=1000, dtout=20, scheme='leapfrog',
66
+ ra_alpha=0.1, raw_filter=True, outfname='qg_run.npz')
67
+ psi, q = QGModel(prm).run('fourier', seed=42)
68
+ ```
69
+
70
+ The same model is available on the command line as `qg-python` (run `qg-python --help`).
71
+ To reproduce the experiments of the paper, clone this repository and run `bash run_all.sh`.
72
+
73
+ ---
74
+
75
+ ### Releasing to PyPI
76
+
77
+ ```
78
+ pip install build twine
79
+ python -m build # dist/qg_python-1.1.0.tar.gz and .whl
80
+ twine check dist/*
81
+ twine upload dist/* # user __token__, password: the PyPI token
82
+ git tag v1.1.0 && git push --tags # Zenodo archives the release
83
+ ```
84
+
85
+ ## Regimes and presets (v1.1)
86
+
87
+ ```
88
+ pip install qg-python # https://pypi.org/project/qg-python/ ; import as `qgpython`, `import qg_model` still works
89
+ ```
90
+
91
+ The defaults of `QGParams` are those of QG-C and of the article above, so that
92
+ every script in this repository reproduces the published results. For **data
93
+ assimilation** (cycling a filter over hundreds of analyses) use the stationary
94
+ preset instead:
95
+
96
+ ```python
97
+ from qgpython import stationary_model, api
98
+ model = stationary_model(mrefin=5) # 49x49, rkh2 = 1e-11, RK4, dt = 0.3125
99
+ x = api.propagate(model, api.pack(model.q, model.psi), [0.0, 20000.0]) # spin-up
100
+ q, psi = api.unpack(model, x)
101
+ ```
102
+
103
+ Why a preset: with the QG-C default dissipation (`rkh2 = 1e-12`) the flow keeps
104
+ intensifying, the standard deviation of `q` is still rising at `t = 8000` and
105
+ beyond, so there is no stationary regime to sample a climatology from. With ten
106
+ times the dissipation (`rkh2 = 1e-11`) it settles: 1.107e4 at `t = 20000`
107
+ against 1.222e4 at `t = 60000`. Sakov & Oke (2008) make the same adjustment.
108
+ What we measured on this implementation (September 2026, `mrefin = 5`):
109
+
110
+ | | `rkh2 = 1e-12` (default) | `rkh2 = 1e-11` (preset) |
111
+ |---|---|---|
112
+ | std of `q` at t = 8000 / 20000 / 60000 | rising / rising / rising | 1.10e4 / 1.11e4 / 1.22e4 |
113
+ | climatology (snapshots every 250 units after t = 20000) | not stationary | 200 decorrelated states |
114
+ | forecast blow-ups in 500+ cycled runs (60-120 analyses) | n/a | none |
115
+ | error growth per 20 units, climatological ensemble | n/a | 1.9x |
116
+ | point perturbation of `q` after 20 / 200 units | n/a | stays in place; centroid moves 0.1 / 1.4 cells |
117
+
118
+ The last line matters for localization: in this model the forecast error grows
119
+ locally by instability and is not advected within an assimilation interval
120
+ (the same holds at `rkh2 = 1e-12` and at `mrefin = 6`). The figure
121
+ `figures/qg_resolutions.png` shows the same stationary state at 49x49, 97x97
122
+ and 193x193.
123
+
124
+ `api` gives the state-vector interface a filter needs: `pack/unpack`,
125
+ `propagate(model, x, [t0, t1])` (stateless between calls, leapfrog memory reset),
126
+ `psi_from_q`, and `interior_mask` (the points off the Dirichlet boundary, the
127
+ ones to estimate). Neighbourhoods for localization must **not** wrap: the basin
128
+ is closed.
129
+
130
+ ## Overview
131
+
132
+ We implement the Leapfrog scheme with Robert-Asselin (RA) and
133
+ Robert-Asselin-Williams (RAW) filters in a 1.5-layer quasi-geostrophic (QG)
134
+ model, and carry out a systematic numerical analysis covering:
135
+
136
+ - Replacement of the original Fortran multigrid Helmholtz solver with a direct
137
+ sparse LU factorization (SuperLU via SciPy), enabling unambiguous stability analysis
138
+ - Eigenspectrum of the linearized operator $J_\mathcal{F}(q_\mathrm{eq})$ at two
139
+ grid resolutions (49×49 and 97×97), revealing resolution-dependent deviation
140
+ from the classical von Neumann bound
141
+ - Maximum stable time step $\Delta t_{\max}$ as a function of grid spacing $h$
142
+ - First estimate of the maximum Lyapunov exponent for this model
143
+ ($\sigma_1 = 0.0318$, $\tau_L = 31.5$ time units)
144
+ - Statistical uncertainty propagation: kinetic energy and enstrophy PDFs,
145
+ energy spectra, and KL divergences across schemes
146
+ - Sensitivity of filter-induced dissipation to the RA parameter
147
+ $\alpha \in \{0.05, 0.10, 0.20, 0.40\}$
148
+
149
+ The QG model is a faithful Python port of the QG-C model by
150
+ [Pavel Sakov (Bureau of Meteorology)](https://github.com/sakov/qg-c),
151
+ extended with the Leapfrog family of integrators.
152
+
153
+ ---
154
+
155
+ ## Repository structure
156
+
157
+ ```
158
+ qg-leapfrog-stability/
159
+ │
160
+ ├── qg_model.py # QG model, all time-stepping schemes
161
+ ├── qg_viewer.py # Interactive field viewer (.npz files)
162
+ ├── run_all.sh # Single script to reproduce all results
163
+ │
164
+ ├── analysis/
165
+ │ ├── plot_style.py # Shared publication style settings
166
+ │ ├── jacobian_spectrum.py # Build J_F and compute eigenspectrum
167
+ │ ├── stability_regions.py # Plot eigenspectrum + stability regions
168
+ │ ├── lyapunov.py # Benettin algorithm for Lyapunov exponent
169
+ │ ├── uncertainty.py # PDFs, power spectra, KL divergence
170
+ │ ├── plot_fields.py # Spatial snapshots of streamfunction
171
+ │ └── plot_alpha_sensitivity.py # RA filter sensitivity figures
172
+ │
173
+ ├── experiments/
174
+ │ ├── run_reference.sh # Spin-up + RK4 reference run
175
+ │ ├── run_leapfrog_long.sh # LF+RA and LF+RAW main runs
176
+ │ ├── run_leapfrog_sweep.sh # RA filter alpha sweep
177
+ │ └── run_analysis.sh # Eigenspectrum + Lyapunov diagnostics
178
+ │
179
+ ├── data/
180
+ │ ├── README.md # Exact command line behind every run
181
+ │ ├── blowup_times.txt # dt=1.0 instability onset, LF+RA / LF+RAW
182
+ │ ├── lyapunov_results.txt # Benettin MLE summary
183
+ │ └── uncertainty_results.txt # KE / enstrophy / KL divergence summary
184
+ │
185
+ ├── paper/
186
+ │ └── README.md # Pointer to the published article
187
+ │
188
+ ├── figures/ # Generated locally, not tracked by git
189
+ ├── requirements.txt
190
+ ├── CITATION.cff
191
+ ├── LICENSE
192
+ └── README.md
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Dependencies
198
+
199
+ ```
200
+ numpy >= 1.24
201
+ scipy >= 1.10
202
+ matplotlib >= 3.7
203
+ ```
204
+
205
+ Install with:
206
+
207
+ ```bash
208
+ pip install -r requirements.txt
209
+ ```
210
+
211
+ No additional compiled extensions are required. The Helmholtz solver uses
212
+ `scipy.sparse.linalg.factorized` (SuperLU backend).
213
+
214
+ ---
215
+
216
+ ## Reproducing the results
217
+
218
+ All steps are collected in a single script. Run from the root of the repository:
219
+
220
+ ```bash
221
+ bash run_all.sh
222
+ ```
223
+
224
+ The script executes the following steps in order:
225
+
226
+ | Step | Description | Runtime (approx.) |
227
+ |------|-------------|-------------------|
228
+ | 1 | Spin-up from rest (mrefin=6, tend=100000, RK4, dt=1) | ~45 min |
229
+ | 2 | RK4 reference run (dt=1, t=100000→110000) | ~25 min |
230
+ | 3 | LF+RA main run (α=0.10, dt=0.5) | ~12 min |
231
+ | 4 | LF+RAW main run (α=0.10, dt=0.5) | ~12 min |
232
+ | 5 | Alpha sensitivity sweep (α=0.05, 0.20, 0.40) | ~36 min |
233
+ | 6 | Jacobian eigenspectrum, mrefin=6 (97×97) | ~2 min |
234
+ | 7a | Spin-up from rest, mrefin=5 (dt=0.1) | ~45 min |
235
+ | 7b | Jacobian eigenspectrum, mrefin=5 (49×49) | ~1 min |
236
+ | 8 | Lyapunov exponent (Benettin, 50000 steps) | ~90 min |
237
+ | 9 | All figures | ~1 min |
238
+
239
+ Total runtime on a modern laptop: approximately 4-5 hours.
240
+
241
+ > **Note:** The mrefin=5 spin-up (Step 7a) requires `dt=0.1`, larger time steps
242
+ > cause blow-up during spin-up at this resolution.
243
+
244
+ ### Running individual steps
245
+
246
+ The pipeline is also split into four self-contained drivers under
247
+ `experiments/`, which can be run in isolation for partial reproduction:
248
+
249
+ ```bash
250
+ bash experiments/run_reference.sh # spin-up + RK4 reference
251
+ bash experiments/run_leapfrog_long.sh # LF+RA and LF+RAW main runs
252
+ bash experiments/run_leapfrog_sweep.sh # alpha sensitivity sweep
253
+ bash experiments/run_analysis.sh # eigenspectrum + Lyapunov
254
+ ```
255
+
256
+ Each command in `run_all.sh` is likewise self-contained. For example, to
257
+ recompute only the eigenspectrum:
258
+
259
+ ```bash
260
+ python analysis/jacobian_spectrum.py --restart qg_zero.npz --neigs 200 --no-matrix --out jacobian_spectrum.npz
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Key results
266
+
267
+ | Quantity | Value |
268
+ |----------|-------|
269
+ | Grid (main experiments) | 97×97 (mrefin=6), $h=0.01042$ |
270
+ | Analytical $\Delta t_{\max}^{LF}$ (mrefin=6) | 0.090 |
271
+ | Numerical $\Delta t_{\max}^{LF}$ (mrefin=6) | 1.236 (factor **13.7×** larger) |
272
+ | Analytical $\Delta t_{\max}^{LF}$ (mrefin=5) | 0.362 |
273
+ | Numerical $\Delta t_{\max}^{LF}$ (mrefin=5) | 0.235 (factor **1.54×** smaller) |
274
+ | Maximum Lyapunov exponent $\sigma_1$ | 0.0318 |
275
+ | Lyapunov time $\tau_L$ | 31.5 time units |
276
+ | LF+RA kinetic energy bias (α=0.10) | −21% |
277
+ | LF+RA enstrophy bias (α=0.10) | −46% |
278
+ | LF+RAW kinetic energy bias | −2% |
279
+ | LF+RAW enstrophy bias | +4% |
280
+
281
+ ---
282
+
283
+ ## Time-stepping schemes
284
+
285
+ | Scheme | Order | RHS evals / step | Notes |
286
+ |--------|-------|-------------------|-------|
287
+ | `order1` | 1 | 1 | Explicit Euler |
288
+ | `order2` | 2 | 2 | Midpoint RK |
289
+ | `rk4` | 4 | 4 | Default in QG-C |
290
+ | `dp5` | 5 | 6 | Dormand-Prince |
291
+ | `leapfrog` | 2 | 1 | + RA or RAW filter |
292
+
293
+ The first Leapfrog step uses a single RK4 step as bootstrap to avoid
294
+ exciting the computational mode from the start.
295
+
296
+ ### Leapfrog filter options
297
+
298
+ ```bash
299
+ # Robert-Asselin filter (classic, Asselin 1972)
300
+ python qg_model.py --scheme leapfrog --ra-alpha 0.1 ...
301
+
302
+ # Robert-Asselin-Williams filter (less dissipative, Williams 2009)
303
+ python qg_model.py --scheme leapfrog --ra-alpha 0.1 --raw ...
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Data availability
309
+
310
+ The simulation output files (`.npz`) are several hundred megabytes and are
311
+ therefore not tracked by git. They are archived, together with a snapshot of
312
+ this repository, on Zenodo:
313
+
314
+ > **DOI**: [10.5281/zenodo.22168803](https://doi.org/10.5281/zenodo.22168803)
315
+ > **Record**: <https://doi.org/10.5281/zenodo.22168803>
316
+ > **Development version**: <https://github.com/enino84/qg-leapfrog-stability>
317
+
318
+ The Zenodo record is the authoritative citation target: it holds both the
319
+ tagged source snapshot (`qg-python-v1.0.zip`) and the simulation outputs. This
320
+ GitHub repository is the development mirror and tracks the source only.
321
+
322
+ The archive contains the equilibrium restart fields at `t = 100000` for both
323
+ resolutions, the eigenvalue clouds of the linearised operator, the Benettin
324
+ trace of the maximum Lyapunov exponent, and the time-resolved kinetic energy,
325
+ enstrophy and isotropic spectra for RK4, LF+RA and LF+RAW across
326
+ alpha in {0.05, 0.10, 0.20, 0.40}. `data/README.md` documents the exact
327
+ command line behind each file, so every output can also be regenerated from
328
+ scratch.
329
+
330
+ ---
331
+
332
+ ## Reference model
333
+
334
+ This work builds on:
335
+
336
+ > Sakov, P. (2024). *QG-C: A quasi-geostrophic model in C*.
337
+ > Bureau of Meteorology, Australia.
338
+ > https://github.com/sakov/qg-c
339
+
340
+ Key numerical methods:
341
+
342
+ > Arakawa, A. (1966). Computational design for long-term numerical integration
343
+ > of the equations of fluid motion.
344
+ > *Journal of Computational Physics*, 1(1), 119-143.
345
+
346
+ > Asselin, R. (1972). Frequency filter for time integrations.
347
+ > *Monthly Weather Review*, 100(6), 487-490.
348
+
349
+ > Williams, P. D. (2009). A proposed modification to the Robert-Asselin
350
+ > time filter. *Monthly Weather Review*, 137(8), 2538-2546.
351
+
352
+ > Li, X. S., & Demmel, J. W. (2003). SuperLU_DIST: A scalable distributed-memory
353
+ > sparse direct solver for unsymmetric linear systems.
354
+ > *ACM Transactions on Mathematical Software*, 29(2), 110-140.
355
+
356
+ ---
357
+
358
+ ## License
359
+
360
+ The source code in this repository is released under the MIT License, see
361
+ [LICENSE](LICENSE). The simulation output files distributed through the Zenodo
362
+ archive are released under
363
+ [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/).
364
+
365
+ ---
366
+
367
+ ## Citation
368
+
369
+ Please cite both the article and the archived software release. Machine-readable
370
+ metadata is in [CITATION.cff](CITATION.cff).
371
+
372
+ ```bibtex
373
+ @article{NinoRuiz2026qgpython,
374
+ author = {Nino-Ruiz, Elias D.},
375
+ title = {{QG-Python}: A Reproducible 1.5-Layer Quasi-Geostrophic Modelling
376
+ Service with Direct Sparse Solvers, Spectral Stability Diagnostics,
377
+ and Filter Sensitivity Data},
378
+ journal = {Geoscience Data Journal},
379
+ year = {2026},
380
+ doi = {10.1002/gdj3.70107}
381
+ }
382
+
383
+ @software{NinoRuiz2026qgpythonsoftware,
384
+ author = {Nino-Ruiz, Elias D.},
385
+ title = {{QG-Python}: a reproducible 1.5-layer quasi-geostrophic modelling
386
+ service with spectral stability and filter sensitivity diagnostics},
387
+ version = {1.0},
388
+ year = {2026},
389
+ publisher = {Zenodo},
390
+ doi = {10.5281/zenodo.22168803}
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## A note on the enstrophy diagnostic
397
+
398
+ Enstrophy is reported throughout as the domain average of
399
+ `(nabla^2 psi)^2`, evaluated with the standard 5-point Laplacian on the
400
+ **interior nodes only**. This is the definition in
401
+ `analysis/uncertainty.py::enstrophy`, and it is the one every other script
402
+ imports.
403
+
404
+ The wall ring must be excluded from the average. `qg_model.py` imposes
405
+ homogeneous Dirichlet conditions `psi = 0` on all four walls, so at a wall node
406
+ the 5-point stencil collapses to `psi_adjacent / h^2`; with `h ~ 0.01` that is
407
+ `O(1e5)`, and squaring it lets a one-node ring, about 4% of the points -
408
+ dominate the domain average and inflate its variance by considerably more,
409
+ while leaving the *relative* biases between time-stepping schemes almost
410
+ unchanged.
411
+
412
+ `analysis/make_tables.py` regenerates the LaTeX rows of both tables in the
413
+ article directly from the `.npz` runs, so the statistical summary and the alpha
414
+ sweep cannot drift apart. Pass `--compare` to also print the wall-inclusive
415
+ average.
416
+
417
+ ---
418
+