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}")
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):
Trueis file handle is close;Falseotherwise. - 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).
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.
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.
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.
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.
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:
Grib2Messageobject when size = 1 or a list of Grib2Messages when size > 1.
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.
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.
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.
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.
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.
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.
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.
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
ais anumpy.ndarray, scalar interpolation will be performed. Ifais atuple, then vector interpolation will be performed with the assumption that u = a[0] and v = a[1] and are bothnumpy.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.ndarrayof dtypenp.float32when scalar interpolation is performed or atupleofnumpy.ndarrays when vector interpolation is performed with the assumptions that 0-index is the interpolated u and 1-index is the interpolated v.
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
ais anumpy.ndarray, scalar interpolation will be performed. Ifais atuple, then vector interpolation will be performed with the assumption that u = a[0] and v = a[1] and are bothnumpy.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.ndarrayof dtypenp.float32when scalar interpolation is performed or atupleofnumpy.ndarrays when vector interpolation is performed with the assumptions that 0-index is the interpolated u and 1-index is the interpolated v.
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.
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.
GRIB2 Section 1, Identification Section
GRIB2 Section 3, Grid Definition Section
Product Definition Template
968 @property 969 def gdtn(self): 970 """Return Grid Definition Template Number""" 971 return self.section3[4]
Return Grid Definition Template Number
973 @property 974 def gdt(self): 975 """Return Grid Definition Template.""" 976 return self.gridDefinitionTemplate
Return Grid Definition Template.
978 @property 979 def pdtn(self): 980 """Return Product Definition Template Number.""" 981 return self.section4[1]
Return Product Definition Template Number.
983 @property 984 def pdt(self): 985 """Return Product Definition Template.""" 986 return self.productDefinitionTemplate
Return Product Definition Template.
988 @property 989 def drtn(self): 990 """Return Data Representation Template Number.""" 991 return self.section5[1]
Return Data Representation Template Number.
993 @property 994 def drt(self): 995 """Return Data Representation Template.""" 996 return self.dataRepresentationTemplate
Return Data Representation Template.
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').
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.
1013 @property 1014 def lons(self): 1015 """Return grid longitudes.""" 1016 return self.latlons()[1]
Return grid longitudes.
1018 @property 1019 def min(self): 1020 """Return minimum value of data.""" 1021 return np.nanmin(self.data)
Return minimum value of data.
1023 @property 1024 def max(self): 1025 """Return maximum value of data.""" 1026 return np.nanmax(self.data)
Return maximum value of data.
1028 @property 1029 def mean(self): 1030 """Return mean value of data.""" 1031 return np.nanmean(self.data)
Return mean value of data.
1033 @property 1034 def median(self): 1035 """Return median value of data.""" 1036 return np.nanmedian(self.data)
Return median value of data.
1038 @property 1039 def shape(self): 1040 """Return shape of data.""" 1041 return self.griddef.shape
Return shape of data.
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.
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.
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.
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.
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.
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.
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, otherwiseFalse, do not.
Returns
- lats, lons (numpy.ndarray): Returns two numpy.ndarrays with dtype=numpy.float32 of grid latitudes and longitudes in units of degrees.
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.
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. IfFalse, message is output as is.
Returns
- to_bytes: Returns GRIB2 formatted message as bytes.
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.
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.
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
Trueif the packed GRIB2 message is complete and well-formed,Falseotherwise.
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.
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.
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.
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.
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
section0throughsection5,sectionOffset,offset, andbmapflag. - 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
Grib2Messageobjects built from the provided index. Each message contains metadata, and iffilehandleis given, also references to on-disk data through aGrib2MessageOnDiskArray.
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
_ondiskarrayattribute is initialized to allow direct access to GRIB2 data values without loading them fully into memory. - The
_msgnumattribute of each message is assigned sequentially to preserve message order.