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.
- swesphere-0.3.0/LICENSE +21 -0
- swesphere-0.3.0/PKG-INFO +194 -0
- swesphere-0.3.0/README.md +155 -0
- swesphere-0.3.0/pyproject.toml +46 -0
- swesphere-0.3.0/setup.cfg +4 -0
- swesphere-0.3.0/src/swesphere/__init__.py +13 -0
- swesphere-0.3.0/src/swesphere/climatology.py +38 -0
- swesphere-0.3.0/src/swesphere/diagnostics.py +39 -0
- swesphere-0.3.0/src/swesphere/dynamics.py +88 -0
- swesphere-0.3.0/src/swesphere/forcing.py +26 -0
- swesphere-0.3.0/src/swesphere/grid.py +161 -0
- swesphere-0.3.0/src/swesphere/initial_conditions.py +160 -0
- swesphere-0.3.0/src/swesphere/integrators.py +22 -0
- swesphere-0.3.0/src/swesphere/model.py +89 -0
- swesphere-0.3.0/src/swesphere/presets.py +64 -0
- swesphere-0.3.0/src/swesphere.egg-info/PKG-INFO +194 -0
- swesphere-0.3.0/src/swesphere.egg-info/SOURCES.txt +19 -0
- swesphere-0.3.0/src/swesphere.egg-info/dependency_links.txt +1 -0
- swesphere-0.3.0/src/swesphere.egg-info/requires.txt +13 -0
- swesphere-0.3.0/src/swesphere.egg-info/top_level.txt +1 -0
- swesphere-0.3.0/tests/test_model.py +69 -0
swesphere-0.3.0/LICENSE
ADDED
|
@@ -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.
|
swesphere-0.3.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/enino84/swesphere/actions/workflows/tests.yml)
|
|
43
|
+
[](https://pypi.org/project/swesphere/)
|
|
44
|
+
[](https://pypi.org/project/swesphere/)
|
|
45
|
+
[](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
[](https://github.com/enino84/swesphere/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/swesphere/)
|
|
5
|
+
[](https://pypi.org/project/swesphere/)
|
|
6
|
+
[](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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,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
|