grib2io

Introduction

grib2io is a Python package that provides an interface to the NCEP GRIB2 C (g2c) library for the purpose of reading and writing WMO GRIdded Binary, Edition 2 (GRIB2) messages. A physical file can contain one or more GRIB2 messages.

GRIB2 file IO is performed directly in Python. The unpacking/packing of GRIB2 integer, coded metadata and data sections is performed by the g2c library functions via the g2clib Cython extension module. The decoding/encoding of GRIB2 metadata is translated into more descriptive, plain language metadata by providing a mapping of the integer coded metadata to the appropriate GRIB2 code tables. The NCEP GRIB2 code tables are a part of the grib2io package.

Index File

As of v2.6.0, grib2io provides the capability to create (and read) an index file for a GRIB2 file. The index file contents are specific to Python and grib2io in that pickle is used to dump the index dictionary to file. Using index files can dramatically improve performance in situations where the same file will be read multiple times. The index file name is the original GRIB2 file name with a hash string appended, followed by the file extension, .grib2ioidx. The hash string is the SHA-1 of the GRIB2 file name and the file size. For example, GRIB2 file,

gfs.t00z.pgrb2.1p00.f024

when opened, grib2io will generate an index file with the following name,

gfs.t00z.pgrb2.1p00.f024_0422a93bfd6d095bd0a942ba5e9fe42e76050123.grib2ioidx

By default, grib2io will generate an index file or use an existing one. The generation and usage of a grib2io index file can be turned off by providing kwargs use_index=False and/or save_index=False in grib2io.open().

Interpolation

As of v2.4.0, spatial interpolation via NCEPLIPS-ip Fortran library is now a part of the grib2io package. Previously, interpolation was handled by a separate component package, grib2io-interp, which is now deprecated. grib2io-interp provided interpolation via a F2PY-generated interface to NCEPLIBS-ip, which has become difficult since the removal of distutils from Python 3.12+.

NCEPLIBS-ip interpolation Fortran subroutines contain the BIND(C) attribute which provides an equivalent C-interface. grib2io now provides a Cython-based interface, iplib, to these Fortran subroutines via their C-interface. If NCEPLIBS-ip was built with OpenMP support, iplib will provide functions for getting and setting the number of OpenMP threads.

Xarray Backend

grib2io provides a Xarray backend engine so that many GRIB2 messages can be represented as N-dimensional DataArray objects and collected along common coordinates as Datasets or DataTrees. The Xarray backend engine API is experimental and is subject to change without backward compatibility.

Tutorials

The following Jupyter Notebooks are available as tutorials:

  1from ._grib2io import (
  2    open,
  3    interpolate,
  4    interpolate_to_stations,
  5    Grib2Message,
  6    _Grib2Message,
  7    Grib2GridDef,
  8    msgs_from_index,
  9    __doc__,
 10)
 11from . import tables, templates, utils
 12
 13try:
 14    from . import __config__
 15
 16    __version__ = __config__.grib2io_version
 17    has_interpolation = __config__.has_interpolation
 18    has_openmp_support = __config__.has_openmp_support
 19    g2c_static = __config__.g2c_static
 20    ip_static = __config__.ip_static
 21    extra_objects = __config__.extra_objects
 22except ImportError:
 23    pass
 24
 25from .g2clib import __version__ as __g2clib_version__
 26from .g2clib import _has_jpeg
 27from .g2clib import _has_png
 28from .g2clib import _has_aec
 29
 30from .tables.originating_centers import _ncep_grib2_table_version
 31
 32__all__ = [
 33    "open",
 34    "show_config",
 35    "interpolate",
 36    "interpolate_to_stations",
 37    "tables",
 38    "templates",
 39    "utils",
 40    "codecs",
 41    "kerchunk",
 42    "Grib2Message",
 43    "_Grib2Message",
 44    "Grib2GridDef",
 45    "msgs_from_index",
 46    "__doc__",
 47]
 48
 49has_jpeg_support = bool(_has_jpeg)
 50has_png_support = bool(_has_png)
 51has_aec_support = bool(_has_aec)
 52
 53ncep_grib2_table_version = _ncep_grib2_table_version
 54g2c_version = __g2clib_version__
 55
 56# ---------------------------------------------------------------------------
 57# Lazy imports for optional-dependency modules
 58# ---------------------------------------------------------------------------
 59# These modules are only imported when explicitly accessed (e.g.,
 60# ``grib2io.codecs``, ``grib2io.kerchunk``).
 61# This keeps the core package lightweight and avoids ImportError when
 62# optional dependencies (numcodecs, kerchunk) are not installed.
 63
 64_LAZY_MODULES = {"codecs", "kerchunk"}
 65
 66# Eagerly import codecs so zarr v3 and numcodecs codec registrations fire
 67# on `import grib2io`, enabling VirtualiZarr and kerchunk to work out of
 68# the box without requiring `import grib2io.codecs` separately.
 69try:
 70    from . import codecs as _codecs_module  # noqa: F401
 71except ImportError:
 72    pass  # zarr/numcodecs not installed; codec registration deferred
 73
 74
 75def __getattr__(name: str):
 76    if name in _LAZY_MODULES:
 77        import importlib
 78
 79        module = importlib.import_module(f".{name}", __name__)
 80        globals()[name] = module
 81        return module
 82    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 83
 84
 85def show_config():
 86    """Print grib2io build configuration information."""
 87    print(f"grib2io version {__version__} Configuration:")
 88    print("")
 89    print(f"NCEPLIBS-g2c library version: {__g2clib_version__}")
 90    print(f"\tStatic library: {g2c_static}")
 91    print(f"\tJPEG compression support: {has_jpeg_support}")
 92    print(f"\tPNG compression support: {has_png_support}")
 93    print(f"\tAEC compression support: {has_aec_support}")
 94    print("")
 95    print(f"NCEPLIPS-ip support: {has_interpolation}")
 96    print(f"\tStatic library: {ip_static}")
 97    print(f"\tOpenMP support: {has_openmp_support}")
 98    print("")
 99    print("Static libs:")
100    for lib in extra_objects:
101        print(f"\t{lib}")
102    print("")
103    print(f"NCEP GRIB2 Table Version: {_ncep_grib2_table_version}")
class open:
118class open:
119    """
120    GRIB2 File Object.
121
122    This class can accommodate a physical file with one or more GRIB2
123    messages or a bytes object containing a GRIB2 messages.
124
125    A physical file can contain one or more GRIB2 messages.  When instantiated,
126    class `grib2io.open`, the file named `filename` is opened for reading (`mode
127    = 'r'`) and is automatically indexed.  The indexing procedure reads some of
128    the GRIB2 metadata for all GRIB2 Messages.  A GRIB2 Message may contain
129    submessages whereby Section 2-7 can be repeated.  grib2io accommodates for
130    this by flattening any GRIB2 submessages into multiple individual messages.
131
132    It is important to note that GRIB2 files from some Meteorological agencies
133    contain other data than GRIB2 messages.  GRIB2 files from ECMWF can contain
134    GRIB1 and GRIB2 messages.  grib2io checks for these and safely ignores them.
135
136    Attributes
137    ----------
138    closed : bool
139        `True` is file handle is close; `False` otherwise.
140    current_message : int
141        Current position of the file in units of GRIB2 Messages. (read only)
142    indexfile : str
143        Index file for the GRIB2 file.
144    levels : tuple
145        Tuple containing a unique list of wgrib2-formatted level/layer strings.
146    messages : int
147        Count of GRIB2 Messages contained in the file.
148    mode : str
149        File IO mode of opening the file.
150    name : str
151        Full path name of the GRIB2 file.
152    save_index : bool
153        Whether to save a pickle-based index file for the GRIB2 file. Default is `True`.
154    size : int
155        Size of the file in units of bytes.
156    use_index
157        Whether to use an existing pickle-based index file for the GRIB2 file. Default is `True`.
158    variables : tuple
159        Tuple containing a unique list of variable short names (i.e. GRIB2
160        abbreviation names).
161    """
162
163    __slots__ = (
164        "_fileid",
165        "_filehandle",
166        "_hasindex",
167        "_index",
168        "_msgs",
169        "_pos",
170        "closed",
171        "current_message",
172        "indexfile",
173        "messages",
174        "mode",
175        "name",
176        "size",
177        "save_index",
178        "use_index",
179    )
180
181    def __init__(
182        self,
183        filename: Union[bytes, str, Path, IO],
184        mode: Literal["r", "w", "x"] = "r",
185        *,
186        save_index=True,
187        use_index=True,
188        _xarray_backend=False,
189        **kwargs,
190    ):
191        """
192        Initialize GRIB2 File object instance.
193
194        Parameters
195        ----------
196        filename
197            File path containing GRIB2 messages OR bytes OR file-like object.
198        mode
199            File access mode where "r" opens the files for reading only; "w"
200            opens the file for overwriting and "x" for writing to a new file.
201        save_index
202            Whether to save a pickle-based index file for the GRIB2 file. Default is True.
203
204            .. versionadded:: 2.6.0
205        use_index
206            Whether to use an existing pickle-based index file for the GRIB2 file. Default is True.
207
208            .. versionadded:: 2.6.0
209        """
210
211        # All write modes are read/write; all modes are binary.
212        if mode in ("a", "x", "w"):
213            mode += "+"
214        mode = mode + "b"
215
216        self._hasindex = False
217        self.indexfile = None
218        self.mode = mode
219        self.save_index = save_index
220        self.size = 0
221        self.use_index = use_index
222
223        if isinstance(filename, bytes):
224            if "r" not in self.mode:
225                raise ValueError(
226                    "Invalid mode for bytes input: GRIB2 data supplied as bytes is read-only. Use mode='r' or provide a filename instead."
227                )
228
229            self.current_message = 0
230            if filename[:2] == _GZIP_HEADER:
231                filename = gzip.decompress(filename)
232            if filename[:4] + filename[-4:] != b"GRIB7777":
233                raise ValueError("Invalid GRIB bytes")
234            self._filehandle = BytesIO(filename)
235            self.name = "<in-memory-file>"
236            self.size = len(filename)
237            self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
238            self._index = build_index(self._filehandle)
239            self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
240            self.messages = len(self._msgs)
241
242        elif all(hasattr(filename, attr) for attr in ("read", "seek", "tell")):
243            # Handle file-like objects (including S3File, etc.)
244            self.current_message = 0
245            self._filehandle = filename
246            self.name = getattr(filename, "name", getattr(filename, "path", filename.__repr__()))
247            try:
248                self.size = self._filehandle.info().get("size", 0)
249            except (AttributeError, TypeError):
250                self.size = 0
251            self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
252
253            if "r" in self.mode:
254                self.indexfile = f"{self.name}.grib2ioidx"
255                idx_loaded = False
256                if self.use_index:
257                    try:
258                        import fsspec
259
260                        with fsspec.open(self.indexfile, "rb") as f:
261                            self._index = pickle.load(f)
262                        idx_loaded = True
263                        self._hasindex = True
264                    except Exception:
265                        pass
266                if not idx_loaded:
267                    self._index = build_index(self._filehandle)
268                self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
269                self.messages = len(self._msgs)
270
271        else:
272            self.current_message = 0
273            self.name = str(filename)
274            is_remote = isinstance(filename, str) and "://" in filename
275            if is_remote:
276                import fsspec
277
278                self._filehandle = fsspec.open(filename, mode=mode, **kwargs).open()
279                self.name = filename
280                try:
281                    self.size = self._filehandle.info().get("size", 0)
282                except (AttributeError, TypeError):
283                    self.size = 0
284                self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
285            else:
286                self.name = os.path.abspath(filename)
287                if "r" in mode:
288                    self._filehandle = builtins.open(filename, mode=mode)
289                    # Some GRIB2 files are gzipped, so check for that here, but
290                    # raise error when using xarray backend.
291                    # Gzip files contain a 2-byte header b'\x1f\x8b'.
292                    if self._filehandle.read(2) == _GZIP_HEADER:
293                        self._filehandle.close()
294                        if _xarray_backend:
295                            raise RuntimeError("Gzip GRIB2 files are not supported by the Xarray backend.")
296                        self._filehandle = gzip.open(filename, mode=mode)
297                    else:
298                        self._filehandle.seek(0)
299                else:
300                    self._filehandle = builtins.open(filename, mode=mode)
301                fstat = os.stat(self.name)
302                self.size = fstat.st_size
303                self._fileid = hashlib.sha1((self.name + str(fstat.st_ino) + str(self.size)).encode("ASCII")).hexdigest()
304
305            if "r" in self.mode:
306                index_paths = [f"{self.name}_{self._fileid}.grib2ioidx", f"{self.name}.grib2ioidx"]
307                idx_paths = [f"{self.name}.idx"]
308                idx_loaded = False
309                remote_index_cache_path = None
310                if is_remote:
311                    cache_root = os.path.join(os.path.expanduser("~"), ".cache", "grib2io")
312                    cache_key = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
313                    remote_index_cache_path = os.path.join(cache_root, f"{cache_key}.grib2ioidx")
314                if self.use_index:
315                    if is_remote and remote_index_cache_path and os.path.exists(remote_index_cache_path):
316                        try:
317                            with builtins.open(remote_index_cache_path, "rb") as f:
318                                self._index = pickle.load(f)
319                            self.indexfile = remote_index_cache_path
320                            idx_loaded = True
321                            self._hasindex = True
322                        except Exception:
323                            idx_loaded = False
324                    for idx_path in index_paths:
325                        if idx_loaded:
326                            break
327                        try:
328                            if is_remote:
329                                import fsspec
330
331                                with fsspec.open(idx_path, "rb", **kwargs) as f:
332                                    self._index = pickle.load(f)
333                            else:
334                                if os.path.exists(idx_path):
335                                    with builtins.open(idx_path, "rb") as f:
336                                        self._index = pickle.load(f)
337                                else:
338                                    continue
339                            self.indexfile = idx_path
340                            idx_loaded = True
341                            self._hasindex = True
342                            break
343                        except Exception:
344                            continue
345                    if not idx_loaded:
346                        for idx_path in idx_paths:
347                            try:
348                                if is_remote:
349                                    import fsspec
350
351                                    with fsspec.open(idx_path, "r", **kwargs) as f:
352                                        offsets = _parse_wgrib2_idx(f)
353                                else:
354                                    if os.path.exists(idx_path):
355                                        with builtins.open(idx_path, "r") as f:
356                                            offsets = _parse_wgrib2_idx(f)
357                                    else:
358                                        continue
359                                if offsets:
360                                    self._index = build_index(self._filehandle, offsets=offsets)
361                                    self.indexfile = idx_path
362                                    idx_loaded = True
363                                    self._hasindex = True
364                                    break
365                            except Exception:
366                                continue
367                    if is_remote and idx_loaded and self.save_index and remote_index_cache_path:
368                        if self.indexfile != remote_index_cache_path:
369                            try:
370                                os.makedirs(os.path.dirname(remote_index_cache_path), exist_ok=True)
371                                serialize_index(self._index, remote_index_cache_path)
372                            except Exception:
373                                pass
374                if not idx_loaded:
375                    self._index = build_index(self._filehandle)
376                    self.indexfile = index_paths[0]
377                    if self.save_index:
378                        try:
379                            if is_remote and remote_index_cache_path:
380                                os.makedirs(os.path.dirname(remote_index_cache_path), exist_ok=True)
381                                serialize_index(self._index, remote_index_cache_path)
382                                self.indexfile = remote_index_cache_path
383                            else:
384                                serialize_index(self._index, self.indexfile)
385                        except Exception as e:
386                            warnings.warn(f"index was not serialized for future use: {e}")
387                self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
388                self.messages = len(self._msgs)
389            elif "w" or "x" in self.mode:
390                self.messages = 0
391                self.current_message = None
392
393        self.closed = self._filehandle.closed
394
395    def __delete__(self, instance):
396        self.close()
397        del self._index
398
399    def __enter__(self):
400        return self
401
402    def __exit__(self, atype, value, traceback):
403        self.close()
404
405    def __iter__(self):
406        yield from self._msgs
407
408    def __len__(self):
409        return self.messages
410
411    def __repr__(self):
412        strings = []
413        for k in self.__slots__:
414            if k.startswith("_"):
415                continue
416            strings.append("%s = %s\n" % (k, eval("self." + k)))
417        return "".join(strings)
418
419    def __getitem__(self, key):
420        if isinstance(key, int):
421            if abs(key) >= len(self._msgs):
422                raise IndexError("index out of range")
423            else:
424                return self._msgs[key]
425        elif isinstance(key, str):
426            return self.select(shortName=key)
427        elif isinstance(key, slice):
428            return self._msgs[key]
429        elif isinstance(key, (list, tuple, set)):
430            if len(key) == 0:
431                return iter(self._msgs)
432            indices = sorted(key) if isinstance(key, set) else key
433
434            def _iter_msgs():
435                for i in indices:
436                    if not isinstance(i, int):
437                        raise TypeError(f"indices must be integers; got {type(i).__name__}")
438                    if abs(i) >= len(self._msgs):
439                        raise IndexError(f"index out of range: {i}")
440                    yield self._msgs[i]
441
442            return _iter_msgs()
443        else:
444            raise KeyError("Key must be an integer, slice, GRIB2 variable shortName, or an iterable of integer indices.")
445
446    @property
447    def levels(self):
448        """Provides a unique tuple of level strings."""
449        if self._hasindex:
450            return tuple(sorted(set([msg.level for msg in self._msgs])))
451        else:
452            return None
453
454    @property
455    def variables(self):
456        """Provides a unique tuple of variable shortName strings."""
457        if self._hasindex:
458            return tuple(sorted(set([msg.shortName for msg in self._msgs])))
459        else:
460            return None
461
462    def close(self):
463        """Close the file handle."""
464        if not self._filehandle.closed:
465            self.messages = 0
466            self.current_message = 0
467            self._filehandle.close()
468            self.closed = self._filehandle.closed
469
470    def read(self, size: Optional[int] = None):
471        """
472        Read size amount of GRIB2 messages from the current position.
473
474        If no argument is given, then size is None and all messages are returned
475        from the current position in the file. This read method follows the
476        behavior of Python's builtin open() function, but whereas that operates
477        on units of bytes, we operate on units of GRIB2 messages.
478
479        Parameters
480        ----------
481        size: default=None
482            The number of GRIB2 messages to read from the current position. If
483            no argument is give, the default value is None and remainder of
484            the file is read.
485
486        Returns
487        -------
488        read
489            ``Grib2Message`` object when size = 1 or a list of Grib2Messages
490            when size > 1.
491        """
492        if size is not None and size < 0:
493            size = None
494        if size is None or size > 1:
495            start = self.tell()
496            stop = self.messages if size is None else start + size
497            if size is None:
498                self.current_message = self.messages - 1
499            else:
500                self.current_message += size
501            return self._msgs[slice(start, stop, 1)]
502        elif size == 1:
503            self.current_message += 1
504            return self._msgs[self.current_message]
505        else:
506            None
507
508    def seek(self, pos: int):
509        """
510        Set the position within the file in units of GRIB2 messages.
511
512        Parameters
513        ----------
514        pos
515            The GRIB2 Message number to set the file pointer to.
516        """
517        if self._hasindex:
518            self._filehandle.seek(self._index["sectionOffset"][0][pos])
519            self.current_message = pos
520
521    def tell(self):
522        """Returns the position of the file in units of GRIB2 Messages."""
523        return self.current_message
524
525    def select(self, **kwargs):
526        """Select GRIB2 messages by `Grib2Message` attributes."""
527        # TODO: Added ability to process multiple values for each keyword (attribute)
528        idxs = []
529        nkeys = len(kwargs.keys())
530        for k, v in kwargs.items():
531            for m in self._msgs:
532                if hasattr(m, k) and getattr(m, k) == v:
533                    idxs.append(m._msgnum)
534        idxs = np.array(idxs, dtype=np.int32)
535        return [self._msgs[i] for i in [ii[0] for ii in collections.Counter(idxs).most_common() if ii[1] == nkeys]]
536
537    def write(self, msg):
538        """
539        Writes GRIB2 message object to file.
540
541        Parameters
542        ----------
543        msg
544            GRIB2 message objects to write to file.
545        """
546        if isinstance(msg, list):
547            for m in msg:
548                self.write(m)
549            return
550
551        if issubclass(msg.__class__, _Grib2Message):
552            # TODO: We can consider letting pack return packed bytes instead of associating with message object
553            if hasattr(msg, "_msg"):
554                # write already packed bytes
555                self._filehandle.write(msg._msg)
556            else:
557                if msg._signature == msg._generate_signature() and msg._data is None and hasattr(msg._ondiskarray, "filehandle"):
558                    # write unchanged message from input
559                    offset = msg._ondiskarray.filehandle.tell()
560                    msg._ondiskarray.filehandle.seek(msg._ondiskarray.offset)
561                    self._filehandle.write(msg._ondiskarray.filehandle.read(msg.section0[-1]))
562                    msg._ondiskarray.filehandle.seek(offset)
563                else:
564                    msg.pack()
565                    self._filehandle.write(msg._msg)
566            self.size = os.path.getsize(self.name)
567            self.messages += 1
568        else:
569            raise TypeError("msg must be a Grib2Message object.")
570        return
571
572    def flush(self):
573        """Flush the file object buffer."""
574        self._filehandle.flush()
575
576    def levels_by_var(self, name: str):
577        """
578        Return a list of level strings given a variable shortName.
579
580        Parameters
581        ----------
582        name
583            Grib2Message variable shortName
584
585        Returns
586        -------
587        levels_by_var
588            A list of unique level strings.
589        """
590        return list(sorted(set([msg.level for msg in self.select(shortName=name)])))
591
592    def vars_by_level(self, level: str):
593        """
594        Return a list of variable shortName strings given a level.
595
596        Parameters
597        ----------
598        level
599            Grib2Message variable level
600
601        Returns
602        -------
603        vars_by_level
604            A list of unique variable shortName strings.
605        """
606        return list(sorted(set([msg.shortName for msg in self.select(level=level)])))

