meteoraster 2.5__tar.gz → 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.
Files changed (40) hide show
  1. meteoraster-3.0/PKG-INFO +610 -0
  2. meteoraster-3.0/README.md +557 -0
  3. meteoraster-3.0/meteoraster/_lazy.py +73 -0
  4. meteoraster-3.0/meteoraster/io.py +493 -0
  5. meteoraster-3.0/meteoraster/meteoraster.py +1516 -0
  6. meteoraster-3.0/meteoraster/resample.py +528 -0
  7. meteoraster-3.0/meteoraster/utils.py +182 -0
  8. meteoraster-3.0/meteoraster/zonal.py +966 -0
  9. meteoraster-3.0/meteoraster.egg-info/PKG-INFO +610 -0
  10. {meteoraster-2.5 → meteoraster-3.0}/meteoraster.egg-info/SOURCES.txt +12 -1
  11. {meteoraster-2.5 → meteoraster-3.0}/meteoraster.egg-info/requires.txt +2 -1
  12. {meteoraster-2.5 → meteoraster-3.0}/pyproject.toml +7 -3
  13. meteoraster-3.0/tests/conftest.py +315 -0
  14. {meteoraster-2.5 → meteoraster-3.0}/tests/test_deprecations.py +14 -0
  15. meteoraster-3.0/tests/test_io.py +353 -0
  16. meteoraster-3.0/tests/test_kml_catchments.py +375 -0
  17. {meteoraster-2.5 → meteoraster-3.0}/tests/test_meteo_raster_base.py +56 -0
  18. {meteoraster-2.5 → meteoraster-3.0}/tests/test_meteo_raster_plots.py +21 -28
  19. meteoraster-3.0/tests/test_quantiles.py +227 -0
  20. meteoraster-3.0/tests/test_real_rasters.py +292 -0
  21. meteoraster-3.0/tests/test_regressions.py +293 -0
  22. {meteoraster-2.5 → meteoraster-3.0}/tests/test_resample.py +4 -0
  23. meteoraster-3.0/tests/test_resample_semantics.py +441 -0
  24. meteoraster-3.0/tests/test_utils_readers.py +258 -0
  25. meteoraster-3.0/tests/test_zonal.py +569 -0
  26. meteoraster-2.5/PKG-INFO +0 -419
  27. meteoraster-2.5/README.md +0 -367
  28. meteoraster-2.5/meteoraster/meteoraster.py +0 -1267
  29. meteoraster-2.5/meteoraster/resample.py +0 -269
  30. meteoraster-2.5/meteoraster/utils.py +0 -359
  31. meteoraster-2.5/meteoraster.egg-info/PKG-INFO +0 -419
  32. meteoraster-2.5/tests/conftest.py +0 -149
  33. {meteoraster-2.5 → meteoraster-3.0}/LICENSE +0 -0
  34. {meteoraster-2.5 → meteoraster-3.0}/MANIFEST.in +0 -0
  35. {meteoraster-2.5 → meteoraster-3.0}/NOTICE +0 -0
  36. {meteoraster-2.5 → meteoraster-3.0}/meteoraster/__init__.py +0 -0
  37. {meteoraster-2.5 → meteoraster-3.0}/meteoraster.egg-info/dependency_links.txt +0 -0
  38. {meteoraster-2.5 → meteoraster-3.0}/meteoraster.egg-info/top_level.txt +0 -0
  39. {meteoraster-2.5 → meteoraster-3.0}/setup.cfg +0 -0
  40. {meteoraster-2.5 → meteoraster-3.0}/tests/test_meteo_raster_extra.py +0 -0
