giamip 0.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.
- giamip-0.1.0/LICENSE +21 -0
- giamip-0.1.0/PKG-INFO +195 -0
- giamip-0.1.0/README.md +161 -0
- giamip-0.1.0/giamip/__init__.py +70 -0
- giamip-0.1.0/giamip/_netcdf.py +217 -0
- giamip-0.1.0/giamip/benchmarks/__init__.py +26 -0
- giamip-0.1.0/giamip/benchmarks/inputs.py +766 -0
- giamip-0.1.0/giamip/benchmarks/martinec2018.py +727 -0
- giamip-0.1.0/giamip/benchmarks/metrics.py +626 -0
- giamip-0.1.0/giamip/benchmarks/results.py +1050 -0
- giamip-0.1.0/giamip/benchmarks/spada2011.py +736 -0
- giamip-0.1.0/giamip/cases.py +113 -0
- giamip-0.1.0/giamip/corrections.py +132 -0
- giamip-0.1.0/giamip/data.py +842 -0
- giamip-0.1.0/giamip/grids.py +181 -0
- giamip-0.1.0/giamip/mip/__init__.py +18 -0
- giamip-0.1.0/giamip/mip/inputs.py +258 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/A.json +76 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/B.json +92 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/C.json +106 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/D.json +117 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/E.json +72 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-01.json +37 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-02.json +38 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-03.json +41 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-04.json +32 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-05.json +42 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-06.json +45 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-07.json +36 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-01.json +62 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-02.json +56 -0
- giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-03.json +50 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp01.json +51 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp02.json +53 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp03.json +56 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp04.json +56 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp05.json +51 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp06.json +53 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp07.json +53 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp08.json +57 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp09.json +57 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp10.json +57 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp11.json +57 -0
- giamip-0.1.0/giamip/resources/cases/giamip/exp12.json +57 -0
- giamip-0.1.0/giamip/resources/checker.json +13 -0
- giamip-0.1.0/giamip/resources/column_layouts.json +12471 -0
- giamip-0.1.0/giamip/resources/corrections.json +1030 -0
- giamip-0.1.0/giamip/resources/loads/martinec2018.json +54 -0
- giamip-0.1.0/giamip/resources/loads/spada2011.json +54 -0
- giamip-0.1.0/giamip/resources/manifest.json +6412 -0
- giamip-0.1.0/giamip/resources/models/giamip-open-range.json +16 -0
- giamip-0.1.0/giamip/resources/models/gowan2021.json +19 -0
- giamip-0.1.0/giamip/resources/models/m3-l70-v01.json +80 -0
- giamip-0.1.0/giamip/resources/models/prem.json +17 -0
- giamip-0.1.0/giamip/resources/models/vm5a.json +27 -0
- giamip-0.1.0/giamip/resources/models/vm7.json +29 -0
- giamip-0.1.0/giamip/resources/models/vss96.json +57 -0
- giamip-0.1.0/giamip/resources/provenance.json +345 -0
- giamip-0.1.0/giamip/resources/spada_column_layouts.json +100890 -0
- giamip-0.1.0/giamip/resources/variables.json +217 -0
- giamip-0.1.0/giamip.egg-info/PKG-INFO +195 -0
- giamip-0.1.0/giamip.egg-info/SOURCES.txt +85 -0
- giamip-0.1.0/giamip.egg-info/dependency_links.txt +1 -0
- giamip-0.1.0/giamip.egg-info/requires.txt +21 -0
- giamip-0.1.0/giamip.egg-info/top_level.txt +1 -0
- giamip-0.1.0/pyproject.toml +89 -0
- giamip-0.1.0/setup.cfg +4 -0
- giamip-0.1.0/tests/test_cases.py +109 -0
- giamip-0.1.0/tests/test_corrections.py +124 -0
- giamip-0.1.0/tests/test_data.py +234 -0
- giamip-0.1.0/tests/test_doc_examples.py +78 -0
- giamip-0.1.0/tests/test_grids.py +112 -0
- giamip-0.1.0/tests/test_import.py +57 -0
- giamip-0.1.0/tests/test_layouts.py +245 -0
- giamip-0.1.0/tests/test_locate.py +431 -0
- giamip-0.1.0/tests/test_manifest.py +165 -0
- giamip-0.1.0/tests/test_martinec2018.py +274 -0
- giamip-0.1.0/tests/test_model_inputs.py +337 -0
- giamip-0.1.0/tests/test_netcdf_io.py +237 -0
- giamip-0.1.0/tests/test_pages.py +100 -0
- giamip-0.1.0/tests/test_reference_curves.py +161 -0
- giamip-0.1.0/tests/test_scoring.py +341 -0
- giamip-0.1.0/tests/test_spada2011.py +1129 -0
- giamip-0.1.0/tests/test_spada_classes.py +700 -0
- giamip-0.1.0/tests/test_spada_layouts.py +305 -0
- giamip-0.1.0/tests/test_spada_records.py +288 -0
- giamip-0.1.0/tests/test_vss96.py +142 -0
giamip-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 G-ADOPT Authors
|
|
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.
|
giamip-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: giamip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Case definitions, reference data and output conventions for the Spada (2011) and Martinec (2018) GIA benchmarks and for GIAMIP.
|
|
5
|
+
Author-email: Siavash Ghelichkhan <siavash.ghelichkhan@anu.edu.au>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/g-adopt/giamip
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=2.0
|
|
16
|
+
Requires-Dist: scipy>=1.14
|
|
17
|
+
Requires-Dist: h5py>=3.16
|
|
18
|
+
Requires-Dist: h5netcdf>=1.8
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "test"
|
|
22
|
+
Requires-Dist: ipython>=8; extra == "test"
|
|
23
|
+
Requires-Dist: jupytext>=1.16; extra == "test"
|
|
24
|
+
Requires-Dist: nbclient>=0.10; extra == "test"
|
|
25
|
+
Requires-Dist: ipykernel>=6.29; extra == "test"
|
|
26
|
+
Requires-Dist: matplotlib>=3.8; extra == "test"
|
|
27
|
+
Requires-Dist: netCDF4>=1.7; extra == "test"
|
|
28
|
+
Provides-Extra: plot
|
|
29
|
+
Requires-Dist: matplotlib>=3.8; extra == "plot"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: jupyter>=1.0; extra == "docs"
|
|
32
|
+
Requires-Dist: matplotlib>=3.8; extra == "docs"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# giamip
|
|
36
|
+
|
|
37
|
+
Case definitions, reference data and output conventions for glacial isostatic
|
|
38
|
+
adjustment (GIA) model comparison.
|
|
39
|
+
|
|
40
|
+
This repository specifies what to run and holds what to compare against. It
|
|
41
|
+
contains no solver. A solver reads a case with `giamip.case`, produces
|
|
42
|
+
output, and uses this package to check that the output obeys the conventions
|
|
43
|
+
and to score it against the reference data.
|
|
44
|
+
|
|
45
|
+
Three collections are covered.
|
|
46
|
+
|
|
47
|
+
| Collection | What it is | Reference answer |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| Spada et al. (2011) | Ten tests of GIA codes: Love numbers, relaxation spectra, polar motion, and surface deformation for analytic loads | Published, several codes per test |
|
|
50
|
+
| Martinec et al. (2018) | Five cases (A to E) of the sea level equation, from no ocean to a moving coastline | Published, the VEGA solution is the reference |
|
|
51
|
+
| GIAMIP | Twelve experiments with global ice histories and present-day topography | None. This is an open intercomparison |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install giamip
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The package holds the code and the small JSON files: the case files, the load
|
|
60
|
+
tables, the earth models, the manifest and the corrections. It holds no data
|
|
61
|
+
file. Each data file is downloaded from data.gadopt.org the first time a
|
|
62
|
+
reader needs it, checked against its SHA-256 sum, and cached. For
|
|
63
|
+
development, install a checkout with `pip install -e ".[test]"`.
|
|
64
|
+
|
|
65
|
+
## How to run a case
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import giamip
|
|
69
|
+
from giamip.benchmarks.results import BenchmarkResult
|
|
70
|
+
|
|
71
|
+
case = giamip.case("martinec2018-B") # the set-up of benchmark B
|
|
72
|
+
h = case.ice_thickness(colatitude, longitude, t_kyr) # the load at your points
|
|
73
|
+
b = case.topography(colatitude, longitude) # the ocean basin
|
|
74
|
+
|
|
75
|
+
run = BenchmarkResult("B", "my-code") # your curves
|
|
76
|
+
for profile in case.profiles: # what the case compares
|
|
77
|
+
for quantity in profile.quantities:
|
|
78
|
+
run.add_profile(profile.name, quantity, colatitude_deg,
|
|
79
|
+
my_curve(profile, quantity))
|
|
80
|
+
run.write("martinec2018-B.nc") # netCDF-4, through h5py
|
|
81
|
+
|
|
82
|
+
rows = case.score(run) # eq. 31, on your own points
|
|
83
|
+
checks = case.envelope_check(run, band_half_width_deg=6.74) # criterion 2
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`giamip.case` gives a `MartinecCase`, a `SpadaCase` or a `GiamipExperiment`.
|
|
87
|
+
`tests/test_doc_examples.py` runs this example. For a solver's own benchmark
|
|
88
|
+
test, with both stages and the per-case list of what a result must hold, read
|
|
89
|
+
the section "Using giamip in a solver's benchmark test" of
|
|
90
|
+
`docs/reference/python-interface.qmd`.
|
|
91
|
+
The reference curves of a Martinec case are VEGA, and
|
|
92
|
+
`docs/protocol/martinec2018.qmd` says how each one is built.
|
|
93
|
+
|
|
94
|
+
Each case also has a page in `docs/results/`, which gives the full
|
|
95
|
+
specification: the earth model, the load, the time interval, the grid, the
|
|
96
|
+
physical constants, the variables to produce and the data to compare against.
|
|
97
|
+
Start with `docs/reference/solver-requirements.qmd`, which lists what a solver
|
|
98
|
+
must be able to do, and `docs/reference/python-interface.qmd`, which shows the
|
|
99
|
+
classes.
|
|
100
|
+
|
|
101
|
+
## Data
|
|
102
|
+
|
|
103
|
+
The data files are not in the package. The third-party originals (341 MB)
|
|
104
|
+
belong to the people who produced them. The converted reference data (10 MB)
|
|
105
|
+
is derived from them and is tracked in this repository under
|
|
106
|
+
`data/converted/`. Both are in the gadopt object store at data.gadopt.org.
|
|
107
|
+
`docs/data/manifest.qmd` records, for every file, its size, its SHA-256 sum,
|
|
108
|
+
where it came from, who supplied it and under what terms.
|
|
109
|
+
|
|
110
|
+
A reader finds a file with `giamip.data.locate`. It looks in the checkout
|
|
111
|
+
(when it runs from one) and in the download root, and downloads the file into
|
|
112
|
+
the download root when neither has it. The download root is
|
|
113
|
+
`$GIAMIP_DATA_ROOT` when it is set, else `data/` of the checkout, else
|
|
114
|
+
`~/.cache/giamip`. `docs/data/fetching.qmd` gives the details.
|
|
115
|
+
|
|
116
|
+
Some of the original files contain defects. `docs/data/corrections.qmd` lists
|
|
117
|
+
every one that is known, with the evidence. The readers apply the corrections.
|
|
118
|
+
The original files are never edited.
|
|
119
|
+
|
|
120
|
+
### On Gadi
|
|
121
|
+
|
|
122
|
+
Gadi compute nodes have no network, and in a job the Firedrake module points
|
|
123
|
+
`XDG_CACHE_HOME` at storage that is deleted when the job ends. So:
|
|
124
|
+
|
|
125
|
+
1. The maintainer of the Firedrake module installs giamip with its
|
|
126
|
+
dependencies h5netcdf and `packaging` (h5py, numpy and scipy are there).
|
|
127
|
+
2. On a login node, fill a directory on `/g/data` once:
|
|
128
|
+
`GIAMIP_DATA_ROOT=/g/data/<project>/giamip python -m giamip.data --stage converted --collection benchmarks`.
|
|
129
|
+
For the GIAMIP inputs (317 MB), run the same command with
|
|
130
|
+
`--collection giamip` and without `--stage converted`: they are originals.
|
|
131
|
+
3. In the job script, set `GIAMIP_DATA_ROOT` to the same directory.
|
|
132
|
+
|
|
133
|
+
A reader on a compute node that does not find a file raises
|
|
134
|
+
`DataUnavailable`, and the message gives the command of step 2.
|
|
135
|
+
|
|
136
|
+
## Layout
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
giamip/ the package: data.py (locate, fetch and verify),
|
|
140
|
+
grids.py (the Gauss-Legendre grid), corrections.py
|
|
141
|
+
(known defects in the originals), benchmarks/ (Martinec
|
|
142
|
+
and Spada case set-up, results, scores and conversion)
|
|
143
|
+
and mip/ (GIAMIP input readers). Not written: the GIAMIP
|
|
144
|
+
output writer
|
|
145
|
+
giamip/resources/ the JSON files the package carries: cases/ (one file per
|
|
146
|
+
case, in benchmarks/ and giamip/), loads/ (the analytic
|
|
147
|
+
load tables), models/ (the earth models), the manifest,
|
|
148
|
+
the provenance record, the corrections and the column
|
|
149
|
+
layouts
|
|
150
|
+
docs/ the Quarto site: one page per collection, per reference
|
|
151
|
+
table and per case, plus the data manifest and the list
|
|
152
|
+
of corrections. docs/cases/ holds the pages for single
|
|
153
|
+
tests as percent-format Python files, which the site
|
|
154
|
+
executes and plots
|
|
155
|
+
scripts/ conversion, manifest and upload scripts, run by hand, and
|
|
156
|
+
check_installed.py, which checks an installed wheel
|
|
157
|
+
submissions/ one directory per submission: its identity, its
|
|
158
|
+
compliance report and its scores. The output fields
|
|
159
|
+
themselves stay in outputs/
|
|
160
|
+
tests/ the test suite
|
|
161
|
+
data/ original/ holds third-party files byte for byte and is
|
|
162
|
+
gitignored; converted/ holds the netCDF-4 files built
|
|
163
|
+
from them and is tracked; versions/ holds downloaded
|
|
164
|
+
converted files by SHA-256 and is gitignored
|
|
165
|
+
outputs/ gitignored. The output fields a solver produced for a case
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Correspondence about the GIAMIP inputs
|
|
169
|
+
|
|
170
|
+
We raised two defects in the GIAMIP inputs with the GIAMIP organisers. The
|
|
171
|
+
correspondence is not in this repository, so this section records the result.
|
|
172
|
+
|
|
173
|
+
**GLAC3 version names** (correction `glac3-version-naming`). The protocol
|
|
174
|
+
table gives the ice history of Exp03, Exp04 and Exp09 to Exp11 as GLAC3b. The
|
|
175
|
+
delivered files are `iceHistory-GLAC3c_profile1.nc` to
|
|
176
|
+
`iceHistory-GLAC3c_profile3.nc`. On 2026-09-12, Holly Han, the lead of the
|
|
177
|
+
protocol, confirmed that these files are the intended inputs. Each file
|
|
178
|
+
combines GLAC3 versions a, b and c, with different versions for different ice
|
|
179
|
+
sheets. The case files give the ice history as `GLAC3_p1`, `GLAC3_p2` or
|
|
180
|
+
`GLAC3_p3`, with no version letter.
|
|
181
|
+
|
|
182
|
+
**Topography source attribute** (correction `topography-source-attribute`).
|
|
183
|
+
In `bedtopo_BedMach3ANT_BedMach5GRL_GEBCO_GLQ256.nc`, the `source` attribute
|
|
184
|
+
names BedMachine Greenland twice. The first of the two entries must name
|
|
185
|
+
BedMachine Antarctica v3. We reported this defect to Jan Swierczek-Jereczek on
|
|
186
|
+
2026-09-12. Until a corrected file arrives, only the filename names the
|
|
187
|
+
Antarctic dataset correctly. A corrected file has a new SHA-256 sum, and
|
|
188
|
+
`tests/test_corrections.py` then fails until someone checks the correction
|
|
189
|
+
against the new file.
|
|
190
|
+
|
|
191
|
+
## Licence
|
|
192
|
+
|
|
193
|
+
MIT for the code, the case definitions and the documentation. The third-party
|
|
194
|
+
data in `data/original/` keeps the terms of whoever supplied it. See
|
|
195
|
+
`LICENSE-DATA.md`.
|
giamip-0.1.0/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# giamip
|
|
2
|
+
|
|
3
|
+
Case definitions, reference data and output conventions for glacial isostatic
|
|
4
|
+
adjustment (GIA) model comparison.
|
|
5
|
+
|
|
6
|
+
This repository specifies what to run and holds what to compare against. It
|
|
7
|
+
contains no solver. A solver reads a case with `giamip.case`, produces
|
|
8
|
+
output, and uses this package to check that the output obeys the conventions
|
|
9
|
+
and to score it against the reference data.
|
|
10
|
+
|
|
11
|
+
Three collections are covered.
|
|
12
|
+
|
|
13
|
+
| Collection | What it is | Reference answer |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Spada et al. (2011) | Ten tests of GIA codes: Love numbers, relaxation spectra, polar motion, and surface deformation for analytic loads | Published, several codes per test |
|
|
16
|
+
| Martinec et al. (2018) | Five cases (A to E) of the sea level equation, from no ocean to a moving coastline | Published, the VEGA solution is the reference |
|
|
17
|
+
| GIAMIP | Twelve experiments with global ice histories and present-day topography | None. This is an open intercomparison |
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install giamip
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The package holds the code and the small JSON files: the case files, the load
|
|
26
|
+
tables, the earth models, the manifest and the corrections. It holds no data
|
|
27
|
+
file. Each data file is downloaded from data.gadopt.org the first time a
|
|
28
|
+
reader needs it, checked against its SHA-256 sum, and cached. For
|
|
29
|
+
development, install a checkout with `pip install -e ".[test]"`.
|
|
30
|
+
|
|
31
|
+
## How to run a case
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import giamip
|
|
35
|
+
from giamip.benchmarks.results import BenchmarkResult
|
|
36
|
+
|
|
37
|
+
case = giamip.case("martinec2018-B") # the set-up of benchmark B
|
|
38
|
+
h = case.ice_thickness(colatitude, longitude, t_kyr) # the load at your points
|
|
39
|
+
b = case.topography(colatitude, longitude) # the ocean basin
|
|
40
|
+
|
|
41
|
+
run = BenchmarkResult("B", "my-code") # your curves
|
|
42
|
+
for profile in case.profiles: # what the case compares
|
|
43
|
+
for quantity in profile.quantities:
|
|
44
|
+
run.add_profile(profile.name, quantity, colatitude_deg,
|
|
45
|
+
my_curve(profile, quantity))
|
|
46
|
+
run.write("martinec2018-B.nc") # netCDF-4, through h5py
|
|
47
|
+
|
|
48
|
+
rows = case.score(run) # eq. 31, on your own points
|
|
49
|
+
checks = case.envelope_check(run, band_half_width_deg=6.74) # criterion 2
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`giamip.case` gives a `MartinecCase`, a `SpadaCase` or a `GiamipExperiment`.
|
|
53
|
+
`tests/test_doc_examples.py` runs this example. For a solver's own benchmark
|
|
54
|
+
test, with both stages and the per-case list of what a result must hold, read
|
|
55
|
+
the section "Using giamip in a solver's benchmark test" of
|
|
56
|
+
`docs/reference/python-interface.qmd`.
|
|
57
|
+
The reference curves of a Martinec case are VEGA, and
|
|
58
|
+
`docs/protocol/martinec2018.qmd` says how each one is built.
|
|
59
|
+
|
|
60
|
+
Each case also has a page in `docs/results/`, which gives the full
|
|
61
|
+
specification: the earth model, the load, the time interval, the grid, the
|
|
62
|
+
physical constants, the variables to produce and the data to compare against.
|
|
63
|
+
Start with `docs/reference/solver-requirements.qmd`, which lists what a solver
|
|
64
|
+
must be able to do, and `docs/reference/python-interface.qmd`, which shows the
|
|
65
|
+
classes.
|
|
66
|
+
|
|
67
|
+
## Data
|
|
68
|
+
|
|
69
|
+
The data files are not in the package. The third-party originals (341 MB)
|
|
70
|
+
belong to the people who produced them. The converted reference data (10 MB)
|
|
71
|
+
is derived from them and is tracked in this repository under
|
|
72
|
+
`data/converted/`. Both are in the gadopt object store at data.gadopt.org.
|
|
73
|
+
`docs/data/manifest.qmd` records, for every file, its size, its SHA-256 sum,
|
|
74
|
+
where it came from, who supplied it and under what terms.
|
|
75
|
+
|
|
76
|
+
A reader finds a file with `giamip.data.locate`. It looks in the checkout
|
|
77
|
+
(when it runs from one) and in the download root, and downloads the file into
|
|
78
|
+
the download root when neither has it. The download root is
|
|
79
|
+
`$GIAMIP_DATA_ROOT` when it is set, else `data/` of the checkout, else
|
|
80
|
+
`~/.cache/giamip`. `docs/data/fetching.qmd` gives the details.
|
|
81
|
+
|
|
82
|
+
Some of the original files contain defects. `docs/data/corrections.qmd` lists
|
|
83
|
+
every one that is known, with the evidence. The readers apply the corrections.
|
|
84
|
+
The original files are never edited.
|
|
85
|
+
|
|
86
|
+
### On Gadi
|
|
87
|
+
|
|
88
|
+
Gadi compute nodes have no network, and in a job the Firedrake module points
|
|
89
|
+
`XDG_CACHE_HOME` at storage that is deleted when the job ends. So:
|
|
90
|
+
|
|
91
|
+
1. The maintainer of the Firedrake module installs giamip with its
|
|
92
|
+
dependencies h5netcdf and `packaging` (h5py, numpy and scipy are there).
|
|
93
|
+
2. On a login node, fill a directory on `/g/data` once:
|
|
94
|
+
`GIAMIP_DATA_ROOT=/g/data/<project>/giamip python -m giamip.data --stage converted --collection benchmarks`.
|
|
95
|
+
For the GIAMIP inputs (317 MB), run the same command with
|
|
96
|
+
`--collection giamip` and without `--stage converted`: they are originals.
|
|
97
|
+
3. In the job script, set `GIAMIP_DATA_ROOT` to the same directory.
|
|
98
|
+
|
|
99
|
+
A reader on a compute node that does not find a file raises
|
|
100
|
+
`DataUnavailable`, and the message gives the command of step 2.
|
|
101
|
+
|
|
102
|
+
## Layout
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
giamip/ the package: data.py (locate, fetch and verify),
|
|
106
|
+
grids.py (the Gauss-Legendre grid), corrections.py
|
|
107
|
+
(known defects in the originals), benchmarks/ (Martinec
|
|
108
|
+
and Spada case set-up, results, scores and conversion)
|
|
109
|
+
and mip/ (GIAMIP input readers). Not written: the GIAMIP
|
|
110
|
+
output writer
|
|
111
|
+
giamip/resources/ the JSON files the package carries: cases/ (one file per
|
|
112
|
+
case, in benchmarks/ and giamip/), loads/ (the analytic
|
|
113
|
+
load tables), models/ (the earth models), the manifest,
|
|
114
|
+
the provenance record, the corrections and the column
|
|
115
|
+
layouts
|
|
116
|
+
docs/ the Quarto site: one page per collection, per reference
|
|
117
|
+
table and per case, plus the data manifest and the list
|
|
118
|
+
of corrections. docs/cases/ holds the pages for single
|
|
119
|
+
tests as percent-format Python files, which the site
|
|
120
|
+
executes and plots
|
|
121
|
+
scripts/ conversion, manifest and upload scripts, run by hand, and
|
|
122
|
+
check_installed.py, which checks an installed wheel
|
|
123
|
+
submissions/ one directory per submission: its identity, its
|
|
124
|
+
compliance report and its scores. The output fields
|
|
125
|
+
themselves stay in outputs/
|
|
126
|
+
tests/ the test suite
|
|
127
|
+
data/ original/ holds third-party files byte for byte and is
|
|
128
|
+
gitignored; converted/ holds the netCDF-4 files built
|
|
129
|
+
from them and is tracked; versions/ holds downloaded
|
|
130
|
+
converted files by SHA-256 and is gitignored
|
|
131
|
+
outputs/ gitignored. The output fields a solver produced for a case
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Correspondence about the GIAMIP inputs
|
|
135
|
+
|
|
136
|
+
We raised two defects in the GIAMIP inputs with the GIAMIP organisers. The
|
|
137
|
+
correspondence is not in this repository, so this section records the result.
|
|
138
|
+
|
|
139
|
+
**GLAC3 version names** (correction `glac3-version-naming`). The protocol
|
|
140
|
+
table gives the ice history of Exp03, Exp04 and Exp09 to Exp11 as GLAC3b. The
|
|
141
|
+
delivered files are `iceHistory-GLAC3c_profile1.nc` to
|
|
142
|
+
`iceHistory-GLAC3c_profile3.nc`. On 2026-09-12, Holly Han, the lead of the
|
|
143
|
+
protocol, confirmed that these files are the intended inputs. Each file
|
|
144
|
+
combines GLAC3 versions a, b and c, with different versions for different ice
|
|
145
|
+
sheets. The case files give the ice history as `GLAC3_p1`, `GLAC3_p2` or
|
|
146
|
+
`GLAC3_p3`, with no version letter.
|
|
147
|
+
|
|
148
|
+
**Topography source attribute** (correction `topography-source-attribute`).
|
|
149
|
+
In `bedtopo_BedMach3ANT_BedMach5GRL_GEBCO_GLQ256.nc`, the `source` attribute
|
|
150
|
+
names BedMachine Greenland twice. The first of the two entries must name
|
|
151
|
+
BedMachine Antarctica v3. We reported this defect to Jan Swierczek-Jereczek on
|
|
152
|
+
2026-09-12. Until a corrected file arrives, only the filename names the
|
|
153
|
+
Antarctic dataset correctly. A corrected file has a new SHA-256 sum, and
|
|
154
|
+
`tests/test_corrections.py` then fails until someone checks the correction
|
|
155
|
+
against the new file.
|
|
156
|
+
|
|
157
|
+
## Licence
|
|
158
|
+
|
|
159
|
+
MIT for the code, the case definitions and the documentation. The third-party
|
|
160
|
+
data in `data/original/` keeps the terms of whoever supplied it. See
|
|
161
|
+
`LICENSE-DATA.md`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Case definitions, reference data and output conventions for GIA model comparison.
|
|
2
|
+
|
|
3
|
+
This package describes and checks work. It never performs it. Nothing here
|
|
4
|
+
imports a solver, and nothing here computes a viscoelastic response. A solver
|
|
5
|
+
reads a case with `case`, for example ``giamip.case("martinec2018-B")``,
|
|
6
|
+
produces output, and uses this package to check
|
|
7
|
+
that the output obeys the conventions and to score it against the published
|
|
8
|
+
reference data.
|
|
9
|
+
|
|
10
|
+
Three collections are covered, and they are different in kind:
|
|
11
|
+
|
|
12
|
+
* Spada et al. (2011), Geophys. J. Int. 185, 106-132, doi:10.1111/j.1365-246X.2011.04952.x
|
|
13
|
+
Ten tests of GIA codes on analytic loads. Published answers from several codes.
|
|
14
|
+
* Martinec et al. (2018), Geophys. J. Int. 215, 389-414, doi:10.1093/gji/ggy280
|
|
15
|
+
Five cases of the sea level equation. The VEGA solution is the reference.
|
|
16
|
+
* GIAMIP, an open intercomparison with twelve experiments and no reference answer.
|
|
17
|
+
|
|
18
|
+
Submodules
|
|
19
|
+
----------
|
|
20
|
+
data
|
|
21
|
+
Fetch the original files from the object store and verify them by SHA-256.
|
|
22
|
+
Imports only the standard library, so it runs before anything is installed.
|
|
23
|
+
grids
|
|
24
|
+
The Gauss-Legendre quadrature grid that GIAMIP prescribes, and a validator
|
|
25
|
+
that checks a file's axes against it.
|
|
26
|
+
corrections
|
|
27
|
+
Known defects in the original files, applied when a file is read. The
|
|
28
|
+
original files themselves are never edited.
|
|
29
|
+
cases
|
|
30
|
+
Look up a case, an earth model or a load table.
|
|
31
|
+
benchmarks
|
|
32
|
+
Martinec (2018) and Spada (2011) case set-up, results, scores and
|
|
33
|
+
conversion to NetCDF.
|
|
34
|
+
mip
|
|
35
|
+
GIAMIP input readers. The output writer and compliance wrapper are not
|
|
36
|
+
written.
|
|
37
|
+
|
|
38
|
+
Figures are drawn inline in the pages of ``docs/``, so the package holds no
|
|
39
|
+
plotting code.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def case(case_id: str):
|
|
46
|
+
"""One case as an object, by id: the entry point for a solver.
|
|
47
|
+
|
|
48
|
+
``martinec2018-*`` gives a `giamip.benchmarks.inputs.MartinecCase`,
|
|
49
|
+
``spada2011-*`` a `giamip.benchmarks.inputs.SpadaCase` and ``giamip-*`` a
|
|
50
|
+
`giamip.mip.inputs.GiamipExperiment`. The classes are imported here and
|
|
51
|
+
not at the top, so that ``import giamip`` stays light.
|
|
52
|
+
|
|
53
|
+
>>> import giamip
|
|
54
|
+
>>> giamip.case("martinec2018-B").terminal_time_kyr
|
|
55
|
+
10.0
|
|
56
|
+
|
|
57
|
+
Raises KeyError for an id that is not a case of the package.
|
|
58
|
+
"""
|
|
59
|
+
from giamip import cases
|
|
60
|
+
if case_id not in cases.ids():
|
|
61
|
+
raise KeyError(f"no case with id {case_id!r}. Known ids: {', '.join(cases.ids())}")
|
|
62
|
+
collection = case_id.split("-", 1)[0]
|
|
63
|
+
if collection == "martinec2018":
|
|
64
|
+
from giamip.benchmarks.inputs import MartinecCase
|
|
65
|
+
return MartinecCase(case_id)
|
|
66
|
+
if collection == "spada2011":
|
|
67
|
+
from giamip.benchmarks.inputs import SpadaCase
|
|
68
|
+
return SpadaCase(case_id)
|
|
69
|
+
from giamip.mip.inputs import GiamipExperiment
|
|
70
|
+
return GiamipExperiment(case_id)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""The small NetCDF helpers that every reader and writer of the package shares.
|
|
2
|
+
|
|
3
|
+
Every NetCDF file in this package is opened through h5netcdf, a netCDF-4
|
|
4
|
+
layer written in Python on top of h5py. Three properties decide this:
|
|
5
|
+
|
|
6
|
+
* h5netcdf applies no mask, no scale factor and no fill value. It returns the
|
|
7
|
+
numbers stored in the file. The six GIAMIP ice files declare
|
|
8
|
+
``_FillValue = 0.0`` on ``ice_thickness``, and a reader that masks would turn
|
|
9
|
+
every ice-free cell into a missing value (AGENTS.md, "Every reader in the
|
|
10
|
+
package opens NetCDF through h5netcdf").
|
|
11
|
+
* It uses the HDF5 library that h5py brings. The Firedrake environment on Gadi
|
|
12
|
+
has h5py and no netCDF4, and a second HDF5 build in the same process is a
|
|
13
|
+
known source of conflicts.
|
|
14
|
+
* The files it writes are ordinary netCDF-4 files: netCDF-C, ncdump and xarray
|
|
15
|
+
read them. tests/test_netcdf_io.py checks this with netCDF4.
|
|
16
|
+
|
|
17
|
+
Only the core API ``h5netcdf.File`` is used. The ``legacyapi`` module failed
|
|
18
|
+
on nested group access in h5netcdf 1.8.1.
|
|
19
|
+
|
|
20
|
+
Two details of the format are handled here, so that no reader repeats them:
|
|
21
|
+
|
|
22
|
+
* A text attribute that netCDF-C wrote comes back from h5py as ``bytes``
|
|
23
|
+
(``numpy.bytes_``), and one that h5netcdf wrote comes back as ``str``.
|
|
24
|
+
`attribute` turns both into ``str``.
|
|
25
|
+
* netCDF-C stores a numeric attribute as an array even when it holds one
|
|
26
|
+
value, so a one-element array is returned as its single value.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import pathlib
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def open_read(path: pathlib.Path):
|
|
37
|
+
"""Open a NetCDF file for reading with h5netcdf.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
path
|
|
42
|
+
The file.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
h5netcdf.File
|
|
47
|
+
The open file. Use it as a context manager.
|
|
48
|
+
"""
|
|
49
|
+
import h5netcdf
|
|
50
|
+
return h5netcdf.File(path, "r")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def open_write(path: pathlib.Path, append: bool):
|
|
54
|
+
"""Open a NetCDF file for writing with h5netcdf.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
path
|
|
59
|
+
The file.
|
|
60
|
+
append
|
|
61
|
+
True to add to an existing file, False to create a new one.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
h5netcdf.File
|
|
66
|
+
The open file. Use it as a context manager.
|
|
67
|
+
"""
|
|
68
|
+
import h5netcdf
|
|
69
|
+
return h5netcdf.File(path, "a" if append else "w")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def attribute(value):
|
|
73
|
+
"""One attribute value as the package uses it.
|
|
74
|
+
|
|
75
|
+
Text becomes ``str`` whether netCDF-C (``bytes``) or h5netcdf (``str``)
|
|
76
|
+
wrote it. A one-element numeric array becomes its single value, a
|
|
77
|
+
NumPy scalar, which is what netCDF4's ``getncattr`` returned. Anything else
|
|
78
|
+
is returned unchanged.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(value, (bytes, np.bytes_)):
|
|
81
|
+
return value.decode("utf-8")
|
|
82
|
+
if isinstance(value, np.ndarray):
|
|
83
|
+
if value.dtype.kind in "SO" and value.size == 1:
|
|
84
|
+
item = value.reshape(-1)[0]
|
|
85
|
+
return item.decode("utf-8") if isinstance(item, (bytes, np.bytes_)) else item
|
|
86
|
+
if value.size == 1:
|
|
87
|
+
return value.reshape(-1)[0]
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def attributes(obj, skip=()) -> dict:
|
|
92
|
+
"""The attributes of a file, group or variable as a dict, decoded.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
obj
|
|
97
|
+
An h5netcdf file, group or variable.
|
|
98
|
+
skip
|
|
99
|
+
Names to leave out.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
dict
|
|
104
|
+
Name to value, each passed through `attribute`. h5netcdf already hides
|
|
105
|
+
the attributes that HDF5 and netCDF-C use for their own bookkeeping
|
|
106
|
+
(``DIMENSION_LIST``, ``_Netcdf4Dimid`` and so on), except
|
|
107
|
+
``_NCProperties``, which is left out here as well: it records library
|
|
108
|
+
versions and is not part of the content.
|
|
109
|
+
"""
|
|
110
|
+
return {k: attribute(v) for k, v in obj.attrs.items()
|
|
111
|
+
if k not in skip and k != "_NCProperties"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def set_attributes(obj, values: dict) -> None:
|
|
115
|
+
"""Write attributes, skipping None.
|
|
116
|
+
|
|
117
|
+
A Python ``bool`` is written as an 8-bit integer, because netCDF has no
|
|
118
|
+
boolean type.
|
|
119
|
+
"""
|
|
120
|
+
for key, value in values.items():
|
|
121
|
+
if value is None:
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(value, (bool, np.bool_)):
|
|
124
|
+
value = np.int8(value)
|
|
125
|
+
obj.attrs[key] = value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def has_group(root, group_path: str) -> bool:
|
|
129
|
+
"""Whether a slash-separated group path exists below an open file or group."""
|
|
130
|
+
group = root
|
|
131
|
+
for part in group_path.split("/"):
|
|
132
|
+
if part not in group.groups:
|
|
133
|
+
return False
|
|
134
|
+
group = group.groups[part]
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def group_at(root, group_path: str):
|
|
139
|
+
"""The group at a slash-separated path below an open file or group."""
|
|
140
|
+
group = root
|
|
141
|
+
for part in group_path.split("/"):
|
|
142
|
+
group = group.groups[part]
|
|
143
|
+
return group
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_group(root, group_path: str):
|
|
147
|
+
"""Create the group at a slash-separated path, and any missing parents.
|
|
148
|
+
|
|
149
|
+
The last part must not exist yet, so that a second writer never adds to a
|
|
150
|
+
group another writer made (the callers check this first and raise with the
|
|
151
|
+
file name).
|
|
152
|
+
"""
|
|
153
|
+
parts = group_path.split("/")
|
|
154
|
+
group = root
|
|
155
|
+
for part in parts[:-1]:
|
|
156
|
+
group = group.groups[part] if part in group.groups else group.create_group(part)
|
|
157
|
+
return group.create_group(parts[-1])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def axis_of(group, variable) -> np.ndarray:
|
|
161
|
+
"""The values of the coordinate variable of a variable's first dimension.
|
|
162
|
+
|
|
163
|
+
Every curve in this package's files is a 1-D variable whose dimension has a
|
|
164
|
+
coordinate variable of the same name in the same group.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
group
|
|
169
|
+
The group that holds the variable.
|
|
170
|
+
variable
|
|
171
|
+
The variable.
|
|
172
|
+
"""
|
|
173
|
+
return np.asarray(group.variables[variable.dimensions[0]][...])
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def data_variables(group):
|
|
177
|
+
"""The variables of a group that are not coordinate variables, by name."""
|
|
178
|
+
return {name: var for name, var in group.variables.items() if name not in group.dimensions}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def write_curves(group, curves, *, axis_name: str, coordinate_attributes: dict,
|
|
182
|
+
compress: bool = True) -> None:
|
|
183
|
+
"""Write 1-D curves into one group, sharing an axis wherever two are equal.
|
|
184
|
+
|
|
185
|
+
The first distinct axis is called ``axis_name``, the next
|
|
186
|
+
``axis_name_2``, and so on, so a group whose curves are sampled alike has
|
|
187
|
+
one axis. The curves are written in the order given.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
group
|
|
192
|
+
An open h5netcdf group.
|
|
193
|
+
curves
|
|
194
|
+
A sequence of ``(name, axis, values, attributes)``.
|
|
195
|
+
axis_name
|
|
196
|
+
The name of the first axis.
|
|
197
|
+
coordinate_attributes
|
|
198
|
+
The attributes of every axis variable, for example the units.
|
|
199
|
+
compress
|
|
200
|
+
Store the values with gzip. For curves of a few hundred points the
|
|
201
|
+
uncompressed form is smaller: compression needs chunked storage, whose
|
|
202
|
+
index costs more than it saves on short arrays.
|
|
203
|
+
"""
|
|
204
|
+
axes: list[tuple[str, np.ndarray]] = []
|
|
205
|
+
for name, axis, values, attrs in curves:
|
|
206
|
+
axis = np.asarray(axis, dtype=float)
|
|
207
|
+
dim = next((d for d, a in axes if a.shape == axis.shape and np.array_equal(a, axis)), None)
|
|
208
|
+
if dim is None:
|
|
209
|
+
dim = axis_name if not axes else f"{axis_name}_{len(axes) + 1}"
|
|
210
|
+
axes.append((dim, axis))
|
|
211
|
+
group.dimensions[dim] = axis.size
|
|
212
|
+
coordinate = group.create_variable(dim, (dim,), "f8", data=axis)
|
|
213
|
+
set_attributes(coordinate, coordinate_attributes)
|
|
214
|
+
variable = group.create_variable(name, (dim,), "f8",
|
|
215
|
+
data=np.asarray(values, dtype=float),
|
|
216
|
+
compression="gzip" if compress else None)
|
|
217
|
+
set_attributes(variable, attrs)
|