GRIB2 File Object.

This class can accommodate a physical file with one or more GRIB2 messages or a bytes object containing a GRIB2 messages.

A physical file can contain one or more GRIB2 messages. When instantiated, class grib2io.open, the file named filename is opened for reading (mode = 'r') and is automatically indexed. The indexing procedure reads some of the GRIB2 metadata for all GRIB2 Messages. A GRIB2 Message may contain submessages whereby Section 2-7 can be repeated. grib2io accommodates for this by flattening any GRIB2 submessages into multiple individual messages.

It is important to note that GRIB2 files from some Meteorological agencies contain other data than GRIB2 messages. GRIB2 files from ECMWF can contain GRIB1 and GRIB2 messages. grib2io checks for these and safely ignores them.

Attributes
  • closed (bool): True is file handle is close; False otherwise.
  • current_message (int): Current position of the file in units of GRIB2 Messages. (read only)
  • indexfile (str): Index file for the GRIB2 file.
  • levels (tuple): Tuple containing a unique list of wgrib2-formatted level/layer strings.
  • messages (int): Count of GRIB2 Messages contained in the file.
  • mode (str): File IO mode of opening the file.
  • name (str): Full path name of the GRIB2 file.
  • save_index (bool): Whether to save a pickle-based index file for the GRIB2 file. Default is True.
  • size (int): Size of the file in units of bytes.
  • use_index: Whether to use an existing pickle-based index file for the GRIB2 file. Default is True.
  • variables (tuple): Tuple containing a unique list of variable short names (i.e. GRIB2 abbreviation names).
open( filename: Union[bytes, str, pathlib.Path, IO], mode: Literal['r', 'w', 'x'] = 'r', *, save_index=True, use_index=True, _xarray_backend=False, **kwargs)
181    def __init__(
182        self,
183        filename: Union[bytes, str, Path, IO],
184        mode: Literal["r", "w", "x"] = "r",
185        *,
186        save_index=True,
187        use_index=True,
188        _xarray_backend=False,
189        **kwargs,
190    ):
191        """
192        Initialize GRIB2 File object instance.
193
194        Parameters
195        ----------
196        filename
197            File path containing GRIB2 messages OR bytes OR file-like object.
198        mode
199            File access mode where "r" opens the files for reading only; "w"
200            opens the file for overwriting and "x" for writing to a new file.
201        save_index
202            Whether to save a pickle-based index file for the GRIB2 file. Default is True.
203
204            .. versionadded:: 2.6.0
205        use_index
206            Whether to use an existing pickle-based index file for the GRIB2 file. Default is True.
207
208            .. versionadded:: 2.6.0
209        """
210
211        # All write modes are read/write; all modes are binary.
212        if mode in ("a", "x", "w"):
213            mode += "+"
214        mode = mode + "b"
215
216        self._hasindex = False
217        self.indexfile = None
218        self.mode = mode
219        self.save_index = save_index
220        self.size = 0
221        self.use_index = use_index
222
223        if isinstance(filename, bytes):
224            if "r" not in self.mode:
225                raise ValueError(
226                    "Invalid mode for bytes input: GRIB2 data supplied as bytes is read-only. Use mode='r' or provide a filename instead."
227                )
228
229            self.current_message = 0
230            if filename[:2] == _GZIP_HEADER:
231                filename = gzip.decompress(filename)
232            if filename[:4] + filename[-4:] != b"GRIB7777":
233                raise ValueError("Invalid GRIB bytes")
234            self._filehandle = BytesIO(filename)
235            self.name = "<in-memory-file>"
236            self.size = len(filename)
237            self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
238            self._index = build_index(self._filehandle)
239            self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
240            self.messages = len(self._msgs)
241
242        elif all(hasattr(filename, attr) for attr in ("read", "seek", "tell")):
243            # Handle file-like objects (including S3File, etc.)
244            self.current_message = 0
245            self._filehandle = filename
246            self.name = getattr(filename, "name", getattr(filename, "path", filename.__repr__()))
247            try:
248                self.size = self._filehandle.info().get("size", 0)
249            except (AttributeError, TypeError):
250                self.size = 0
251            self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
252
253            if "r" in self.mode:
254                self.indexfile = f"{self.name}.grib2ioidx"
255                idx_loaded = False
256                if self.use_index:
257                    try:
258                        import fsspec
259
260                        with fsspec.open(self.indexfile, "rb") as f:
261                            self._index = pickle.load(f)
262                        idx_loaded = True
263                        self._hasindex = True
264                    except Exception:
265                        pass
266                if not idx_loaded:
267                    self._index = build_index(self._filehandle)
268                self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
269                self.messages = len(self._msgs)
270
271        else:
272            self.current_message = 0
273            self.name = str(filename)
274            is_remote = isinstance(filename, str) and "://" in filename
275            if is_remote:
276                import fsspec
277
278                self._filehandle = fsspec.open(filename, mode=mode, **kwargs).open()
279                self.name = filename
280                try:
281                    self.size = self._filehandle.info().get("size", 0)
282                except (AttributeError, TypeError):
283                    self.size = 0
284                self._fileid = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
285            else:
286                self.name = os.path.abspath(filename)
287                if "r" in mode:
288                    self._filehandle = builtins.open(filename, mode=mode)
289                    # Some GRIB2 files are gzipped, so check for that here, but
290                    # raise error when using xarray backend.
291                    # Gzip files contain a 2-byte header b'\x1f\x8b'.
292                    if self._filehandle.read(2) == _GZIP_HEADER:
293                        self._filehandle.close()
294                        if _xarray_backend:
295                            raise RuntimeError("Gzip GRIB2 files are not supported by the Xarray backend.")
296                        self._filehandle = gzip.open(filename, mode=mode)
297                    else:
298                        self._filehandle.seek(0)
299                else:
300                    self._filehandle = builtins.open(filename, mode=mode)
301                fstat = os.stat(self.name)
302                self.size = fstat.st_size
303                self._fileid = hashlib.sha1((self.name + str(fstat.st_ino) + str(self.size)).encode("ASCII")).hexdigest()
304
305            if "r" in self.mode:
306                index_paths = [f"{self.name}_{self._fileid}.grib2ioidx", f"{self.name}.grib2ioidx"]
307                idx_paths = [f"{self.name}.idx"]
308                idx_loaded = False
309                remote_index_cache_path = None
310                if is_remote:
311                    cache_root = os.path.join(os.path.expanduser("~"), ".cache", "grib2io")
312                    cache_key = hashlib.sha1((self.name + str(self.size)).encode("ASCII")).hexdigest()
313                    remote_index_cache_path = os.path.join(cache_root, f"{cache_key}.grib2ioidx")
314                if self.use_index:
315                    if is_remote and remote_index_cache_path and os.path.exists(remote_index_cache_path):
316                        try:
317                            with builtins.open(remote_index_cache_path, "rb") as f:
318                                self._index = pickle.load(f)
319                            self.indexfile = remote_index_cache_path
320                            idx_loaded = True
321                            self._hasindex = True
322                        except Exception:
323                            idx_loaded = False
324                    for idx_path in index_paths:
325                        if idx_loaded:
326                            break
327                        try:
328                            if is_remote:
329                                import fsspec
330
331                                with fsspec.open(idx_path, "rb", **kwargs) as f:
332                                    self._index = pickle.load(f)
333                            else:
334                                if os.path.exists(idx_path):
335                                    with builtins.open(idx_path, "rb") as f:
336                                        self._index = pickle.load(f)
337                                else:
338                                    continue
339                            self.indexfile = idx_path
340                            idx_loaded = True
341                            self._hasindex = True
342                            break
343                        except Exception:
344                            continue
345                    if not idx_loaded:
346                        for idx_path in idx_paths:
347                            try:
348                                if is_remote:
349                                    import fsspec
350
351                                    with fsspec.open(idx_path, "r", **kwargs) as f:
352                                        offsets = _parse_wgrib2_idx(f)
353                                else:
354                                    if os.path.exists(idx_path):
355                                        with builtins.open(idx_path, "r") as f:
356                                            offsets = _parse_wgrib2_idx(f)
357                                    else:
358                                        continue
359                                if offsets:
360                                    self._index = build_index(self._filehandle, offsets=offsets)
361                                    self.indexfile = idx_path
362                                    idx_loaded = True
363                                    self._hasindex = True
364                                    break
365                            except Exception:
366                                continue
367                    if is_remote and idx_loaded and self.save_index and remote_index_cache_path:
368                        if self.indexfile != remote_index_cache_path:
369                            try:
370                                os.makedirs(os.path.dirname(remote_index_cache_path), exist_ok=True)
371                                serialize_index(self._index, remote_index_cache_path)
372                            except Exception:
373                                pass
374                if not idx_loaded:
375                    self._index = build_index(self._filehandle)
376                    self.indexfile = index_paths[0]
377                    if self.save_index:
378                        try:
379                            if is_remote and remote_index_cache_path:
380                                os.makedirs(os.path.dirname(remote_index_cache_path), exist_ok=True)
381                                serialize_index(self._index, remote_index_cache_path)
382                                self.indexfile = remote_index_cache_path
383                            else:
384                                serialize_index(self._index, self.indexfile)
385                        except Exception as e:
386                            warnings.warn(f"index was not serialized for future use: {e}")
387                self._msgs = msgs_from_index(self._index, filehandle=self._filehandle)
388                self.messages = len(self._msgs)
389            elif "w" or "x" in self.mode:
390                self.messages = 0
391                self.current_message = None
392
393        self.closed = self._filehandle.closed

Initialize GRIB2 File object instance.

Parameters
  • filename: File path containing GRIB2 messages OR bytes OR file-like object.
  • mode: File access mode where "r" opens the files for reading only; "w" opens the file for overwriting and "x" for writing to a new file.
  • save_index: Whether to save a pickle-based index file for the GRIB2 file. Default is True.

New in version 2.6.0.

  • use_index: Whether to use an existing pickle-based index file for the GRIB2 file. Default is True.

New in version 2.6.0.

indexfile
mode
save_index
size
use_index
closed
levels
446    @property
447    def levels(self):
448        """Provides a unique tuple of level strings."""
449        if self._hasindex:
450            return tuple(sorted(set([msg.level for msg in self._msgs])))
451        else:
452            return None

Provides a unique tuple of level strings.

variables
454    @property
455    def variables(self):
456        """Provides a unique tuple of variable shortName strings."""
457        if self._hasindex:
458            return tuple(sorted(set([msg.shortName for msg in self._msgs])))
459        else:
460            return None

Provides a unique tuple of variable shortName strings.

def close(self):
462    def close(self):
463        """Close the file handle."""
464        if not self._filehandle.closed:
465            self.messages = 0
466            self.current_message = 0
467            self._filehandle.close()
468            self.closed = self._filehandle.closed

Close the file handle.

def read(self, size: Optional[int] = None):
470    def read(self, size: Optional[int] = None):
471        """
472        Read size amount of GRIB2 messages from the current position.
473
474        If no argument is given, then size is None and all messages are returned
475        from the current position in the file. This read method follows the
476        behavior of Python's builtin open() function, but whereas that operates
477        on units of bytes, we operate on units of GRIB2 messages.
478
479        Parameters
480        ----------
481        size: default=None
482            The number of GRIB2 messages to read from the current position. If
483            no argument is give, the default value is None and remainder of
484            the file is read.
485
486        Returns
487        -------
488        read
489            ``Grib2Message`` object when size = 1 or a list of Grib2Messages
490            when size > 1.
491        """
492        if size is not None and size < 0:
493            size = None
494        if size is None or size > 1:
495            start = self.tell()
496            stop = self.messages if size is None else start + size
497            if size is None:
498                self.current_message = self.messages - 1
499            else:
500                self.current_message += size
501            return self._msgs[slice(start, stop, 1)]
502        elif size == 1:
503            self.current_message += 1
504            return self._msgs[self.current_message]
505        else:
506            None

Read size amount of GRIB2 messages from the current position.

If no argument is given, then size is None and all messages are returned from the current position in the file. This read method follows the behavior of Python's builtin open() function, but whereas that operates on units of bytes, we operate on units of GRIB2 messages.

Parameters
  • size (default=None): The number of GRIB2 messages to read from the current position. If no argument is give, the default value is None and remainder of the file is read.
Returns
  • read: Grib2Message object when size = 1 or a list of Grib2Messages when size > 1.
def seek(self, pos: int):
508    def seek(self, pos: int):
509        """
510        Set the position within the file in units of GRIB2 messages.
511
512        Parameters
513        ----------
514        pos
515            The GRIB2 Message number to set the file pointer to.
516        """
517        if self._hasindex:
518            self._filehandle.seek(self._index["sectionOffset"][0][pos])
519            self.current_message = pos

Set the position within the file in units of GRIB2 messages.

Parameters
  • pos: The GRIB2 Message number to set the file pointer to.
def tell(self):
521    def tell(self):
522        """Returns the position of the file in units of GRIB2 Messages."""
523        return self.current_message

Returns the position of the file in units of GRIB2 Messages.

def select(self, **kwargs):
525    def select(self, **kwargs):
526        """Select GRIB2 messages by `Grib2Message` attributes."""
527        # TODO: Added ability to process multiple values for each keyword (attribute)
528        idxs = []
529        nkeys = len(kwargs.keys())
530        for k, v in kwargs.items():
531            for m in self._msgs:
532                if hasattr(m, k) and getattr(m, k) == v:
533                    idxs.append(m._msgnum)
534        idxs = np.array(idxs, dtype=np.int32)
535        return [self._msgs[i] for i in [ii[0] for ii in collections.Counter(idxs).most_common() if ii[1] == nkeys]]

Select GRIB2 messages by Grib2Message attributes.

def write(self, msg):
537    def write(self, msg):
538        """
539        Writes GRIB2 message object to file.
540
541        Parameters
542        ----------
543        msg
544            GRIB2 message objects to write to file.
545        """
546        if isinstance(msg, list):
547            for m in msg:
548                self.write(m)
549            return
550
551        if issubclass(msg.__class__, _Grib2Message):
552            # TODO: We can consider letting pack return packed bytes instead of associating with message object
553            if hasattr(msg, "_msg"):
554                # write already packed bytes
555                self._filehandle.write(msg._msg)
556            else:
557                if msg._signature == msg._generate_signature() and msg._data is None and hasattr(msg._ondiskarray, "filehandle"):
558                    # write unchanged message from input
559                    offset = msg._ondiskarray.filehandle.tell()
560                    msg._ondiskarray.filehandle.seek(msg._ondiskarray.offset)
561                    self._filehandle.write(msg._ondiskarray.filehandle.read(msg.section0[-1]))
562                    msg._ondiskarray.filehandle.seek(offset)
563                else:
564                    msg.pack()
565                    self._filehandle.write(msg._msg)
566            self.size = os.path.getsize(self.name)
567            self.messages += 1
568        else:
569            raise TypeError("msg must be a Grib2Message object.")
570        return

