diff --git a/.github/workflows/tests-conda.yml b/.github/workflows/tests-conda.yml index db323cff..b263c1cc 100644 --- a/.github/workflows/tests-conda.yml +++ b/.github/workflows/tests-conda.yml @@ -38,59 +38,69 @@ jobs: with: fetch-depth: 2 - - name: CACHING - Anaconda packages - uses: actions/cache@v3 - id: cache-pkg - with: - path: ~/conda_pkgs_dir - key: - conda-pkg-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ - env.CACHE_NUMBER }}-${{ hashFiles('environment-test.yml') }} - env: - # Increase this value if `environment-test.yml` has not changed, - # but you still want to reset the cache. - CACHE_NUMBER: 0 + #- name: CACHING - Anaconda packages + # uses: actions/cache@v3 + # id: cache-pkg + # with: + # path: ~/conda_pkgs_dir + # key: + # conda-pkg-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ + # env.CACHE_NUMBER }}-${{ hashFiles('ci/environment.yml') }} + # env: + # # Increase this value if `environment.yml` has not changed, + # # but you still want to reset the cache. + # CACHE_NUMBER: 0 - - name: INSTALL - Anaconda setup (Mambaforge) - uses: conda-incubator/setup-miniconda@v2 + - name: INSTALL - Conda/Mamba setup (Miniforge) + uses: conda-incubator/setup-miniconda@v3 with: + auto-update-conda: true python-version: ${{ matrix.python-version }} - miniforge-variant: Mambaforge miniforge-version: latest mamba-version: "*" use-mamba: true - channels: conda-forge,defaults + channels: conda-forge + conda-remove-defaults: "true" channel-priority: true activate-environment: herbie-test + environment-file: ci/environment.yml auto-activate-base: false - use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly! + #use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly! + + #- name: CACHING - Anaconda environment + # uses: actions/cache@v3 + # id: cache-env + # with: + # path: ${{ env.CONDA }}/envs + # key: + # conda-env-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ + # env.CACHE_NUMBER }}-${{ hashFiles('ci/environment.yml') }} + # env: + # # Increase this value if `ci/environment.yml` has not changed, + # # but you still want to reset the cache. + # CACHE_NUMBER: 0 + + - name: DEBUG - mamba info + run: | + mamba --version + mamba info + + - name: DEBUG - mamba list + run: mamba list + + - name: DEBUG - mamba configuration + run: mamba config --show - - name: CACHING - Anaconda environment - uses: actions/cache@v3 - id: cache-env - with: - path: ${{ env.CONDA }}/envs - key: - conda-env-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ - env.CACHE_NUMBER }}-${{ hashFiles('environment-test.yml') }} - env: - # Increase this value if `environment-test.yml` has not changed, - # but you still want to reset the cache. - CACHE_NUMBER: 0 - - - name: DEBUG - Anaconda info - run: conda info - - name: DEBUG - Anaconda configuration - run: conda config --show - name: DEBUG - Environment variables run: printenv | sort + - name: DEBUG - Program paths run: | command -v conda command -v mamba - - name: INSTALL - Update Anaconda environment - run: mamba env update --name herbie-test --file environment-test.yml + - name: INSTALL - Update Mamba environment + run: mamba env update --name herbie-test --file ci/environment.yml if: steps.cache-env.outputs.cache-hit != 'true' - name: INSTALL - Project diff --git a/check_pygrib_vs_herbie_crs_extraction.ipynb b/check_pygrib_vs_herbie_crs_extraction.ipynb new file mode 100644 index 00000000..625724a0 --- /dev/null +++ b/check_pygrib_vs_herbie_crs_extraction.ipynb @@ -0,0 +1,5809 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check CRS extraction\n", + "\n", + "I was having issues with pygrib running into segmentation faults in my GitHub Actions after it was updated to support Numpy v2+. I was only using pygrib to extract coordinate reference system (CRS) data, and I would like to remove it as a dependency.\n", + "\n", + "Here I am comparing the CRS proj parameters extracted by pygrib and what is extracted by Herbie (from the keys read by cfgrib). \n", + "\n", + "**Since pygrib doesn't work in GitHub actions for me for whatever reason, I should run this notebook before any release to manually test the values between pygrib and Herbie agree.**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from herbie import Herbie\n", + "import pygrib\n", + "from pyproj import CRS\n", + "from herbie.crs import get_cf_crs" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Model: HRRR\n", + " pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'}\n", + " equal= True\n", + "\n", + "Model: HRRRAK\n", + " pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 90.0, 'lat_ts': 60.0, 'lon_0': 225.0, 'proj': 'stere'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 90, 'lat_ts': 60.0, 'lon_0': 225.0, 'proj': 'stere'}\n", + " equal= True\n", + "\n", + "Model: GFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "👨🏻‍🏭 Created directory: [/home/blaylock/data/graphcast/20250101]\n", + "\n", + "Model: GRAPHCAST\n", + " pygrib {'a': 4326.0, 'b': 4326.0, 'proj': 'longlat'}\n", + " Herbie {'a': 4326.0, 'b': 4326.0, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: CFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: IFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: AIFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: GEFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: GEFS\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: HAFSA\n", + " pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'}\n", + " equal= True\n", + "\n", + "Model: HREF\n", + " pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " equal= True\n", + "\n", + "Model: NAM\n", + " pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'}\n", + " Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'}\n", + " equal= True\n", + "\n", + "Model: URMA\n", + " pygrib {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " Herbie {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " equal= True\n", + "\n", + "Model: RTMA\n", + " pygrib {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " Herbie {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'}\n", + " equal= True\n" + ] + } + ], + "source": [ + "for model, search in [\n", + " (dict(model=\"hrrr\"), \":TMP:2 m above\"),\n", + " (dict(model=\"hrrrak\"), \":TMP:2 m above\"),\n", + " (dict(model=\"gfs\"), \":TMP:2 m above\"),\n", + " (dict(model=\"graphcast\"), \":TMP:2 m above\"),\n", + " (dict(model=\"cfs\", member=1, product=\"6_hourly\", kind=\"flxf\"), \":TMP:2 m above\"),\n", + " (dict(model=\"ifs\"), \":2t:\"),\n", + " (dict(model=\"aifs\"), \":2t:\"),\n", + " (dict(model=\"gefs\", member=1), \":TMP:2 m above\"),\n", + " (dict(model=\"gefs\", product=\"wave\", member=1), \":WIND:surf\"),\n", + " (dict(model=\"hafsa\", product=\"storm.atm\", storm=\"07s\"), \":TMP:2 m above\"),\n", + " (dict(model=\"href\", products=\"mean\", domain=\"conus\", fxx=1), \":TMP:2 m above\"),\n", + " (dict(model=\"nam\"), \":TMP:2 m above\"),\n", + " (dict(model=\"urma\"), \":TMP:2 m above\"),\n", + " (dict(model=\"rtma\"), \":TMP:2 m above\"),\n", + "]:\n", + " if model[\"model\"] in (\"hafsa\", \"href\"): # only on nomads\n", + " date = \"2025-01-16 06:00\"\n", + " else:\n", + " date = \"2025-01-01\"\n", + "\n", + " ds = Herbie(date, verbose=False, **model).xarray(\n", + " search, remove_grib=False, _use_pygrib_for_crs=False\n", + " )\n", + "\n", + " print()\n", + " print(f\"Model: {model['model'].upper()}\")\n", + " with pygrib.open(str(ds.local_grib)) as grb:\n", + " msg = grb.message(1)\n", + " projparams_dict_pygrib = dict(sorted(msg.projparams.items()))\n", + " print(\" pygrib\", projparams_dict_pygrib)\n", + "\n", + " projparams_dict_herbie = dict(\n", + " sorted(get_cf_crs(ds, _return_projparams=True).items())\n", + " )\n", + " print(\" Herbie\", projparams_dict_herbie)\n", + " print(\" equal=\", projparams_dict_herbie == projparams_dict_pygrib)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check the crs accessor" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Found ┊ model=hrrr ┊ \u001b[3mproduct=sfc\u001b[0m ┊ \u001b[38;2;41;130;13m2025-Jan-01 00:00 UTC\u001b[92m F00\u001b[0m ┊ \u001b[38;2;255;153;0m\u001b[3mGRIB2 @ aws\u001b[0m ┊ \u001b[38;2;255;153;0m\u001b[3mIDX @ aws\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/blaylock/GITHUB/Herbie/herbie/core.py:1117: UserWarning: Will not remove GRIB file because it previously existed.\n", + " warnings.warn(\"Will not remove GRIB file because it previously existed.\")\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-01-16T20:06:01.010322\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "
<cartopy.crs.LambertConformal object at 0x7f42b9af7cd0>
" + ], + "text/plain": [ + "\n", + "Name: unknown\n", + "Axis Info [cartesian]:\n", + "- E[east]: Easting (metre)\n", + "- N[north]: Northing (metre)\n", + "Area of Use:\n", + "- undefined\n", + "Coordinate Operation:\n", + "- name: unknown\n", + "- method: Lambert Conic Conformal (2SP)\n", + "Datum: unknown\n", + "- Ellipsoid: unknown\n", + "- Prime Meridian: Greenwich" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = Herbie(\"2025-1-1\").xarray(\":TMP:2 m ab\")\n", + "ds.herbie.crs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "herbie-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ci/environment.yml b/ci/environment.yml index c882e0ee..ff24f016 100644 --- a/ci/environment.yml +++ b/ci/environment.yml @@ -1,18 +1,30 @@ -name: herbie +# Used to for Herbie tests on Windows +# .github/workflows/tests-conda.yml + +name: herbie-test channels: - conda-forge dependencies: + # Binaries - curl - - cfgrib>=0.9.10.4 - eccodes - - matplotlib>=3.8.2 + - geos + - proj + - python + + # Python requirements + - cartopy + - cfgrib + - matplotlib - metpy - - numpy>=1.26.2 - - pandas>=2.1.4 - - pygrib>=2.1.6 - - pytest - - requests>=2.31.0 + - numpy + - pandas + - pyproj + - requests - scikit-learn - toml - - xarray>=2023.12.0 - - flake8 + - xarray + + # Testing + - pytest + - pytest-cov diff --git a/ci/requirements.txt b/ci/requirements.txt index 3ce36f2a..75d314c4 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -2,7 +2,7 @@ cfgrib>=0.9.15 metpy>=1.3.0 numpy>=1.22.3 pandas>=2.2.3 -pygrib>=2.1.6 +pyproj>=3.7.0 requests>=2.32.3 toml>=0.10.2 xarray>=2025.1.1 diff --git a/environment-test.yml b/environment-test.yml index 1c15cda5..f58140bf 100644 --- a/environment-test.yml +++ b/environment-test.yml @@ -1,10 +1,10 @@ -## Used to test Herbie install from PyPI +## Used to test Herbie GitHub Actions for windows name: herbie-test channels: - conda-forge dependencies: - - python>=3.8 + - python>=3.12 - pip #============== diff --git a/herbie/__init__.py b/herbie/__init__.py index 07277612..053e416e 100644 --- a/herbie/__init__.py +++ b/herbie/__init__.py @@ -205,3 +205,4 @@ def template(self): from herbie.fast import FastHerbie from herbie.latest import HerbieLatest, HerbieWait from herbie.wgrib2 import wgrib2 +from herbie.accessors import HerbieAccessor diff --git a/herbie/accessors.py b/herbie/accessors.py index f8f0f4eb..3ccb972f 100644 --- a/herbie/accessors.py +++ b/herbie/accessors.py @@ -18,7 +18,6 @@ import numpy as np import pandas as pd -import pygrib import xarray as xr from pyproj import CRS @@ -56,6 +55,10 @@ def add_proj_info(ds: xr.Dataset): """Add projection info to a Dataset.""" + raise NotImplementedError("This function `add_proj_info` is not yet implemented.") + + # TODO: remove pyproj dependency + match = re.search(r'"source": "(.*?)"', ds.history) FILE = Path(match.group(1)) diff --git a/herbie/core.py b/herbie/core.py index d8ae9996..c78f7c73 100644 --- a/herbie/core.py +++ b/herbie/core.py @@ -18,17 +18,17 @@ from datetime import datetime, timedelta from io import StringIO from shutil import which -from typing import Union, Optional, Literal +from typing import Literal, Optional, Union import cfgrib import pandas as pd -import pygrib import requests import xarray as xr from pyproj import CRS import herbie.models as model_templates from herbie import Path, config +from herbie.crs import get_cf_crs from herbie.help import _search_help from herbie.misc import ANSI @@ -38,17 +38,6 @@ # from the file ${HOME}/.config/herbie/config.toml # Path is imported from __init__ because it has my custom methods. -try: - # Load custom xarray accessors - import herbie.accessors # noqa: F401 -except Exception: - warnings.warn( - "herbie xarray accessors could not be imported." - "Probably missing a dependency like MetPy." - "If you want to use these functions, try" - "`pip install metpy`" - ) - log = logging.getLogger(__name__) # Location of wgrib2 command, if it exists. Required to make missing idx files. @@ -1067,6 +1056,7 @@ def xarray( searchString=None, backend_kwargs: dict = {}, remove_grib: bool = True, + _use_pygrib_for_crs: bool = False, **download_kwargs, ) -> xr.Dataset: """ @@ -1079,6 +1069,10 @@ def xarray( remove_grib : bool If True, grib file will be removed ONLY IF it didn't exist before we downloaded it. + _use_pygrib_for_crs : bool + If you have pygrib, you can use it to extract the CRS + information instead of using values extracted from cfgrib + by Herbie. """ # TODO: Remove this eventually if searchString is not None: @@ -1128,7 +1122,19 @@ def xarray( backend_kwargs.setdefault("indexpath", "") backend_kwargs.setdefault( "read_keys", - ["parameterName", "parameterUnits", "stepRange", "uvRelativeToGrid"], + [ + "parameterName", + "parameterUnits", + "stepRange", + "uvRelativeToGrid", + "shapeOfTheEarth", + "orientationOfTheGridInDegrees", + "southPoleOnProjectionPlane", + "LaDInDegrees", + "LoVInDegrees", + "Latin1InDegrees", + "Latin2InDegrees", + ], ) backend_kwargs.setdefault("errors", "raise") @@ -1139,20 +1145,20 @@ def xarray( backend_kwargs=backend_kwargs, ) - # Get CF grid projection information with pygrib and pyproj because - # this is something cfgrib doesn't do (https://github.com/ecmwf/cfgrib/issues/251) - # NOTE: Assumes the projection is the same for all variables - # TODO: Issues with pygrib in tests. Segmentation Fault. Is it Numpy 2??? - use_pygrib = False - if use_pygrib: - with pygrib.open(str(local_file)) as grb: - msg = grb.message(1) - cf_params = CRS(msg.projparams).to_cf() + for ds in Hxr: + # Need model attribute before using get_cf_crs + ds.attrs["model"] = str(self.model) - #grb = pygrib.open(str(local_file)) - #msg = grb.message(1) - #cf_params = CRS(msg.projparams).to_cf() - #grb.close() + # Get CF convention coordinate reference system (crs) information. + # NOTE: Assumes the projection is the same for all variables. + if _use_pygrib_for_crs: + # Get CF grid projection information with pygrib and pyproj because + # this is something cfgrib doesn't do (https://github.com/ecmwf/cfgrib/issues/251) + import pygrib + + with pygrib.open(str(local_file)) as grb: + msg = grb.message(1) + cf_params = CRS(msg.projparams).to_cf() # Funny stuff with polar stereographic (https://github.com/pyproj4/pyproj/issues/856) # TODO: Is there a better way to handle this? What about south pole? @@ -1161,7 +1167,7 @@ def xarray( "latitude_of_projection_origin", 90 ) else: - cf_params = {} + cf_params = get_cf_crs(Hxr[0]) # Here I'm looping over each dataset in the list returned by cfgrib for ds in Hxr: diff --git a/herbie/crs.py b/herbie/crs.py new file mode 100644 index 00000000..b02e9d64 --- /dev/null +++ b/herbie/crs.py @@ -0,0 +1,172 @@ +"""CF convention coordinate reference system (CRS). + +Check out the notebook `check_pygrib_vs_herbie_crs_extraction.ipynb` +to test how Herbie and pygrib are extracting the CRS information. +""" + +from typing import Any + +from pyproj import CRS + +import xarray as xr + + +def get_cf_crs( + ds: "xr.Dataset", variable: str | None = None, _return_projparams=False +) -> dict[str, Any]: + """ + Extract the CF coordinate reference system (CRS) from a cfgrib xarray dataset. + + Note: + I originally used pygrib to do this, but it was hard to maintain an + additional grib package dependency. I had issues with pygrib after + it was updated to support Numpy version 2, so thought it would be + best to code this in Herbie. This may be incomplete. + """ + # Assume the first variable in the Dataset has the same grid crs + # as all other variables in the Dataset. + if variable is None: + variable = next(iter(ds.data_vars)) + da = ds[variable] + + # Shape of the Earth reference system + # https://codes.ecmwf.int/grib/format/grib2/ctables/3/2/ + shapeOfTheEarth = da.GRIB_shapeOfTheEarth + if shapeOfTheEarth == 0: + # Earth assumed spherical with radius = 6 367 470.0 m + a = 6_367_470 + b = 6_367_470 + elif shapeOfTheEarth == 1 and ds.attrs["model"] == "graphcast": + # Earth assumed spherical with radius specified (in m) by data producer + # TODO: Why is model='graphcast' using this value? + a = 4326.0 + b = 4326.0 + elif shapeOfTheEarth == 1 and ds.attrs["model"] in ["urma", "rtma"]: + # Earth assumed spherical with radius specified (in m) by data producer + # TODO: Why is urma and rtma using this value? + a = 6371200.0 + b = 6371200.0 + elif shapeOfTheEarth == 6: + # Earth assumed spherical with radius of 6,371,229.0 m + a = 6_371_229 + b = 6_371_229 + + # Grid type definition + # https://codes.ecmwf.int/grib/format/grib2/ctables/3/1/ + if da.GRIB_gridType == "lambert": + projparams = {"proj": "lcc"} + projparams["a"] = a + projparams["b"] = b + projparams["lon_0"] = da.GRIB_LoVInDegrees + projparams["lat_0"] = da.GRIB_LaDInDegrees + projparams["lat_1"] = da.GRIB_Latin1InDegrees + projparams["lat_2"] = da.GRIB_Latin2InDegrees + + elif da.GRIB_gridType == "regular_ll": + projparams = {"proj": "longlat"} + projparams["a"] = a + projparams["b"] = b + + elif da.GRIB_gridType == "regular_gg": + projparams = {"proj": "longlat"} + projparams["a"] = a + projparams["b"] = b + + elif da.GRIB_gridType == "polar_stereographic": + projparams = {"proj": "stere"} + projparams["a"] = a + projparams["b"] = b + projparams["lat_ts"] = da.GRIB_LaDInDegrees + projparams["lat_0"] = 90 + projparams["lon_0"] = da.GRIB_orientationOfTheGridInDegrees + + else: + raise NotImplementedError(f"gridType {da.GRIB_gridType} is not implemented.") + + if _return_projparams: + return projparams + else: + return CRS(projparams).to_cf() + + +""" +Look at how pygrib parses with this... +with pygrib.open(str(ds.local_grib)) as grb: + msg = grb.message(1) + print(msg.projparams) + +Also, look for clues by dumping all keys with: +grib_dump -j > filedump.json + +----------------------------------------- + +Model: HRRR + pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'} + Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'} + equal= True + +Model: HRRRAK + pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 90.0, 'lat_ts': 60.0, 'lon_0': 225.0, 'proj': 'stere'} + Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 90, 'lat_ts': 60.0, 'lon_0': 225.0, 'proj': 'stere'} + equal= True + +Model: GFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: GRAPHCAST + pygrib {'a': 4326.0, 'b': 4326.0, 'proj': 'longlat'} + Herbie {'a': 4326.0, 'b': 4326.0, 'proj': 'longlat'} + equal= True + +Model: CFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: IFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: AIFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: GEFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: GEFS + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: HAFSA + pygrib {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + Herbie {'a': 6371229, 'b': 6371229, 'proj': 'longlat'} + equal= True + +Model: HREF + pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + equal= True + +Model: NAM + pygrib {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'} + Herbie {'a': 6371229, 'b': 6371229, 'lat_0': 38.5, 'lat_1': 38.5, 'lat_2': 38.5, 'lon_0': 262.5, 'proj': 'lcc'} + equal= True + +Model: URMA + pygrib {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + Herbie {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + equal= True + +Model: RTMA + pygrib {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + Herbie {'a': 6371200.0, 'b': 6371200.0, 'lat_0': 25.0, 'lat_1': 25.0, 'lat_2': 25.0, 'lon_0': 265.0, 'proj': 'lcc'} + equal= True +""" diff --git a/herbie/models/cfs.py b/herbie/models/cfs.py index 6fa4ea90..2c0319d9 100644 --- a/herbie/models/cfs.py +++ b/herbie/models/cfs.py @@ -136,10 +136,6 @@ class cfs: def template(self): - warnings.warn( - "Herbie's CFS templates are and subject to major changes. PRs are welcome to improve it." - ) - self.DESCRIPTION = "Climate Forecast System" self.DETAILS = { "NOMADS product description": "https://www.nco.ncep.noaa.gov/pmb/products/cfs/", diff --git a/herbie/models/gdps.py b/herbie/models/gdps.py index a8f8c927..2788fc86 100644 --- a/herbie/models/gdps.py +++ b/herbie/models/gdps.py @@ -2,7 +2,7 @@ ## April 9, 2024 """ -A Herbie template for the GEM Global or Global Deterministic Prediction System (GDPS) +A Herbie template for the GEM Global or Global Deterministic Prediction System (GDPS). Meteorological Service of Canada (MSC) The GDPS is Canada's 15 km deterministic global model diff --git a/herbie/models/hrdps.py b/herbie/models/hrdps.py index e3ec2de6..5c05d5aa 100644 --- a/herbie/models/hrdps.py +++ b/herbie/models/hrdps.py @@ -4,7 +4,7 @@ ## April 9, 2024 """ -A Herbie template for the High Resolution Deterministic Prediction System (HRDPS) +A Herbie template for the High Resolution Deterministic Prediction System (HRDPS). Meteorological Service of Canada (MSC) The HRDPS is Canada's 2.5 km deterministic model diff --git a/herbie/models/href.py b/herbie/models/href.py index 7ab56c52..31d6fa92 100644 --- a/herbie/models/href.py +++ b/herbie/models/href.py @@ -1,7 +1,7 @@ ## Added by Karl Schneider (June 2024) """ -A Herbie template for the The High Resolution Ensemble Forecast (HREF) +A Herbie template for the The High Resolution Ensemble Forecast (HREF). Description ----------- @@ -66,4 +66,4 @@ def template(self): "nomads": f"https://nomads.ncep.noaa.gov/pub/data/nccf/com/href/prod/href.{self.date:%Y%m%d}/ensprod/href.t{self.date:%H}z.{self.domain}.{self.product}.f{self.fxx:02d}.grib2" } self.IDX_SUFFIX = [".grib2.idx", ".idx"] - self.LOCALFILE = f"{self.get_remoteFileName}" \ No newline at end of file + self.LOCALFILE = f"{self.get_remoteFileName}" diff --git a/herbie/models/rdps.py b/herbie/models/rdps.py index 61acf553..132367c6 100644 --- a/herbie/models/rdps.py +++ b/herbie/models/rdps.py @@ -2,7 +2,7 @@ ## April 9, 2024 """ -A Herbie template for the GEM Regional or Regional Deterministic Prediction System (RDPS) +A Herbie template for the GEM Regional or Regional Deterministic Prediction System (RDPS). Meteorological Service of Canada (MSC) The RDPS is Canada's 10 km deterministic regional model diff --git a/pyproject.toml b/pyproject.toml index 3517d18f..6b8bac6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,10 @@ maintainers = [{ name = "Brian K. Blaylock", email = "blaylockbk@gmail.com" }] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", @@ -33,9 +33,9 @@ dependencies = [ "cfgrib>=0.9.15", "numpy>=2.2.1", "pandas>=2.2.3", - "pygrib==2.1.6", + "pyproj>=3.7.0", "requests>=2.23.3", - "toml>=0.10.2", + "toml>=0.10.2", # TODO: Drop in favor of tomllib when Python >=3.11 is required. "xarray>=2025.1.1", ] dynamic = ["version"] @@ -49,6 +49,8 @@ dynamic = ["version"] [project.optional-dependencies] extras = ["cartopy", "metpy", "scikit-learn"] +test = ["pytest", "pytest-cov", "ruff"] +pygrib = ["pygrib"] docs = [ "autodocsumm", "ipython", @@ -65,7 +67,6 @@ docs = [ "sphinx-markdown-tables", "sphinxcontrib-mermaid", ] -test = ["pytest", "pytest-cov", "ruff"] [build-system] requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] diff --git a/requirements.txt b/requirements.txt index 2932dfdb..7cbfa381 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ matplotlib metpy numpy pandas -pygrib +pyproj scikit-learn toml xarray