swesphere 0.3.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elias D. Nino-Ruiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: swesphere
3
+ Version: 0.3.0
4
+ Summary: Shallow-water equations on the rotating sphere: a verified model with documented, reproducible flow regimes
5
+ Author-email: "Elias D. Nino-Ruiz" <enino@uninorte.edu.co>
6
+ License: MIT
7
+ Project-URL: Homepage, https://aml-cs.github.io/software/swesphere/
8
+ Project-URL: Documentation, https://aml-cs.github.io/software/swesphere/
9
+ Project-URL: Repository, https://github.com/enino84/swesphere
10
+ Project-URL: Issues, https://github.com/enino84/swesphere/issues
11
+ Project-URL: Changelog, https://github.com/enino84/swesphere/blob/main/CHANGELOG.md
12
+ Keywords: shallow water,sphere,geophysical fluid dynamics,numerical methods,data assimilation,testbed
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
23
+ Classifier: Topic :: Scientific/Engineering :: Physics
24
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.24
29
+ Requires-Dist: scipy>=1.10
30
+ Requires-Dist: pyshtools>=4.10
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest; extra == "test"
33
+ Provides-Extra: plots
34
+ Requires-Dist: matplotlib; extra == "plots"
35
+ Provides-Extra: experiments
36
+ Requires-Dist: pandas; extra == "experiments"
37
+ Requires-Dist: matplotlib; extra == "experiments"
38
+ Dynamic: license-file
39
+
40
+ # swesphere
41
+
42
+ [![tests](https://github.com/enino84/swesphere/actions/workflows/tests.yml/badge.svg)](https://github.com/enino84/swesphere/actions/workflows/tests.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/swesphere.svg)](https://pypi.org/project/swesphere/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/swesphere.svg)](https://pypi.org/project/swesphere/)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/enino84/swesphere/blob/main/LICENSE)
46
+
47
+ Shallow-water equations on the sphere with three documented flow regimes: a small geophysical
48
+ flow for numerical experiments, verified against the standard test cases and reproducible from a
49
+ container. This package is the model only; assimilation filters live in `pyteda`.
50
+
51
+ ![The two_jets preset on the sphere](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/hero_two_jets.png)
52
+
53
+ *A state of the `two_jets` record: zonal wind, meridional wind, and the departure of `h` from its
54
+ zonal mean drawn as relief (exaggerated). Polar caps under the sponge are grey. Rendered with
55
+ `python figures/make_globe3d.py two_jets`.*
56
+
57
+ ## Install
58
+
59
+ ```
60
+ pip install swesphere
61
+ ```
62
+
63
+ Documentation and API: https://aml-cs.github.io/software/swesphere/
64
+
65
+ ## Quick start
66
+
67
+ ```python
68
+ from swesphere import presets, climatology
69
+ model, x0 = presets.two_jets()
70
+ S = climatology.build_climatology(model, x0, spinup_days=40, n_snapshots=200, every_days=2)
71
+ scales, per_field = climatology.anomaly_scales(model, S)
72
+ ```
73
+
74
+ State vector `[u, v, h]` (row-major, north to south), `model.var_blocks`,
75
+ `model.interior_mask()` (outside the polar sponge), `model.propagate(x, [t0, t1])`.
76
+
77
+ ## The model
78
+
79
+ Equations: vector-invariant momentum and flux-form continuity,
80
+
81
+ du/dt = eta v - (1 / a cos(lat)) dB/dlon eta = zeta + f, B = g h + (u^2 + v^2) / 2
82
+ dv/dt = -eta u - (1 / a) dB/dlat
83
+ dh/dt = -div(h V)
84
+
85
+ Discretization: regular latitude-longitude (Driscoll-Healy) grid, `Nlat = 2 (LMAX + 1)`,
86
+ `Nlon = 4 (LMAX + 1)` (66 x 132 at the default `LMAX = 32`); exact zonal derivative by FFT,
87
+ second-order centered meridional difference; RK4 in time (`dt = 120 s`). Dissipation: an
88
+ exponential filter in spherical harmonics (`pyshtools`) applied every `filter_every = 3` steps,
89
+ and a multiplicative sponge on the wind over the `n_pole_rows = 10` rows next to each pole
90
+ (poleward of about 63 deg), which is how the model avoids the polar singularity of the grid.
91
+ The rows under the sponge are not meant to be used (`model.interior_mask()`).
92
+
93
+ ![Verification](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/verification.png)
94
+
95
+ *Left: normalized l2 error of `h` in Williamson test case 2, with and without the polar sponge.
96
+ Right: relative vorticity of the Galewsky instability at day 6, LMAX 64 (compare with the
97
+ published reference solution).*
98
+
99
+ Two consequences that the experiments quantify:
100
+
101
+ - The scheme is second order (EXP-00: Williamson test case 2 without the sponge, l2 error of h
102
+ 2.2e-4 at LMAX 32 and 5.5e-5 at LMAX 64 after 5 days). With the sponge the steady state of
103
+ that test is disturbed from the polar caps and the error in the interior is about 1e-2.
104
+ - The dissipation acts per step, not per second, so the eddy amplitude of a regime depends on
105
+ `dt * filter_every`. The statistics quoted for the presets hold for the defaults (EXP-01).
106
+
107
+ ## Presets
108
+
109
+ | preset | what it is | regime |
110
+ |---|---|---|
111
+ | `waves` | solid-body rotation (Williamson TC2) + wave packets (wavenumber 4 north, 3 south), no forcing | quasi-linear Rossby waves on a zonal flow, **slowly decaying** (no energy source; the filter is the only sink) |
112
+ | `one_jet` | Galewsky jet at 45 N + relaxation of the zonal mean (5 d) | stationary, fairly regular eddies in the north |
113
+ | `two_jets` | Galewsky jets at +-45 deg + the same relaxation | stationary irregular turbulence in both hemispheres; errors advected by the flow |
114
+
115
+ ![The three regimes](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/regimes.png)
116
+
117
+ The measured statistics of each regime (eddy std of h, wind, anomaly scales, lag correlations,
118
+ first-half against second-half amplitude) are written by EXP-02 to `results/EXP-02/regimes.csv`.
119
+ Measured with the defaults: eddy std of `h` 8.8 m (`waves`, decaying), 124 m (`one_jet`) and
120
+ 156 m (`two_jets`); `|U|max` about 42, 76 and 76 m/s. Note the scales in the figure above: the
121
+ meridional wind and the depth anomaly of `waves` are an order of magnitude weaker than those of
122
+ the forced presets.
123
+
124
+ ![Nine days of the two_jets regime](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/chaos_vorticity.png)
125
+
126
+ *Relative vorticity of the `two_jets` regime over nine days, starting from a spun-up state. The jet
127
+ meanders, sheds eddies and reforms: the flow is chaotic but statistically stationary, which is what
128
+ makes it usable as a reference.*
129
+
130
+ ## The article's experiments (Docker)
131
+
132
+ ```
133
+ docker compose build test
134
+ docker compose run --rm test # unit tests
135
+ QUICK=1 docker compose run --rm all # smoke
136
+ docker compose run --rm all # everything, one core
137
+ ```
138
+
139
+ or detached and in parallel (`verification`, `stability` and `regimes` are independent;
140
+ `footprint` needs `regimes`; `figures` needs all):
141
+
142
+ ```
143
+ docker compose run -d verification; docker compose run -d stability; docker compose run -d regimes
144
+ docker compose run -d footprint # when regimes has finished
145
+ docker compose run --rm figures # when everything has finished
146
+ ```
147
+
148
+ | experiment | what it measures | output |
149
+ |---|---|---|
150
+ | `exp00_verification.py` | Williamson TC2 error norms (LMAX 32, 64; with and without sponge), Galewsky instability: invariants and vorticity | `results/EXP-00/` |
151
+ | `exp01_stability.py` | the stable range of `dt` for the shipped RK4 (pushed until it blows up) and the eddy amplitude against the filter interval | `results/EXP-01/` |
152
+ | `exp02_regimes.py` | 200-state record per preset, per-snapshot series, statistics, stationarity, decorrelation | `results/EXP-02/` |
153
+ | `exp03_footprint.py` | balanced against unbalanced perturbations: advection against gravity waves | `results/EXP-03/` |
154
+ | `figures/make_figures.py` | figures and tables of the article, one file per panel | `results/figures/` |
155
+
156
+ Besides those, `figures/make_globe3d.py <preset> [state.npy]` renders the illustrative
157
+ three-dimensional views above (it spins the preset up itself if no state is given).
158
+
159
+ Progress of a detached run: `docker ps`, `docker logs -f <name>`; every script logs the preset
160
+ and the step, and rewrites its CSV after every case.
161
+
162
+ ## Using your own integrator
163
+
164
+ The package publishes the right-hand side and ships RK4 as the tested stepper. Any other
165
+ scheme can be plugged in and runs with the same filter, sponge and forcing (its stable range
166
+ of `dt` is its own: rerun EXP-01):
167
+
168
+ ```python
169
+ from swesphere import presets
170
+ from swesphere.integrators import INTEGRATORS
171
+
172
+ def my_step(rhs, u, v, h, dt): # rhs(u, v, h) -> (du, dv, dh)
173
+ du, dv, dh = rhs(u, v, h)
174
+ return u + dt * du, v + dt * dv, h + dt * dh
175
+
176
+ INTEGRATORS["mine"] = my_step
177
+ model, x0 = presets.two_jets(scheme="mine")
178
+ ```
179
+
180
+ `swesphere.dynamics.rhs(u, v, h, grid)` is the physics alone; `model.rhs(u, v, h)` adds the
181
+ preset's forcing. `swesphere.diagnostics` gives mass, energy, potential enstrophy and the
182
+ Williamson error norms.
183
+
184
+ Changes between versions are listed in `CHANGELOG.md` (0.2.0 fixes the continuity equation of 0.1.0).
185
+
186
+ ## Citing, contributing and license
187
+
188
+ If you use swesphere in your work, please cite it: the `CITATION.cff` file gives the reference, and
189
+ GitHub shows it under "Cite this repository". Bug reports and pull requests are welcome; see
190
+ `CONTRIBUTING.md`. Questions: enino@uninorte.edu.co.
191
+
192
+ swesphere is released under the MIT license (`LICENSE`). Its dependencies are NumPy and SciPy
193
+ (BSD-3-Clause) and pyshtools (BSD-3-Clause), all compatible with it.
194
+
@@ -0,0 +1,155 @@
1
+ # swesphere
2
+
3
+ [![tests](https://github.com/enino84/swesphere/actions/workflows/tests.yml/badge.svg)](https://github.com/enino84/swesphere/actions/workflows/tests.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/swesphere.svg)](https://pypi.org/project/swesphere/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/swesphere.svg)](https://pypi.org/project/swesphere/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/enino84/swesphere/blob/main/LICENSE)
7
+
8
+ Shallow-water equations on the sphere with three documented flow regimes: a small geophysical
9
+ flow for numerical experiments, verified against the standard test cases and reproducible from a
10
+ container. This package is the model only; assimilation filters live in `pyteda`.
11
+
12
+ ![The two_jets preset on the sphere](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/hero_two_jets.png)
13
+
14
+ *A state of the `two_jets` record: zonal wind, meridional wind, and the departure of `h` from its
15
+ zonal mean drawn as relief (exaggerated). Polar caps under the sponge are grey. Rendered with
16
+ `python figures/make_globe3d.py two_jets`.*
17
+
18
+ ## Install
19
+
20
+ ```
21
+ pip install swesphere
22
+ ```
23
+
24
+ Documentation and API: https://aml-cs.github.io/software/swesphere/
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ from swesphere import presets, climatology
30
+ model, x0 = presets.two_jets()
31
+ S = climatology.build_climatology(model, x0, spinup_days=40, n_snapshots=200, every_days=2)
32
+ scales, per_field = climatology.anomaly_scales(model, S)
33
+ ```
34
+
35
+ State vector `[u, v, h]` (row-major, north to south), `model.var_blocks`,
36
+ `model.interior_mask()` (outside the polar sponge), `model.propagate(x, [t0, t1])`.
37
+
38
+ ## The model
39
+
40
+ Equations: vector-invariant momentum and flux-form continuity,
41
+
42
+ du/dt = eta v - (1 / a cos(lat)) dB/dlon eta = zeta + f, B = g h + (u^2 + v^2) / 2
43
+ dv/dt = -eta u - (1 / a) dB/dlat
44
+ dh/dt = -div(h V)
45
+
46
+ Discretization: regular latitude-longitude (Driscoll-Healy) grid, `Nlat = 2 (LMAX + 1)`,
47
+ `Nlon = 4 (LMAX + 1)` (66 x 132 at the default `LMAX = 32`); exact zonal derivative by FFT,
48
+ second-order centered meridional difference; RK4 in time (`dt = 120 s`). Dissipation: an
49
+ exponential filter in spherical harmonics (`pyshtools`) applied every `filter_every = 3` steps,
50
+ and a multiplicative sponge on the wind over the `n_pole_rows = 10` rows next to each pole
51
+ (poleward of about 63 deg), which is how the model avoids the polar singularity of the grid.
52
+ The rows under the sponge are not meant to be used (`model.interior_mask()`).
53
+
54
+ ![Verification](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/verification.png)
55
+
56
+ *Left: normalized l2 error of `h` in Williamson test case 2, with and without the polar sponge.
57
+ Right: relative vorticity of the Galewsky instability at day 6, LMAX 64 (compare with the
58
+ published reference solution).*
59
+
60
+ Two consequences that the experiments quantify:
61
+
62
+ - The scheme is second order (EXP-00: Williamson test case 2 without the sponge, l2 error of h
63
+ 2.2e-4 at LMAX 32 and 5.5e-5 at LMAX 64 after 5 days). With the sponge the steady state of
64
+ that test is disturbed from the polar caps and the error in the interior is about 1e-2.
65
+ - The dissipation acts per step, not per second, so the eddy amplitude of a regime depends on
66
+ `dt * filter_every`. The statistics quoted for the presets hold for the defaults (EXP-01).
67
+
68
+ ## Presets
69
+
70
+ | preset | what it is | regime |
71
+ |---|---|---|
72
+ | `waves` | solid-body rotation (Williamson TC2) + wave packets (wavenumber 4 north, 3 south), no forcing | quasi-linear Rossby waves on a zonal flow, **slowly decaying** (no energy source; the filter is the only sink) |
73
+ | `one_jet` | Galewsky jet at 45 N + relaxation of the zonal mean (5 d) | stationary, fairly regular eddies in the north |
74
+ | `two_jets` | Galewsky jets at +-45 deg + the same relaxation | stationary irregular turbulence in both hemispheres; errors advected by the flow |
75
+
76
+ ![The three regimes](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/regimes.png)
77
+
78
+ The measured statistics of each regime (eddy std of h, wind, anomaly scales, lag correlations,
79
+ first-half against second-half amplitude) are written by EXP-02 to `results/EXP-02/regimes.csv`.
80
+ Measured with the defaults: eddy std of `h` 8.8 m (`waves`, decaying), 124 m (`one_jet`) and
81
+ 156 m (`two_jets`); `|U|max` about 42, 76 and 76 m/s. Note the scales in the figure above: the
82
+ meridional wind and the depth anomaly of `waves` are an order of magnitude weaker than those of
83
+ the forced presets.
84
+
85
+ ![Nine days of the two_jets regime](https://raw.githubusercontent.com/enino84/swesphere/main/docs/img/chaos_vorticity.png)
86
+
87
+ *Relative vorticity of the `two_jets` regime over nine days, starting from a spun-up state. The jet
88
+ meanders, sheds eddies and reforms: the flow is chaotic but statistically stationary, which is what
89
+ makes it usable as a reference.*
90
+
91
+ ## The article's experiments (Docker)
92
+
93
+ ```
94
+ docker compose build test
95
+ docker compose run --rm test # unit tests
96
+ QUICK=1 docker compose run --rm all # smoke
97
+ docker compose run --rm all # everything, one core
98
+ ```
99
+
100
+ or detached and in parallel (`verification`, `stability` and `regimes` are independent;
101
+ `footprint` needs `regimes`; `figures` needs all):
102
+
103
+ ```
104
+ docker compose run -d verification; docker compose run -d stability; docker compose run -d regimes
105
+ docker compose run -d footprint # when regimes has finished
106
+ docker compose run --rm figures # when everything has finished
107
+ ```
108
+
109
+ | experiment | what it measures | output |
110
+ |---|---|---|
111
+ | `exp00_verification.py` | Williamson TC2 error norms (LMAX 32, 64; with and without sponge), Galewsky instability: invariants and vorticity | `results/EXP-00/` |
112
+ | `exp01_stability.py` | the stable range of `dt` for the shipped RK4 (pushed until it blows up) and the eddy amplitude against the filter interval | `results/EXP-01/` |
113
+ | `exp02_regimes.py` | 200-state record per preset, per-snapshot series, statistics, stationarity, decorrelation | `results/EXP-02/` |
114
+ | `exp03_footprint.py` | balanced against unbalanced perturbations: advection against gravity waves | `results/EXP-03/` |
115
+ | `figures/make_figures.py` | figures and tables of the article, one file per panel | `results/figures/` |
116
+
117
+ Besides those, `figures/make_globe3d.py <preset> [state.npy]` renders the illustrative
118
+ three-dimensional views above (it spins the preset up itself if no state is given).
119
+
120
+ Progress of a detached run: `docker ps`, `docker logs -f <name>`; every script logs the preset
121
+ and the step, and rewrites its CSV after every case.
122
+
123
+ ## Using your own integrator
124
+
125
+ The package publishes the right-hand side and ships RK4 as the tested stepper. Any other
126
+ scheme can be plugged in and runs with the same filter, sponge and forcing (its stable range
127
+ of `dt` is its own: rerun EXP-01):
128
+
129
+ ```python
130
+ from swesphere import presets
131
+ from swesphere.integrators import INTEGRATORS
132
+
133
+ def my_step(rhs, u, v, h, dt): # rhs(u, v, h) -> (du, dv, dh)
134
+ du, dv, dh = rhs(u, v, h)
135
+ return u + dt * du, v + dt * dv, h + dt * dh
136
+
137
+ INTEGRATORS["mine"] = my_step
138
+ model, x0 = presets.two_jets(scheme="mine")
139
+ ```
140
+
141
+ `swesphere.dynamics.rhs(u, v, h, grid)` is the physics alone; `model.rhs(u, v, h)` adds the
142
+ preset's forcing. `swesphere.diagnostics` gives mass, energy, potential enstrophy and the
143
+ Williamson error norms.
144
+
145
+ Changes between versions are listed in `CHANGELOG.md` (0.2.0 fixes the continuity equation of 0.1.0).
146
+
147
+ ## Citing, contributing and license
148
+
149
+ If you use swesphere in your work, please cite it: the `CITATION.cff` file gives the reference, and
150
+ GitHub shows it under "Cite this repository". Bug reports and pull requests are welcome; see
151
+ `CONTRIBUTING.md`. Questions: enino@uninorte.edu.co.
152
+
153
+ swesphere is released under the MIT license (`LICENSE`). Its dependencies are NumPy and SciPy
154
+ (BSD-3-Clause) and pyshtools (BSD-3-Clause), all compatible with it.
155
+
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "swesphere"
7
+ version = "0.3.0"
8
+ description = "Shallow-water equations on the rotating sphere: a verified model with documented, reproducible flow regimes"
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [{name = "Elias D. Nino-Ruiz", email = "enino@uninorte.edu.co"}]
13
+ keywords = ["shallow water", "sphere", "geophysical fluid dynamics", "numerical methods", "data assimilation", "testbed"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
25
+ "Topic :: Scientific/Engineering :: Physics",
26
+ "Topic :: Scientific/Engineering :: Mathematics",
27
+ ]
28
+ dependencies = ["numpy>=1.24", "scipy>=1.10", "pyshtools>=4.10"]
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest"]
32
+ plots = ["matplotlib"]
33
+ experiments = ["pandas", "matplotlib"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://aml-cs.github.io/software/swesphere/"
37
+ Documentation = "https://aml-cs.github.io/software/swesphere/"
38
+ Repository = "https://github.com/enino84/swesphere"
39
+ Issues = "https://github.com/enino84/swesphere/issues"
40
+ Changelog = "https://github.com/enino84/swesphere/blob/main/CHANGELOG.md"
41
+
42
+ [tool.setuptools]
43
+ package-dir = {"" = "src"}
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """swesphere: shallow-water equations on the sphere for data-assimilation testbeds.
2
+
3
+ from swesphere import presets, climatology
4
+ model, x0 = presets.two_jets()
5
+ S = climatology.build_climatology(model, x0, spinup_days=40, n_snapshots=200, every_days=2)
6
+ """
7
+ from .model import SWEModel
8
+ from .grid import SphereGrid
9
+ from .forcing import ZonalRelaxation
10
+ from . import presets, climatology, initial_conditions, integrators, dynamics, diagnostics
11
+
12
+ __version__ = "0.3.0"
13
+ __all__ = ["SWEModel", "SphereGrid", "ZonalRelaxation", "presets", "climatology", "initial_conditions", "integrators", "dynamics", "diagnostics", "__version__"]
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Climatology builder: spin-up, snapshots, decorrelation check, normalization."""
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ DAY = 86400.0
8
+
9
+
10
+ def build_climatology(model, x0, spinup_days, n_snapshots, every_days, verbose=False, label=""):
11
+ """Spin up ``spinup_days`` from ``x0`` and keep ``n_snapshots`` states ``every_days`` apart (float32).
12
+
13
+ For a forced preset this samples a stationary regime. For the unforced ``waves`` preset the record is a
14
+ slowly decaying flow (the filter is the only sink), so its early and late states differ in amplitude.
15
+ """
16
+ if verbose:
17
+ print(f" {label} spin-up of {spinup_days:g} days", flush=True)
18
+ x = model.propagate(x0, [0.0, spinup_days * DAY])
19
+ if verbose:
20
+ print(f" {label} spin-up done", flush=True)
21
+ snaps = [x.astype(np.float32)]
22
+ for k in range(n_snapshots - 1):
23
+ x = model.propagate(x, [0.0, every_days * DAY]); snaps.append(x.astype(np.float32))
24
+ if verbose and (k + 1) % 10 == 0:
25
+ print(f" {label} {k + 1}/{n_snapshots} snapshots", flush=True)
26
+ return np.array(snaps)
27
+
28
+
29
+ def anomaly_scales(model, snapshots):
30
+ """Per-field standard deviation of the climatological anomalies, repeated over the state."""
31
+ A = snapshots - snapshots.mean(axis=0); fs = model.field_size
32
+ sd = np.array([A[:, k * fs:(k + 1) * fs].std() for k in range(3)])
33
+ return np.repeat(sd, fs), sd
34
+
35
+
36
+ def lag_correlation(model, snapshots, lag=1, field="h"):
37
+ A = snapshots - snapshots.mean(axis=0); blk = model.var_blocks[field]; a = A[:, blk]
38
+ return float(np.mean([np.corrcoef(a[i], a[i + lag])[0, 1] for i in range(len(a) - lag)]))
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Global invariants of the shallow-water equations and error norms.
3
+
4
+ All integrals are area weighted (cos φ) over the whole grid, or over the rows
5
+ outside the polar sponge with ``interior=True``. The continuous equations
6
+ conserve mass, total energy and potential enstrophy; the discrete model loses a
7
+ little of each through the spectral filter and the polar sponge, and the forced
8
+ presets exchange them with the relaxation. EXP-00 reports the measured drifts.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+ from .dynamics import relative_vorticity
15
+
16
+
17
+ def area_weights(model, interior: bool = False) -> np.ndarray:
18
+ w = np.clip(np.asarray(model.grid.cosL), 0.0, None).copy()
19
+ if interior:
20
+ k = model.n_pole_rows; w[:k] = 0.0; w[model.Nlat - k:] = 0.0
21
+ return w / w.sum()
22
+
23
+
24
+ def invariants(model, x, interior: bool = False) -> dict:
25
+ """Mean depth (m), total energy per unit area (J/kg m) and potential enstrophy."""
26
+ u, v, h = model.unpack(np.asarray(x, dtype=float)); G = model.grid; w = area_weights(model, interior)
27
+ eta = relative_vorticity(u, v, G) + G.f_cor
28
+ return dict(mass=float((w * h).sum()),
29
+ energy=float((w * (0.5 * h * (u ** 2 + v ** 2) + 0.5 * G.g * h ** 2)).sum()),
30
+ enstrophy=float((w * 0.5 * eta ** 2 / h).sum()))
31
+
32
+
33
+ def error_norms(model, x, x_ref, field: str = "h", interior: bool = True) -> dict:
34
+ """Normalized l1, l2 and l-infinity errors of Williamson et al. (1992)."""
35
+ i = "uvh".index(field); q = model.unpack(np.asarray(x, dtype=float))[i]; r = model.unpack(np.asarray(x_ref, dtype=float))[i]
36
+ w = area_weights(model, interior); m = w > 0
37
+ return dict(l1=float((w * np.abs(q - r)).sum() / (w * np.abs(r)).sum()),
38
+ l2=float(np.sqrt((w * (q - r) ** 2).sum() / (w * r ** 2).sum())),
39
+ linf=float(np.abs(q - r)[m].max() / np.abs(r)[m].max()))
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Vector-invariant form of the shallow-water equations on the sphere.
4
+
5
+ State variables
6
+ ---------------
7
+ * ``u(λ, φ)``, zonal velocity (m/s)
8
+ * ``v(λ, φ)``, meridional velocity (m/s)
9
+ * ``h(λ, φ)``, layer thickness (m)
10
+
11
+ Equations (Hack & Jakob 1992 form)
12
+ ----------------------------------
13
+ ∂u/∂t = η v − (1 / a cosφ) ∂B/∂λ
14
+ ∂v/∂t = −η u − (1 / a) ∂B/∂φ
15
+ ∂h/∂t = −∇·(h V) = −(1 / a cosφ) [∂(h u)/∂λ + ∂(h v cosφ)/∂φ]
16
+
17
+ where:
18
+ ζ = (1 / a cosφ) ∂v/∂λ − (1/a) ∂u/∂φ + (tanφ/a) u (relative vorticity)
19
+ η = ζ + f (absolute vorticity)
20
+ B = g h + ½(u² + v²) (Bernoulli potential)
21
+ δ = (1 / a cosφ) ∂u/∂λ + (1 / a cosφ) ∂(v cosφ)/∂φ (divergence, diagnostic only)
22
+
23
+ The continuity equation is discretized in flux form, so the zonal FFT derivative and
24
+ the centered meridional difference telescope and the global mass changes only through
25
+ the spectral filter and the polar rows. (Versions before 0.2.0 used ``dh = -h δ`` and
26
+ omitted the advection of ``h``; see CHANGELOG.md.)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Tuple
32
+
33
+ import numpy as np
34
+
35
+ from .grid import SphereGrid, d_lon, d_lat
36
+
37
+
38
+ def rhs(
39
+ u: np.ndarray,
40
+ v: np.ndarray,
41
+ h: np.ndarray,
42
+ grid: SphereGrid,
43
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
44
+ """Return ``(du/dt, dv/dt, dh/dt)`` for the SWE system."""
45
+ cos = grid.cos_safe
46
+ a = grid.a
47
+
48
+ # Relative vorticity ζ
49
+ zeta = (d_lon(v, grid) / (a * cos)
50
+ - d_lat(u, grid) / a
51
+ + grid.sinL / cos * u / a)
52
+
53
+ # Absolute vorticity η
54
+ eta = zeta + grid.f_cor
55
+
56
+ # Bernoulli potential
57
+ B = grid.g * h + 0.5 * (u ** 2 + v ** 2)
58
+
59
+ # Tendencies: vector-invariant momentum, flux-form continuity
60
+ du = eta * v - d_lon(B, grid) / (a * cos)
61
+ dv = -eta * u - d_lat(B, grid) / a
62
+ dh = -(d_lon(h * u, grid) + d_lat(h * v * grid.cosL, grid)) / (a * cos)
63
+
64
+ return du, dv, dh
65
+
66
+
67
+ def relative_vorticity(u: np.ndarray, v: np.ndarray,
68
+ grid: SphereGrid) -> np.ndarray:
69
+ """Diagnostic: relative vorticity ζ from (u, v)."""
70
+ cos = grid.cos_safe
71
+ a = grid.a
72
+ return (d_lon(v, grid) / (a * cos)
73
+ - d_lat(u, grid) / a
74
+ + grid.sinL / cos * u / a)
75
+
76
+
77
+ def divergence(u: np.ndarray, v: np.ndarray,
78
+ grid: SphereGrid) -> np.ndarray:
79
+ """Diagnostic: horizontal divergence δ from (u, v)."""
80
+ cos = grid.cos_safe
81
+ a = grid.a
82
+ return (d_lon(u, grid) / (a * cos)
83
+ + d_lat(v * grid.cosL, grid) / (a * cos))
84
+
85
+
86
+ def divergence(u: np.ndarray, v: np.ndarray, grid: SphereGrid) -> np.ndarray:
87
+ """Diagnostic: horizontal divergence δ from (u, v)."""
88
+ return (d_lon(u, grid) + d_lat(v * grid.cosL, grid)) / (grid.a * grid.cos_safe)
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Forcing that gives the model a stationary turbulent regime.
3
+
4
+ Without forcing the shallow-water equations on the sphere do not sustain eddies: the filter dissipates
5
+ whatever the initial condition put in and the flow relaxes toward a zonal state with weak, slowly
6
+ decaying waves (the ``waves`` preset). ``ZonalRelaxation`` relaxes only the zonal mean of (u, v, h)
7
+ toward a reference jet and leaves the eddies alone; with a barotropically unstable reference
8
+ (Galewsky) the instability regenerates eddies against the dissipation and the flow reaches a
9
+ stationary turbulent state (EXP-02 measures its statistics). Relaxing the full fields toward the
10
+ zonal state kills the eddies instead.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import numpy as np
15
+
16
+
17
+ class ZonalRelaxation:
18
+ def __init__(self, u_ref, v_ref, h_ref, tau):
19
+ self.k = 1.0 / float(tau)
20
+ self.ref = tuple(np.repeat(np.asarray(f).mean(axis=1, keepdims=True), np.asarray(f).shape[1], axis=1) for f in (u_ref, v_ref, h_ref))
21
+
22
+ def __call__(self, u, v, h, du, dv, dh):
23
+ for f, r_, d_ in ((u, self.ref[0], du), (v, self.ref[1], dv), (h, self.ref[2], dh)):
24
+ zm = np.repeat(f.mean(axis=1, keepdims=True), f.shape[1], axis=1)
25
+ d_ -= self.k * (zm - r_)
26
+ return du, dv, dh