Writes GRIB2 message object to file.

Parameters
  • msg: GRIB2 message objects to write to file.
def flush(self):
572    def flush(self):
573        """Flush the file object buffer."""
574        self._filehandle.flush()

Flush the file object buffer.

def levels_by_var(self, name: str):
576    def levels_by_var(self, name: str):
577        """
578        Return a list of level strings given a variable shortName.
579
580        Parameters
581        ----------
582        name
583            Grib2Message variable shortName
584
585        Returns
586        -------
587        levels_by_var
588            A list of unique level strings.
589        """
590        return list(sorted(set([msg.level for msg in self.select(shortName=name)])))

Return a list of level strings given a variable shortName.

Parameters
  • name: Grib2Message variable shortName
Returns
  • levels_by_var: A list of unique level strings.
def vars_by_level(self, level: str):
592    def vars_by_level(self, level: str):
593        """
594        Return a list of variable shortName strings given a level.
595
596        Parameters
597        ----------
598        level
599            Grib2Message variable level
600
601        Returns
602        -------
603        vars_by_level
604            A list of unique variable shortName strings.
605        """
606        return list(sorted(set([msg.shortName for msg in self.select(level=level)])))

Return a list of variable shortName strings given a level.

Parameters
  • level: Grib2Message variable level
Returns
  • vars_by_level: A list of unique variable shortName strings.
current_message
messages
name
def show_config():
 86def show_config():
 87    """Print grib2io build configuration information."""
 88    print(f"grib2io version {__version__} Configuration:")
 89    print("")
 90    print(f"NCEPLIBS-g2c library version: {__g2clib_version__}")
 91    print(f"\tStatic library: {g2c_static}")
 92    print(f"\tJPEG compression support: {has_jpeg_support}")
 93    print(f"\tPNG compression support: {has_png_support}")
 94    print(f"\tAEC compression support: {has_aec_support}")
 95    print("")
 96    print(f"NCEPLIPS-ip support: {has_interpolation}")
 97    print(f"\tStatic library: {ip_static}")
 98    print(f"\tOpenMP support: {has_openmp_support}")
 99    print("")
100    print("Static libs:")
101    for lib in extra_objects:
102        print(f"\t{lib}")
103    print("")
104    print(f"NCEP GRIB2 Table Version: {_ncep_grib2_table_version}")

Print grib2io build configuration information.

def interpolate( a, method: Union[int, str], grid_def_in, grid_def_out, method_options=None, num_threads=1):
1968def interpolate(
1969    a,
1970    method: Union[int, str],
1971    grid_def_in,
1972    grid_def_out,
1973    method_options=None,
1974    num_threads=1,
1975):
1976    """
1977    This is the module-level interpolation function.
1978
1979    This interfaces with the "d" version of[NCEPLIBS-ip library](https://github.com/NOAA-EMC/NCEPLIBS-ip)
1980    through grib2io's internal iplib Cython extension module. The "d" version
1981    defines 4-byte integers and 8-byte reals.
1982
1983    Parameters
1984    ----------
1985    a : numpy.ndarray or tuple
1986        Input data.  If `a` is a `numpy.ndarray`, scalar interpolation will be
1987        performed.  If `a` is a `tuple`, then vector interpolation will be
1988        performed with the assumption that u = a[0] and v = a[1] and are both
1989        `numpy.ndarray`.
1990
1991        These data are expected to be in 2-dimensional form with shape (ny, nx)
1992        or 3-dimensional (:, ny, nx) where the 1st dimension represents another
1993        spatial, temporal, or classification (i.e. ensemble members) dimension.
1994        The function will properly flatten the (ny,nx) dimensions into (nx * ny)
1995        acceptable for input into the interpolation subroutines. If needed, these
1996        data will be converted to `np.float32`.
1997    method
1998        Interpolate method to use. This can either be an integer or string using
1999        the following mapping:
2000
2001        | Interpolate Scheme | Integer Value |
2002        | :---:              | :---:         |
2003        | 'bilinear'         | 0             |
2004        | 'bicubic'          | 1             |
2005        | 'neighbor'         | 2             |
2006        | 'budget'           | 3             |
2007        | 'spectral'         | 4             |
2008        | 'neighbor-budget'  | 6             |
2009
2010    grid_def_in : grib2io.Grib2GridDef
2011        Grib2GridDef object for the input grid.
2012    grid_def_out : grib2io.Grib2GridDef
2013        Grib2GridDef object for the output grid or station points.
2014    method_options : list of ints, optional
2015        Interpolation options. See the NCEPLIBS-ip documentation for
2016        more information on how these are used.
2017    num_threads : int, optional
2018        Number of OpenMP threads to use for interpolation. The default
2019        value is 1. If NCEPLIBS-ip and grib2io's iplib extension module
2020        was not built with OpenMP, then this keyword argument and value
2021        will have no impact.
2022
2023    Returns
2024    -------
2025    interpolate
2026        Returns a `numpy.ndarray` of dtype `np.float32` when scalar interpolation
2027        is performed or a `tuple` of `numpy.ndarray`s when vector interpolation is
2028        performed with the assumptions that 0-index is the interpolated u and
2029        1-index is the interpolated v.
2030    """
2031
2032    try:
2033        from . import iplib
2034    except ImportError:
2035        raise ImportError("NCEPLIBS-ip library not found. Interpolation is not available.")
2036
2037    prev_num_threads = 1
2038    try:
2039        prev_num_threads = iplib.openmp_get_num_threads()
2040        iplib.openmp_set_num_threads(num_threads)
2041    except AttributeError:
2042        pass
2043
2044    print(f"grib2io.interpolate thread report: OpenMP num threads = {iplib.openmp_get_num_threads()}")
2045
2046    if isinstance(method, int) and method not in _interp_schemes.values():
2047        raise ValueError("Invalid interpolation method.")
2048    elif isinstance(method, str):
2049        if method in _interp_schemes.keys():
2050            method = _interp_schemes[method]
2051        else:
2052            raise ValueError("Invalid interpolation method.")
2053
2054    if method_options is None:
2055        method_options = np.zeros((20), dtype=np.int32)
2056        if method in {3, 6}:
2057            method_options[0:2] = -1
2058
2059    mi = grid_def_in.npoints
2060    mo = grid_def_out.npoints
2061
2062    # Adjust shape of input array(s)
2063    a, newshp = _adjust_array_shape_for_interp(a, grid_def_in, grid_def_out)
2064
2065    # Call interpolation subroutines according to type of a.
2066    if isinstance(a, np.ndarray):
2067        # Scalar
2068        km = a.shape[0]
2069        if np.any(np.isnan(a)):
2070            ibi = np.ones((km), dtype=np.int32)
2071            li = np.where(np.isnan(a), 0, 1).astype(np.uint8)
2072        else:
2073            ibi = np.zeros((km), dtype=np.int32)
2074            li = np.zeros(a.shape, dtype=np.uint8)
2075        no, rlat, rlon, ibo, lo, go, iret = iplib.interpolate_scalar(
2076            method,
2077            method_options,
2078            grid_def_in.gdtn,
2079            np.array(grid_def_in.gdt, dtype=np.int32),
2080            grid_def_out.gdtn,
2081            np.array(grid_def_out.gdt, dtype=np.int32),
2082            mi,
2083            mo,
2084            km,
2085            ibi,
2086            li,
2087            a.astype(np.float64),
2088        )
2089        out = np.where(lo == 0, np.nan, go).reshape(newshp)
2090    elif isinstance(a, tuple):
2091        # Vector
2092        km = a[0].shape[0]
2093        if np.any(np.isnan(a)):
2094            ibi = np.ones((km), dtype=np.int32)
2095            li = np.where(np.isnan(a), 0, 1).astype(np.uint8)
2096        else:
2097            ibi = np.zeros((km), dtype=np.int32)
2098            li = np.zeros(a[0].shape, dtype=np.uint8)
2099        no, rlat, rlon, crot, srot, ibo, lo, uo, vo, iret = iplib.interpolate_vector(
2100            method,
2101            method_options,
2102            grid_def_in.gdtn,
2103            np.array(grid_def_in.gdt, dtype=np.int32),
2104            grid_def_out.gdtn,
2105            np.array(grid_def_out.gdt, dtype=np.int32),
2106            mi,
2107            mo,
2108            km,
2109            ibi,
2110            li,
2111            a[0].astype(np.float64),
2112            a[1].astype(np.float64),
2113        )
2114        uo = np.where(lo == 0, np.nan, uo).reshape(newshp)
2115        vo = np.where(lo == 0, np.nan, vo).reshape(newshp)
2116        out = (uo.astype(np.float32), vo.astype(np.float32))
2117
2118    try:
2119        iplib.openmp_set_num_threads(prev_num_threads)
2120    except AttributeError:
2121        pass
2122
2123    return out

This is the module-level interpolation function.

This interfaces with the "d" version ofNCEPLIBS-ip library through grib2io's internal iplib Cython extension module. The "d" version defines 4-byte integers and 8-byte reals.

Parameters
  • a (numpy.ndarray or tuple): Input data. If a is a numpy.ndarray, scalar interpolation will be performed. If a is a tuple, then vector interpolation will be performed with the assumption that u = a[0] and v = a[1] and are both numpy.ndarray.

    These data are expected to be in 2-dimensional form with shape (ny, nx) or 3-dimensional (:, ny, nx) where the 1st dimension represents another spatial, temporal, or classification (i.e. ensemble members) dimension. The function will properly flatten the (ny,nx) dimensions into (nx * ny) acceptable for input into the interpolation subroutines. If needed, these data will be converted to np.float32.

  • method: Interpolate method to use. This can either be an integer or string using the following mapping:
Interpolate Scheme Integer Value
'bilinear' 0
'bicubic' 1
'neighbor' 2
'budget' 3
'spectral' 4
'neighbor-budget' 6

  • grid_def_in (grib2io.Grib2GridDef): Grib2GridDef object for the input grid.
  • grid_def_out (grib2io.Grib2GridDef): Grib2GridDef object for the output grid or station points.
  • method_options (list of ints, optional): Interpolation options. See the NCEPLIBS-ip documentation for more information on how these are used.
  • num_threads (int, optional): Number of OpenMP threads to use for interpolation. The default value is 1. If NCEPLIBS-ip and grib2io's iplib extension module was not built with OpenMP, then this keyword argument and value will have no impact.
Returns
  • interpolate: Returns a numpy.ndarray of dtype np.float32 when scalar interpolation is performed or a tuple of numpy.ndarrays when vector interpolation is performed with the assumptions that 0-index is the interpolated u and 1-index is the interpolated v.
def interpolate_to_stations( a, method: Union[int, str], grid_def_in, lats, lons, method_options=None, num_threads=1):
2126def interpolate_to_stations(
2127    a,
2128    method: Union[int, str],
2129    grid_def_in,
2130    lats,
2131    lons,
2132    method_options=None,
2133    num_threads=1,
2134):
2135    """
2136    Module-level interpolation function for interpolation to stations.
2137
2138    Interfaces with the "d" version of [NCEPLIBS-ip library](https://github.com/NOAA-EMC/NCEPLIBS-ip)
2139    via grib2io's iplib Cython exntension module. It supports scalar and
2140    vector interpolation according to the type of object a.
2141
2142    Parameters
2143    ----------
2144    a : numpy.ndarray or tuple
2145        Input data.  If `a` is a `numpy.ndarray`, scalar interpolation will be
2146        performed.  If `a` is a `tuple`, then vector interpolation will be
2147        performed with the assumption that u = a[0] and v = a[1] and are both
2148        `numpy.ndarray`.
2149
2150        These data are expected to be in 2-dimensional form with shape (ny, nx)
2151        or 3-dimensional (:, ny, nx) where the 1st dimension represents another
2152        spatial, temporal, or classification (i.e. ensemble members) dimension.
2153        The function will properly flatten the (ny,nx) dimensions into (nx * ny)
2154        acceptable for input into the interpolation subroutines. If needed, these
2155        data will be converted to `np.float32`.
2156    method
2157        Interpolate method to use. This can either be an integer or string using
2158        the following mapping:
2159
2160        | Interpolate Scheme | Integer Value |
2161        | :---:              | :---:         |
2162        | 'bilinear'         | 0             |
2163        | 'bicubic'          | 1             |
2164        | 'neighbor'         | 2             |
2165        | 'budget'           | 3             |
2166        | 'spectral'         | 4             |
2167        | 'neighbor-budget'  | 6             |
2168
2169    grid_def_in : grib2io.Grib2GridDef
2170        Grib2GridDef object for the input grid.
2171    lats : numpy.ndarray or list
2172        Latitudes for station points
2173    lons : numpy.ndarray or list
2174        Longitudes for station points
2175    method_options : list of ints, optional
2176        Interpolation options. See the NCEPLIBS-ip documentation for
2177        more information on how these are used.
2178    num_threads : int, optional
2179        Number of OpenMP threads to use for interpolation. The default
2180        value is 1. If NCEPLIBS-ip and grib2io's iplib extension module
2181        was not built with OpenMP, then this keyword argument and value
2182        will have no impact.
2183
2184    Returns
2185    -------
2186    interpolate_to_stations
2187        Returns a `numpy.ndarray` of dtype `np.float32` when scalar
2188        interpolation is performed or a `tuple` of `numpy.ndarray`s
2189        when vector interpolation is performed with the assumptions
2190        that 0-index is the interpolated u and 1-index is the
2191        interpolated v.
2192    """
2193    try:
2194        from . import iplib
2195    except ImportError:
2196        raise ImportError("NCEPLIBS-ip library not found. Interpolation is not available.")
2197
2198    # Define function to apply mask when stations are outside grid domain
2199    def _reshape_and_mask_post_interp(a, shape, mask):
2200        a = a.reshape(shape)
2201        if a.shape[-1] != mask.shape[0]:
2202            raise ValueError("Station mask length does not match interpolated data.")
2203        a[..., mask] = np.nan
2204        return a
2205
2206    prev_num_threads = 1
2207    try:
2208        prev_num_threads = iplib.openmp_get_num_threads()
2209        iplib.openmp_set_num_threads(num_threads)
2210    except AttributeError:
2211        pass
2212
2213    if isinstance(method, int) and method not in _interp_schemes.values():
2214        raise ValueError("Invalid interpolation method.")
2215    elif isinstance(method, str):
2216        if method in _interp_schemes.keys():
2217            method = _interp_schemes[method]
2218        else:
2219            raise ValueError("Invalid interpolation method.")
2220
2221    if method_options is None:
2222        method_options = np.zeros((20), dtype=np.int32)
2223        if method in {3, 6}:
2224            method_options[0:2] = -1
2225
2226    # Check lats and lons
2227    if isinstance(lats, list):
2228        nlats = len(lats)
2229    elif isinstance(lats, np.ndarray) and len(lats.shape) == 1:
2230        nlats = lats.shape[0]
2231    else:
2232        raise ValueError("Station latitudes must be a list or 1-D NumPy array.")
2233    if isinstance(lons, list):
2234        nlons = len(lons)
2235    elif isinstance(lons, np.ndarray) and len(lons.shape) == 1:
2236        nlons = lons.shape[0]
2237    else:
2238        raise ValueError("Station longitudes must be a list or 1-D NumPy array.")
2239    if nlats != nlons:
2240        raise ValueError("Station lats and lons must be same size.")
2241
2242    mi = grid_def_in.npoints
2243    mo = nlats
2244
2245    # Adjust shape of input array(s)
2246    a, newshp = _adjust_array_shape_for_interp_stations(a, grid_def_in, mo)
2247
2248    # Use gdtn = -1 for stations and an empty template array
2249    gdtn_out = -1
2250    gdt_out = np.zeros((200), dtype=np.int32)
2251
2252    # Before we interpolate, get the grid coordinates for stations.
2253    xloc, yloc = utils.latlon_to_ij(
2254        grid_def_in.gdtn,
2255        grid_def_in.gdt,
2256        np.array(lats, dtype=np.float32),
2257        np.array(lons, dtype=np.float32),
2258    )
2259    xloc_mask = np.where(np.isnan(xloc), True, False)
2260    yloc_mask = np.where(np.isnan(yloc), True, False)
2261    mask = xloc_mask & yloc_mask
2262
2263    # Call interpolation subroutines according to type of a.
2264    if isinstance(a, np.ndarray):
2265        # Scalar
2266        km = a.shape[0]
2267        ibi = np.zeros((km), dtype=np.int32)
2268        li = np.zeros(a.shape, dtype=np.uint8)
2269        no, rlat, rlon, ibo, lo, go, iret = iplib.interpolate_scalar(
2270            method,
2271            method_options,
2272            grid_def_in.gdtn,
2273            np.array(grid_def_in.gdt, dtype=np.int32),
2274            gdtn_out,
2275            gdt_out,
2276            mi,
2277            mo,
2278            km,
2279            ibi,
2280            li,
2281            a.astype(np.float64),
2282            lats=np.array(lats, dtype=np.float64),
2283            lons=np.array(lons, dtype=np.float64),
2284        )
2285        out = _reshape_and_mask_post_interp(go, newshp, mask)
2286
2287    elif isinstance(a, tuple):
2288        # Vector
2289        km = a[0].shape[0]
2290        ibi = np.zeros((km), dtype=np.int32)
2291        li = np.zeros(a[0].shape, dtype=np.uint8)
2292        no, rlat, rlon, crot, srot, ibo, lo, uo, vo, iret = iplib.interpolate_vector(
2293            method,
2294            method_options,
2295            grid_def_in.gdtn,
2296            np.array(grid_def_in.gdt, dtype=np.int32),
2297            gdtn_out,
2298            gdt_out,
2299            mi,
2300            mo,
2301            km,
2302            ibi,
2303            li,
2304            a[0].astype(np.float64),
2305            a[1].astype(np.float64),
2306            lats=np.array(lats, dtype=np.float64),
2307            lons=np.array(lons, dtype=np.float64),
2308        )
2309        out = (
2310            _reshape_and_mask_post_interp(uo.astype(np.float32), newshp, mask),
2311            _reshape_and_mask_post_interp(vo.astype(np.float32), newshp, mask),
2312        )
2313
2314    try:
2315        iplib.openmp_set_num_threads(prev_num_threads)
2316    except AttributeError:
2317        pass
2318
2319    return out