@@ -0,0 +1,610 @@
1
+ Metadata-Version: 2.4
2
+ Name: meteoraster
3
+ Version: 3.0
4
+ Summary: Module for handling distributed ensemble and probabilistic forecast data (5-D meteorological rasters)
5
+ Author-email: Jose Pedro Matos <jose.matos@tecnico.ulisboa.pt>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster
8
+ Project-URL: Repository, https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster
9
+ Project-URL: Issues, https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster/issues
10
+ Project-URL: Changelog, https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster/releases
11
+ Project-URL: FORESIGHT, https://foresight.tecnico.ulisboa.pt/
12
+ Keywords: meteorology,forecast,ensemble,raster,netcdf,grib,ERA5,ERA5-Land
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ License-File: NOTICE
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: scipy
26
+ Requires-Dist: pandas
27
+ Requires-Dist: xarray
28
+ Requires-Dist: netcdf4
29
+ Requires-Dist: h5netcdf
30
+ Requires-Dist: h5py
31
+ Requires-Dist: cfgrib
32
+ Requires-Dist: cdsapi
33
+ Requires-Dist: scikit-learn
34
+ Requires-Dist: matplotlib
35
+ Requires-Dist: seaborn
36
+ Requires-Dist: statsmodels
37
+ Requires-Dist: cartopy
38
+ Requires-Dist: beautifulsoup4
39
+ Requires-Dist: openpyxl
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest; extra == "test"
42
+ Requires-Dist: pytest-cov; extra == "test"
43
+ Provides-Extra: build
44
+ Requires-Dist: build; extra == "build"
45
+ Requires-Dist: twine; extra == "build"
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest; extra == "dev"
48
+ Requires-Dist: pytest-cov; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: twine; extra == "dev"
51
+ Requires-Dist: jupyterlab; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <p align="center">
55
+ <img src="images/foresight.png" alt="FORESIGHT" width="320">
56
+ </p>
57
+
58
+ # Tethys meteoraster
59
+
60
+ <p align="center">
61
+ <a href="https://pypi.org/project/meteoraster/"><img src="https://img.shields.io/pypi/v/meteoraster.svg" alt="PyPI version"></a>
62
+ <a href="https://pypi.org/project/meteoraster/"><img src="https://img.shields.io/pypi/pyversions/meteoraster.svg" alt="Supported Python versions"></a>
63
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License: Apache-2.0"></a>
64
+ </p>
65
+
66
+ **`meteoraster`** is a Python library, developed by [FORESIGHT — Forecasting and
67
+ Optimization for Resilient Environmental Systems through Investigation with
68
+ Groundbreaking Hydrological Tools](https://foresight.tecnico.ulisboa.pt/), for
69
+ **handling distributed ensemble and probabilistic meteorological forecasts**.
70
+
71
+ Its core data structure, `MeteoRaster`, holds a single **5-D** array indexed by
72
+ `[production_datetime, ensemble_member, leadtime, latitude, longitude]`, and
73
+ wraps it with everything you need to turn raw reanalysis/forecast files into
74
+ report-ready time series, maps and aggregated statistics:
75
+
76
+ - read ERA5-Land GRIB (monthly means and hourly, with de-accumulation) into a common model,
77
+ - crop, join, trim, resample and check the completeness of the data,
78
+ - extract point time series or **catchment/zone aggregates from KML polygons**
79
+ (area-weighted, missing-data aware), as means or as exact weighted quantiles,
80
+ - persist to compressed NetCDF (`h5netcdf`) and reload losslessly — with a read
81
+ path that survives an install on a different xarray,
82
+ - **stream a file too large to hold in memory**, one production at a time,
83
+ - and plot means, seasonal cycles, coordinates and availability with `cartopy`.
84
+
85
+ Only `numpy`, `scipy`, `pandas`, `xarray`, `h5netcdf` and `h5py` are needed to read,
86
+ write, subset and aggregate. GDAL, matplotlib, seaborn and cartopy are imported the
87
+ moment you use them, so `import meteoraster` works in a slim container that has
88
+ none of them.
89
+
90
+ ---
91
+
92
+ ## Features
93
+
94
+ | Category | What you get |
95
+ |---|---|
96
+ | **Data model** | `MeteoRaster` — 5-D `[production_datetime, ensemble_member, leadtime, lat, lon]`; 2-D lat/lon grids; automatic Greenwich (0–360 → −180–180) fix and latitude flip |
97
+ | **Format readers** | `meteoraster.utils` — `readERA5Land_monthly`, `read_ERA5Land_hourly` (GRIB via `cfgrib`) |
98
+ | **Subsetting / merging** | `get_cropped`, `join`, `trim`, `adjust_leadtimes` |
99
+ | **Resampling** | `resample_timestep` (pandas rules) and the standalone `Resample` class |
100
+ | **Extraction** | `get_values_from_latlon`, `get_values_from_KML`, `get_quantiles_from_KML`, `get_values_from_latlon_by_event` |
101
+ | **Spatial aggregation** | `meteoraster.zonal` — sparse coverage by exact fractional overlap, area-weighted means and quantiles, missing-data aware per zone |
102
+ | **Quality / diagnostics** | `is_complete`, `get_complete_index`, `get_missing`, `get_completeness` |
103
+ | **I/O** | `save` / `load` (compressed NetCDF via `h5netcdf`), `to_xarray`, and `meteoraster.io` for header-only and streaming reads |
104
+ | **Visualisation** | `plot_mean`, `plot_mean_projected`, `plot_seasonal`, `plot_coordinates`, `plot_availability`, `add_shapefile`, `add_shape` |
105
+
106
+ ---
107
+
108
+ ## Installation
109
+
110
+ The **core** — reading, writing, subsetting, streaming and aggregating — needs only
111
+ `numpy`, `scipy`, `pandas`, `xarray`, `h5netcdf` and `h5py`, all pip-installable:
112
+
113
+ ```bat
114
+ pip install meteoraster
115
+ ```
116
+
117
+ The **geospatial and plotting extras** (**GDAL/`osgeo`**, `cartopy`, `cfgrib`) are
118
+ most reliably installed with **conda**, and are imported only when used. Zonal
119
+ aggregation from KML or PostGIS geometry needs GDAL; the map plots need cartopy.
120
+ `import meteoraster` works without either — a missing one raises at the call site
121
+ with the command to install it.
122
+
123
+ Two paths follow: install the **released package from PyPI** on top of a conda
124
+ environment (use the package), or install **from source** (develop / run the tests).
125
+
126
+ ### A · Install from PyPI
127
+
128
+ Create a conda environment that provides the geospatial libraries, then install
129
+ [`meteoraster`](https://pypi.org/project/meteoraster/) into it:
130
+
131
+ ```bat
132
+ conda env create -f environment.yml
133
+ conda activate tethys_rasters
134
+ pip install meteoraster
135
+ ```
136
+
137
+ Upgrading later needs no URL — `pip install --upgrade meteoraster` always picks up
138
+ the newest release.
139
+
140
+ > The package declares its pure-Python dependencies, but **GDAL is intentionally
141
+ > not** a pip dependency (it is a system/conda package). The conda environment
142
+ > above provides it. If you do not need zonal aggregation from vector geometry,
143
+ > `pip install meteoraster` into any environment is enough — including a slim
144
+ > container image, where `import meteoraster`, `save`, `load` and the streaming
145
+ > readers all work with no GDAL, cartopy, matplotlib or seaborn present.
146
+
147
+ Every release is also attached to the
148
+ [GitHub releases page](https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster/releases),
149
+ which is useful to pin an exact build without going through PyPI:
150
+
151
+ ```bat
152
+ pip install https://github.com/FORESIGHT-ULisboa/Tethys-meteoraster/releases/download/v3.0/meteoraster-3.0-py3-none-any.whl
153
+ ```
154
+
155
+ ### B · Install from source (development)
156
+
157
+ #### 1 · Create the conda environment
158
+
159
+ ```bat
160
+ conda env create -f environment.yml
161
+ conda activate tethys_rasters
162
+ ```
163
+
164
+ #### 2 · Install the package (editable) with the dev extras
165
+
166
+ From the repository root:
167
+
168
+ ```bat
169
+ pip install -e ".[dev]"
170
+ ```
171
+
172
+ That adds `pytest`, `pytest-cov`, `build`, `twine` and `jupyterlab` on top of the
173
+ runtime dependencies.
174
+
175
+ ---
176
+
177
+ ## Quick start
178
+
179
+ > **Worked examples:** [`notebooks/`](notebooks/) holds seven short Jupyter notebooks —
180
+ > the 5-D data model, I/O and xarray, cropping, plotting, point time series, catchment
181
+ > aggregation from KML, and resampling. They run on the real files in
182
+ > [tests/resources](tests/resources) and are committed **with their outputs**, so every
183
+ > table and figure is readable on GitHub without running anything.
184
+
185
+ ```python
186
+ import numpy as np
187
+ import pandas as pd
188
+ from meteoraster import MeteoRaster
189
+
190
+ # data axes: [production_datetime, ensemble_member, leadtime, lat, lon]
191
+ data = np.random.rand(2, 3, 2, 4, 5)
192
+
193
+ mr = MeteoRaster(
194
+ data=data,
195
+ latitudes=np.linspace(40, 30, 4), # 1-D is auto-meshed to 2-D
196
+ longitudes=np.linspace(-10, 10, 5),
197
+ production_datetime=pd.to_datetime(["2023-01-01", "2023-01-02"]).values,
198
+ leadtimes=np.array([pd.Timedelta(days=1), pd.Timedelta(days=2)]),
199
+ units="mm",
200
+ variable="precip",
201
+ )
202
+ ```
203
+
204
+ ### Read a real dataset
205
+
206
+ The ERA5-Land readers live in `meteoraster.utils` and return a ready-to-use
207
+ `MeteoRaster`:
208
+
209
+ ```python
210
+ from meteoraster.utils import readERA5Land_monthly, read_ERA5Land_hourly
211
+
212
+ # monthly means: tp comes back as mm/month, t2m as degC
213
+ era5 = readERA5Land_monthly("era5land_monthly.grib", "tp")
214
+
215
+ # hourly: accumulations are de-accumulated to per-hour increments (mm/hr).
216
+ # A CDS request omits the last hour of its final day - it is delivered with the
217
+ # next period, so pass that file to fill it in.
218
+ hourly = read_ERA5Land_hourly("era5land_2025_11.grib", "tp",
219
+ file_next_year="era5land_2025_12.grib")
220
+ ```
221
+
222
+ These need `cfgrib` and the ecCodes binary. On Windows ecCodes lives in
223
+ `<env>/Library/bin` and is only found through `PATH`, so **activate** the conda
224
+ environment rather than calling its `python.exe` directly — otherwise `import cfgrib`
225
+ raises `RuntimeError: Cannot find the ecCodes library`.
226
+
227
+ Anything already converted by Tethys needs no reader — `MeteoRaster.load` reads it
228
+ directly.
229
+
230
+ ### Extract a point time series
231
+
232
+ ```python
233
+ # DataFrame: index = production_datetime, columns = MultiIndex(leadtime, ensemble_member)
234
+ ts = mr.get_values_from_latlon(lat=35.0, lon=0.0)
235
+ ```
236
+
237
+ ### Aggregate over KML zones (e.g. catchments)
238
+
239
+ ```python
240
+ # agg: DataFrame indexed by production date, columns MultiIndex(zone, leadtime, ensemble_member)
241
+ # centroids: DataFrame of zone centroids (x, y)
242
+ agg, centroids = mr.get_values_from_KML("zones.kml", nameField="zone_id")
243
+ ```
244
+
245
+ Spatial **quantiles** over the same zones, area-weighted so a cell half inside a
246
+ zone carries half the probability mass:
247
+
248
+ ```python
249
+ agg, centroids = mr.get_quantiles_from_KML("zones.kml", nameField="zone_id",
250
+ quantiles=[0.1, 0.5, 0.9])
251
+
252
+ # Pool an hourly forecast into one distribution per day. `pool` names the axis
253
+ # explicitly because "time" is the leadtime axis for a forecast and the production
254
+ # axis for an observation raster - the event time is production + leadtime.
255
+ agg, _ = mr.get_quantiles_from_KML("zones.kml", nameField="zone_id",
256
+ pool=("space", "leadtime"), resampling="1D")
257
+ ```
258
+
259
+ The coverage is reusable, and the lower-level API needs no `MeteoRaster` at all —
260
+ useful for caching it, or for feeding geometry straight out of PostGIS:
261
+
262
+ ```python
263
+ from meteoraster import zonal
264
+
265
+ coverage = zonal.coverage_from_kml("zones.kml", mr.latitudes, mr.longitudes,
266
+ name_field="zone_id")
267
+ means = zonal.apply_coverage(coverage, mr.data) # [..., y, x] -> [..., zone]
268
+
269
+ coverage.save("coverage.npz") # pickle-free
270
+ coverage = zonal.Coverage.load("coverage.npz")
271
+ assert coverage.matches(mr.latitudes, mr.longitudes) # fingerprints the coordinates
272
+
273
+ # WKB bytes or WKT work too, so PostGIS geometry needs no intermediate file
274
+ coverage = zonal.coverage_from_geometries(mr.latitudes, mr.longitudes,
275
+ [feature.geom.wkb], ["basin"])
276
+ ```
277
+
278
+ ### Crop, save and reload
279
+
280
+ ```python
281
+ sub = mr.get_cropped(from_lat=32, to_lat=38, from_lon=-2, to_lon=7,
282
+ from_prod_date=pd.Timestamp("2023-01-02"))
283
+
284
+ sub.save("forecast.nc") # compressed NetCDF (h5netcdf)
285
+ reloaded = MeteoRaster.load("forecast.nc")
286
+ ```
287
+
288
+ ### Read a file that does not fit in memory
289
+
290
+ `load()` holds the whole cube. For a large archive, read the metadata first and
291
+ then stream one production at a time — peak memory is one block whatever the file
292
+ size (measured 126 MB against 467 MB for `load()` on a 352 MB cube):
293
+
294
+ ```python
295
+ from meteoraster import io as mrio, zonal
296
+
297
+ header = mrio.read_header("archive.nct") # coordinates, shape, units; no values
298
+ print(header.shape, header.n_leadtimes, header.complete)
299
+
300
+ coverage = zonal.coverage_from_kml("zones.kml", header.latitudes, header.longitudes,
301
+ name_field="zone_id")
302
+
303
+ for production, block in mrio.iter_productions("archive.nct", reduce_members="mean"):
304
+ series = zonal.apply_coverage(coverage, block) # [leadtime, zone]
305
+ ```
306
+
307
+ `load()` also takes a subset directly, and casting on the way in genuinely halves
308
+ peak memory for a float64 file rather than casting after it is already resident:
309
+
310
+ ```python
311
+ mr = MeteoRaster.load("archive.nct", leadtimes=slice(0, 6), dtype="float32")
312
+ ```
313
+
314
+ Reads try `h5netcdf`, then `netcdf4`, then `scipy`, and pin the decoding so a file
315
+ written by another install returns the same dtypes. If HDF5 locking fails on a
316
+ network share or a bind mount, set `HDF5_USE_FILE_LOCKING=FALSE` in the
317
+ environment; the library will not set it for you.
318
+
319
+ ### Resample in time
320
+
321
+ ```python
322
+ import numpy as np
323
+
324
+ mr.resample_timestep("MS", fun=np.nanmean) # to month start, ensemble-wise
325
+
326
+ # Or resample a production×leadtime DataFrame with the standalone helper:
327
+ from meteoraster import Resample
328
+ resampled = Resample.resample(df, timestep_frequency="1D", resamplingType="sum")
329
+ ```
330
+
331
+ ### Plot
332
+
333
+ ```python
334
+ ax, cbar = mr.plot_mean(central_longitude=0, coastline=True, borders=True,
335
+ colorbar=True, cmap="viridis")
336
+ mr.add_shapefile(ax, "basin.shp")
337
+ mr.plot_seasonal(lat=35.0, lon=0.0)
338
+ mr.plot_availability()
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Project structure
344
+
345
+ ```
346
+ Tethys-meteoraster/
347
+ ├── meteoraster/
348
+ │ ├── __init__.py # exports MeteoRaster, Resample
349
+ │ ├── meteoraster.py # MeteoRaster core class
350
+ │ ├── io.py # portable NetCDF reads: header-only and streaming
351
+ │ ├── zonal.py # sparse coverage, area-weighted means and quantiles
352
+ │ ├── resample.py # Resample (time-step resampling of DataFrames)
353
+ │ ├── _lazy.py # on-demand GDAL / matplotlib / seaborn / cartopy
354
+ │ └── utils.py # ERA5-Land GRIB readers
355
+ ├── notebooks/ # seven worked examples, executed (see notebooks/README.md)
356
+ ├── tests/ # pytest suite + fixtures (conftest.py)
357
+ │ └── resources/ # real GRIB/NetCDF inputs (see PROVENANCE.md)
358
+ ├── tools/make_fixtures.py # regenerates the subsetted test fixtures
359
+ ├── images/foresight.png # README header
360
+ ├── environment.yml # conda environment (tethys_rasters)
361
+ ├── pyproject.toml # packaging + pytest configuration
362
+ ├── AGENTS.md # canonical guidance for humans & AI agents
363
+ ├── CLAUDE.md # → points to AGENTS.md
364
+ ├── .github/copilot-instructions.md # → points to AGENTS.md
365
+ ├── .github/workflows/release.yml # build + publish to PyPI on tag push
366
+ ├── .github/scripts/check_version.py # release guard: version declared consistently
367
+ ├── LICENSE # Apache 2.0
368
+ ├── NOTICE # attribution required by Apache 2.0
369
+ └── README.md
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Running the tests
375
+
376
+ ```bat
377
+ conda activate tethys_rasters
378
+ pip install -e ".[dev]"
379
+ pytest -ra
380
+ ```
381
+
382
+ The suite covers construction, cropping, point/KML extraction, NetCDF round-trips,
383
+ completeness checks, resampling and the deprecated-alias warnings. Since v3.0 it also
384
+ runs against **real inputs** in `tests/resources` — ERA5-Land GRIB for the readers,
385
+ converted Tethys forecasts (ECMWF SEAS5, IPMA, NOAA GFS) for the monthly adaptation and
386
+ the resampling units contract, and a real 26-catchment KML layer for zonal aggregation.
387
+ Those files are subsets; see
388
+ [tests/resources/PROVENANCE.md](tests/resources/PROVENANCE.md) and
389
+ `tools/make_fixtures.py`.
390
+
391
+ Tests skip — by name, never error — when a resource is missing or when an optional
392
+ dependency is absent, so a partial clone and a slim install both stay green. **Activate**
393
+ the conda environment rather than calling its `python.exe` directly, or the GRIB tests
394
+ skip on a `PATH` that hides ecCodes.
395
+
396
+ ---
397
+
398
+ ## Building a distribution (wheel + sdist)
399
+
400
+ The package builds with the standard [PEP 517](https://peps.python.org/pep-0517/)
401
+ toolchain:
402
+
403
+ ```bat
404
+ conda activate tethys_rasters
405
+ pip install -e ".[build]"
406
+ python -m build
407
+ ```
408
+
409
+ This produces both artifacts in `dist/`:
410
+
411
+ ```
412
+ dist/
413
+ ├── meteoraster-3.0-py3-none-any.whl
414
+ └── meteoraster-3.0.tar.gz
415
+ ```
416
+
417
+ Install the wheel anywhere (provided GDAL is available):
418
+
419
+ ```bat
420
+ pip install dist/meteoraster-3.0-py3-none-any.whl
421
+ ```
422
+
423
+ Notes:
424
+ - The version lives in [pyproject.toml](pyproject.toml) (`project.version`) and is
425
+ mirrored in `MeteoRaster.VERSION` — bump both together.
426
+ [.github/scripts/check_version.py](.github/scripts/check_version.py) enforces
427
+ that at release time.
428
+ - Building by hand is only needed to inspect an artifact locally. Actual releases
429
+ are produced by CI — see [Releasing](#releasing) below.
430
+
431
+ ---
432
+
433
+ ## Releasing
434
+
435
+ Releases are automated by
436
+ [.github/workflows/release.yml](.github/workflows/release.yml). To cut version
437
+ `X.Y`:
438
+
439
+ 1. Bump the version in **both** [pyproject.toml](pyproject.toml) and
440
+ `MeteoRaster.VERSION`, then commit.
441
+ 2. Tag and push:
442
+
443
+ ```bat
444
+ git tag vX.Y
445
+ git push origin vX.Y
446
+ ```
447
+
448
+ The workflow then verifies the two version declarations agree with the tag, builds
449
+ the wheel and sdist, runs `twine check --strict`, publishes to
450
+ [PyPI](https://pypi.org/project/meteoraster/), and attaches both artifacts to the
451
+ GitHub release.
452
+
453
+ To rehearse without touching PyPI, run the workflow manually from the **Actions**
454
+ tab and leave the target as `testpypi`; it builds and uploads to
455
+ [TestPyPI](https://test.pypi.org/project/meteoraster/) with no tag required.
456
+
457
+ > **PyPI uploads are immutable** — a version number can never be reused, even
458
+ > after deletion. Get the version right before pushing the tag.
459
+
460
+ ### One-time setup (maintainers)
461
+
462
+ Publishing is authenticated with
463
+ [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC):
464
+ GitHub mints a short-lived credential at publish time that PyPI accepts only for
465
+ this repository, this workflow file and this environment. **No API token is
466
+ created, stored or pasted anywhere.** Every value below is a public fact about
467
+ the repository — none of it is a secret, which is precisely the point.
468
+
469
+ **On PyPI** — once per index (repeat on [TestPyPI](https://test.pypi.org/) if you
470
+ want the rehearsal path):
471
+
472
+ 1. *Your account → Publishing → Add a pending publisher* → GitHub, and fill in:
473
+
474
+ | Field | Value |
475
+ |---|---|
476
+ | PyPI project name | `meteoraster` |
477
+ | Owner | `FORESIGHT-ULisboa` |
478
+ | Repository | `Tethys-meteoraster` |
479
+ | Workflow name | `release.yml` |
480
+ | Environment | `pypi` (on TestPyPI: `testpypi`) |
481
+
482
+ The environment name must match the `environment:` key of the corresponding
483
+ job in [release.yml](.github/workflows/release.yml), or PyPI rejects the
484
+ exchange. A *pending* publisher is what you register before the project exists
485
+ on an index; it turns into an ordinary one on the first successful upload.
486
+
487
+ **On GitHub** — once:
488
+
489
+ 2. *Settings → Environments* → create `pypi` (and `testpypi`).
490
+ 3. Add a **required reviewer** to `pypi`. Every publish then pauses for an
491
+ explicit approval, so a tag push alone cannot ship a release.
492
+
493
+ Things worth keeping that way:
494
+
495
+ - **Do not add a PyPI API token to repository secrets.** Trusted Publishing
496
+ exists to remove that long-lived credential; a token in secrets is usable by
497
+ any workflow in the repository and stays valid until someone revokes it.
498
+ Nothing here needs one.
499
+ - The publisher trusts the workflow **by filename**, so anyone able to change
500
+ [release.yml](.github/workflows/release.yml) or push a `v*` tag can trigger a
501
+ publish. Protect `main` and restrict who may push tags; the environment
502
+ reviewer from step 3 is the backstop.
503
+ - Only the two publish jobs request `id-token: write`; the build job is
504
+ `contents: read` and the workflow default is `permissions: {}`. Keep that split
505
+ when editing it — the job that produces the artifacts never holds the identity
506
+ that can upload them.
507
+
508
+ ---
509
+
510
+ ## API reference
511
+
512
+ ### `MeteoRaster`
513
+
514
+ **Construction & conversion**
515
+
516
+ | Member | Description |
517
+ |---|---|
518
+ | `MeteoRaster(data, latitudes, longitudes, production_datetime, leadtimes, units='unknown', variable='unknown', …)` | Build from a 5-D array (or a dict with the same keys). 1-D lat/lon are auto-meshed to 2-D; longitudes are wrapped to [−180, 180] and latitudes flipped if needed. |
519
+ | `copy()` | Deep copy. |
520
+ | `to_xarray()` | Convert to a labelled `xarray.DataArray`. |
521
+
522
+ **Subsetting, merging & resampling**
523
+
524
+ | Member | Description |
525
+ |---|---|
526
+ | `get_cropped(from_prod_date, to_prod_date, from_lat, to_lat, from_lon, to_lon, from_leadtime, to_leadtime)` | Crop in time and space; returns a new `MeteoRaster`. |
527
+ | `join(meteoRaster, strickt=False, trim=False)` | Concatenate another raster along production dates (aligns ensembles/leadtimes). |
528
+ | `trim()` | Drop leading/trailing all-NaN production dates. |
529
+ | `adjust_leadtimes(period='months')` | Align leadtimes to relative periods (experimental). |
530
+ | `resample_timestep(rule, fun=np.mean)` | Resample production dates with pandas rules (experimental). |
531
+
532
+ **Extraction & aggregation**
533
+
534
+ | Member | Description |
535
+ |---|---|
536
+ | `get_values_from_latlon(lat, lon)` | Time series from the nearest pixel as a `DataFrame`. |
537
+ | `get_values_from_KML(kml, nameField=None, …)` | Area-weighted aggregate over KML polygons → `(agg, centroids)`. |
538
+ | `get_quantiles_from_KML(kml, nameField=None, quantiles=…, pool=('space',), resampling=None, …)` | Area-weighted spatial quantiles over KML zones → `(agg, centroids)`. `pool` names the axes folded into the sample; `resampling` is the block size within a pooled time axis. |
539
+ | `get_values_from_latlon_by_event(production_date_dataframe)` | *(static)* Reindex a production-date frame by event date. |
540
+
541
+ Since **v3.0** `get_values_from_KML` runs on `meteoraster.zonal` (below). Its numbers
542
+ changed for any zone touching the edge of the grid: the legacy builder gave the
543
+ outermost row and column no weight and took its validity threshold from the first
544
+ zone. `ensemble_member` labels are now 0-based, matching the other extractors.
545
+
546
+ **`meteoraster.zonal`** — needs no `MeteoRaster`; takes plain coordinate arrays.
547
+
548
+ | Member | Description |
549
+ |---|---|
550
+ | `coverage_from_kml(kml, latitudes, longitudes, name_field=None, …)` | Build a `Coverage` from KML Placemark polygons. Namespace-agnostic, and it does not touch the input file. |
551
+ | `coverage_from_geometries(latitudes, longitudes, geometries, names=None, …)` | Same from `ogr.Geometry`, WKB bytes or WKT — so PostGIS geometry feeds straight in. |
552
+ | `apply_coverage(coverage, values, min_coverage=0.8)` | Area-weighted mean per zone of any array whose last two axes are (y, x). Renormalises for missing data and applies the threshold per zone and per slice. |
553
+ | `quantiles_from_coverage(coverage, values, quantiles, pool_axes=(), …)` | Area-weighted spatial quantiles per zone. |
554
+ | `weighted_quantiles(values, weights, quantiles, method='hazen')` | The quantile primitive. Reduces exactly to `np.quantile` for equal weights. |
555
+ | `Coverage.save/load`, `Coverage.matches`, `grid_fingerprint` | Persist a coverage to a pickle-free `.npz` and check it against a grid; the fingerprint hashes the coordinates, so a shifted grid of the same shape is a miss. |
556
+ | `cell_corners`, `cell_areas`, `cell_bounds` | The grid geometry, extrapolated half a cell at the boundary so no cell is skipped. |
557
+
558
+ **`meteoraster.io`** — the portable read path.
559
+
560
+ | Member | Description |
561
+ |---|---|
562
+ | `read_header(file)` | `Header` with coordinates, shape, dtype, units and the `complete` flag; reads no values. |
563
+ | `iter_productions(file, productions=None, leadtimes=None, reduce_members=None, dtype=None)` | Yield `(production_datetime, block)`; peak memory is one block. |
564
+ | `read_cube(file, …)` | Fill a whole or partial cube production by production. |
565
+ | `open_raster(file, engine=None, **kwargs)` | Context manager over `xr.open_dataset` with engine fallback and pinned decoding. |
566
+ | `read_completeness(file)` | The `complete` flag alone; `None` when the file has none. |
567
+ | `available_engines()` | Which of `h5netcdf`, `netcdf4`, `scipy` this install can use. |
568
+
569
+ **Quality & diagnostics**
570
+
571
+ | Member | Description |
572
+ |---|---|
573
+ | `is_complete(full_ensemble=True, space_completeness=False)` | Whether every production×leadtime slice has finite data. |
574
+ | `get_complete_index(full_ensemble=True, space_completeness=False)` | Boolean `DataFrame` (production × leadtime) of completeness. |
575
+ | `get_missing()` | Fraction of missing pixels per leadtime/ensemble member. |
576
+ | `get_completeness(file)` | *(classmethod)* Read the `complete` flag from a saved file. |
577
+
578
+ **I/O**
579
+
580
+ | Member | Description |
581
+ |---|---|
582
+ | `save(file, complevel=1, complete=None, tmp_dir=None, mode=0o644, engine=None)` | Write compressed NetCDF via `h5netcdf`. Staged in the destination directory and swapped in with `os.replace`, so nothing goes through the system temp dir and the swap is atomic. |
583
+ | `load(file, verbose=None, productions=None, leadtimes=None, dtype=None, engine=None)` | *(classmethod)* Reload a NetCDF written by `save`, optionally only part of it. |
584
+ | `read_header(file)` / `iter_productions(file, …)` | *(classmethods)* Metadata-only and streaming reads; see `meteoraster.io`. |
585
+
586
+ **Visualisation**
587
+
588
+ | Member | Description |
589
+ |---|---|
590
+ | `create_plot(central_longitude, …)` | Create a `cartopy` axes. |
591
+ | `plot_mean(ax=None, coastline=False, borders=False, colorbar=True, cmap='viridis', …)` | Map of the ensemble/temporal mean → `(ax, cbar)`. |
592
+ | `plot_mean_projected`, `plot_coordinates`, `plot_seasonal`, `plot_availability` | Projected mean, grid coordinates, seasonal cycle, data availability. |
593
+ | `add_shapefile(ax, shapefile_path, …)` / `add_shape(ax, path, …)` | Overlay shapefile geometries/boundaries. |
594
+
595
+ > Deprecated camelCase aliases (`getCropped`, `getValuesFromKML`,
596
+ > `getDataFromLatLon`, `getQuantilesFromKML`, `resampleTimeStep`) still work but
597
+ > emit a `DeprecationWarning`; use the snake_case names above.
598
+
599
+ ### `Resample`
600
+
601
+ | Member | Description |
602
+ |---|---|
603
+ | `Resample.resample(data, timestep_frequency, resamplingType, date_from=None, date_to=None, print_func=None)` | *(classmethod)* Resample a production×leadtime `DataFrame`. `resamplingType` ∈ {`'sum'`, `'mean'`, `'linear'`, `'max'`}. |
604
+
605
+ ---
606
+
607
+ ## License
608
+
609
+ Licensed under the **Apache License, Version 2.0** — see [LICENSE](LICENSE) and
610
+ [NOTICE](NOTICE).