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.
Files changed (87) hide show
  1. giamip-0.1.0/LICENSE +21 -0
  2. giamip-0.1.0/PKG-INFO +195 -0
  3. giamip-0.1.0/README.md +161 -0
  4. giamip-0.1.0/giamip/__init__.py +70 -0
  5. giamip-0.1.0/giamip/_netcdf.py +217 -0
  6. giamip-0.1.0/giamip/benchmarks/__init__.py +26 -0
  7. giamip-0.1.0/giamip/benchmarks/inputs.py +766 -0
  8. giamip-0.1.0/giamip/benchmarks/martinec2018.py +727 -0
  9. giamip-0.1.0/giamip/benchmarks/metrics.py +626 -0
  10. giamip-0.1.0/giamip/benchmarks/results.py +1050 -0
  11. giamip-0.1.0/giamip/benchmarks/spada2011.py +736 -0
  12. giamip-0.1.0/giamip/cases.py +113 -0
  13. giamip-0.1.0/giamip/corrections.py +132 -0
  14. giamip-0.1.0/giamip/data.py +842 -0
  15. giamip-0.1.0/giamip/grids.py +181 -0
  16. giamip-0.1.0/giamip/mip/__init__.py +18 -0
  17. giamip-0.1.0/giamip/mip/inputs.py +258 -0
  18. giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/A.json +76 -0
  19. giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/B.json +92 -0
  20. giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/C.json +106 -0
  21. giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/D.json +117 -0
  22. giamip-0.1.0/giamip/resources/cases/benchmarks/martinec2018/E.json +72 -0
  23. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-01.json +37 -0
  24. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-02.json +38 -0
  25. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-03.json +41 -0
  26. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-04.json +32 -0
  27. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-05.json +42 -0
  28. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-06.json +45 -0
  29. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t01-07.json +36 -0
  30. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-01.json +62 -0
  31. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-02.json +56 -0
  32. giamip-0.1.0/giamip/resources/cases/benchmarks/spada2011/t02-03.json +50 -0
  33. giamip-0.1.0/giamip/resources/cases/giamip/exp01.json +51 -0
  34. giamip-0.1.0/giamip/resources/cases/giamip/exp02.json +53 -0
  35. giamip-0.1.0/giamip/resources/cases/giamip/exp03.json +56 -0
  36. giamip-0.1.0/giamip/resources/cases/giamip/exp04.json +56 -0
  37. giamip-0.1.0/giamip/resources/cases/giamip/exp05.json +51 -0
  38. giamip-0.1.0/giamip/resources/cases/giamip/exp06.json +53 -0
  39. giamip-0.1.0/giamip/resources/cases/giamip/exp07.json +53 -0
  40. giamip-0.1.0/giamip/resources/cases/giamip/exp08.json +57 -0
  41. giamip-0.1.0/giamip/resources/cases/giamip/exp09.json +57 -0
  42. giamip-0.1.0/giamip/resources/cases/giamip/exp10.json +57 -0
  43. giamip-0.1.0/giamip/resources/cases/giamip/exp11.json +57 -0
  44. giamip-0.1.0/giamip/resources/cases/giamip/exp12.json +57 -0
  45. giamip-0.1.0/giamip/resources/checker.json +13 -0
  46. giamip-0.1.0/giamip/resources/column_layouts.json +12471 -0
  47. giamip-0.1.0/giamip/resources/corrections.json +1030 -0
  48. giamip-0.1.0/giamip/resources/loads/martinec2018.json +54 -0
  49. giamip-0.1.0/giamip/resources/loads/spada2011.json +54 -0
  50. giamip-0.1.0/giamip/resources/manifest.json +6412 -0
  51. giamip-0.1.0/giamip/resources/models/giamip-open-range.json +16 -0
  52. giamip-0.1.0/giamip/resources/models/gowan2021.json +19 -0
  53. giamip-0.1.0/giamip/resources/models/m3-l70-v01.json +80 -0
  54. giamip-0.1.0/giamip/resources/models/prem.json +17 -0
  55. giamip-0.1.0/giamip/resources/models/vm5a.json +27 -0
  56. giamip-0.1.0/giamip/resources/models/vm7.json +29 -0
  57. giamip-0.1.0/giamip/resources/models/vss96.json +57 -0
  58. giamip-0.1.0/giamip/resources/provenance.json +345 -0
  59. giamip-0.1.0/giamip/resources/spada_column_layouts.json +100890 -0
  60. giamip-0.1.0/giamip/resources/variables.json +217 -0
  61. giamip-0.1.0/giamip.egg-info/PKG-INFO +195 -0
  62. giamip-0.1.0/giamip.egg-info/SOURCES.txt +85 -0
  63. giamip-0.1.0/giamip.egg-info/dependency_links.txt +1 -0
  64. giamip-0.1.0/giamip.egg-info/requires.txt +21 -0
  65. giamip-0.1.0/giamip.egg-info/top_level.txt +1 -0
  66. giamip-0.1.0/pyproject.toml +89 -0
  67. giamip-0.1.0/setup.cfg +4 -0
  68. giamip-0.1.0/tests/test_cases.py +109 -0
  69. giamip-0.1.0/tests/test_corrections.py +124 -0
  70. giamip-0.1.0/tests/test_data.py +234 -0
  71. giamip-0.1.0/tests/test_doc_examples.py +78 -0
  72. giamip-0.1.0/tests/test_grids.py +112 -0
  73. giamip-0.1.0/tests/test_import.py +57 -0
  74. giamip-0.1.0/tests/test_layouts.py +245 -0
  75. giamip-0.1.0/tests/test_locate.py +431 -0
  76. giamip-0.1.0/tests/test_manifest.py +165 -0
  77. giamip-0.1.0/tests/test_martinec2018.py +274 -0
  78. giamip-0.1.0/tests/test_model_inputs.py +337 -0
  79. giamip-0.1.0/tests/test_netcdf_io.py +237 -0
  80. giamip-0.1.0/tests/test_pages.py +100 -0
  81. giamip-0.1.0/tests/test_reference_curves.py +161 -0
  82. giamip-0.1.0/tests/test_scoring.py +341 -0
  83. giamip-0.1.0/tests/test_spada2011.py +1129 -0
  84. giamip-0.1.0/tests/test_spada_classes.py +700 -0
  85. giamip-0.1.0/tests/test_spada_layouts.py +305 -0
  86. giamip-0.1.0/tests/test_spada_records.py +288 -0
  87. 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)