Module-level interpolation function for interpolation to stations.

Interfaces with the "d" version of NCEPLIBS-ip library via grib2io's iplib Cython exntension module. It supports scalar and vector interpolation according to the type of object a.

Parameters
  • a (numpy.ndarray or tuple): Input data. If a is a numpy.ndarray, scalar interpolation will be performed. If a is a tuple, then vector interpolation will be performed with the assumption that u = a[0] and v = a[1] and are both numpy.ndarray.

    These data are expected to be in 2-dimensional form with shape (ny, nx) or 3-dimensional (:, ny, nx) where the 1st dimension represents another spatial, temporal, or classification (i.e. ensemble members) dimension. The function will properly flatten the (ny,nx) dimensions into (nx * ny) acceptable for input into the interpolation subroutines. If needed, these data will be converted to np.float32.

  • method: Interpolate method to use. This can either be an integer or string using the following mapping:
Interpolate Scheme Integer Value
'bilinear' 0
'bicubic' 1
'neighbor' 2
'budget' 3
'spectral' 4
'neighbor-budget' 6

  • grid_def_in (grib2io.Grib2GridDef): Grib2GridDef object for the input grid.
  • lats (numpy.ndarray or list): Latitudes for station points
  • lons (numpy.ndarray or list): Longitudes for station points
  • method_options (list of ints, optional): Interpolation options. See the NCEPLIBS-ip documentation for more information on how these are used.
  • num_threads (int, optional): Number of OpenMP threads to use for interpolation. The default value is 1. If NCEPLIBS-ip and grib2io's iplib extension module was not built with OpenMP, then this keyword argument and value will have no impact.
Returns
  • interpolate_to_stations: Returns a numpy.ndarray of dtype np.float32 when scalar interpolation is performed or a tuple of numpy.ndarrays when vector interpolation is performed with the assumptions that 0-index is the interpolated u and 1-index is the interpolated v.
class Grib2Message:
720class Grib2Message:
721    """
722    Creation class for a GRIB2 message.
723
724    This class returns a dynamically-created Grib2Message object that
725    inherits from `_Grib2Message` and grid, product, data representation
726    template classes according to the template numbers for the respective
727    sections. If `section3`, `section4`, or `section5` are omitted, then
728    the appropriate keyword arguments for the template number `gdtn=`,
729    `pdtn=`, or `drtn=` must be provided.
730
731    Parameters
732    ----------
733    section0
734        GRIB2 section 0 array.
735    section1
736        GRIB2 section 1 array.
737    section2
738        Local Use section data.
739    section3
740        GRIB2 section 3 array.
741    section4
742        GRIB2 section 4 array.
743    section5
744        GRIB2 section 5 array.
745
746    Returns
747    -------
748    Msg
749        A dynamically-create Grib2Message object that inherits from
750        _Grib2Message, a grid definition template class, product
751        definition template class, and a data representation template
752        class.
753    """
754
755    def __new__(
756        self,
757        section0: NDArray = np.array([struct.unpack(">I", b"GRIB")[0], 0, 0, 2, 0]),
758        section1: NDArray = np.zeros((13), dtype=np.int64),
759        section2: Optional[bytes] = None,
760        section3: Optional[NDArray] = None,
761        section4: Optional[NDArray] = None,
762        section5: Optional[NDArray] = None,
763        *args,
764        **kwargs,
765    ):
766        if np.all(section1 == 0):
767            try:
768                # Python >= 3.10
769                section1[5:11] = datetime.datetime.fromtimestamp(0, datetime.UTC).timetuple()[:6]
770            except AttributeError:
771                # Python < 3.10
772                section1[5:11] = datetime.datetime.utcfromtimestamp(0).timetuple()[:6]
773
774        bases = list()
775        if section3 is None:
776            if "gdtn" in kwargs.keys():
777                gdtn = kwargs["gdtn"]
778                Gdt = templates.gdt_class_by_gdtn(gdtn)
779                bases.append(Gdt)
780                section3 = np.zeros((Gdt._len + 5), dtype=np.int64)
781                section3[4] = gdtn
782            else:
783                raise ValueError("Must provide GRIB2 Grid Definition Template Number or section 3 array")
784        else:
785            gdtn = section3[4]
786            Gdt = templates.gdt_class_by_gdtn(gdtn)
787            bases.append(Gdt)
788
789        if section4 is None:
790            if "pdtn" in kwargs.keys():
791                pdtn = kwargs["pdtn"]
792                Pdt = templates.pdt_class_by_pdtn(pdtn)
793                bases.append(Pdt)
794                section4 = np.zeros((Pdt._len + 2), dtype=np.int64)
795                section4[1] = pdtn
796            else:
797                raise ValueError("Must provide GRIB2 Production Definition Template Number or section 4 array")
798        else:
799            pdtn = section4[1]
800            Pdt = templates.pdt_class_by_pdtn(pdtn)
801            bases.append(Pdt)
802
803        if section5 is None:
804            if "drtn" in kwargs.keys():
805                drtn = kwargs["drtn"]
806                Drt = templates.drt_class_by_drtn(drtn)
807                bases.append(Drt)
808                section5 = np.zeros((Drt._len + 2), dtype=np.int64)
809                section5[1] = drtn
810            else:
811                raise ValueError("Must provide GRIB2 Data Representation Template Number or section 5 array")
812        else:
813            drtn = section5[1]
814            Drt = templates.drt_class_by_drtn(drtn)
815            bases.append(Drt)
816
817        # attempt to use existing Msg class if it has already been made with gdtn,pdtn,drtn combo
818        try:
819            Msg = _msg_class_store[f"{gdtn}:{pdtn}:{drtn}"]
820        except KeyError:
821
822            @dataclass(init=False, repr=False)
823            class Msg(_Grib2Message, *bases):
824                pass
825
826            _msg_class_store[f"{gdtn}:{pdtn}:{drtn}"] = Msg
827
828        return Msg(section0, section1, section2, section3, section4, section5, *args)

Creation class for a GRIB2 message.

This class returns a dynamically-created Grib2Message object that inherits from _Grib2Message and grid, product, data representation template classes according to the template numbers for the respective sections. If section3, section4, or section5 are omitted, then the appropriate keyword arguments for the template number gdtn=, pdtn=, or drtn= must be provided.

Parameters
  • section0: GRIB2 section 0 array.
  • section1: GRIB2 section 1 array.
  • section2: Local Use section data.
  • section3: GRIB2 section 3 array.
  • section4: GRIB2 section 4 array.
  • section5: GRIB2 section 5 array.
Returns
  • Msg: A dynamically-create Grib2Message object that inherits from _Grib2Message, a grid definition template class, product definition template class, and a data representation template class.
@dataclass
class _Grib2Message:
 831@dataclass
 832class _Grib2Message:
 833    """
 834    GRIB2 Message base class.
 835    """
 836
 837    # GRIB2 Sections
 838    section0: NDArray = field(init=True, repr=False)
 839    section1: NDArray = field(init=True, repr=False)
 840    section2: bytes = field(init=True, repr=False)
 841    section3: NDArray = field(init=True, repr=False)
 842    section4: NDArray = field(init=True, repr=False)
 843    section5: NDArray = field(init=True, repr=False)
 844    bitMapFlag: templates.Grib2Metadata = field(init=True, repr=False, default=255)
 845
 846    # Section 0 looked up attributes
 847    indicatorSection: NDArray = field(init=False, repr=False, default=templates.IndicatorSection())
 848    discipline: templates.Grib2Metadata = field(init=False, repr=False, default=templates.Discipline())
 849
 850    # Section 1 looked up attributes
 851    identificationSection: NDArray = field(init=False, repr=False, default=templates.IdentificationSection())
 852    originatingCenter: templates.Grib2Metadata = field(init=False, repr=False, default=templates.OriginatingCenter())
 853    originatingSubCenter: templates.Grib2Metadata = field(init=False, repr=False, default=templates.OriginatingSubCenter())
 854    masterTableInfo: templates.Grib2Metadata = field(init=False, repr=False, default=templates.MasterTableInfo())
 855    localTableInfo: templates.Grib2Metadata = field(init=False, repr=False, default=templates.LocalTableInfo())
 856    significanceOfReferenceTime: templates.Grib2Metadata = field(init=False, repr=False, default=templates.SignificanceOfReferenceTime())
 857    year: int = field(init=False, repr=False, default=templates.Year())
 858    month: int = field(init=False, repr=False, default=templates.Month())
 859    day: int = field(init=False, repr=False, default=templates.Day())
 860    hour: int = field(init=False, repr=False, default=templates.Hour())
 861    minute: int = field(init=False, repr=False, default=templates.Minute())
 862    second: int = field(init=False, repr=False, default=templates.Second())
 863    refDate: datetime.datetime = field(init=False, repr=False, default=templates.RefDate())
 864    productionStatus: templates.Grib2Metadata = field(init=False, repr=False, default=templates.ProductionStatus())
 865    typeOfData: templates.Grib2Metadata = field(init=False, repr=False, default=templates.TypeOfData())
 866
 867    # Section 3 looked up common attributes.  Other looked up attributes are available according
 868    # to the Grid Definition Template.
 869    gridDefinitionSection: NDArray = field(init=False, repr=False, default=templates.GridDefinitionSection())
 870    sourceOfGridDefinition: int = field(init=False, repr=False, default=templates.SourceOfGridDefinition())
 871    numberOfDataPoints: int = field(init=False, repr=False, default=templates.NumberOfDataPoints())
 872    interpretationOfListOfNumbers: templates.Grib2Metadata = field(init=False, repr=False, default=templates.InterpretationOfListOfNumbers())
 873    gridDefinitionTemplateNumber: templates.Grib2Metadata = field(init=False, repr=False, default=templates.GridDefinitionTemplateNumber())
 874    gridDefinitionTemplate: list = field(init=False, repr=False, default=templates.GridDefinitionTemplate())
 875    _earthparams: dict = field(init=False, repr=False, default=templates.EarthParams())
 876    _dxsign: float = field(init=False, repr=False, default=templates.DxSign())
 877    _dysign: float = field(init=False, repr=False, default=templates.DySign())
 878    _llscalefactor: float = field(init=False, repr=False, default=templates.LLScaleFactor())
 879    _lldivisor: float = field(init=False, repr=False, default=templates.LLDivisor())
 880    _xydivisor: float = field(init=False, repr=False, default=templates.XYDivisor())
 881    shapeOfEarth: templates.Grib2Metadata = field(init=False, repr=False, default=templates.ShapeOfEarth())
 882    earthShape: str = field(init=False, repr=False, default=templates.EarthShape())
 883    earthRadius: float = field(init=False, repr=False, default=templates.EarthRadius())
 884    earthMajorAxis: float = field(init=False, repr=False, default=templates.EarthMajorAxis())
 885    earthMinorAxis: float = field(init=False, repr=False, default=templates.EarthMinorAxis())
 886    resolutionAndComponentFlags: list = field(init=False, repr=False, default=templates.ResolutionAndComponentFlags())
 887    ny: int = field(init=False, repr=False, default=templates.Ny())
 888    nx: int = field(init=False, repr=False, default=templates.Nx())
 889    scanModeFlags: list = field(init=False, repr=False, default=templates.ScanModeFlags())
 890    projParameters: dict = field(init=False, repr=False, default=templates.ProjParameters())
 891
 892    # Section 4
 893    productDefinitionTemplateNumber: templates.Grib2Metadata = field(init=False, repr=False, default=templates.ProductDefinitionTemplateNumber())
 894    productDefinitionTemplate: NDArray = field(init=False, repr=False, default=templates.ProductDefinitionTemplate())
 895
 896    # Section 5 looked up common attributes.  Other looked up attributes are
 897    # available according to the Data Representation Template.
 898    numberOfPackedValues: int = field(init=False, repr=False, default=templates.NumberOfPackedValues())
 899    dataRepresentationTemplateNumber: templates.Grib2Metadata = field(init=False, repr=False, default=templates.DataRepresentationTemplateNumber())
 900    dataRepresentationTemplate: list = field(init=False, repr=False, default=templates.DataRepresentationTemplate())
 901    typeOfValues: templates.Grib2Metadata = field(init=False, repr=False, default=templates.TypeOfValues())
 902
 903    def __copy__(self):
 904        """Shallow copy"""
 905        new = Grib2Message(
 906            self.section0,
 907            self.section1,
 908            self.section2,
 909            self.section3,
 910            self.section4,
 911            drtn=self.drtn,
 912        )
 913        return new
 914
 915    def __deepcopy__(self, memo):
 916        """Deep copy"""
 917        new = Grib2Message(
 918            np.copy(self.section0),
 919            np.copy(self.section1),
 920            copy.deepcopy(self.section2),
 921            np.copy(self.section3),
 922            np.copy(self.section4),
 923            np.copy(self.section5),
 924        )
 925        memo[id(self)] = new
 926        new.data = np.copy(self.data)
 927        new.bitmap = None if self.bitmap is None else np.copy(self.bitmap)
 928        return new
 929
 930    def __post_init__(self):
 931        """Set some attributes after init."""
 932        self._auto_nans = _AUTO_NANS
 933        self._coordlist = np.zeros((0), dtype=np.float32)
 934        self._data = None
 935        self._deflist = np.zeros((0), dtype=np.int64)
 936        self._msgnum = -1
 937        self._ondiskarray = None
 938        self._orig_section5 = np.copy(self.section5)
 939        self._signature = self._generate_signature()
 940        try:
 941            self._sha1_section3 = hashlib.sha1(self.section3).hexdigest()
 942        except TypeError:
 943            pass
 944        self.bitMapFlag = templates.Grib2Metadata(self.bitMapFlag, table="6.0")
 945        self.bitmap = None
 946
 947    @property
 948    def _isNDFD(self):
 949        """Check if GRIB2 message is from NWS NDFD"""
 950        return np.all(self.section1[0:2] == [8, 65535])
 951
 952    @property
 953    def _isAerosol(self):
 954        """Check if GRIB2 message contains aerosol data"""
 955        is_aero_template = self.productDefinitionTemplateNumber.value in tables.AEROSOL_PDTNS
 956        is_aero_param = (self.parameterCategory in {13, 20}) and (self.parameterNumber in tables.AEROSOL_PARAMS)
 957        # Check table 4.205 aerosol presence
 958        is_aero_type = self.parameterCategory == 205 and self.parameterNumber == 1
 959        return is_aero_template or is_aero_param or is_aero_type
 960
 961    @property
 962    def _isChemical(self):
 963        """Check if GRIB2 message contains chemical data"""
 964        is_chem_template = self.productDefinitionTemplateNumber.value in tables.CHEMICAL_PDTNS
 965        is_chem_param = self.parameterCategory == 20
 966        return is_chem_template or is_chem_param
 967
 968    @property
 969    def gdtn(self):
 970        """Return Grid Definition Template Number"""
 971        return self.section3[4]
 972
 973    @property
 974    def gdt(self):
 975        """Return Grid Definition Template."""
 976        return self.gridDefinitionTemplate
 977
 978    @property
 979    def pdtn(self):
 980        """Return Product Definition Template Number."""
 981        return self.section4[1]
 982
 983    @property
 984    def pdt(self):
 985        """Return Product Definition Template."""
 986        return self.productDefinitionTemplate
 987
 988    @property
 989    def drtn(self):
 990        """Return Data Representation Template Number."""
 991        return self.section5[1]
 992
 993    @property
 994    def drt(self):
 995        """Return Data Representation Template."""
 996        return self.dataRepresentationTemplate
 997
 998    @property
 999    def pdy(self):
1000        """Return the PDY ('YYYYMMDD')."""
1001        return "".join([str(i) for i in self.section1[5:8]])
1002
1003    @property
1004    def griddef(self):
1005        """Return a Grib2GridDef instance for a GRIB2 message."""
1006        return Grib2GridDef.from_section3(self.section3)
1007
1008    @property
1009    def lats(self):
1010        """Return grid latitudes."""
1011        return self.latlons()[0]
1012
1013    @property
1014    def lons(self):
1015        """Return grid longitudes."""
1016        return self.latlons()[1]
1017
1018    @property
1019    def min(self):
1020        """Return minimum value of data."""
1021        return np.nanmin(self.data)
1022
1023    @property
1024    def max(self):
1025        """Return maximum value of data."""
1026        return np.nanmax(self.data)
1027
1028    @property
1029    def mean(self):
1030        """Return mean value of data."""
1031        return np.nanmean(self.data)
1032
1033    @property
1034    def median(self):
1035        """Return median value of data."""
1036        return np.nanmedian(self.data)
1037
1038    @property
1039    def shape(self):
1040        """Return shape of data."""
1041        return self.griddef.shape
1042
1043    def __repr__(self):
1044        """
1045        Return an unambiguous string representation of the object.
1046
1047        Returns
1048        -------
1049        repr
1050            A string representation of the object, including information from
1051            sections 0, 1, 3, 4, 5, and 6.
1052        """
1053        info = ""
1054        for sect in [0, 1, 3, 4, 5, 6]:
1055            for k, v in self.attrs_by_section(sect, values=True).items():
1056                info += f"Section {sect}: {k} = {v}\n"
1057        return info
1058
1059    def __str__(self):
1060        """
1061        Return a readable string representation of the object.
1062
1063        Returns
1064        -------
1065        str
1066            A formatted string representation of the object, including
1067            selected attributes.
1068        """
1069        pdtn = self.pdtn
1070        prefix = f"{self._msgnum}:d={self.refDate}:{self.shortName}:{self.fullName}"
1071
1072        if pdtn in {5, 9}:
1073            return f"{prefix} (%):{self.level}:{self.leadTime}:{self.duration}:{self.threshold} ({self.parameterUnits})"
1074
1075        if pdtn in {6, 10}:
1076            percentile = utils.percentile_string(self.percentileValue)
1077            return f"{prefix} ({self.units}):{self.level}:{self.leadTime}:{self.duration}:{percentile}"
1078
1079        if pdtn == 8:
1080            return f"{prefix} ({self.units}):{self.level}:{self.leadTime}:{self.duration} {self.statisticalProcess.definition}"
1081
1082        return f"{prefix} ({self.units}):{self.level}:{self.leadTime}"
1083
1084    def _generate_signature(self):
1085        """Generature SHA-1 hash string from GRIB2 integer sections."""
1086        return hashlib.sha1(
1087            np.concatenate(
1088                (
1089                    self.section0,
1090                    self.section1,
1091                    self.section3,
1092                    self.section4,
1093                    self.section5,
1094                )
1095            )
1096        ).hexdigest()
1097
1098    def attrs_by_section(self, sect: int, values: bool = False):
1099        """
1100        Provide a tuple of attribute names for the given GRIB2 section.
1101
1102        Parameters
1103        ----------
1104        sect
1105            The GRIB2 section number.
1106        values
1107            Optional (default is `False`) argument to return attributes values.
1108
1109        Returns
1110        -------
1111        attrs_by_section
1112            A list of attribute names or dict of name:value pairs if `values =
1113            True`.
1114        """
1115        if sect in {0, 1, 6}:
1116            attrs = templates._section_attrs[sect]
1117        elif sect in {3, 4, 5}:
1118
1119            def _find_class_index(n):
1120                _key = {3: "Grid", 4: "Product", 5: "Data"}
1121                for i, c in enumerate(self.__class__.__mro__):
1122                    if _key[n] in c.__name__:
1123                        return i
1124                else:
1125                    return []
1126
1127            if sys.version_info.minor <= 8:
1128                attrs = templates._section_attrs[sect] + [a for a in dir(self.__class__.__mro__[_find_class_index(sect)]) if not a.startswith("_")]
1129            else:
1130                attrs = templates._section_attrs[sect] + self.__class__.__mro__[_find_class_index(sect)]._attrs()
1131        else:
1132            attrs = []
1133        if values:
1134            return {k: getattr(self, k) for k in attrs}
1135        else:
1136            return attrs
1137
1138    def copy(self, deep: bool = True):
1139        """Returns a copy of this Grib2Message.
1140
1141        When `deep=True`, a copy is made of each of the GRIB2 section arrays and
1142        the data are unpacked from the source object and copied into the new
1143        object. Otherwise, a shallow copy of each array is performed and no data
1144        are copied.
1145
1146        Parameters
1147        ----------
1148        deep : bool, default: True
1149            Whether each GRIB2 section array and data are copied onto
1150            the new object. Default is True.
1151
1152        Returns
1153        -------
1154        object : Grib2Message
1155            New Grib2Message object.
1156
1157            .. versionadded:: 2.6.0
1158        """
1159        return copy.deepcopy(self) if deep else copy.copy(self)
1160
1161    def pack(self):
1162        """
1163        Pack GRIB2 section data into a binary message.
1164
1165        It is the user's responsibility to populate the GRIB2 section
1166        information with appropriate metadata.
1167        """
1168        # Create beginning of packed binary message with section 0 and 1 data.
1169        self._sections = []
1170        self._msg, self._pos = g2clib.grib2_create(self.indicatorSection[2:4], self.identificationSection)
1171        self._sections += [0, 1]
1172
1173        # Add section 2 if present.
1174        if isinstance(self.section2, bytes) and len(self.section2) > 0:
1175            self._msg, self._pos = g2clib.grib2_addlocal(self._msg, self.section2)
1176            self._sections.append(2)
1177
1178        # Add section 3.
1179        self.section3[1] = self.nx * self.ny
1180        self._msg, self._pos = g2clib.grib2_addgrid(
1181            self._msg,
1182            self.gridDefinitionSection,
1183            self.gridDefinitionTemplate,
1184            self._deflist,
1185        )
1186        self._sections.append(3)
1187
1188        # Prepare data.
1189        if self._data is None:
1190            if self._ondiskarray is None:
1191                raise ValueError("Grib2Message object has no data, thus it cannot be packed.")
1192        field = np.copy(self.data)
1193        if self.scanModeFlags is not None:
1194            if self.scanModeFlags[3]:
1195                fieldsave = field.astype("f")  # Casting makes a copy
1196                field[1::2, :] = fieldsave[1::2, ::-1]
1197        fld = field.astype("f")
1198
1199        # Prepare bitmap, if necessary
1200        bitmapflag = self.bitMapFlag.value
1201        if bitmapflag == 0:
1202            if self.bitmap is not None:
1203                bmap = np.ravel(self.bitmap).astype(DEFAULT_NUMPY_INT)
1204            else:
1205                bmap = np.ravel(np.where(np.isnan(fld), 0, 1)).astype(DEFAULT_NUMPY_INT)
1206        else:
1207            bmap = None
1208
1209        # Prepare data for packing if nans are present
1210        fld = np.ravel(fld)
1211        if bitmapflag in {0, 254}:
1212            fld = np.where(np.isnan(fld), 0, fld)
1213        else:
1214            if np.isnan(fld).any():
1215                if hasattr(self, "priMissingValue"):
1216                    fld = np.where(np.isnan(fld), self.priMissingValue, fld)
1217            if hasattr(self, "_missvalmap"):
1218                if hasattr(self, "priMissingValue"):
1219                    fld = np.where(self._missvalmap == 1, self.priMissingValue, fld)
1220                if hasattr(self, "secMissingValue"):
1221                    fld = np.where(self._missvalmap == 2, self.secMissingValue, fld)
1222
1223        # Add sections 4, 5, 6, and 7.
1224        self._msg, self._pos = g2clib.grib2_addfield(
1225            self._msg,
1226            self.pdtn,
1227            self.productDefinitionTemplate,
1228            self._coordlist,
1229            self.drtn,
1230            self.dataRepresentationTemplate,
1231            fld,
1232            bitmapflag,
1233            bmap,
1234        )
1235        self._sections.append(4)
1236        self._sections.append(5)
1237        self._sections.append(6)
1238        self._sections.append(7)
1239
1240        # Finalize GRIB2 message with section 8.
1241        self._msg, self._pos = g2clib.grib2_end(self._msg)
1242        self._sections.append(8)
1243        self.section0[-1] = len(self._msg)
1244
1245    @property
1246    def data(self) -> np.array:
1247        """Access the unpacked data values."""
1248        if self._data is None:
1249            if self._auto_nans != _AUTO_NANS:
1250                self._data = self._ondiskarray
1251            self._data = np.asarray(self._ondiskarray)
1252        return self._data
1253
1254    @data.setter
1255    def data(self, arr):
1256        """
1257        Set the internal data array, enforcing shape (ny, nx) and dtype float32.
1258
1259        If the Grid Definition Section (section 3) of Grib2Message object is
1260        not fully formed (i.e. nx, ny = 0, 0), then the shape of the data array
1261        will be used to set nx and ny of the Grib2Message object. It will be the
1262        responsibility of the user to populate the rest of the Grid Definition
1263        Section attributes.
1264
1265        Parameters
1266        ----------
1267        arr : array_like
1268            A 2D array whose shape must match ``(self.ny, self.nx)``.
1269            It will be converted to ``float32`` and C-contiguous if needed.
1270
1271        Raises
1272        ------
1273        ValueError
1274            If the shape of `arr` does not match the expected dimensions.
1275        """
1276        if not isinstance(arr, np.ndarray):
1277            raise ValueError("Grib2Message data only supports numpy arrays")
1278        if self.nx == 0 and self.ny == 0:
1279            self.ny = arr.shape[0]
1280            self.nx = arr.shape[1]
1281        if arr.shape != (self.ny, self.nx):
1282            raise ValueError(f"Data shape mismatch: expected ({self.ny}, {self.nx}), got {arr.shape}")
1283        # Ensure contiguous memory layout (important for C interoperability)
1284        if not arr.flags["C_CONTIGUOUS"]:
1285            arr = np.ascontiguousarray(arr, dtype=np.float32)
1286        self._data = arr
1287
1288    def flush_data(self):
1289        """
1290        Flush the unpacked data values from the Grib2Message object.
1291
1292        Notes
1293        -----
1294        If the Grib2Message object was constructed from "scratch" (i.e.
1295        not read from file), this method will remove the data array from
1296        the object and it cannot be recovered.
1297        """
1298        self._data = None
1299        self.bitmap = None
1300
1301    def __getitem__(self, item):
1302        return self.data[item]
1303
1304    def __setitem__(self, item):
1305        raise NotImplementedError("assignment of data not supported via setitem")
1306
1307    def latlons(self, *args, **kwrgs):
1308        """Alias for `grib2io.Grib2Message.grid` method."""
1309        return self.grid(*args, **kwrgs)
1310
1311    def grid(self, unrotate: bool = True):
1312        """
1313        Return lats,lons (in degrees) of grid.
1314
1315        Currently can handle reg. lat/lon,cglobal Gaussian, mercator,
1316        stereographic, lambert conformal, albers equal-area, space-view and
1317        azimuthal equidistant grids.
1318
1319        Parameters
1320        ----------
1321        unrotate
1322            If `True` [DEFAULT], and grid is rotated lat/lon, then unrotate the
1323            grid, otherwise `False`, do not.
1324
1325        Returns
1326        -------
1327        lats, lons : numpy.ndarray
1328            Returns two numpy.ndarrays with dtype=numpy.float32 of grid
1329            latitudes and longitudes in units of degrees.
1330        """
1331        if self._sha1_section3 in _latlon_datastore.keys():
1332            return (
1333                _latlon_datastore[self._sha1_section3]["latitude"],
1334                _latlon_datastore[self._sha1_section3]["longitude"],
1335            )
1336        gdtn = self.gridDefinitionTemplateNumber.value
1337        reggrid = self.gridDefinitionSection[2] == 0  # This means regular 2-d grid
1338        if gdtn == 0:
1339            # Regular lat/lon grid
1340            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1341            lon2, lat2 = self.longitudeLastGridpoint, self.latitudeLastGridpoint
1342            dlon = self.gridlengthXDirection
1343            if lon2 < lon1 and dlon < 0:
1344                lon1 = -lon1
1345            lats = np.linspace(lat1, lat2, self.ny)
1346            if reggrid:
1347                lons = np.linspace(lon1, lon2, self.nx)
1348            else:
1349                lons = np.linspace(lon1, lon2, self.ny * 2)
1350            lons, lats = np.meshgrid(lons, lats)  # Make 2-d arrays.
1351        elif gdtn == 1:  # Rotated Lat/Lon grid
1352            pj = pyproj.Proj(self.projParameters)
1353            lat1, lon1 = self.latitudeFirstGridpoint, self.longitudeFirstGridpoint
1354            lat2, lon2 = self.latitudeLastGridpoint, self.longitudeLastGridpoint
1355            if lon1 > 180.0:
1356                lon1 -= 360.0
1357            if lon2 > 180.0:
1358                lon2 -= 360.0
1359            lats = np.linspace(lat1, lat2, self.ny)
1360            lons = np.linspace(lon1, lon2, self.nx)
1361            lons, lats = np.meshgrid(lons, lats)  # Make 2-d arrays.
1362            if unrotate:
1363                from grib2io.utils import rotated_grid
1364
1365                lats, lons = rotated_grid.unrotate(
1366                    lats,
1367                    lons,
1368                    self.anglePoleRotation,
1369                    self.latitudeSouthernPole,
1370                    self.longitudeSouthernPole,
1371                )
1372        elif gdtn == 40:  # Gaussian grid (only works for global!)
1373            from grib2io.utils.gauss_grid import gaussian_latitudes
1374
1375            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1376            lon2, lat2 = self.longitudeLastGridpoint, self.latitudeLastGridpoint
1377            nlats = self.ny
1378            if not reggrid:  # Reduced Gaussian grid.
1379                nlons = 2 * nlats
1380                dlon = 360.0 / nlons
1381            else:
1382                nlons = self.nx
1383                dlon = self.gridlengthXDirection
1384            lons = np.linspace(lon1, lon2, nlons)
1385            # Compute Gaussian lats (north to south)
1386            lats = gaussian_latitudes(nlats)
1387            if lat1 < lat2:  # reverse them if necessary
1388                lats = lats[::-1]
1389            lons, lats = np.meshgrid(lons, lats)
1390        elif gdtn in {10, 20, 30, 31, 110}:
1391            # Mercator, Lambert Conformal, Stereographic, Albers Equal Area,
1392            # Azimuthal Equidistant
1393            dx, dy = self.gridlengthXDirection, self.gridlengthYDirection
1394            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1395            pj = pyproj.Proj(self.projParameters)
1396            llcrnrx, llcrnry = pj(lon1, lat1)
1397            x = llcrnrx + dx * np.arange(self.nx)
1398            y = llcrnry + dy * np.arange(self.ny)
1399            x, y = np.meshgrid(x, y)
1400            lons, lats = pj(x, y, inverse=True)
1401        elif gdtn == 90:
1402            # Satellite Projection
1403            dx = self.gridlengthXDirection
1404            dy = self.gridlengthYDirection
1405            pj = pyproj.Proj(self.projParameters)
1406            x = dx * np.indices((self.ny, self.nx), "f")[1, :, :]
1407            x -= 0.5 * x.max()
1408            y = dy * np.indices((self.ny, self.nx), "f")[0, :, :]
1409            y -= 0.5 * y.max()
1410            lons, lats = pj(x, y, inverse=True)
1411            # Set lons,lats to 1.e30 where undefined
1412            abslons = np.fabs(lons)
1413            abslats = np.fabs(lats)
1414            lons = np.where(abslons < 1.0e20, lons, 1.0e30)
1415            lats = np.where(abslats < 1.0e20, lats, 1.0e30)
1416        elif gdtn == 32769:
1417            # Special NCEP Grid, Rotated Lat/Lon, Arakawa E-Grid (Non-Staggered)
1418            from grib2io.utils import arakawa_rotated_grid
1419
1420            di, dj = 0.0, 0.0
1421            do_180 = False
1422            idir = 1 if self.scanModeFlags[0] == 0 else -1
1423            jdir = -1 if self.scanModeFlags[1] == 0 else 1
1424            0 if self.resolutionAndComponentFlags[4] == 0 else 1
1425            la1 = self.latitudeFirstGridpoint
1426            lo1 = self.longitudeFirstGridpoint
1427            clon = self.longitudeCenterGridpoint
1428            clat = self.latitudeCenterGridpoint
1429            lasp = clat - 90.0
1430            losp = clon
1431            llat, llon = arakawa_rotated_grid.ll2rot(la1, lo1, lasp, losp)
1432            la2, lo2 = arakawa_rotated_grid.rot2ll(-llat, -llon, lasp, losp)
1433            rlat = -llat
1434            rlon = -llon
1435            if self.nx == 1:
1436                di = 0.0
1437            elif idir == 1:
1438                ti = rlon
1439                while ti < llon:
1440                    ti += 360.0
1441                di = (ti - llon) / float(self.nx - 1)
1442            else:
1443                ti = llon
1444                while ti < rlon:
1445                    ti += 360.0
1446                di = (ti - rlon) / float(self.nx - 1)
1447            if self.ny == 1:
1448                dj = 0.0
1449            else:
1450                dj = (rlat - llat) / float(self.ny - 1)
1451                if dj < 0.0:
1452                    dj = -dj
1453            if idir == 1:
1454                if llon > rlon:
1455                    llon -= 360.0
1456                if llon < 0 and rlon > 0:
1457                    do_180 = True
1458            else:
1459                if rlon > llon:
1460                    rlon -= 360.0
1461                if rlon < 0 and llon > 0:
1462                    do_180 = True
1463            xlat1d = llat + (np.arange(self.ny) * jdir * dj)
1464            xlon1d = llon + (np.arange(self.nx) * idir * di)
1465            xlons, xlats = np.meshgrid(xlon1d, xlat1d)
1466            rot2ll_vectorized = np.vectorize(arakawa_rotated_grid.rot2ll)
1467            lats, lons = rot2ll_vectorized(xlats, xlons, lasp, losp)
1468            if do_180:
1469                lons = np.where(lons > 180.0, lons - 360.0, lons)
1470            vector_rotation_angles_vectorized = np.vectorize(arakawa_rotated_grid.vector_rotation_angles)
1471            rots = vector_rotation_angles_vectorized(lats, lons, clat, losp, xlats)
1472            del xlat1d, xlon1d, xlats, xlons
1473        else:
1474            raise ValueError("Unsupported grid")
1475
1476        _latlon_datastore[self._sha1_section3] = dict(latitude=lats, longitude=lons)
1477        try:
1478            _latlon_datastore[self._sha1_section3]["vector_rotation_angles"] = rots
1479        except NameError:
1480            pass
1481
1482        return lats, lons
1483
1484    def map_keys(self):
1485        """
1486        Unpack data grid replacing integer values with strings.
1487
1488        These types of fields are categorical or classifications where data
1489        values do not represent an observable or predictable physical quantity.
1490        An example of such a field would be [Dominant Precipitation Type -
1491        DPTYPE](https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_table4-201.shtml)
1492
1493        Returns
1494        -------
1495        map_keys
1496            numpy.ndarray of string values per element.
1497        """
1498        hold_auto_nans = _AUTO_NANS
1499        set_auto_nans(False)
1500
1501        if (np.all(self.section1[0:2] == [7, 14]) and self.shortName == "PWTHER") or (self._isNDFD and self.shortName in {"WX", "WWA"}):
1502            keys = utils.decode_wx_strings(self.section2)
1503            if hasattr(self, "priMissingValue") and self.priMissingValue not in [
1504                None,
1505                0,
1506            ]:
1507                keys[int(self.priMissingValue)] = "Missing"
1508            if hasattr(self, "secMissingValue") and self.secMissingValue not in [
1509                None,
1510                0,
1511            ]:
1512                keys[int(self.secMissingValue)] = "Missing"
1513            u, inv = np.unique(self.data, return_inverse=True)
1514            fld = np.array([keys[x] for x in u])[inv].reshape(self.data.shape)
1515        else:
1516            # For data whose units are defined in a code table (i.e. classification or mask)
1517            tblname = re.findall(r"\d\.\d+", self.units, re.IGNORECASE)[0]
1518            fld = self.data.astype(np.int32).astype(str)
1519            tbl = tables.get_table(tblname, expand=True)
1520            for val in np.unique(fld):
1521                fld = np.where(fld == val, tbl[val], fld)
1522        set_auto_nans(hold_auto_nans)
1523        return fld
1524
1525    def to_bytes(self, validate: bool = True):
1526        """
1527        Return packed GRIB2 message in bytes format.
1528
1529        This will be useful for exporting data in non-file formats. For example,
1530        can be used to output grib data directly to S3 using the boto3 client
1531        without the need to write a temporary file to upload first.
1532
1533        Parameters
1534        ----------
1535        validate: default=True
1536            If `True` (DEFAULT), validates first/last four bytes for proper
1537            formatting, else returns None. If `False`, message is output as is.
1538
1539        Returns
1540        -------
1541        to_bytes
1542            Returns GRIB2 formatted message as bytes.
1543        """
1544        if hasattr(self, "_msg"):
1545            if validate:
1546                if self.validate():
1547                    return self._msg
1548                else:
1549                    return None
1550            else:
1551                return self._msg
1552        else:
1553            return None
1554
1555    def interpolate(self, method, grid_def_out, method_options=None, drtn=None, num_threads=1):
1556        """
1557        Grib2Message Interpolator
1558
1559        Performs spatial interpolation via the [NCEPLIBS-ip
1560        library](https://github.com/NOAA-EMC/NCEPLIBS-ip). This interpolate
1561        method only supports scalar interpolation. If you need to perform
1562        vector interpolation, use the module-level `grib2io.interpolate`
1563        function.
1564
1565        Parameters
1566        ----------
1567        method
1568            Interpolate method to use. This can either be an integer or string
1569            using the following mapping:
1570
1571            | Interpolate Scheme | Integer Value |
1572            | :---:              | :---:         |
1573            | 'bilinear'         | 0             |
1574            | 'bicubic'          | 1             |
1575            | 'neighbor'         | 2             |
1576            | 'budget'           | 3             |
1577            | 'spectral'         | 4             |
1578            | 'neighbor-budget'  | 6             |
1579
1580        grid_def_out : grib2io.Grib2GridDef
1581            Grib2GridDef object of the output grid.
1582        method_options : list of ints, optional
1583            Interpolation options. See the NCEPLIBS-ip documentation for
1584            more information on how these are used.
1585        drtn
1586            Data Representation Template to be used for the returned
1587            interpolated GRIB2 message. When `None`, the data representation
1588            template of the source GRIB2 message is used. Once again, it is the
1589            user's responsibility to properly set the Data Representation
1590            Template attributes.
1591        num_threads : int, optional
1592            Number of OpenMP threads to use for interpolation. The default
1593            value is 1. If NCEPLIBS-ip and grib2io's iplib extension module
1594            was not built with OpenMP, then this keyword argument and value
1595            will have no impact.
1596
1597        Returns
1598        -------
1599        interpolate
1600            If interpolating to a grid, a new Grib2Message object is returned.
1601            The GRIB2 metadata of the new Grib2Message object is identical to
1602            the input except where required to be different because of the new
1603            grid specs and possibly a new data representation template.
1604
1605            If interpolating to station points, the interpolated data values are
1606            returned as a numpy.ndarray.
1607        """
1608        section0 = self.section0
1609        section0[-1] = 0
1610        gds = [0, grid_def_out.npoints, 0, 255, grid_def_out.gdtn]
1611        section3 = np.concatenate((gds, grid_def_out.gdt))
1612        drtn = self.drtn if drtn is None else drtn
1613
1614        msg = Grib2Message(
1615            section0,
1616            self.section1,
1617            self.section2,
1618            section3,
1619            self.section4,
1620            None,
1621            self.bitMapFlag.value,
1622            drtn=drtn,
1623        )
1624
1625        msg._msgnum = -1
1626        msg._deflist = self._deflist
1627        msg._coordlist = self._coordlist
1628        if msg.typeOfValues == 0:
1629            pass
1630        elif msg.typeOfValues == 1:
1631            pass
1632        newdata = interpolate(
1633            self.data,
1634            method,
1635            Grib2GridDef.from_section3(self.section3),
1636            grid_def_out,
1637            method_options=method_options,
1638            num_threads=num_threads,
1639        ).reshape(msg.ny, msg.nx)
1640        msg.section5[0] = grid_def_out.npoints
1641        msg._data = newdata.astype(np.float32)
1642        return msg
1643
1644    def subset(self, lats, lons):
1645        """
1646        Return a spatial subset.
1647
1648        Currently only supports regular grids of the following types:
1649
1650        | Grid Type                                                    | gdtn  |
1651        | :---:                                                        | :---: |
1652        | Latitude/Longitude, Equidistant Cylindrical, or Plate Carree | 0     |
1653        | Rotated Latitude/Longitude                                   | 1     |
1654        | Mercator                                                     | 10    |
1655        | Polar Stereographic                                          | 20    |
1656        | Lambert Conformal                                            | 30    |
1657        | Albers Equal-Area                                            | 31    |
1658        | Gaussian Latitude/Longitude                                  | 40    |
1659        | Equatorial Azimuthal Equidistant Projection                  | 110   |
1660
1661        Parameters
1662        ----------
1663        lats
1664            List or tuple of latitudes.  The minimum and maximum latitudes will
1665            be used to define the southern and northern boundaries.
1666
1667            The order of the latitudes is not important.  The function will
1668            determine which is the minimum and maximum.
1669
1670            The latitudes should be in decimal degrees with 0.0 at the equator,
1671            positive values in the northern hemisphere increasing to 90, and
1672            negative values in the southern hemisphere decreasing to -90.
1673        lons
1674            List or tuple of longitudes.  The minimum and maximum longitudes
1675            will be used to define the western and eastern boundaries.
1676
1677            The order of the longitudes is not important.  The function will
1678            determine which is the minimum and maximum.
1679
1680            GRIB2 longitudes should be in decimal degrees with 0.0 at the prime
1681            meridian, positive values increasing eastward to 360.  There are no
1682            negative GRIB2 longitudes.
1683
1684            The typical west longitudes that start at 0.0 at the prime meridian
1685            and decrease to -180 westward, are converted to GRIB2 longitudes by
1686            '360 - (absolute value of the west longitude)' where typical
1687            eastern longitudes are unchanged as GRIB2 longitudes.
1688
1689        Returns
1690        -------
1691        subset
1692            A spatial subset of a GRIB2 message.
1693        """
1694        if self.gdtn not in [0, 1, 10, 20, 30, 31, 40, 110]:
1695            raise ValueError(
1696                """
1697
1698Subset only works for
1699    Latitude/Longitude, Equidistant Cylindrical, or Plate Carree (gdtn=0)
1700    Rotated Latitude/Longitude (gdtn=1)
1701    Mercator (gdtn=10)
1702    Polar Stereographic (gdtn=20)
1703    Lambert Conformal (gdtn=30)
1704    Albers Equal-Area (gdtn=31)
1705    Gaussian Latitude/Longitude (gdtn=40)
1706    Equatorial Azimuthal Equidistant Projection (gdtn=110)
1707
1708"""
1709            )
1710
1711        if self.nx == 0 or self.ny == 0:
1712            raise ValueError(
1713                """
1714
1715Subset only works for regular grids.
1716
1717"""
1718            )
1719
1720        newmsg = Grib2Message(
1721            np.copy(self.section0),
1722            np.copy(self.section1),
1723            np.copy(self.section2),
1724            np.copy(self.section3),
1725            np.copy(self.section4),
1726            np.copy(self.section5),
1727        )
1728
1729        msg_latitude, inlons = self.grid()
1730
1731        if lats is None:
1732            lats = (msg_latitude.flatten()[-1], msg_latitude.flatten()[0])
1733
1734        lats = (min(lats), max(lats))
1735
1736        if lons is None:
1737            lons = (inlons.flatten()[0], inlons.flatten()[-1])
1738
1739        lons = (min(lons), max(lons))
1740
1741        # Internally work in common lon data representation (0->360 positive eastward from 0)
1742        lons = np.mod(np.array(lons) + 360, 360)
1743        msg_longitude = np.mod(inlons + 360, 360)
1744
1745        spatial.verify_lat_lon_bounds(lats, lons)
1746
1747        snap_first_point = spatial.snap_to_nearest_cell_center(msg_latitude, msg_longitude, lats[0], lons[0])
1748        snap_last_point = spatial.snap_to_nearest_cell_center(msg_latitude, msg_longitude, lats[1], lons[1])
1749        lats = (snap_first_point[0], snap_last_point[0])
1750        lons = (snap_first_point[1], snap_last_point[1])
1751
1752        if len(msg_latitude.shape) == 2:
1753            mask_lats = np.any((msg_latitude >= lats[0]) & (msg_latitude <= lats[1]), axis=1)
1754        else:
1755            mask_lats = np.any((msg_latitude >= lats[0]) & (msg_latitude <= lats[1]), axis=0)
1756        mask_lons = np.any((msg_longitude >= lons[0]) & (msg_longitude <= lons[1]), axis=0)
1757
1758        newlats = msg_latitude[mask_lats, :][:, mask_lons]
1759        newlons = msg_longitude[mask_lats, :][:, mask_lons]
1760
1761        setattr(newmsg, "latitudeFirstGridpoint", newlats.flatten()[0])
1762        setattr(newmsg, "longitudeFirstGridpoint", newlons.flatten()[0])
1763        setattr(newmsg, "nx", np.count_nonzero(mask_lons))
1764        setattr(newmsg, "ny", np.count_nonzero(mask_lats))
1765
1766        # Set *LastGridpoint attributes even if only used for gdtn=[0, 1, 40].
1767        # Even though unnecessary for some supported grid types, it won't
1768        # affect a grib2io message to set them.
1769        setattr(newmsg, "latitudeLastGridpoint", newlats.flatten()[-1])
1770        setattr(newmsg, "longitudeLastGridpoint", newlons.flatten()[-1])
1771
1772        setattr(
1773            newmsg,
1774            "data",
1775            self.data[mask_lats, :][:, mask_lons],
1776        )
1777
1778        # Need to reset the '_sha1_section3' attribute to the hash of section 3
1779        # so the '.grid()' method ignores the cached lat/lon and instead
1780        # force the '.grid()' method to recompute the lat/lon values for the
1781        # new, subsetted grid.
1782        newmsg._sha1_section3 = hashlib.sha1(newmsg.section3).hexdigest()
1783        newmsg.grid()
1784
1785        return newmsg
1786
1787    def validate(self):
1788        """
1789        Validate a complete GRIB2 message.
1790
1791        The g2c library does its own internal validation when g2_gribend() is called, but
1792        we will check in grib2io also. The validation checks if the first 4 bytes in
1793        self._msg is 'GRIB' and '7777' as the last 4 bytes and that the message length in
1794        section 0 equals the length of the packed message.
1795
1796        Returns
1797        -------
1798        `True` if the packed GRIB2 message is complete and well-formed, `False` otherwise.
1799        """
1800        valid = False
1801        if hasattr(self, "_msg"):
1802            if self._msg[0:4] + self._msg[-4:] == b"GRIB7777":
1803                if self.section0[-1] == len(self._msg):
1804                    valid = True
1805        return valid

GRIB2 Message base class.

_Grib2Message( section0: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]], section1: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]], section2: bytes, section3: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]], section4: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]], section5: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]], bitMapFlag: grib2io.templates.Grib2Metadata = 255)
section0: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
section1: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
section2: bytes
section3: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
section4: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
section5: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
indicatorSection: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
identificationSection: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]

GRIB2 Section 1, Identification Section

year: int

Year of reference time

month: int

Month of reference time

day: int

Day of reference time

hour: int

Hour of reference time

minute: int

Minute of reference time

second: int

Second of reference time

refDate: datetime.datetime

Reference Date. NOTE: This is a datetime.datetime object.

gridDefinitionSection: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]

GRIB2 Section 3, Grid Definition Section

sourceOfGridDefinition: int
numberOfDataPoints: int

Number of Data Points

interpretationOfListOfNumbers: grib2io.templates.Grib2Metadata

Interpretation of List of Numbers

gridDefinitionTemplate: list

Grid definition template

earthShape: str

Description of the shape of the Earth

earthRadius: float

Radius of the Earth (Assumes "spherical")

earthMajorAxis: float

Major Axis of the Earth (Assumes "oblate spheroid" or "ellipsoid")

earthMinorAxis: float

Minor Axis of the Earth (Assumes "oblate spheroid" or "ellipsoid")

resolutionAndComponentFlags: list
ny: int

Number of grid points in the Y-direction (generally North-South)

nx: int

Number of grid points in the X-direction (generally East-West)

scanModeFlags: list
projParameters: dict

PROJ Parameters to define the reference system

productDefinitionTemplate: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]

Product Definition Template

numberOfPackedValues: int

Number of Packed Values

dataRepresentationTemplate: list

Data Representation Template

gdtn
968    @property
969    def gdtn(self):
970        """Return Grid Definition Template Number"""
971        return self.section3[4]

Return Grid Definition Template Number

gdt
973    @property
974    def gdt(self):
975        """Return Grid Definition Template."""
976        return self.gridDefinitionTemplate

Return Grid Definition Template.

pdtn
978    @property
979    def pdtn(self):
980        """Return Product Definition Template Number."""
981        return self.section4[1]

Return Product Definition Template Number.

pdt
983    @property
984    def pdt(self):
985        """Return Product Definition Template."""
986        return self.productDefinitionTemplate

Return Product Definition Template.

drtn
988    @property
989    def drtn(self):
990        """Return Data Representation Template Number."""
991        return self.section5[1]

Return Data Representation Template Number.

drt
993    @property
994    def drt(self):
995        """Return Data Representation Template."""
996        return self.dataRepresentationTemplate

Return Data Representation Template.

pdy
 998    @property
 999    def pdy(self):
1000        """Return the PDY ('YYYYMMDD')."""
1001        return "".join([str(i) for i in self.section1[5:8]])

Return the PDY ('YYYYMMDD').

griddef
1003    @property
1004    def griddef(self):
1005        """Return a Grib2GridDef instance for a GRIB2 message."""
1006        return Grib2GridDef.from_section3(self.section3)

Return a Grib2GridDef instance for a GRIB2 message.

lats
1008    @property
1009    def lats(self):
1010        """Return grid latitudes."""
1011        return self.latlons()[0]

Return grid latitudes.

lons
1013    @property
1014    def lons(self):
1015        """Return grid longitudes."""
1016        return self.latlons()[1]

Return grid longitudes.

min
1018    @property
1019    def min(self):
1020        """Return minimum value of data."""
1021        return np.nanmin(self.data)

Return minimum value of data.

max
1023    @property
1024    def max(self):
1025        """Return maximum value of data."""
1026        return np.nanmax(self.data)

Return maximum value of data.

mean
1028    @property
1029    def mean(self):
1030        """Return mean value of data."""
1031        return np.nanmean(self.data)

Return mean value of data.

median
1033    @property
1034    def median(self):
1035        """Return median value of data."""
1036        return np.nanmedian(self.data)

Return median value of data.

shape
1038    @property
1039    def shape(self):
1040        """Return shape of data."""
1041        return self.griddef.shape

Return shape of data.

def attrs_by_section(self, sect: int, values: bool = False):
1098    def attrs_by_section(self, sect: int, values: bool = False):
1099        """
1100        Provide a tuple of attribute names for the given GRIB2 section.
1101
1102        Parameters
1103        ----------
1104        sect
1105            The GRIB2 section number.
1106        values
1107            Optional (default is `False`) argument to return attributes values.
1108
1109        Returns
1110        -------
1111        attrs_by_section
1112            A list of attribute names or dict of name:value pairs if `values =
1113            True`.
1114        """
1115        if sect in {0, 1, 6}:
1116            attrs = templates._section_attrs[sect]
1117        elif sect in {3, 4, 5}:
1118
1119            def _find_class_index(n):
1120                _key = {3: "Grid", 4: "Product", 5: "Data"}
1121                for i, c in enumerate(self.__class__.__mro__):
1122                    if _key[n] in c.__name__:
1123                        return i
1124                else:
1125                    return []
1126
1127            if sys.version_info.minor <= 8:
1128                attrs = templates._section_attrs[sect] + [a for a in dir(self.__class__.__mro__[_find_class_index(sect)]) if not a.startswith("_")]
1129            else:
1130                attrs = templates._section_attrs[sect] + self.__class__.__mro__[_find_class_index(sect)]._attrs()
1131        else:
1132            attrs = []
1133        if values:
1134            return {k: getattr(self, k) for k in attrs}
1135        else:
1136            return attrs

Provide a tuple of attribute names for the given GRIB2 section.

Parameters
  • sect: The GRIB2 section number.
  • values: Optional (default is False) argument to return attributes values.
Returns
  • attrs_by_section: A list of attribute names or dict of name:value pairs if values = True.
def copy(self, deep: bool = True):
1138    def copy(self, deep: bool = True):
1139        """Returns a copy of this Grib2Message.
1140
1141        When `deep=True`, a copy is made of each of the GRIB2 section arrays and
1142        the data are unpacked from the source object and copied into the new
1143        object. Otherwise, a shallow copy of each array is performed and no data
1144        are copied.
1145
1146        Parameters
1147        ----------
1148        deep : bool, default: True
1149            Whether each GRIB2 section array and data are copied onto
1150            the new object. Default is True.
1151
1152        Returns
1153        -------
1154        object : Grib2Message
1155            New Grib2Message object.
1156
1157            .. versionadded:: 2.6.0
1158        """
1159        return copy.deepcopy(self) if deep else copy.copy(self)

Returns a copy of this Grib2Message.

When deep=True, a copy is made of each of the GRIB2 section arrays and the data are unpacked from the source object and copied into the new object. Otherwise, a shallow copy of each array is performed and no data are copied.

Parameters
  • deep : bool, default (True): Whether each GRIB2 section array and data are copied onto the new object. Default is True.
Returns
  • object (Grib2Message): New Grib2Message object.

    New in version 2.6.0.

def pack(self):
1161    def pack(self):
1162        """
1163        Pack GRIB2 section data into a binary message.
1164
1165        It is the user's responsibility to populate the GRIB2 section
1166        information with appropriate metadata.
1167        """
1168        # Create beginning of packed binary message with section 0 and 1 data.
1169        self._sections = []
1170        self._msg, self._pos = g2clib.grib2_create(self.indicatorSection[2:4], self.identificationSection)
1171        self._sections += [0, 1]
1172
1173        # Add section 2 if present.
1174        if isinstance(self.section2, bytes) and len(self.section2) > 0:
1175            self._msg, self._pos = g2clib.grib2_addlocal(self._msg, self.section2)
1176            self._sections.append(2)
1177
1178        # Add section 3.
1179        self.section3[1] = self.nx * self.ny
1180        self._msg, self._pos = g2clib.grib2_addgrid(
1181            self._msg,
1182            self.gridDefinitionSection,
1183            self.gridDefinitionTemplate,
1184            self._deflist,
1185        )
1186        self._sections.append(3)
1187
1188        # Prepare data.
1189        if self._data is None:
1190            if self._ondiskarray is None:
1191                raise ValueError("Grib2Message object has no data, thus it cannot be packed.")
1192        field = np.copy(self.data)
1193        if self.scanModeFlags is not None:
1194            if self.scanModeFlags[3]:
1195                fieldsave = field.astype("f")  # Casting makes a copy
1196                field[1::2, :] = fieldsave[1::2, ::-1]
1197        fld = field.astype("f")
1198
1199        # Prepare bitmap, if necessary
1200        bitmapflag = self.bitMapFlag.value
1201        if bitmapflag == 0:
1202            if self.bitmap is not None:
1203                bmap = np.ravel(self.bitmap).astype(DEFAULT_NUMPY_INT)
1204            else:
1205                bmap = np.ravel(np.where(np.isnan(fld), 0, 1)).astype(DEFAULT_NUMPY_INT)
1206        else:
1207            bmap = None
1208
1209        # Prepare data for packing if nans are present
1210        fld = np.ravel(fld)
1211        if bitmapflag in {0, 254}:
1212            fld = np.where(np.isnan(fld), 0, fld)
1213        else:
1214            if np.isnan(fld).any():
1215                if hasattr(self, "priMissingValue"):
1216                    fld = np.where(np.isnan(fld), self.priMissingValue, fld)
1217            if hasattr(self, "_missvalmap"):
1218                if hasattr(self, "priMissingValue"):
1219                    fld = np.where(self._missvalmap == 1, self.priMissingValue, fld)
1220                if hasattr(self, "secMissingValue"):
1221                    fld = np.where(self._missvalmap == 2, self.secMissingValue, fld)
1222
1223        # Add sections 4, 5, 6, and 7.
1224        self._msg, self._pos = g2clib.grib2_addfield(
1225            self._msg,
1226            self.pdtn,
1227            self.productDefinitionTemplate,
1228            self._coordlist,
1229            self.drtn,
1230            self.dataRepresentationTemplate,
1231            fld,
1232            bitmapflag,
1233            bmap,
1234        )
1235        self._sections.append(4)
1236        self._sections.append(5)
1237        self._sections.append(6)
1238        self._sections.append(7)
1239
1240        # Finalize GRIB2 message with section 8.
1241        self._msg, self._pos = g2clib.grib2_end(self._msg)
1242        self._sections.append(8)
1243        self.section0[-1] = len(self._msg)

Pack GRIB2 section data into a binary message.

It is the user's responsibility to populate the GRIB2 section information with appropriate metadata.

data: <built-in function array>
1245    @property
1246    def data(self) -> np.array:
1247        """Access the unpacked data values."""
1248        if self._data is None:
1249            if self._auto_nans != _AUTO_NANS:
1250                self._data = self._ondiskarray
1251            self._data = np.asarray(self._ondiskarray)
1252        return self._data

Access the unpacked data values.

def flush_data(self):
1288    def flush_data(self):
1289        """
1290        Flush the unpacked data values from the Grib2Message object.
1291
1292        Notes
1293        -----
1294        If the Grib2Message object was constructed from "scratch" (i.e.
1295        not read from file), this method will remove the data array from
1296        the object and it cannot be recovered.
1297        """
1298        self._data = None
1299        self.bitmap = None

Flush the unpacked data values from the Grib2Message object.

Notes

If the Grib2Message object was constructed from "scratch" (i.e. not read from file), this method will remove the data array from the object and it cannot be recovered.

def latlons(self, *args, **kwrgs):
1307    def latlons(self, *args, **kwrgs):
1308        """Alias for `grib2io.Grib2Message.grid` method."""
1309        return self.grid(*args, **kwrgs)

Alias for grib2io.Grib2Message.grid method.

def grid(self, unrotate: bool = True):
1311    def grid(self, unrotate: bool = True):
1312        """
1313        Return lats,lons (in degrees) of grid.
1314
1315        Currently can handle reg. lat/lon,cglobal Gaussian, mercator,
1316        stereographic, lambert conformal, albers equal-area, space-view and
1317        azimuthal equidistant grids.
1318
1319        Parameters
1320        ----------
1321        unrotate
1322            If `True` [DEFAULT], and grid is rotated lat/lon, then unrotate the
1323            grid, otherwise `False`, do not.
1324
1325        Returns
1326        -------
1327        lats, lons : numpy.ndarray
1328            Returns two numpy.ndarrays with dtype=numpy.float32 of grid
1329            latitudes and longitudes in units of degrees.
1330        """
1331        if self._sha1_section3 in _latlon_datastore.keys():
1332            return (
1333                _latlon_datastore[self._sha1_section3]["latitude"],
1334                _latlon_datastore[self._sha1_section3]["longitude"],
1335            )
1336        gdtn = self.gridDefinitionTemplateNumber.value
1337        reggrid = self.gridDefinitionSection[2] == 0  # This means regular 2-d grid
1338        if gdtn == 0:
1339            # Regular lat/lon grid
1340            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1341            lon2, lat2 = self.longitudeLastGridpoint, self.latitudeLastGridpoint
1342            dlon = self.gridlengthXDirection
1343            if lon2 < lon1 and dlon < 0:
1344                lon1 = -lon1
1345            lats = np.linspace(lat1, lat2, self.ny)
1346            if reggrid:
1347                lons = np.linspace(lon1, lon2, self.nx)
1348            else:
1349                lons = np.linspace(lon1, lon2, self.ny * 2)
1350            lons, lats = np.meshgrid(lons, lats)  # Make 2-d arrays.
1351        elif gdtn == 1:  # Rotated Lat/Lon grid
1352            pj = pyproj.Proj(self.projParameters)
1353            lat1, lon1 = self.latitudeFirstGridpoint, self.longitudeFirstGridpoint
1354            lat2, lon2 = self.latitudeLastGridpoint, self.longitudeLastGridpoint
1355            if lon1 > 180.0:
1356                lon1 -= 360.0
1357            if lon2 > 180.0:
1358                lon2 -= 360.0
1359            lats = np.linspace(lat1, lat2, self.ny)
1360            lons = np.linspace(lon1, lon2, self.nx)
1361            lons, lats = np.meshgrid(lons, lats)  # Make 2-d arrays.
1362            if unrotate:
1363                from grib2io.utils import rotated_grid
1364
1365                lats, lons = rotated_grid.unrotate(
1366                    lats,
1367                    lons,
1368                    self.anglePoleRotation,
1369                    self.latitudeSouthernPole,
1370                    self.longitudeSouthernPole,
1371                )
1372        elif gdtn == 40:  # Gaussian grid (only works for global!)
1373            from grib2io.utils.gauss_grid import gaussian_latitudes
1374
1375            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1376            lon2, lat2 = self.longitudeLastGridpoint, self.latitudeLastGridpoint
1377            nlats = self.ny
1378            if not reggrid:  # Reduced Gaussian grid.
1379                nlons = 2 * nlats
1380                dlon = 360.0 / nlons
1381            else:
1382                nlons = self.nx
1383                dlon = self.gridlengthXDirection
1384            lons = np.linspace(lon1, lon2, nlons)
1385            # Compute Gaussian lats (north to south)
1386            lats = gaussian_latitudes(nlats)
1387            if lat1 < lat2:  # reverse them if necessary
1388                lats = lats[::-1]
1389            lons, lats = np.meshgrid(lons, lats)
1390        elif gdtn in {10, 20, 30, 31, 110}:
1391            # Mercator, Lambert Conformal, Stereographic, Albers Equal Area,
1392            # Azimuthal Equidistant
1393            dx, dy = self.gridlengthXDirection, self.gridlengthYDirection
1394            lon1, lat1 = self.longitudeFirstGridpoint, self.latitudeFirstGridpoint
1395            pj = pyproj.Proj(self.projParameters)
1396            llcrnrx, llcrnry = pj(lon1, lat1)
1397            x = llcrnrx + dx * np.arange(self.nx)
1398            y = llcrnry + dy * np.arange(self.ny)
1399            x, y = np.meshgrid(x, y)
1400            lons, lats = pj(x, y, inverse=True)
1401        elif gdtn == 90:
1402            # Satellite Projection
1403            dx = self.gridlengthXDirection
1404            dy = self.gridlengthYDirection
1405            pj = pyproj.Proj(self.projParameters)
1406            x = dx * np.indices((self.ny, self.nx), "f")[1, :, :]
1407            x -= 0.5 * x.max()
1408            y = dy * np.indices((self.ny, self.nx), "f")[0, :, :]
1409            y -= 0.5 * y.max()
1410            lons, lats = pj(x, y, inverse=True)
1411            # Set lons,lats to 1.e30 where undefined
1412            abslons = np.fabs(lons)
1413            abslats = np.fabs(lats)
1414            lons = np.where(abslons < 1.0e20, lons, 1.0e30)
1415            lats = np.where(abslats < 1.0e20, lats, 1.0e30)
1416        elif gdtn == 32769:
1417            # Special NCEP Grid, Rotated Lat/Lon, Arakawa E-Grid (Non-Staggered)
1418            from grib2io.utils import arakawa_rotated_grid
1419
1420            di, dj = 0.0, 0.0
1421            do_180 = False
1422            idir = 1 if self.scanModeFlags[0] == 0 else -1
1423            jdir = -1 if self.scanModeFlags[1] == 0 else 1
1424            0 if self.resolutionAndComponentFlags[4] == 0 else 1
1425            la1 = self.latitudeFirstGridpoint
1426            lo1 = self.longitudeFirstGridpoint
1427            clon = self.longitudeCenterGridpoint
1428            clat = self.latitudeCenterGridpoint
1429            lasp = clat - 90.0
1430            losp = clon
1431            llat, llon = arakawa_rotated_grid.ll2rot(la1, lo1, lasp, losp)
1432            la2, lo2 = arakawa_rotated_grid.rot2ll(-llat, -llon, lasp, losp)
1433            rlat = -llat
1434            rlon = -llon
1435            if self.nx == 1:
1436                di = 0.0
1437            elif idir == 1:
1438                ti = rlon
1439                while ti < llon:
1440                    ti += 360.0
1441                di = (ti - llon) / float(self.nx - 1)
1442            else:
1443                ti = llon
1444                while ti < rlon:
1445                    ti += 360.0
1446                di = (ti - rlon) / float(self.nx - 1)
1447            if self.ny == 1:
1448                dj = 0.0
1449            else:
1450                dj = (rlat - llat) / float(self.ny - 1)
1451                if dj < 0.0:
1452                    dj = -dj
1453            if idir == 1:
1454                if llon > rlon:
1455                    llon -= 360.0
1456                if llon < 0 and rlon > 0:
1457                    do_180 = True
1458            else:
1459                if rlon > llon:
1460                    rlon -= 360.0
1461                if rlon < 0 and llon > 0:
1462                    do_180 = True
1463            xlat1d = llat + (np.arange(self.ny) * jdir * dj)
1464            xlon1d = llon + (np.arange(self.nx) * idir * di)
1465            xlons, xlats = np.meshgrid(xlon1d, xlat1d)
1466            rot2ll_vectorized = np.vectorize(arakawa_rotated_grid.rot2ll)
1467            lats, lons = rot2ll_vectorized(xlats, xlons, lasp, losp)
1468            if do_180:
1469                lons = np.where(lons > 180.0, lons - 360.0, lons)
1470            vector_rotation_angles_vectorized = np.vectorize(arakawa_rotated_grid.vector_rotation_angles)
1471            rots = vector_rotation_angles_vectorized(lats, lons, clat, losp, xlats)
1472            del xlat1d, xlon1d, xlats, xlons
1473        else:
1474            raise ValueError("Unsupported grid")
1475
1476        _latlon_datastore[self._sha1_section3] = dict(latitude=lats, longitude=lons)
1477        try:
1478            _latlon_datastore[self._sha1_section3]["vector_rotation_angles"] = rots
1479        except NameError:
1480            pass
1481
1482        return lats, lons

Return lats,lons (in degrees) of grid.

Currently can handle reg. lat/lon,cglobal Gaussian, mercator, stereographic, lambert conformal, albers equal-area, space-view and azimuthal equidistant grids.

Parameters
  • unrotate: If True [DEFAULT], and grid is rotated lat/lon, then unrotate the grid, otherwise False, do not.
Returns
  • lats, lons (numpy.ndarray): Returns two numpy.ndarrays with dtype=numpy.float32 of grid latitudes and longitudes in units of degrees.
def map_keys(self):
1484    def map_keys(self):
1485        """
1486        Unpack data grid replacing integer values with strings.
1487
1488        These types of fields are categorical or classifications where data
1489        values do not represent an observable or predictable physical quantity.
1490        An example of such a field would be [Dominant Precipitation Type -
1491        DPTYPE](https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_table4-201.shtml)
1492
1493        Returns
1494        -------
1495        map_keys
1496            numpy.ndarray of string values per element.
1497        """
1498        hold_auto_nans = _AUTO_NANS
1499        set_auto_nans(False)
1500
1501        if (np.all(self.section1[0:2] == [7, 14]) and self.shortName == "PWTHER") or (self._isNDFD and self.shortName in {"WX", "WWA"}):
1502            keys = utils.decode_wx_strings(self.section2)
1503            if hasattr(self, "priMissingValue") and self.priMissingValue not in [
1504                None,
1505                0,
1506            ]:
1507                keys[int(self.priMissingValue)] = "Missing"
1508            if hasattr(self, "secMissingValue") and self.secMissingValue not in [
1509                None,
1510                0,
1511            ]:
1512                keys[int(self.secMissingValue)] = "Missing"
1513            u, inv = np.unique(self.data, return_inverse=True)
1514            fld = np.array([keys[x] for x in u])[inv].reshape(self.data.shape)
1515        else:
1516            # For data whose units are defined in a code table (i.e. classification or mask)
1517            tblname = re.findall(r"\d\.\d+", self.units, re.IGNORECASE)[0]
1518            fld = self.data.astype(np.int32).astype(str)
1519            tbl = tables.get_table(tblname, expand=True)
1520            for val in np.unique(fld):
1521                fld = np.where(fld == val, tbl[val], fld)
1522        set_auto_nans(hold_auto_nans)
1523        return fld

Unpack data grid replacing integer values with strings.

These types of fields are categorical or classifications where data values do not represent an observable or predictable physical quantity. An example of such a field would be Dominant Precipitation Type - DPTYPE

Returns
  • map_keys: numpy.ndarray of string values per element.
def to_bytes(self, validate: bool = True):
1525    def to_bytes(self, validate: bool = True):
1526        """
1527        Return packed GRIB2 message in bytes format.
1528
1529        This will be useful for exporting data in non-file formats. For example,
1530        can be used to output grib data directly to S3 using the boto3 client
1531        without the need to write a temporary file to upload first.
1532
1533        Parameters
1534        ----------
1535        validate: default=True
1536            If `True` (DEFAULT), validates first/last four bytes for proper
1537            formatting, else returns None. If `False`, message is output as is.
1538
1539        Returns
1540        -------
1541        to_bytes
1542            Returns GRIB2 formatted message as bytes.
1543        """
1544        if hasattr(self, "_msg"):
1545            if validate:
1546                if self.validate():
1547                    return self._msg
1548                else:
1549                    return None
1550            else:
1551                return self._msg
1552        else:
1553            return None

Return packed GRIB2 message in bytes format.

This will be useful for exporting data in non-file formats. For example, can be used to output grib data directly to S3 using the boto3 client without the need to write a temporary file to upload first.

Parameters
  • validate (default=True): If True (DEFAULT), validates first/last four bytes for proper formatting, else returns None. If False, message is output as is.
Returns
  • to_bytes: Returns GRIB2 formatted message as bytes.
def interpolate( self, method, grid_def_out, method_options=None, drtn=None, num_threads=1):
1555    def interpolate(self, method, grid_def_out, method_options=None, drtn=None, num_threads=1):
1556        """
1557        Grib2Message Interpolator
1558
1559        Performs spatial interpolation via the [NCEPLIBS-ip
1560        library](https://github.com/NOAA-EMC/NCEPLIBS-ip). This interpolate
1561        method only supports scalar interpolation. If you need to perform
1562        vector interpolation, use the module-level `grib2io.interpolate`
1563        function.
1564
1565        Parameters
1566        ----------
1567        method
1568            Interpolate method to use. This can either be an integer or string
1569            using the following mapping:
1570
1571            | Interpolate Scheme | Integer Value |
1572            | :---:              | :---:         |
1573            | 'bilinear'         | 0             |
1574            | 'bicubic'          | 1             |
1575            | 'neighbor'         | 2             |
1576            | 'budget'           | 3             |
1577            | 'spectral'         | 4             |
1578            | 'neighbor-budget'  | 6             |
1579
1580        grid_def_out : grib2io.Grib2GridDef
1581            Grib2GridDef object of the output grid.
1582        method_options : list of ints, optional
1583            Interpolation options. See the NCEPLIBS-ip documentation for
1584            more information on how these are used.
1585        drtn
1586            Data Representation Template to be used for the returned
1587            interpolated GRIB2 message. When `None`, the data representation
1588            template of the source GRIB2 message is used. Once again, it is the
1589            user's responsibility to properly set the Data Representation
1590            Template attributes.
1591        num_threads : int, optional
1592            Number of OpenMP threads to use for interpolation. The default
1593            value is 1. If NCEPLIBS-ip and grib2io's iplib extension module
1594            was not built with OpenMP, then this keyword argument and value
1595            will have no impact.
1596
1597        Returns
1598        -------
1599        interpolate
1600            If interpolating to a grid, a new Grib2Message object is returned.
1601            The GRIB2 metadata of the new Grib2Message object is identical to
1602            the input except where required to be different because of the new
1603            grid specs and possibly a new data representation template.
1604
1605            If interpolating to station points, the interpolated data values are
1606            returned as a numpy.ndarray.
1607        """
1608        section0 = self.section0
1609        section0[-1] = 0
1610        gds = [0, grid_def_out.npoints, 0, 255, grid_def_out.gdtn]
1611        section3 = np.concatenate((gds, grid_def_out.gdt))
1612        drtn = self.drtn if drtn is None else drtn
1613
1614        msg = Grib2Message(
1615            section0,
1616            self.section1,
1617            self.section2,
1618            section3,
1619            self.section4,
1620            None,
1621            self.bitMapFlag.value,
1622            drtn=drtn,
1623        )
1624
1625        msg._msgnum = -1
1626        msg._deflist = self._deflist
1627        msg._coordlist = self._coordlist
1628        if msg.typeOfValues == 0:
1629            pass
1630        elif msg.typeOfValues == 1:
1631            pass
1632        newdata = interpolate(
1633            self.data,
1634            method,
1635            Grib2GridDef.from_section3(self.section3),
1636            grid_def_out,
1637            method_options=method_options,
1638            num_threads=num_threads,
1639        ).reshape(msg.ny, msg.nx)
1640        msg.section5[0] = grid_def_out.npoints
1641        msg._data = newdata.astype(np.float32)
1642        return msg

Grib2Message Interpolator

Performs spatial interpolation via the NCEPLIBS-ip library. This interpolate method only supports scalar interpolation. If you need to perform vector interpolation, use the module-level grib2io.interpolate function.

Parameters
  • method: Interpolate method to use. This can either be an integer or string using the following mapping:
Interpolate Scheme Integer Value
'bilinear' 0
'bicubic' 1
'neighbor' 2
'budget' 3
'spectral' 4
'neighbor-budget' 6

  • grid_def_out (grib2io.Grib2GridDef): Grib2GridDef object of the output grid.
  • method_options (list of ints, optional): Interpolation options. See the NCEPLIBS-ip documentation for more information on how these are used.
  • drtn: Data Representation Template to be used for the returned interpolated GRIB2 message. When None, the data representation template of the source GRIB2 message is used. Once again, it is the user's responsibility to properly set the Data Representation Template attributes.
  • num_threads (int, optional): Number of OpenMP threads to use for interpolation. The default value is 1. If NCEPLIBS-ip and grib2io's iplib extension module was not built with OpenMP, then this keyword argument and value will have no impact.
Returns
  • interpolate: If interpolating to a grid, a new Grib2Message object is returned. The GRIB2 metadata of the new Grib2Message object is identical to the input except where required to be different because of the new grid specs and possibly a new data representation template.

If interpolating to station points, the interpolated data values are returned as a numpy.ndarray.

def subset(self, lats, lons):
1644    def subset(self, lats, lons):
1645        """
1646        Return a spatial subset.
1647
1648        Currently only supports regular grids of the following types:
1649
1650        | Grid Type                                                    | gdtn  |
1651        | :---:                                                        | :---: |
1652        | Latitude/Longitude, Equidistant Cylindrical, or Plate Carree | 0     |
1653        | Rotated Latitude/Longitude                                   | 1     |
1654        | Mercator                                                     | 10    |
1655        | Polar Stereographic                                          | 20    |
1656        | Lambert Conformal                                            | 30    |
1657        | Albers Equal-Area                                            | 31    |
1658        | Gaussian Latitude/Longitude                                  | 40    |
1659        | Equatorial Azimuthal Equidistant Projection                  | 110   |
1660
1661        Parameters
1662        ----------
1663        lats
1664            List or tuple of latitudes.  The minimum and maximum latitudes will
1665            be used to define the southern and northern boundaries.
1666
1667            The order of the latitudes is not important.  The function will
1668            determine which is the minimum and maximum.
1669
1670            The latitudes should be in decimal degrees with 0.0 at the equator,
1671            positive values in the northern hemisphere increasing to 90, and
1672            negative values in the southern hemisphere decreasing to -90.
1673        lons
1674            List or tuple of longitudes.  The minimum and maximum longitudes
1675            will be used to define the western and eastern boundaries.
1676
1677            The order of the longitudes is not important.  The function will
1678            determine which is the minimum and maximum.
1679
1680            GRIB2 longitudes should be in decimal degrees with 0.0 at the prime
1681            meridian, positive values increasing eastward to 360.  There are no
1682            negative GRIB2 longitudes.
1683
1684            The typical west longitudes that start at 0.0 at the prime meridian
1685            and decrease to -180 westward, are converted to GRIB2 longitudes by
1686            '360 - (absolute value of the west longitude)' where typical
1687            eastern longitudes are unchanged as GRIB2 longitudes.
1688
1689        Returns
1690        -------
1691        subset
1692            A spatial subset of a GRIB2 message.
1693        """
1694        if self.gdtn not in [0, 1, 10, 20, 30, 31, 40, 110]:
1695            raise ValueError(
1696                """
1697
1698Subset only works for
1699    Latitude/Longitude, Equidistant Cylindrical, or Plate Carree (gdtn=0)
1700    Rotated Latitude/Longitude (gdtn=1)
1701    Mercator (gdtn=10)
1702    Polar Stereographic (gdtn=20)
1703    Lambert Conformal (gdtn=30)
1704    Albers Equal-Area (gdtn=31)
1705    Gaussian Latitude/Longitude (gdtn=40)
1706    Equatorial Azimuthal Equidistant Projection (gdtn=110)
1707
1708"""
1709            )
1710
1711        if self.nx == 0 or self.ny == 0:
1712            raise ValueError(
1713                """
1714
1715Subset only works for regular grids.
1716
1717"""
1718            )
1719
1720        newmsg = Grib2Message(
1721            np.copy(self.section0),
1722            np.copy(self.section1),
1723            np.copy(self.section2),
1724            np.copy(self.section3),
1725            np.copy(self.section4),
1726            np.copy(self.section5),
1727        )
1728
1729        msg_latitude, inlons = self.grid()
1730
1731        if lats is None:
1732            lats = (msg_latitude.flatten()[-1], msg_latitude.flatten()[0])
1733
1734        lats = (min(lats), max(lats))
1735
1736        if lons is None:
1737            lons = (inlons.flatten()[0], inlons.flatten()[-1])
1738
1739        lons = (min(lons), max(lons))
1740
1741        # Internally work in common lon data representation (0->360 positive eastward from 0)
1742        lons = np.mod(np.array(lons) + 360, 360)
1743        msg_longitude = np.mod(inlons + 360, 360)
1744
1745        spatial.verify_lat_lon_bounds(lats, lons)
1746
1747        snap_first_point = spatial.snap_to_nearest_cell_center(msg_latitude, msg_longitude, lats[0], lons[0])
1748        snap_last_point = spatial.snap_to_nearest_cell_center(msg_latitude, msg_longitude, lats[1], lons[1])
1749        lats = (snap_first_point[0], snap_last_point[0])
1750        lons = (snap_first_point[1], snap_last_point[1])
1751
1752        if len(msg_latitude.shape) == 2:
1753            mask_lats = np.any((msg_latitude >= lats[0]) & (msg_latitude <= lats[1]), axis=1)
1754        else:
1755            mask_lats = np.any((msg_latitude >= lats[0]) & (msg_latitude <= lats[1]), axis=0)
1756        mask_lons = np.any((msg_longitude >= lons[0]) & (msg_longitude <= lons[1]), axis=0)
1757
1758        newlats = msg_latitude[mask_lats, :][:, mask_lons]
1759        newlons = msg_longitude[mask_lats, :][:, mask_lons]
1760
1761        setattr(newmsg, "latitudeFirstGridpoint", newlats.flatten()[0])
1762        setattr(newmsg, "longitudeFirstGridpoint", newlons.flatten()[0])
1763        setattr(newmsg, "nx", np.count_nonzero(mask_lons))
1764        setattr(newmsg, "ny", np.count_nonzero(mask_lats))
1765
1766        # Set *LastGridpoint attributes even if only used for gdtn=[0, 1, 40].
1767        # Even though unnecessary for some supported grid types, it won't
1768        # affect a grib2io message to set them.
1769        setattr(newmsg, "latitudeLastGridpoint", newlats.flatten()[-1])
1770        setattr(newmsg, "longitudeLastGridpoint", newlons.flatten()[-1])
1771
1772        setattr(
1773            newmsg,
1774            "data",
1775            self.data[mask_lats, :][:, mask_lons],
1776        )
1777
1778        # Need to reset the '_sha1_section3' attribute to the hash of section 3
1779        # so the '.grid()' method ignores the cached lat/lon and instead
1780        # force the '.grid()' method to recompute the lat/lon values for the
1781        # new, subsetted grid.
1782        newmsg._sha1_section3 = hashlib.sha1(newmsg.section3).hexdigest()
1783        newmsg.grid()
1784
1785        return newmsg

Return a spatial subset.

Currently only supports regular grids of the following types:

Grid Type gdtn
Latitude/Longitude, Equidistant Cylindrical, or Plate Carree 0
Rotated Latitude/Longitude 1
Mercator 10
Polar Stereographic 20
Lambert Conformal 30
Albers Equal-Area 31
Gaussian Latitude/Longitude 40
Equatorial Azimuthal Equidistant Projection 110
Parameters
  • lats: List or tuple of latitudes. The minimum and maximum latitudes will be used to define the southern and northern boundaries.

The order of the latitudes is not important. The function will determine which is the minimum and maximum.

The latitudes should be in decimal degrees with 0.0 at the equator, positive values in the northern hemisphere increasing to 90, and negative values in the southern hemisphere decreasing to -90.

  • lons: List or tuple of longitudes. The minimum and maximum longitudes will be used to define the western and eastern boundaries.

The order of the longitudes is not important. The function will determine which is the minimum and maximum.

GRIB2 longitudes should be in decimal degrees with 0.0 at the prime meridian, positive values increasing eastward to 360. There are no negative GRIB2 longitudes.

The typical west longitudes that start at 0.0 at the prime meridian and decrease to -180 westward, are converted to GRIB2 longitudes by '360 - (absolute value of the west longitude)' where typical eastern longitudes are unchanged as GRIB2 longitudes.

Returns
  • subset: A spatial subset of a GRIB2 message.
def validate(self):
1787    def validate(self):
1788        """
1789        Validate a complete GRIB2 message.
1790
1791        The g2c library does its own internal validation when g2_gribend() is called, but
1792        we will check in grib2io also. The validation checks if the first 4 bytes in
1793        self._msg is 'GRIB' and '7777' as the last 4 bytes and that the message length in
1794        section 0 equals the length of the packed message.
1795
1796        Returns
1797        -------
1798        `True` if the packed GRIB2 message is complete and well-formed, `False` otherwise.
1799        """
1800        valid = False
1801        if hasattr(self, "_msg"):
1802            if self._msg[0:4] + self._msg[-4:] == b"GRIB7777":
1803                if self.section0[-1] == len(self._msg):
1804                    valid = True
1805        return valid

Validate a complete GRIB2 message.

The g2c library does its own internal validation when g2_gribend() is called, but we will check in grib2io also. The validation checks if the first 4 bytes in self._msg is 'GRIB' and '7777' as the last 4 bytes and that the message length in section 0 equals the length of the packed message.

Returns
  • True if the packed GRIB2 message is complete and well-formed, False otherwise.
@dataclass
class Grib2GridDef:
2322@dataclass
2323class Grib2GridDef:
2324    """
2325    Class for Grid Definition Template Number and Template as attributes.
2326
2327    This allows for cleaner looking code when passing these metadata around.
2328    For example, the `grib2io._Grib2Message.interpolate` method and
2329    `grib2io.interpolate` function accepts these objects.
2330    """
2331
2332    gdtn: int
2333    gdt: NDArray
2334
2335    @classmethod
2336    def from_section3(cls, section3):
2337        return cls(section3[4], section3[5:])
2338
2339    @property
2340    def nx(self):
2341        """Number of grid points in x-direction."""
2342        return int(self.gdt[7])
2343
2344    @property
2345    def ny(self):
2346        """Number of grid points in y-direction."""
2347        return int(self.gdt[8])
2348
2349    @property
2350    def npoints(self):
2351        """Total number of grid points."""
2352        return int(self.gdt[7] * self.gdt[8])
2353
2354    @property
2355    def shape(self):
2356        """Shape of the grid."""
2357        return (int(self.ny), int(self.nx))
2358
2359    def to_section3(self):
2360        """Return a full GRIB2 section3 array."""
2361        return np.array([0, self.npoints, 0, 0, self.gdtn] + list(self.gdt)).astype(np.int64)

Class for Grid Definition Template Number and Template as attributes.

This allows for cleaner looking code when passing these metadata around. For example, the grib2io._Grib2Message.interpolate method and grib2io.interpolate function accepts these objects.

Grib2GridDef( gdtn: int, gdt: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]])
gdtn: int
gdt: numpy.ndarray[tuple[typing.Any, ...], numpy.dtype[~_ScalarT]]
@classmethod
def from_section3(cls, section3):
2335    @classmethod
2336    def from_section3(cls, section3):
2337        return cls(section3[4], section3[5:])
nx
2339    @property
2340    def nx(self):
2341        """Number of grid points in x-direction."""
2342        return int(self.gdt[7])

Number of grid points in x-direction.

ny
2344    @property
2345    def ny(self):
2346        """Number of grid points in y-direction."""
2347        return int(self.gdt[8])

Number of grid points in y-direction.

npoints
2349    @property
2350    def npoints(self):
2351        """Total number of grid points."""
2352        return int(self.gdt[7] * self.gdt[8])

Total number of grid points.

shape
2354    @property
2355    def shape(self):
2356        """Shape of the grid."""
2357        return (int(self.ny), int(self.nx))

Shape of the grid.

def to_section3(self):
2359    def to_section3(self):
2360        """Return a full GRIB2 section3 array."""
2361        return np.array([0, self.npoints, 0, 0, self.gdtn] + list(self.gdt)).astype(np.int64)

Return a full GRIB2 section3 array.

def msgs_from_index(index: dict, filehandle=None):
648def msgs_from_index(index: dict, filehandle=None):
649    """
650    Construct a list of Grib2Message objects from an index dictionary.
651
652    This function reconstructs a sequence of `Grib2Message` instances using
653    metadata sections stored in an index dictionary. If an open file handle is
654    provided, each message is linked to its on-disk binary data through a
655    `Grib2MessageOnDiskArray`, allowing deferred reading of the actual data
656    values from the GRIB2 file.
657
658    Parameters
659    ----------
660    index : dict
661        Dictionary containing parsed GRIB2 index information, including
662        section data arrays such as ``section0`` through ``section5``,
663        ``sectionOffset``, ``offset``, and ``bmapflag``.
664    filehandle : file-like object, optional
665        An open binary file handle to the GRIB2 file corresponding to the index.
666        If provided, the returned messages can access on-disk data arrays via
667        memory offsets. If not provided, only metadata will be available.
668
669    Returns
670    -------
671    list of Grib2Message
672        List of reconstructed `Grib2Message` objects built from the provided
673        index. Each message contains metadata, and if `filehandle` is given,
674        also references to on-disk data through a `Grib2MessageOnDiskArray`.
675
676    Notes
677    -----
678    - Each message is constructed by zipping the corresponding section entries
679      (sections 0–5 and bitmap flags).
680    - When a file handle is supplied, each message’s `_ondiskarray` attribute is
681      initialized to allow direct access to GRIB2 data values without loading
682      them fully into memory.
683    - The `_msgnum` attribute of each message is assigned sequentially to
684      preserve message order.
685    """
686    n = len(index["section4"])
687
688    def _expand(lst):
689        if len(lst) < n:
690            return [lst[0].copy() for _ in range(n)]
691        return lst
692
693    zipped = zip(
694        _expand(index["section0"]),
695        _expand(index["section1"]),
696        index["section2"],
697        _expand(index["section3"]),
698        index["section4"],
699        index["section5"],
700        index["bmapflag"],
701    )
702    msgs = [Grib2Message(*sections) for sections in zipped]
703
704    if filehandle is not None:
705        for n, (msg, offset, secpos) in enumerate(zip(msgs, index["offset"], index["sectionOffset"])):
706            msg._ondiskarray = Grib2MessageOnDiskArray(
707                shape=(msg.ny, msg.nx),
708                ndim=2,
709                dtype=TYPE_OF_VALUES_DTYPE[msg.typeOfValues],
710                filehandle=filehandle,
711                msg=msg,
712                offset=offset,
713                bitmap_offset=secpos[6],
714                data_offset=secpos[7],
715            )
716            msg._msgnum = n
717    return msgs

Construct a list of Grib2Message objects from an index dictionary.

This function reconstructs a sequence of Grib2Message instances using metadata sections stored in an index dictionary. If an open file handle is provided, each message is linked to its on-disk binary data through a Grib2MessageOnDiskArray, allowing deferred reading of the actual data values from the GRIB2 file.

Parameters
  • index (dict): Dictionary containing parsed GRIB2 index information, including section data arrays such as section0 through section5, sectionOffset, offset, and bmapflag.
  • filehandle (file-like object, optional): An open binary file handle to the GRIB2 file corresponding to the index. If provided, the returned messages can access on-disk data arrays via memory offsets. If not provided, only metadata will be available.
Returns
  • list of Grib2Message: List of reconstructed Grib2Message objects built from the provided index. Each message contains metadata, and if filehandle is given, also references to on-disk data through a Grib2MessageOnDiskArray.
Notes
  • Each message is constructed by zipping the corresponding section entries (sections 0–5 and bitmap flags).
  • When a file handle is supplied, each message’s _ondiskarray attribute is initialized to allow direct access to GRIB2 data values without loading them fully into memory.
  • The _msgnum attribute of each message is assigned sequentially to preserve message order.