tdlpackio

Introduction

tdlpackio is a Python package for reading and writing TDLPACK-formatted data contained in Fortran-based unformatted ("sequential") and direct-access ("random-access") files. tdlpackio provides a Cython extension module, tdlpacklib, for interfacing to the libtdlpack Fortran library, a subset of subroutines from the MOS-2000 (MOS2K) software system.

The TDLPACK data format is a GRIB-like binary data format that is exclusive to MOS2K Fortran-based sofftware system. This software system was developed at the NOAA/NWS/Meteorological Development Laboratory (MDL) and its primary purpose is to perform statistical post-processing of meteorological data. TDLPACK format is based on the World Meteorological Organizations (WMO) GRIdded Binary (GRIB), version 1, but with modifications to support the needs of MDL's statistical post-processing needs -- mainly the ability to store 1D (vector), datasets such as station observations, along with 2D gridded data.

The TDLPACK data format are contained in two types of Fortran-based files; sequential or random-access. Sequential files are variable length, record-based, and unformatted. Random-access files are fixed-length and direct-access. tdlpackio accommodates reading and writing of both types of TDLPACK files.

TDLPACK files can contain two other types of records: a station call letter record and trailer record. A station call letter record can exist in both types of TDLPACK files and contains a stream of alphanumeric characters (CHARACTER(LEN=8)). A trailer record exists to signal that either another station call letter record is about to be read or we have reached the end of the file (EOF). A trailer record is not written to a random-access file.

For more information on the MOS-2000 software system and TDLPACK format, user is referred to the official MOS-2000 documentation.

Verison 2.0 Refactor

tdlpackio v2.0 is a major factor of pytdlpack v1.x. The API, code design, and structure borrow heavily from grib2io v2. As stated, TDLPACK data can live in a sequential or random-access file, can contain vector or gridded data. tdlpackio v2 aims to provide data to the user in a consistent manner. When opening a TDLPACK file, records are lazily-indexed so that data are only read and unpacked only when necessary. tdlpackio performs the TDLPACK file reading natively in Python. The open class contains methods for reading and indexing. However, packing and unpacking TDLPACK data as well as writing to files are performed by the subroutines in libtdlpack.

Tutorials

The following Jupyter Notebooks are available as tutorials:

 1from __future__ import annotations
 2
 3# init for tdlpackio package
 4from ._tdlpackio import *
 5from ._tdlpackio import __doc__
 6
 7from .grids import GRIDS, TdlpackGridDef, get_grid, has_grid
 8from .version import version as __version__
 9
10__all__ = [
11    "__version__",
12    "open",
13    "TdlpackRecord",
14    "TdlpackStationRecord",
15    "TdlpackTrailerRecord",
16    "TdlpackID",
17    "TdlpackGridDef",
18    "GRIDS",
19    "get_grid",
20    "has_grid",
21]
22
23try:
24    from . import __config__
25
26    __version__ = __config__.tdlpackio_version
27    has_openmp_support = __config__.has_openmp_support
28    tdlpack_static = __config__.tdlpack_static
29    extra_objects = __config__.extra_objects
30except ImportError:
31    has_openmp_support = None
32    tdlpack_static = None
33    extra_objects = []
34
35__tdlpacklib_version__ = "1.0.0"  # For now...
36
37
38def show_config():
39    """Print tdlpackio build configuration information."""
40    print(f"tdlpackio version {__version__} Configuration:")
41    print("")
42    print(f"libtdlpack library version: {__tdlpacklib_version__}")
43    print(f"\tStatic library: {tdlpack_static}")
44    print(f"\tOpenMP support: {has_openmp_support}")
45    print("")
46    print("Static libs:")
47    for lib in extra_objects:
48        print(f"\t{lib}")
__version__ = '2.0.0b4'
class open:
 90class open:
 91    """
 92    Open class for tdlpackio.
 93
 94    Parameters
 95    ----------
 96    path : str
 97        File name.
 98
 99    mode : {'r', 'w'}, default 'r'
100        File handle mode.
101
102    format : {'sequential', 'random-access'}, optional
103        File type when creating a new file.
104
105    ra_template : {'small', 'large'}, optional
106        Template used when creating random-access files.
107    """
108
109    _filetype_map = {
110        "random-access": 1,
111        "sequential": 2,
112    }
113
114    def __init__(self, path, mode="r", format=None, ra_template=None):
115        if mode not in {"r", "w", "a"}:
116            raise ValueError(f"Invalid mode: {mode}")
117        if mode in {"r", "w"}:
118            mode = mode + "b"
119        elif mode == "a":
120            mode = "wb"
121        self.path = path
122        self.mode = mode
123        self.format = format
124        self.ra_template = ra_template
125        self._hasindex = False
126        self._index = {}
127        self._lun = -1
128        self.mode = mode
129        self.name = os.path.abspath(path)
130        self.records = 0
131
132        # Perform indexing on read
133        if "r" in self.mode:
134            self._filehandle = builtins.open(path, mode=mode)
135            self.filetype = self._get_tdlpack_file_type()
136            self._build_index()
137
138        elif "w" in self.mode:
139            self.bytes_written = 0
140            self.records_written = 0
141            self.filetype = format if format is not None else "sequential"
142            if self.filetype == "random-access":
143                ra_template = "small" if ra_template is None else ra_template
144                iret, self._lun = tdlpacklib.open_tdlpack_file(self.name, self.mode, self._ifiletype, ra_template=ra_template)
145            elif self.filetype == "sequential":
146                self._filehandle = builtins.open(path, mode=mode)
147                iret, self._lun = tdlpacklib.open_tdlpack_file(self.name, self.mode, self._ifiletype, ra_template=ra_template)
148            self._station_id_record_hashes = []
149
150        # Add self to file data store
151        _open_file_store[self.name] = self
152
153    @property
154    def _ifiletype(self):
155        """Return numeric filetype"""
156        return self._filetype_map[self.filetype]
157
158    @property
159    def size(self):
160        """Return the file size."""
161        return os.path.getsize(self.name)
162
163    def __enter__(self):
164        """"""
165        return self
166
167    def __exit__(self, atype, value, traceback):
168        """"""
169        self.close()
170
171    def __iter__(self):
172        """"""
173        yield from self._index["record"]
174
175    def __repr__(self):
176        """"""
177        strings = []
178        keys = list(self.__dict__.keys())
179        for k in keys:
180            if not k.startswith("_"):
181                strings.append(f"{k} = {self.__dict__[k]}\n")
182        # Attach size property.
183        strings.append(f"size = {self.size}\n")
184        return "".join(strings)
185
186    def __getitem__(self, key):
187        """"""
188        if isinstance(key, slice):
189            return self._index["record"][key]
190        elif isinstance(key, int):
191            return self._index["record"][key]
192        else:
193            raise KeyError("Key must be an integer record number or a slice")
194
195    def _get_tdlpack_file_type(self):
196        """Determine the type of TDLPACK file"""
197        self._filehandle.seek(0)
198        a = struct.unpack(">i", self._filehandle.read(4))[0]
199        b = struct.unpack(">i", self._filehandle.read(4))[0]
200        self._filehandle.seek(0)
201        return "random-access" if [a, b] == [0, 4] else "sequential"
202
203    def _build_index(self):
204        """Record Indexer"""
205        # Initialize index dictionary
206        self._index["offset"] = []
207        self._index["size"] = []
208        self._index["type"] = []
209        self._index["record"] = []
210
211        if self.filetype == "random-access":
212            self._randomaccess_file_indexer()
213        elif self.filetype == "sequential":
214            self._sequential_file_indexer()
215        self._hasindex = True
216
217    def _randomaccess_file_indexer(self):
218        """Indexer for random-access TDLPACK files"""
219        # Read master key
220        version, nids, nwords, nkyrec, maxent, lastky = struct.unpack(">iiiiii", self._filehandle.read(24))
221        nbytes = nwords * NBYPWD
222        last_key_check = [99999999] if lastky > 9999 else [9999, 99999999]
223        self.master_key = dict(
224            version=version,
225            nids=nids,
226            nwords=nwords,
227            nkyrec=nkyrec,
228            maxent=maxent,
229            lastky=lastky,
230        )
231        self.key_records = []
232        # Set file position to first key record
233        self._filehandle.seek(nbytes)
234
235        last_station_id_rec = -1
236        last_station_lat_rec = -1
237        last_station_lon_rec = -1
238
239        # Iterate over all key records
240        while True:
241            # Read key record "header" data
242            nkeys, prec_this_key, prec_next_key = struct.unpack(">iii", self._filehandle.read(12))
243            self.key_records.append(
244                dict(
245                    nkeys=nkeys,
246                    prec_this_key=prec_this_key,
247                    prec_next_key=prec_next_key,
248                )
249            )
250
251            ids = list()
252            nsize = list()
253            prec_begin = list()
254
255            # Read key record information
256            for i in range(nkeys):
257                id1, id2, id3, id4, nd, bprec = struct.unpack(">iiiiii", self._filehandle.read(24))
258                ids.append([id1, id2, id3, id4])
259                nsize.append(nd)
260                prec_begin.append(bprec)
261
262            # Using key record info, move around file to inventory TDLPACK data
263            for m, n, b in zip(ids, nsize, prec_begin):
264                # Disect prec_begin
265                prec1 = int(b / 1000.0)
266
267                # Offset to the data record
268                offset = (prec1 - 1) * nbytes
269                self._filehandle.seek(offset)
270                self._index["offset"].append(offset)
271                self._index["size"].append(n * NBYPWD)
272
273                # Determine record type.  Since the 4-word ID is stored in the
274                # key record, we can use it to determine station call letter
275                # record or TDLPACK data record.
276                if m[0] == 400001000:
277                    # Station ID record...not in TDLPACK format
278                    rec = TdlpackStationRecord()
279                    last_station_id_rec = self.records
280                    rec._recnum = self.records
281                    rec._source = self.name
282                    rec._nsta_expected = int(n / 2)
283                    self._index["record"].append(rec)
284                    self._index["type"].append("station")
285                else:
286                    if m[0] == 400006000:
287                        last_station_lat_rec = self.records
288                    elif m[0] == 400007000:
289                        last_station_lon_rec = self.records
290                    # TDLPACK data record
291                    ipack = np.frombuffer(self._filehandle.read(132), dtype=">i4").astype(np.int32)
292                    iret, is0, is1, is2, is4 = tdlpacklib.unpack_meta(ipack)
293                    if np.all(is2 == 0):
294                        is2 = None
295                    rec = TdlpackRecord(is0, is1, is2, is4)
296                    rec._recnum = self.records
297                    rec._linked_station_id_record = last_station_id_rec
298                    rec._linked_station_lat_record = last_station_lat_rec
299                    rec._linked_station_lon_record = last_station_lon_rec
300                    rec._source = self.name
301                    shape = (rec.ny, rec.nx) if rec.type == "grid" else (rec.numberOfPackedValues,)
302                    ndim = len(shape)
303                    dtype = "float32"
304                    rec._data = TdlpackRecordOnDiskArray(
305                        shape,
306                        ndim,
307                        dtype,
308                        self.filetype,
309                        self._filehandle,
310                        rec,
311                        self._index["offset"][-1],
312                        self._index["size"][-1],
313                    )
314                    self._index["record"].append(rec)
315                    self._index["type"].append("data")
316                self.records += 1
317
318            # Hold the record number of the last station ID record
319            if self._index["type"][-1] == "station":
320                _last_station_id_record = self.records  # This should be OK.
321
322            # Break loop here, at last key record
323            if prec_next_key in last_key_check:
324                break
325            offset = (prec_next_key - 1) * nbytes
326            self._filehandle.seek(offset)
327
328        # Make key_records immutable
329        self.key_records = tuple(self.key_records)
330
331    def _sequential_file_indexer(self):
332        """Indexer for sequential TDLPACK files"""
333        last_station_id_rec = -1
334        last_station_lat_rec = -1
335        last_station_lon_rec = -1
336
337        # Iterate
338        while True:
339            try:
340                # First read 4-byte Fortran record header
341                pos = self._filehandle.tell()
342                fortran_header = struct.unpack(">i", self._filehandle.read(4))[0]
343                if fortran_header >= 132:
344                    bytes_to_read = 132
345                else:
346                    bytes_to_read = fortran_header
347
348                pos = self._filehandle.tell()
349                ioctet = np.frombuffer(self._filehandle.read(8), dtype=">i8").astype(np.int64)[0]
350                ipack = np.frombuffer(self._filehandle.read(bytes_to_read - 8), dtype=">i4").astype(np.int32)
351                _header = struct.unpack(">4s", ipack[0])[0].decode()
352
353                # Check to first 4 bytes of the data record to determine the data
354                # record type.
355                if _header == "PLDT":
356                    if ipack[5] == 400006000:
357                        last_station_lat_rec = self.records
358                    elif ipack[5] == 400007000:
359                        last_station_lon_rec = self.records
360                    # TDLPACK data record
361                    iret, is0, is1, is2, is4 = tdlpacklib.unpack_meta(ipack)
362                    self._index["offset"].append(pos)
363                    self._index["size"].append(fortran_header)  # Size given by Fortran header
364                    if np.all(is2 == 0):
365                        is2 = None
366                    rec = TdlpackRecord(is0, is1, is2, is4)
367                    rec._recnum = self.records
368                    rec._linked_station_id_record = last_station_id_rec
369                    rec._linked_station_lat_record = last_station_lat_rec
370                    rec._linked_station_lon_record = last_station_lon_rec
371                    rec._source = self.name
372                    shape = (rec.ny, rec.nx) if rec.type == "grid" else (rec.numberOfPackedValues,)
373                    ndim = len(shape)
374                    dtype = "float32"
375                    rec._data = TdlpackRecordOnDiskArray(
376                        shape,
377                        ndim,
378                        dtype,
379                        self.filetype,
380                        self._filehandle,
381                        rec,
382                        self._index["offset"][-1],
383                        self._index["size"][-1],
384                    )
385                    self._index["record"].append(rec)
386                    self._index["type"].append("data")
387                else:
388                    if ioctet == 24 and ipack[4] == 9999:
389                        # Trailer record
390                        rec = TdlpackTrailerRecord()
391                        rec._recnum = self.records
392                        rec._source = self.name
393                        self._index["offset"].append(pos)
394                        self._index["size"].append(fortran_header)
395                        self._index["type"].append("trailer")
396                        self._index["record"].append(rec)
397                    else:
398                        # Station ID record
399                        rec = TdlpackStationRecord()
400                        last_station_id_rec = self.records
401                        rec._recnum = self.records
402                        rec._source = self.name
403                        rec._nsta_expected = int(ioctet / 8)
404                        self._index["offset"].append(pos)
405                        self._index["size"].append(fortran_header)
406                        self._index["type"].append("station")
407                        self._index["record"].append(rec)
408
409                # At this point we have successfully identified a TDLPACK record from
410                # the file. Increment self.records and position the file pointer to
411                # now read the Fortran trailer.
412                self.records += 1  # Includes trailer records
413                self._filehandle.seek(fortran_header - bytes_to_read, 1)
414                fortran_trailer = struct.unpack(">i", self._filehandle.read(4))[0]
415
416                # Check Fortran header and trailer for the record.
417                if fortran_header != fortran_trailer:
418                    raise IOError("Bad Fortran record.")
419
420                # Hold the record number of the last station ID record
421                if self._index["type"][-1] == "station":
422                    _last_station_id_record = self.records  # This should be OK.
423
424            except struct.error:
425                self._filehandle.seek(0)
426                break
427
428    def read(self, n):
429        """
430        Read record from file.
431
432        Parameters
433        ----------
434        n : int
435            Record number.
436
437        Returns
438        -------
439        numpy.ndarray
440            Record data as a NumPy array. The returned dtype depends on the
441            record type:
442
443            - ``data`` or ``trailer`` : ``int32`` array.
444            - ``station`` : fixed-width byte string array (``S8``).
445
446        Notes
447        -----
448        The file pointer is positioned using the internal index before reading.
449        Record size is determined from the file header (sequential) or index
450        (random-access).
451        """
452        if "w" in self.mode:
453            pass  # Remove this at some point....
454        # Position file pointer to the beginning of the TDLPACK record.
455        self._filehandle.seek(self._index["offset"][n])
456        if self.filetype == "sequential":
457            size = np.frombuffer(self._filehandle.read(8), dtype=">i8").astype(np.int64)[0]
458        elif self.filetype == "random-access":
459            size = self._index["size"][n]
460
461        if self._index["type"][n] in {"data", "trailer"}:
462            return np.frombuffer(self._filehandle.read(size), dtype=">i4").astype(np.int32)
463        elif self._index["type"][n] == "station":
464            return np.frombuffer(self._filehandle.read(size), dtype="S8")
465
466    def write(self, record):
467        """
468        Write record(s) to file.
469
470        Parameters
471        ----------
472        record : TdlpackStationRecord, _TdlpackRecord, TdlpackTrailerRecord, or list
473            Record or list of records to write.
474
475            - ``TdlpackStationRecord`` : Station record. Station identifiers
476              are padded to ``NCHAR``.
477            - ``_TdlpackRecord`` : Packed data record.
478            - ``TdlpackTrailerRecord`` : Trailer record.
479            - ``list`` : List of supported record types.
480
481        Returns
482        -------
483        None
484
485        Notes
486        -----
487        Updates ``bytes_written``, ``records_written``, ``records``, and
488        ``_type_lastrecord_written``.
489        """
490        if isinstance(record, list):
491            for rec in record:
492                self.write(rec)
493            return
494
495        nreplace, ncheck = 0, 0
496
497        if isinstance(record, TdlpackStationRecord):
498            # Adjust string length of each station to NCHAR.
499            stns = [s.ljust(NCHAR) for s in record.stations]
500            iret, self.bytes_written, self.records_written = tdlpacklib.write_station_record(
501                self.name,
502                self._lun,
503                self._ifiletype,
504                stns,
505                self.bytes_written,
506                self.records_written,
507                nreplace,
508                ncheck,
509            )
510            self._station_id_record_hashes.append(hash(record))
511
512        elif issubclass(record.__class__, _TdlpackRecord):
513            # When writing a vector record to a sequential file, we need to make sure a station call letter
514            # record exists and is valid for the data record.
515            if self.filetype == "sequential" and record.type == "vector":
516                if record._source is not None:
517                    if hash(_open_file_store[record._source][record._linked_station_id_record]) not in self._station_id_record_hashes:
518                        if self.records > 0:
519                            self.write(TdlpackTrailerRecord())
520                        # Write the appropriate station call letter record
521                        self.write(_open_file_store[record._source][record._linked_station_id_record])
522
523            if not hasattr(record, "_ipack"):
524                record.pack()
525
526            iret, self.bytes_written, self.records_written = tdlpacklib.write_tdlpack_record(
527                self.name,
528                self._lun,
529                self._ifiletype,
530                record._ipack,
531                self.bytes_written,
532                self.records_written,
533                nreplace,
534                ncheck,
535            )
536
537        elif isinstance(record, TdlpackTrailerRecord):
538            iret, self.bytes_written, self.records_written = tdlpacklib.write_trailer_record(
539                self._lun, self._ifiletype, self.bytes_written, self.records_written
540            )
541
542        self._type_lastrecord_written = record.type
543        self.records += 1
544
545    def close(self):
546        """
547        Close the file.
548
549        Returns
550        -------
551        None
552
553        Notes
554        -----
555        For write mode, a trailer record may be written for sequential files
556        if the last record type requires it. The underlying TDLpack file handle
557        is then closed. The file is removed from the internal open file store.
558        """
559        if "r" in self.mode:
560            self._filehandle.close()
561        if "w" in self.mode:
562            if self.filetype == "sequential":
563                if self._type_lastrecord_written == "vector":
564                    iret = tdlpacklib.write_trailer_record(
565                        self._lun,
566                        self._ifiletype,
567                        self.bytes_written,
568                        self.records_written,
569                    )
570            iret = tdlpacklib.close_tdlpack_file(self._lun, self._ifiletype)
571            if iret != 0:
572                raise ValueError("return from tdlpacklib.close_tdlpack_file is non-zero")
573        if self.name in _open_file_store.keys():
574            del _open_file_store[self.name]
575
576    def select(self, **kwargs):
577        """
578        Select records by attribute.
579
580        Parameters
581        ----------
582        **kwargs : dict
583            Keyword arguments specifying ``TdlpackRecord`` attributes and
584            values to match.
585
586        Returns
587        -------
588        list
589            List of records matching all provided attribute/value pairs.
590
591        Notes
592        -----
593        Selection is performed against records in the internal index. Records
594        must match all specified attributes to be included in the result.
595        """
596        _id_keys = [
597            "word1",
598            "word2",
599            "word3",
600            "word4",
601            "ccc",
602            "fff",
603            "b",
604            "dd",
605            "v",
606            "llll",
607            "uuuu",
608            "t",
609            "rr",
610            "o",
611            "hh",
612            "tau",
613            "thresh",
614            "i",
615            "s",
616            "g",
617        ]
618
619        # TODO: Add ability to process multiple values for each keyword (attribute)
620        idxs = []
621        nkeys = len(kwargs.keys())
622        for k, v in kwargs.items():
623            if k in _id_keys:
624                for rec in self._index["record"]:
625                    # if hasattr(rec, k) and getattr(rec, k) == v:
626                    if getattr(rec.id, k) == v:
627                        idxs.append(rec._recnum)
628            else:
629                for rec in self._index["record"]:
630                    if hasattr(rec, k) and getattr(rec, k) == v:
631                        idxs.append(rec._recnum)
632        idxs = np.array(idxs, dtype=">i4")
633        return [self._index["record"][i] for i in [ii[0] for ii in collections.Counter(idxs).most_common() if ii[1] == nkeys]]

Open class for tdlpackio.

Parameters
  • path (str): File name.
  • mode ({'r', 'w'}, default 'r'): File handle mode.
  • format ({'sequential', 'random-access'}, optional): File type when creating a new file.
  • ra_template ({'small', 'large'}, optional): Template used when creating random-access files.
open(path, mode='r', format=None, ra_template=None)
114    def __init__(self, path, mode="r", format=None, ra_template=None):
115        if mode not in {"r", "w", "a"}:
116            raise ValueError(f"Invalid mode: {mode}")
117        if mode in {"r", "w"}:
118            mode = mode + "b"
119        elif mode == "a":
120            mode = "wb"
121        self.path = path
122        self.mode = mode
123        self.format = format
124        self.ra_template = ra_template
125        self._hasindex = False
126        self._index = {}
127        self._lun = -1
128        self.mode = mode
129        self.name = os.path.abspath(path)
130        self.records = 0
131
132        # Perform indexing on read
133        if "r" in self.mode:
134            self._filehandle = builtins.open(path, mode=mode)
135            self.filetype = self._get_tdlpack_file_type()
136            self._build_index()
137
138        elif "w" in self.mode:
139            self.bytes_written = 0
140            self.records_written = 0
141            self.filetype = format if format is not None else "sequential"
142            if self.filetype == "random-access":
143                ra_template = "small" if ra_template is None else ra_template
144                iret, self._lun = tdlpacklib.open_tdlpack_file(self.name, self.mode, self._ifiletype, ra_template=ra_template)
145            elif self.filetype == "sequential":
146                self._filehandle = builtins.open(path, mode=mode)
147                iret, self._lun = tdlpacklib.open_tdlpack_file(self.name, self.mode, self._ifiletype, ra_template=ra_template)
148            self._station_id_record_hashes = []
149
150        # Add self to file data store
151        _open_file_store[self.name] = self
path
mode
format
ra_template
name
records
size
158    @property
159    def size(self):
160        """Return the file size."""
161        return os.path.getsize(self.name)

Return the file size.

def read(self, n):
428    def read(self, n):
429        """
430        Read record from file.
431
432        Parameters
433        ----------
434        n : int
435            Record number.
436
437        Returns
438        -------
439        numpy.ndarray
440            Record data as a NumPy array. The returned dtype depends on the
441            record type:
442
443            - ``data`` or ``trailer`` : ``int32`` array.
444            - ``station`` : fixed-width byte string array (``S8``).
445
446        Notes
447        -----
448        The file pointer is positioned using the internal index before reading.
449        Record size is determined from the file header (sequential) or index
450        (random-access).
451        """
452        if "w" in self.mode:
453            pass  # Remove this at some point....
454        # Position file pointer to the beginning of the TDLPACK record.
455        self._filehandle.seek(self._index["offset"][n])
456        if self.filetype == "sequential":
457            size = np.frombuffer(self._filehandle.read(8), dtype=">i8").astype(np.int64)[0]
458        elif self.filetype == "random-access":
459            size = self._index["size"][n]
460
461        if self._index["type"][n] in {"data", "trailer"}:
462            return np.frombuffer(self._filehandle.read(size), dtype=">i4").astype(np.int32)
463        elif self._index["type"][n] == "station":
464            return np.frombuffer(self._filehandle.read(size), dtype="S8")

Read record from file.

Parameters
  • n (int): Record number.
Returns
  • numpy.ndarray: Record data as a NumPy array. The returned dtype depends on the record type:

    • data or trailer : int32 array.
    • station : fixed-width byte string array (S8).
Notes

The file pointer is positioned using the internal index before reading. Record size is determined from the file header (sequential) or index (random-access).

def write(self, record):
466    def write(self, record):
467        """
468        Write record(s) to file.
469
470        Parameters
471        ----------
472        record : TdlpackStationRecord, _TdlpackRecord, TdlpackTrailerRecord, or list
473            Record or list of records to write.
474
475            - ``TdlpackStationRecord`` : Station record. Station identifiers
476              are padded to ``NCHAR``.
477            - ``_TdlpackRecord`` : Packed data record.
478            - ``TdlpackTrailerRecord`` : Trailer record.
479            - ``list`` : List of supported record types.
480
481        Returns
482        -------
483        None
484
485        Notes
486        -----
487        Updates ``bytes_written``, ``records_written``, ``records``, and
488        ``_type_lastrecord_written``.
489        """
490        if isinstance(record, list):
491            for rec in record:
492                self.write(rec)
493            return
494
495        nreplace, ncheck = 0, 0
496
497        if isinstance(record, TdlpackStationRecord):
498            # Adjust string length of each station to NCHAR.
499            stns = [s.ljust(NCHAR) for s in record.stations]
500            iret, self.bytes_written, self.records_written = tdlpacklib.write_station_record(
501                self.name,
502                self._lun,
503                self._ifiletype,
504                stns,
505                self.bytes_written,
506                self.records_written,
507                nreplace,
508                ncheck,
509            )
510            self._station_id_record_hashes.append(hash(record))
511
512        elif issubclass(record.__class__, _TdlpackRecord):
513            # When writing a vector record to a sequential file, we need to make sure a station call letter
514            # record exists and is valid for the data record.
515            if self.filetype == "sequential" and record.type == "vector":
516                if record._source is not None:
517                    if hash(_open_file_store[record._source][record._linked_station_id_record]) not in self._station_id_record_hashes:
518                        if self.records > 0:
519                            self.write(TdlpackTrailerRecord())
520                        # Write the appropriate station call letter record
521                        self.write(_open_file_store[record._source][record._linked_station_id_record])
522
523            if not hasattr(record, "_ipack"):
524                record.pack()
525
526            iret, self.bytes_written, self.records_written = tdlpacklib.write_tdlpack_record(
527                self.name,
528                self._lun,
529                self._ifiletype,
530                record._ipack,
531                self.bytes_written,
532                self.records_written,
533                nreplace,
534                ncheck,
535            )
536
537        elif isinstance(record, TdlpackTrailerRecord):
538            iret, self.bytes_written, self.records_written = tdlpacklib.write_trailer_record(
539                self._lun, self._ifiletype, self.bytes_written, self.records_written
540            )
541
542        self._type_lastrecord_written = record.type
543        self.records += 1

Write record(s) to file.

Parameters
  • record (TdlpackStationRecord, _TdlpackRecord, TdlpackTrailerRecord, or list): Record or list of records to write.

Returns
  • None
Notes

Updates bytes_written, records_written, records, and _type_lastrecord_written.

def close(self):
545    def close(self):
546        """
547        Close the file.
548
549        Returns
550        -------
551        None
552
553        Notes
554        -----
555        For write mode, a trailer record may be written for sequential files
556        if the last record type requires it. The underlying TDLpack file handle
557        is then closed. The file is removed from the internal open file store.
558        """
559        if "r" in self.mode:
560            self._filehandle.close()
561        if "w" in self.mode:
562            if self.filetype == "sequential":
563                if self._type_lastrecord_written == "vector":
564                    iret = tdlpacklib.write_trailer_record(
565                        self._lun,
566                        self._ifiletype,
567                        self.bytes_written,
568                        self.records_written,
569                    )
570            iret = tdlpacklib.close_tdlpack_file(self._lun, self._ifiletype)
571            if iret != 0:
572                raise ValueError("return from tdlpacklib.close_tdlpack_file is non-zero")
573        if self.name in _open_file_store.keys():
574            del _open_file_store[self.name]

Close the file.

Returns
  • None
Notes

For write mode, a trailer record may be written for sequential files if the last record type requires it. The underlying TDLpack file handle is then closed. The file is removed from the internal open file store.

def select(self, **kwargs):
576    def select(self, **kwargs):
577        """
578        Select records by attribute.
579
580        Parameters
581        ----------
582        **kwargs : dict
583            Keyword arguments specifying ``TdlpackRecord`` attributes and
584            values to match.
585
586        Returns
587        -------
588        list
589            List of records matching all provided attribute/value pairs.
590
591        Notes
592        -----
593        Selection is performed against records in the internal index. Records
594        must match all specified attributes to be included in the result.
595        """
596        _id_keys = [
597            "word1",
598            "word2",
599            "word3",
600            "word4",
601            "ccc",
602            "fff",
603            "b",
604            "dd",
605            "v",
606            "llll",
607            "uuuu",
608            "t",
609            "rr",
610            "o",
611            "hh",
612            "tau",
613            "thresh",
614            "i",
615            "s",
616            "g",
617        ]
618
619        # TODO: Add ability to process multiple values for each keyword (attribute)
620        idxs = []
621        nkeys = len(kwargs.keys())
622        for k, v in kwargs.items():
623            if k in _id_keys:
624                for rec in self._index["record"]:
625                    # if hasattr(rec, k) and getattr(rec, k) == v:
626                    if getattr(rec.id, k) == v:
627                        idxs.append(rec._recnum)
628            else:
629                for rec in self._index["record"]:
630                    if hasattr(rec, k) and getattr(rec, k) == v:
631                        idxs.append(rec._recnum)
632        idxs = np.array(idxs, dtype=">i4")
633        return [self._index["record"][i] for i in [ii[0] for ii in collections.Counter(idxs).most_common() if ii[1] == nkeys]]

Select records by attribute.

Parameters
  • **kwargs (dict): Keyword arguments specifying TdlpackRecord attributes and values to match.
Returns
  • list: List of records matching all provided attribute/value pairs.
Notes

Selection is performed against records in the internal index. Records must match all specified attributes to be included in the result.

class TdlpackRecord:
636class TdlpackRecord:
637    """
638    Creation class for TDLPACK record objects.
639
640    This class dynamically constructs and returns an instance of a
641    ``_TdlpackRecord`` subclass based on the provided section arrays and
642    optional keyword arguments. Record classes are cached by type to avoid
643    repeated class construction.
644
645    Parameters
646    ----------
647    is0 : numpy.ndarray, optional
648        Section 0 array. Default is zero-initialized ``int32`` array of size ``ND7``.
649    is1 : numpy.ndarray, optional
650        Section 1 array. Default is zero-initialized ``int32`` array of size ``ND7``.
651    is2 : numpy.ndarray, optional
652        Section 2 array. Default is zero-initialized ``int32`` array of size ``ND7``.
653    is4 : numpy.ndarray, optional
654        Section 4 array. Default is zero-initialized ``int32`` array of size ``ND7``.
655    *args : tuple
656        Additional positional arguments passed to the constructed record class.
657    **kwargs : dict
658        Optional keyword arguments. The following key is recognized:
659
660        - ``type`` : str, optional
661          Record type. Default is ``"vector"``. If ``is2`` contains any
662          non-zero values, the type is automatically set to ``"grid"``.
663        - ``grid`` : str, optional
664          Predefined grid. Valid values can be found in ``tdlpackio.grids.GRIDS``.
665          This will force the type to be ``"grid"`` and override values in the ``is2``
666          argument.
667
668    Returns
669    -------
670    _TdlpackRecord
671        Instance of a dynamically generated subclass of ``_TdlpackRecord``.
672
673    Notes
674    -----
675    - Record subclasses are created dynamically and cached in
676      ``_record_class_store`` using the record type as the key.
677    - For ``"grid"`` records, ``templates.GridDefinitionSection`` is added
678      as a base class and ``is1[1]`` is set to indicate the presence of a
679      grid definition section.
680    """
681
682    def __new__(
683        self,
684        is0: np.array = np.zeros((ND7), dtype=np.int32),
685        is1: np.array = np.zeros((ND7), dtype=np.int32),
686        is2: np.array = np.zeros((ND7), dtype=np.int32),
687        is4: np.array = np.zeros((ND7), dtype=np.int32),
688        *args,
689        **kwargs,
690    ):
691
692        rectype = "vector"
693        if "type" in kwargs.keys():
694            rectype = kwargs["type"]
695            if rectype not in {"grid", "vector"}:
696                raise ValueError('Invalid "type" argument. Expected "grid" or "vector".')
697
698        if bool(np.any(is2)):
699            rectype = "grid"
700
701        if "grid" in kwargs.keys():
702            rectype = "grid"
703            if kwargs["grid"] not in grids.GRIDS.keys():
704                raise ValueError('Invalid "grid" argument.')
705            is2 = grids.get_grid(kwargs["grid"]).to_is2()
706
707        bases = list()
708        if rectype == "grid":
709            bases.append(templates.GridDefinitionSection)
710            is1[1] = 1  # Flag in is1 to state that a grid definition section exists
711
712        try:
713            Record = _record_class_store[rectype]
714        except KeyError:
715
716            @dataclass(init=False, repr=False)
717            class Record(_TdlpackRecord, *bases):
718                __hash__ = _TdlpackRecord.__hash__
719                pass
720
721            _record_class_store[rectype] = Record
722
723        return Record(is0, is1, is2, is4, *args)

Creation class for TDLPACK record objects.

This class dynamically constructs and returns an instance of a _TdlpackRecord subclass based on the provided section arrays and optional keyword arguments. Record classes are cached by type to avoid repeated class construction.

Parameters
  • is0 (numpy.ndarray, optional): Section 0 array. Default is zero-initialized int32 array of size ND7.
  • is1 (numpy.ndarray, optional): Section 1 array. Default is zero-initialized int32 array of size ND7.
  • is2 (numpy.ndarray, optional): Section 2 array. Default is zero-initialized int32 array of size ND7.
  • is4 (numpy.ndarray, optional): Section 4 array. Default is zero-initialized int32 array of size ND7.
  • *args (tuple): Additional positional arguments passed to the constructed record class.
  • **kwargs (dict): Optional keyword arguments. The following key is recognized:

    • type : str, optional Record type. Default is "vector". If is2 contains any non-zero values, the type is automatically set to "grid".
    • grid : str, optional Predefined grid. Valid values can be found in tdlpackio.grids.GRIDS. This will force the type to be "grid" and override values in the is2 argument.
Returns
  • _TdlpackRecord: Instance of a dynamically generated subclass of _TdlpackRecord.
Notes
  • Record subclasses are created dynamically and cached in _record_class_store using the record type as the key.
  • For "grid" records, templates.GridDefinitionSection is added as a base class and is1[1] is set to indicate the presence of a grid definition section.
@dataclass
class TdlpackStationRecord:
1149@dataclass
1150class TdlpackStationRecord:
1151    """
1152    TDLPACK Station Record class
1153    """
1154
1155    stations_in: InitVar[Optional[Iterable[str]]] = None
1156
1157    type: str = field(init=False, repr=False, default="station")
1158    stations: ClassVar[templates.Stations] = templates.Stations()
1159
1160    # Private class variable holding the list
1161    _stations: Optional[list[str]] = field(init=False, repr=False, default=None)
1162
1163    def __post_init__(self, stations_in):
1164        self._nsta_expected = 0
1165        self._recnum = -1
1166        self._source = None
1167        self._stations = None
1168        self._type = "station"
1169        self.id = TdlpackID([400001000, 0, 0, 0], self)
1170
1171        if stations_in is not None:
1172            self.stations = stations_in
1173
1174    def __hash__(self):
1175        return hash(tuple(self.stations))
1176
1177    def __str__(self):
1178        return f"{self._recnum}:d=0000000000:STATION CALL LETTER RECORD:{self.numberOfStations}"
1179
1180    @property
1181    def numberOfStations(self):
1182        if self._source is not None and (isinstance(self._stations, list) or self._stations is None):
1183            return self._nsta_expected
1184        return 0 if self.stations is None else len(self.stations)
1185
1186    @property
1187    def data(self):
1188        pass
1189
1190    def pack(self):
1191        pass

TDLPACK Station Record class

TdlpackStationRecord( stations_in: dataclasses.InitVar[typing.Optional[typing.Iterable[str]]] = None)
stations_in: dataclasses.InitVar[typing.Optional[typing.Iterable[str]]] = None
type: str = 'station'
stations: ClassVar[tdlpackio.templates.Stations]

Descriptor class for handling station lists

numberOfStations
1180    @property
1181    def numberOfStations(self):
1182        if self._source is not None and (isinstance(self._stations, list) or self._stations is None):
1183            return self._nsta_expected
1184        return 0 if self.stations is None else len(self.stations)
data
1186    @property
1187    def data(self):
1188        pass
def pack(self):
1190    def pack(self):
1191        pass
@dataclass
class TdlpackTrailerRecord:
1194@dataclass
1195class TdlpackTrailerRecord:
1196    """
1197    TDLPACK Trailer Record class
1198    """
1199
1200    type: str = field(init=False, repr=False, default="trailer")
1201
1202    def __post_init__(self):
1203        """"""
1204        self._recnum = -1
1205        self._type = "trailer"
1206        self.id = TdlpackID([0, 0, 0, 0], self)
1207
1208    def __hash__(self):
1209        return hash((""))
1210
1211    def __str__(self):
1212        """"""
1213        return f"{self._recnum}:d=0000000000:TRAILER RECORD"
1214
1215    @property
1216    def data(self):
1217        """"""
1218        pass
1219
1220    def pack(self):
1221        """"""
1222        pass

TDLPACK Trailer Record class

type: str = 'trailer'
data
1215    @property
1216    def data(self):
1217        """"""
1218        pass
def pack(self):
1220    def pack(self):
1221        """"""
1222        pass
class TdlpackID:
1225class TdlpackID:
1226    """
1227    TDLPACK variable ID class
1228    """
1229
1230    __slots__ = ("_id", "_rec")
1231
1232    def __init__(self, id=[0, 0, 0, 0], linked_rec=None):
1233        """
1234        Initialize TDLPACK variable ID
1235
1236        Parameters
1237        ----------
1238        id : list of ints
1239            The 4-word TDLPACK variable ID
1240        linked_rec : TdlpackRecord, optional
1241            TDLPACK record object. This optional argument provides a mechanism
1242            for updating the TDLPACK variable ID in the TdlpackRecord is1 array.
1243        """
1244        self._id = utils.parse_id(id)
1245        self._rec = linked_rec
1246
1247    def __eq__(self, value):
1248        if isinstance(value, list) or isinstance(value, tuple):
1249            return self.word1 == value[0] and self.word2 == value[1] and self.word3 == value[2] and self.word4 == value[3]
1250        else:
1251            return False
1252
1253    def __format__(self, spec: str) -> str:
1254        if not spec:
1255            spec = "basic"
1256        return self.format(spec)
1257
1258    def __repr__(self):
1259        return repr(utils.unparse_id(self._id))
1260
1261    def __setitem__(self, key, value):
1262        self._id[key] = value
1263
1264    def __getitem__(self, key):
1265        return self._id[key]
1266
1267    @classmethod
1268    def from_string(cls, idstr):
1269        """
1270        Create a TdlpackID object from a TDLPACK ID string.
1271
1272        Parameters
1273        ----------
1274        idstr : str
1275            String containing the TDLPACK ID with leading zeros and delimited
1276            by a non-numeric character.
1277
1278        Returns
1279        -------
1280            An instance of TdlpackID.
1281        """
1282        delim = idstr[9]
1283        if {idstr[19], idstr[29]} != {delim, delim}:
1284            raise ValueError(f"Invalid TDLPACK ID string format")
1285        return cls([int(i.lstrip("0")) if len(i.lstrip("0")) > 0 else 0 for i in idstr.split(delim)])
1286
1287    def format(self, style: str = "basic") -> str:
1288        """
1289        Format the TDLPACK ID identifier as a string.
1290
1291        This method returns a string representation of the identifier in one
1292        of several supported formats commonly used in MOS-2000 workflows.
1293
1294        Parameters
1295        ----------
1296        style : {'basic', 'b', 'mos', 'm', 'parsed', 'p'}, optional
1297            Output format style (case-insensitive):
1298
1299            - ``"basic"`` or ``"b"``
1300                Four-word identifier. Each word is printed as a zero-padded
1301                integer field.
1302
1303            - ``"mos"`` or ``"m"``
1304                MOS-style identifier consisting of the first three words
1305                followed by the ISG components and the threshold value
1306                formatted in scientific notation (``.0000e±00``).
1307
1308            - ``"parsed"`` or ``"p"``
1309                Identifier parsed into its individual components as defined
1310                by the internal ID mapping. All components are printed as
1311                zero-padded integers except the threshold value, which is
1312                printed as a floating-point value with ``F13.6`` formatting.
1313
1314        Returns
1315        -------
1316        str
1317            String representation of the identifier in the requested format.
1318
1319        Notes
1320        -----
1321        The MOS-style threshold representation removes the leading zero
1322        from scientific notation (e.g., ``0.0000e+00`` → ``.0000e+00``)
1323        to match legacy MOS-2000 formatting conventions.
1324
1325        Examples
1326        --------
1327        Using the ``format`` method:
1328
1329        >>> rec.format("basic")
1330        '001000008 000000500 000000000 0000000000'
1331
1332        >>> rec.format("mos")
1333        '001000008 000000500 000000000 000 .0000e+00'
1334
1335        >>> rec.format("parsed")
1336        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1337
1338        Using Python's format protocol (``__format__``):
1339
1340        >>> f"{rec.id:basic}"
1341        '001000008 000000500 000000000 0000000000'
1342
1343        >>> f"{rec.id:mos}"
1344        '001000008 000000500 000000000 000 .0000e+00'
1345
1346        >>> f"{rec.id:parsed}"
1347        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1348        """
1349        style = style.lower()
1350
1351        if style in {"basic", "b"}:
1352            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {str(self.word4).zfill(10)}"
1353        elif style in {"mos", "m"}:
1354            thresh = f"{self.thresh:.4e}"
1355            if thresh.startswith("0"):
1356                thresh = thresh[1:]
1357            elif thresh.startswith("-0"):
1358                thresh = "-" + thresh[2:]
1359            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {self.i}{self.s}{self.g} {thresh}"
1360        elif style in {"parsed", "p"}:
1361            parsed = ""
1362            for k, v in self._id.items():
1363                if "thresh" not in k:
1364                    parsed += f"{str(v).zfill(len(k))} "
1365                else:
1366                    parsed += f"{v:13.6f}"
1367            return parsed
1368
1369        raise ValueError(f"Unknown TdlpackID format style: {style!r}")
1370
1371    def to_dict(self):
1372        """
1373        Return TDLPACK variable ID as dict.
1374
1375        Returns
1376        -------
1377            String of the 4-word TDLPACK variable ID.
1378        """
1379        return self._id
1380
1381    def to_string(self, delim=" "):
1382        """
1383        Return TDLPACK variable ID as string.
1384
1385        Parameters
1386        ----------
1387        delim : str, optional
1388            Delimiter character between each TDLPACK variable ID word.
1389
1390        Returns
1391        -------
1392            String of the 4-word TDLPACK variable ID.
1393        """
1394        strlen = (9, 9, 9, 10)
1395        return delim.join([str(i).zfill(word) for word, i in zip(strlen, utils.unparse_id(self._id))])
1396
1397    @property
1398    def word1(self):
1399        """
1400        First ID word.
1401
1402        Returns
1403        -------
1404        int
1405            First parsed ID word.
1406        """
1407        return utils.unparse_id(self._id)[0]
1408
1409    @word1.setter
1410    def word1(self, value):
1411        """
1412        Set first ID word.
1413
1414        Parameters
1415        ----------
1416        value : int
1417            New value.
1418
1419        Notes
1420        -----
1421        Updates internal ID and ``is1[8]`` if record is attached.
1422        """
1423        newid = utils.unparse_id(self._id)
1424        newid[0] = value
1425        self._id = utils.parse_id(newid)
1426        if self._rec is not None:
1427            self._rec.is1[8] = newid[0]
1428
1429    @property
1430    def word2(self):
1431        """
1432        Second ID word.
1433
1434        Returns
1435        -------
1436        int
1437            Second parsed ID word.
1438        """
1439        return utils.unparse_id(self._id)[1]
1440
1441    @word2.setter
1442    def word2(self, value):
1443        """
1444        Set second ID word.
1445
1446        Parameters
1447        ----------
1448        value : int
1449            New value.
1450
1451        Notes
1452        -----
1453        Updates internal ID and ``is1[9]`` if record is attached.
1454        """
1455        newid = utils.unparse_id(self._id)
1456        newid[1] = value
1457        self._id = utils.parse_id(newid)
1458        if self._rec is not None:
1459            self._rec.is1[9] = newid[1]
1460
1461    @property
1462    def word3(self):
1463        """
1464        Third ID word.
1465
1466        Returns
1467        -------
1468        int
1469            Third parsed ID word.
1470        """
1471        return utils.unparse_id(self._id)[2]
1472
1473    @word3.setter
1474    def word3(self, value):
1475        """
1476        Set third ID word.
1477
1478        Parameters
1479        ----------
1480        value : int
1481            New value.
1482
1483        Notes
1484        -----
1485        Updates internal ID and ``is1[10]`` if record is attached.
1486        """
1487        newid = utils.unparse_id(self._id)
1488        newid[2] = value
1489        self._id = utils.parse_id(newid)
1490        if self._rec is not None:
1491            self._rec.is1[10] = newid[2]
1492
1493    @property
1494    def word4(self):
1495        """
1496        Fourth ID word.
1497
1498        Returns
1499        -------
1500        int
1501            Fourth parsed ID word.
1502        """
1503        return utils.unparse_id(self._id)[3]
1504
1505    @word4.setter
1506    def word4(self, value):
1507        """
1508        Set fourth ID word.
1509
1510        Parameters
1511        ----------
1512        value : int
1513            New value.
1514
1515        Notes
1516        -----
1517        Updates internal ID and ``is1[11]`` if record is attached.
1518        """
1519        newid = utils.unparse_id(self._id)
1520        newid[3] = value
1521        self._id = utils.parse_id(newid)
1522        if self._rec is not None:
1523            self._rec.is1[11] = newid[3]
1524
1525    @property
1526    def ccc(self):
1527        """
1528        CCC identifier component.
1529
1530        Returns
1531        -------
1532        int
1533        """
1534        return self._id["ccc"]
1535
1536    @ccc.setter
1537    def ccc(self, value):
1538        """
1539        Set CCC identifier component.
1540
1541        Parameters
1542        ----------
1543        value : int
1544
1545        Notes
1546        -----
1547        Updates ``is1[8]`` if record is attached.
1548        """
1549        self._id["ccc"] = value
1550        if self._rec is not None:
1551            self._rec.is1[8] = utils.unparse_id(self._id)[0]
1552
1553    @property
1554    def fff(self):
1555        """
1556        FFF identifier component.
1557
1558        Returns
1559        -------
1560        int
1561        """
1562        return self._id["fff"]
1563
1564    @fff.setter
1565    def fff(self, value):
1566        """
1567        Set FFF identifier component.
1568
1569        Parameters
1570        ----------
1571        value : int
1572
1573        Notes
1574        -----
1575        Updates ``is1[8]`` if record is attached.
1576        """
1577        self._id["fff"] = value
1578        if self._rec is not None:
1579            self._rec.is1[8] = utils.unparse_id(self._id)[0]
1580
1581    @property
1582    def cccfff(self):
1583        """
1584        Combined CCCFFF identifier.
1585
1586        Returns
1587        -------
1588        int
1589            Integer representation of ``word1 / 1000``.
1590        """
1591        return int(self.word1 / 1000)
1592
1593    @property
1594    def b(self):
1595        """
1596        B identifier component.
1597
1598        Returns
1599        -------
1600        int
1601        """
1602        return self._id["b"]
1603
1604    @b.setter
1605    def b(self, value):
1606        """
1607        Set B identifier component.
1608
1609        Parameters
1610        ----------
1611        value : int
1612
1613        Notes
1614        -----
1615        Updates ``is1[8]`` if record is attached.
1616        """
1617        self._id["b"] = value
1618        if self._rec is not None:
1619            self._rec.is1[8] = utils.unparse_id(self._id)[0]
1620
1621    @property
1622    def dd(self):
1623        """
1624        DD identifier component.
1625
1626        Returns
1627        -------
1628        int
1629        """
1630        return self._id["dd"]
1631
1632    @dd.setter
1633    def dd(self, value):
1634        """
1635        Set DD identifier component.
1636
1637        Parameters
1638        ----------
1639        value : int
1640
1641        Notes
1642        -----
1643        Updates ``is1[8]`` and ``is1[14]`` if record is attached.
1644        """
1645        self._id["dd"] = value
1646        if self._rec is not None:
1647            self._rec.is1[8] = utils.unparse_id(self._id)[0]
1648            self._rec.is1[14] = value
1649
1650    @property
1651    def v(self):
1652        """
1653        V identifier component.
1654
1655        Returns
1656        -------
1657        int
1658        """
1659        return self._id["v"]
1660
1661    @v.setter
1662    def v(self, value):
1663        """
1664        Set V identifier component.
1665
1666        Parameters
1667        ----------
1668        value : int
1669
1670        Notes
1671        -----
1672        Updates ``is1[9]`` if record is attached.
1673        """
1674        self._id["v"] = value
1675        if self._rec is not None:
1676            self._rec.is1[9] = utils.unparse_id(self._id)[1]
1677
1678    @property
1679    def llll(self):
1680        """
1681        LLLL identifier component.
1682
1683        Returns
1684        -------
1685        int
1686        """
1687        return self._id["llll"]
1688
1689    @llll.setter
1690    def llll(self, value):
1691        """
1692        Set LLLL identifier component.
1693
1694        Parameters
1695        ----------
1696        value : int
1697
1698        Notes
1699        -----
1700        Updates ``is1[9]`` if record is attached.
1701        """
1702        self._id["llll"] = value
1703        if self._rec is not None:
1704            self._rec.is1[9] = utils.unparse_id(self._id)[1]
1705
1706    @property
1707    def uuuu(self):
1708        """
1709        UUUU identifier component.
1710
1711        Returns
1712        -------
1713        int
1714        """
1715        return self._id["uuuu"]
1716
1717    @uuuu.setter
1718    def uuuu(self, value):
1719        """
1720        Set UUUU identifier component.
1721
1722        Parameters
1723        ----------
1724        value : int
1725
1726        Notes
1727        -----
1728        Updates ``is1[9]`` if record is attached.
1729        """
1730        self._id["uuuu"] = value
1731        if self._rec is not None:
1732            self._rec.is1[9] = utils.unparse_id(self._id)[1]
1733
1734    @property
1735    def t(self):
1736        """
1737        T identifier component.
1738
1739        Returns
1740        -------
1741        int
1742        """
1743        return self._id["t"]
1744
1745    @t.setter
1746    def t(self, value):
1747        """
1748        Set T identifier component.
1749
1750        Parameters
1751        ----------
1752        value : int
1753
1754        Notes
1755        -----
1756        Updates ``is1[10]`` if record is attached.
1757        """
1758        self._id["t"] = value
1759        if self._rec is not None:
1760            self._rec.is1[10] = utils.unparse_id(self._id)[2]
1761
1762    @property
1763    def rr(self):
1764        """
1765        RR identifier component.
1766
1767        Returns
1768        -------
1769        int
1770        """
1771        return self._id["rr"]
1772
1773    @rr.setter
1774    def rr(self, value):
1775        """
1776        Set RR identifier component.
1777
1778        Parameters
1779        ----------
1780        value : int
1781
1782        Notes
1783        -----
1784        Updates ``is1[10]`` if record is attached.
1785        """
1786        self._id["rr"] = value
1787        if self._rec is not None:
1788            self._rec.is1[10] = utils.unparse_id(self._id)[2]
1789
1790    @property
1791    def o(self):
1792        """
1793        O identifier component.
1794
1795        Returns
1796        -------
1797        int
1798        """
1799        return self._id["o"]
1800
1801    @o.setter
1802    def o(self, value):
1803        """
1804        Set O identifier component.
1805
1806        Parameters
1807        ----------
1808        value : int
1809
1810        Notes
1811        -----
1812        Updates ``is1[10]`` if record is attached.
1813        """
1814        self._id["o"] = value
1815        if self._rec is not None:
1816            self._rec.is1[10] = utils.unparse_id(self._id)[2]
1817
1818    @property
1819    def hh(self):
1820        """
1821        HH identifier component.
1822
1823        Returns
1824        -------
1825        int
1826        """
1827        return self._id["hh"]
1828
1829    @hh.setter
1830    def hh(self, value):
1831        """
1832        Set HH identifier component.
1833
1834        Parameters
1835        ----------
1836        value : int
1837
1838        Notes
1839        -----
1840        Updates ``is1[10]`` if record is attached.
1841        """
1842        self._id["hh"] = value
1843        if self._rec is not None:
1844            self._rec.is1[10] = utils.unparse_id(self._id)[2]
1845
1846    @property
1847    def tau(self):
1848        """
1849        Forecast hour (tau).
1850
1851        Returns
1852        -------
1853        int
1854        """
1855        return self._id["tau"]
1856
1857    @tau.setter
1858    def tau(self, value):
1859        """
1860        Set forecast hour (tau).
1861
1862        Parameters
1863        ----------
1864        value : int
1865
1866        Notes
1867        -----
1868        Updates ``is1[10]`` and ``is1[12]`` if record is attached.
1869        """
1870        self._id["tau"] = value
1871        if self._rec is not None:
1872            self._rec.is1[10] = utils.unparse_id(self._id)[2]
1873            self._rec.is1[12] = value
1874
1875    @property
1876    def thresh(self):
1877        """
1878        Threshold identifier component.
1879
1880        Returns
1881        -------
1882        int
1883        """
1884        return self._id["thresh"]
1885
1886    @thresh.setter
1887    def thresh(self, value):
1888        """
1889        Set threshold identifier component.
1890
1891        Parameters
1892        ----------
1893        value : int
1894
1895        Notes
1896        -----
1897        Updates ``is1[11]`` if record is attached.
1898        """
1899        self._id["thresh"] = value
1900        if self._rec is not None:
1901            self._rec.is1[11] = utils.unparse_id(self._id)[3]
1902
1903    @property
1904    def i(self):
1905        """
1906        I identifier component.
1907
1908        Returns
1909        -------
1910        int
1911        """
1912        return self._id["i"]
1913
1914    @i.setter
1915    def i(self, value):
1916        """
1917        Set I identifier component.
1918
1919        Parameters
1920        ----------
1921        value : int
1922
1923        Notes
1924        -----
1925        Updates ``is1[11]`` if record is attached.
1926        """
1927        self._id["i"] = value
1928        if self._rec is not None:
1929            self._rec.is1[11] = utils.unparse_id(self._id)[3]
1930
1931    @property
1932    def s(self):
1933        """
1934        S identifier component.
1935
1936        Returns
1937        -------
1938        int
1939        """
1940        return self._id["s"]
1941
1942    @s.setter
1943    def s(self, value):
1944        """
1945        Set S identifier component.
1946
1947        Parameters
1948        ----------
1949        value : int
1950
1951        Notes
1952        -----
1953        Updates ``is1[11]`` if record is attached.
1954        """
1955        self._id["s"] = value
1956        if self._rec is not None:
1957            self._rec.is1[11] = utils.unparse_id(self._id)[3]
1958
1959    @property
1960    def g(self):
1961        """
1962        G identifier component.
1963
1964        Returns
1965        -------
1966        int
1967        """
1968        return self._id["g"]
1969
1970    @g.setter
1971    def g(self, value):
1972        """
1973        Set G identifier component.
1974
1975        Parameters
1976        ----------
1977        value : int
1978
1979        Notes
1980        -----
1981        Updates ``is1[11]`` if record is attached.
1982        """
1983        self._id["g"] = value
1984        if self._rec is not None:
1985            self._rec.is1[11] = utils.unparse_id(self._id)[3]

TDLPACK variable ID class

TdlpackID(id=[0, 0, 0, 0], linked_rec=None)
1232    def __init__(self, id=[0, 0, 0, 0], linked_rec=None):
1233        """
1234        Initialize TDLPACK variable ID
1235
1236        Parameters
1237        ----------
1238        id : list of ints
1239            The 4-word TDLPACK variable ID
1240        linked_rec : TdlpackRecord, optional
1241            TDLPACK record object. This optional argument provides a mechanism
1242            for updating the TDLPACK variable ID in the TdlpackRecord is1 array.
1243        """
1244        self._id = utils.parse_id(id)
1245        self._rec = linked_rec

Initialize TDLPACK variable ID

Parameters
  • id (list of ints): The 4-word TDLPACK variable ID
  • linked_rec (TdlpackRecord, optional): TDLPACK record object. This optional argument provides a mechanism for updating the TDLPACK variable ID in the TdlpackRecord is1 array.
@classmethod
def from_string(cls, idstr):
1267    @classmethod
1268    def from_string(cls, idstr):
1269        """
1270        Create a TdlpackID object from a TDLPACK ID string.
1271
1272        Parameters
1273        ----------
1274        idstr : str
1275            String containing the TDLPACK ID with leading zeros and delimited
1276            by a non-numeric character.
1277
1278        Returns
1279        -------
1280            An instance of TdlpackID.
1281        """
1282        delim = idstr[9]
1283        if {idstr[19], idstr[29]} != {delim, delim}:
1284            raise ValueError(f"Invalid TDLPACK ID string format")
1285        return cls([int(i.lstrip("0")) if len(i.lstrip("0")) > 0 else 0 for i in idstr.split(delim)])

Create a TdlpackID object from a TDLPACK ID string.

Parameters
  • idstr (str): String containing the TDLPACK ID with leading zeros and delimited by a non-numeric character.
Returns
  • An instance of TdlpackID.
def format(self, style: str = 'basic') -> str:
1287    def format(self, style: str = "basic") -> str:
1288        """
1289        Format the TDLPACK ID identifier as a string.
1290
1291        This method returns a string representation of the identifier in one
1292        of several supported formats commonly used in MOS-2000 workflows.
1293
1294        Parameters
1295        ----------
1296        style : {'basic', 'b', 'mos', 'm', 'parsed', 'p'}, optional
1297            Output format style (case-insensitive):
1298
1299            - ``"basic"`` or ``"b"``
1300                Four-word identifier. Each word is printed as a zero-padded
1301                integer field.
1302
1303            - ``"mos"`` or ``"m"``
1304                MOS-style identifier consisting of the first three words
1305                followed by the ISG components and the threshold value
1306                formatted in scientific notation (``.0000e±00``).
1307
1308            - ``"parsed"`` or ``"p"``
1309                Identifier parsed into its individual components as defined
1310                by the internal ID mapping. All components are printed as
1311                zero-padded integers except the threshold value, which is
1312                printed as a floating-point value with ``F13.6`` formatting.
1313
1314        Returns
1315        -------
1316        str
1317            String representation of the identifier in the requested format.
1318
1319        Notes
1320        -----
1321        The MOS-style threshold representation removes the leading zero
1322        from scientific notation (e.g., ``0.0000e+00`` → ``.0000e+00``)
1323        to match legacy MOS-2000 formatting conventions.
1324
1325        Examples
1326        --------
1327        Using the ``format`` method:
1328
1329        >>> rec.format("basic")
1330        '001000008 000000500 000000000 0000000000'
1331
1332        >>> rec.format("mos")
1333        '001000008 000000500 000000000 000 .0000e+00'
1334
1335        >>> rec.format("parsed")
1336        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1337
1338        Using Python's format protocol (``__format__``):
1339
1340        >>> f"{rec.id:basic}"
1341        '001000008 000000500 000000000 0000000000'
1342
1343        >>> f"{rec.id:mos}"
1344        '001000008 000000500 000000000 000 .0000e+00'
1345
1346        >>> f"{rec.id:parsed}"
1347        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1348        """
1349        style = style.lower()
1350
1351        if style in {"basic", "b"}:
1352            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {str(self.word4).zfill(10)}"
1353        elif style in {"mos", "m"}:
1354            thresh = f"{self.thresh:.4e}"
1355            if thresh.startswith("0"):
1356                thresh = thresh[1:]
1357            elif thresh.startswith("-0"):
1358                thresh = "-" + thresh[2:]
1359            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {self.i}{self.s}{self.g} {thresh}"
1360        elif style in {"parsed", "p"}:
1361            parsed = ""
1362            for k, v in self._id.items():
1363                if "thresh" not in k:
1364                    parsed += f"{str(v).zfill(len(k))} "
1365                else:
1366                    parsed += f"{v:13.6f}"
1367            return parsed
1368
1369        raise ValueError(f"Unknown TdlpackID format style: {style!r}")

Format the TDLPACK ID identifier as a string.

This method returns a string representation of the identifier in one of several supported formats commonly used in MOS-2000 workflows.

Parameters
  • style ({'basic', 'b', 'mos', 'm', 'parsed', 'p'}, optional): Output format style (case-insensitive):

    • "basic" or "b" Four-word identifier. Each word is printed as a zero-padded integer field.

    • "mos" or "m" MOS-style identifier consisting of the first three words followed by the ISG components and the threshold value formatted in scientific notation (.0000e±00).

    • "parsed" or "p" Identifier parsed into its individual components as defined by the internal ID mapping. All components are printed as zero-padded integers except the threshold value, which is printed as a floating-point value with F13.6 formatting.

Returns
  • str: String representation of the identifier in the requested format.
Notes

The MOS-style threshold representation removes the leading zero from scientific notation (e.g., 0.0000e+00 → .0000e+00) to match legacy MOS-2000 formatting conventions.

Examples

Using the format method:

>>> rec.format("basic")
'001000008 000000500 000000000 0000000000'
>>> rec.format("mos")
'001000008 000000500 000000000 000 .0000e+00'
>>> rec.format("parsed")
'001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'

Using Python's format protocol (__format__):

>>> f"{rec.id:basic}"
'001000008 000000500 000000000 0000000000'
>>> f"{rec.id:mos}"
'001000008 000000500 000000000 000 .0000e+00'
>>> f"{rec.id:parsed}"
'001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
def to_dict(self):
1371    def to_dict(self):
1372        """
1373        Return TDLPACK variable ID as dict.
1374
1375        Returns
1376        -------
1377            String of the 4-word TDLPACK variable ID.
1378        """
1379        return self._id

Return TDLPACK variable ID as dict.

Returns
  • String of the 4-word TDLPACK variable ID.
def to_string(self, delim=' '):
1381    def to_string(self, delim=" "):
1382        """
1383        Return TDLPACK variable ID as string.
1384
1385        Parameters
1386        ----------
1387        delim : str, optional
1388            Delimiter character between each TDLPACK variable ID word.
1389
1390        Returns
1391        -------
1392            String of the 4-word TDLPACK variable ID.
1393        """
1394        strlen = (9, 9, 9, 10)
1395        return delim.join([str(i).zfill(word) for word, i in zip(strlen, utils.unparse_id(self._id))])

Return TDLPACK variable ID as string.

Parameters
  • delim (str, optional): Delimiter character between each TDLPACK variable ID word.
Returns
  • String of the 4-word TDLPACK variable ID.
word1
1397    @property
1398    def word1(self):
1399        """
1400        First ID word.
1401
1402        Returns
1403        -------
1404        int
1405            First parsed ID word.
1406        """
1407        return utils.unparse_id(self._id)[0]

First ID word.

Returns
  • int: First parsed ID word.
word2
1429    @property
1430    def word2(self):
1431        """
1432        Second ID word.
1433
1434        Returns
1435        -------
1436        int
1437            Second parsed ID word.
1438        """
1439        return utils.unparse_id(self._id)[1]

Second ID word.

Returns
  • int: Second parsed ID word.
word3
1461    @property
1462    def word3(self):
1463        """
1464        Third ID word.
1465
1466        Returns
1467        -------
1468        int
1469            Third parsed ID word.
1470        """
1471        return utils.unparse_id(self._id)[2]

Third ID word.

Returns
  • int: Third parsed ID word.
word4
1493    @property
1494    def word4(self):
1495        """
1496        Fourth ID word.
1497
1498        Returns
1499        -------
1500        int
1501            Fourth parsed ID word.
1502        """
1503        return utils.unparse_id(self._id)[3]

Fourth ID word.

Returns
  • int: Fourth parsed ID word.
ccc
1525    @property
1526    def ccc(self):
1527        """
1528        CCC identifier component.
1529
1530        Returns
1531        -------
1532        int
1533        """
1534        return self._id["ccc"]

CCC identifier component.

Returns
  • int
fff
1553    @property
1554    def fff(self):
1555        """
1556        FFF identifier component.
1557
1558        Returns
1559        -------
1560        int
1561        """
1562        return self._id["fff"]

FFF identifier component.

Returns
  • int
cccfff
1581    @property
1582    def cccfff(self):
1583        """
1584        Combined CCCFFF identifier.
1585
1586        Returns
1587        -------
1588        int
1589            Integer representation of ``word1 / 1000``.
1590        """
1591        return int(self.word1 / 1000)

Combined CCCFFF identifier.

Returns
  • int: Integer representation of word1 / 1000.
b
1593    @property
1594    def b(self):
1595        """
1596        B identifier component.
1597
1598        Returns
1599        -------
1600        int
1601        """
1602        return self._id["b"]

B identifier component.

Returns
  • int
dd
1621    @property
1622    def dd(self):
1623        """
1624        DD identifier component.
1625
1626        Returns
1627        -------
1628        int
1629        """
1630        return self._id["dd"]

DD identifier component.

Returns
  • int
v
1650    @property
1651    def v(self):
1652        """
1653        V identifier component.
1654
1655        Returns
1656        -------
1657        int
1658        """
1659        return self._id["v"]

V identifier component.

Returns
  • int
llll
1678    @property
1679    def llll(self):
1680        """
1681        LLLL identifier component.
1682
1683        Returns
1684        -------
1685        int
1686        """
1687        return self._id["llll"]

LLLL identifier component.

Returns
  • int
uuuu
1706    @property
1707    def uuuu(self):
1708        """
1709        UUUU identifier component.
1710
1711        Returns
1712        -------
1713        int
1714        """
1715        return self._id["uuuu"]

UUUU identifier component.

Returns
  • int
t
1734    @property
1735    def t(self):
1736        """
1737        T identifier component.
1738
1739        Returns
1740        -------
1741        int
1742        """
1743        return self._id["t"]

T identifier component.

Returns
  • int
rr
1762    @property
1763    def rr(self):
1764        """
1765        RR identifier component.
1766
1767        Returns
1768        -------
1769        int
1770        """
1771        return self._id["rr"]

RR identifier component.

Returns
  • int
o
1790    @property
1791    def o(self):
1792        """
1793        O identifier component.
1794
1795        Returns
1796        -------
1797        int
1798        """
1799        return self._id["o"]

O identifier component.

Returns
  • int
hh
1818    @property
1819    def hh(self):
1820        """
1821        HH identifier component.
1822
1823        Returns
1824        -------
1825        int
1826        """
1827        return self._id["hh"]

HH identifier component.

Returns
  • int
tau
1846    @property
1847    def tau(self):
1848        """
1849        Forecast hour (tau).
1850
1851        Returns
1852        -------
1853        int
1854        """
1855        return self._id["tau"]

Forecast hour (tau).

Returns
  • int
thresh
1875    @property
1876    def thresh(self):
1877        """
1878        Threshold identifier component.
1879
1880        Returns
1881        -------
1882        int
1883        """
1884        return self._id["thresh"]

Threshold identifier component.

Returns
  • int
i
1903    @property
1904    def i(self):
1905        """
1906        I identifier component.
1907
1908        Returns
1909        -------
1910        int
1911        """
1912        return self._id["i"]

I identifier component.

Returns
  • int
s
1931    @property
1932    def s(self):
1933        """
1934        S identifier component.
1935
1936        Returns
1937        -------
1938        int
1939        """
1940        return self._id["s"]

S identifier component.

Returns
  • int
g
1959    @property
1960    def g(self):
1961        """
1962        G identifier component.
1963
1964        Returns
1965        -------
1966        int
1967        """
1968        return self._id["g"]

G identifier component.

Returns
  • int
@dataclass(frozen=True, slots=True)
class TdlpackGridDef:
20@dataclass(frozen=True, slots=True)
21class TdlpackGridDef:
22    """Definition of a TDLPACK grid.
23
24    Stored longitude convention is MOS-2000:
25    west longitude is positive.
26    """
27
28    mapProjection: int
29    nx: int
30    ny: int
31    latitudeLowerLeft: float
32    longitudeLowerLeft: float
33    standardLatitude: float
34    orientationLongitude: float
35    gridLength: float
36
37    # Array mapping
38    IS2_INDEX: ClassVar[dict[str, int]] = {
39        "mapProjection": 1,
40        "nx": 2,
41        "ny": 3,
42        "latitudeLowerLeft": 4,
43        "longitudeLowerLeft": 5,
44        "orientationLongitude": 6,
45        "gridLength": 7,
46        "standardLatitude": 8,
47    }
48
49    # Attribute scale factors used to convert to whole integers.
50    SCALE_FACTOR: ClassVar[dict[str, int]] = {
51        "mapProjection": 1,
52        "nx": 1,
53        "ny": 1,
54        "latitudeLowerLeft": 10_000,
55        "longitudeLowerLeft": 10_000,
56        "orientationLongitude": 10_000,
57        "gridLength": 1000,
58        "standardLatitude": 10_000,
59    }
60
61    def scaled_value(self, name: str) -> int:
62        """Return one grid attribute scaled to an integer."""
63        value = getattr(self, name)
64        scale = self.SCALE_FACTOR[name]
65        return int(round(value * scale))
66
67    def to_is2(self) -> list[int]:
68        """Return the grid definition as a scaled integer array."""
69        is2 = np.zeros((_tdlpackio.ND7), dtype=np.int32)
70
71        for name, index in self.IS2_INDEX.items():
72            is2[index] = self.scaled_value(name)
73
74        return is2
75
76    def to_int_dict(self) -> dict[str, int]:
77        """Return scaled integer values keyed by attribute name."""
78        return {name: self.scaled_value(name) for name in self.IS2_INDEX}

Definition of a TDLPACK grid.

Stored longitude convention is MOS-2000: west longitude is positive.

TdlpackGridDef( mapProjection: int, nx: int, ny: int, latitudeLowerLeft: float, longitudeLowerLeft: float, standardLatitude: float, orientationLongitude: float, gridLength: float)
mapProjection: int
nx: int
ny: int
latitudeLowerLeft: float
longitudeLowerLeft: float
standardLatitude: float
orientationLongitude: float
gridLength: float
IS2_INDEX: ClassVar[dict[str, int]] = {'mapProjection': 1, 'nx': 2, 'ny': 3, 'latitudeLowerLeft': 4, 'longitudeLowerLeft': 5, 'orientationLongitude': 6, 'gridLength': 7, 'standardLatitude': 8}
SCALE_FACTOR: ClassVar[dict[str, int]] = {'mapProjection': 1, 'nx': 1, 'ny': 1, 'latitudeLowerLeft': 10000, 'longitudeLowerLeft': 10000, 'orientationLongitude': 10000, 'gridLength': 1000, 'standardLatitude': 10000}
def scaled_value(self, name: str) -> int:
61    def scaled_value(self, name: str) -> int:
62        """Return one grid attribute scaled to an integer."""
63        value = getattr(self, name)
64        scale = self.SCALE_FACTOR[name]
65        return int(round(value * scale))

Return one grid attribute scaled to an integer.

def to_is2(self) -> list[int]:
67    def to_is2(self) -> list[int]:
68        """Return the grid definition as a scaled integer array."""
69        is2 = np.zeros((_tdlpackio.ND7), dtype=np.int32)
70
71        for name, index in self.IS2_INDEX.items():
72            is2[index] = self.scaled_value(name)
73
74        return is2

Return the grid definition as a scaled integer array.

def to_int_dict(self) -> dict[str, int]:
76    def to_int_dict(self) -> dict[str, int]:
77        """Return scaled integer values keyed by attribute name."""
78        return {name: self.scaled_value(name) for name in self.IS2_INDEX}

Return scaled integer values keyed by attribute name.

GRIDS = mappingproxy({'nbmak': TdlpackGridDef(mapProjection=5, nx=1649, ny=1105, latitudeLowerLeft=40.5301, longitudeLowerLeft=178.5713, standardLatitude=60.0, orientationLongitude=150.0, gridLength=2976.560059), 'nbmco': TdlpackGridDef(mapProjection=3, nx=2345, ny=1597, latitudeLowerLeft=19.229, longitudeLowerLeft=126.2766, standardLatitude=25.0, orientationLongitude=95.0, gridLength=2539.702881), 'nbmgu': TdlpackGridDef(mapProjection=7, nx=193, ny=193, latitudeLowerLeft=12.3499, longitudeLowerLeft=216.313, standardLatitude=20.0, orientationLongitude=360.0, gridLength=2500.0), 'nbmhi': TdlpackGridDef(mapProjection=7, nx=625, ny=561, latitudeLowerLeft=14.3515, longitudeLowerLeft=164.9695, standardLatitude=20.0, orientationLongitude=160.0, gridLength=2500.0), 'nbmoc': TdlpackGridDef(mapProjection=7, nx=2517, ny=1817, latitudeLowerLeft=-30.4192, longitudeLowerLeft=230.0942, standardLatitude=20.0, orientationLongitude=360.0, gridLength=10000.0), 'nbmpr': TdlpackGridDef(mapProjection=7, nx=353, ny=257, latitudeLowerLeft=16.828, longitudeLowerLeft=68.1954, standardLatitude=20.0, orientationLongitude=65.0, gridLength=1250.0), 'nbmswp': TdlpackGridDef(mapProjection=7, nx=3683, ny=1903, latitudeLowerLeft=-26.0, longitudeLowerLeft=230.0, standardLatitude=-5.0, orientationLongitude=360.0, gridLength=2500.0), 'gfs23': TdlpackGridDef(mapProjection=5, nx=593, ny=337, latitudeLowerLeft=2.832, longitudeLowerLeft=150.0003, standardLatitude=60.0, orientationLongitude=105.0, gridLength=23812.5), 'gfs47': TdlpackGridDef(mapProjection=5, nx=297, ny=169, latitudeLowerLeft=2.832, longitudeLowerLeft=150.0003, standardLatitude=60.0, orientationLongitude=105.0, gridLength=47625.0), 'gfs95': TdlpackGridDef(mapProjection=5, nx=149, ny=85, latitudeLowerLeft=2.832, longitudeLowerLeft=150.0003, standardLatitude=60.0, orientationLongitude=105.0, gridLength=95250.0), 'nam151': TdlpackGridDef(mapProjection=5, nx=425, ny=281, latitudeLowerLeft=0.7279, longitudeLowerLeft=150.3583, standardLatitude=60.0, orientationLongitude=110.0, gridLength=33812.0), 'nam221': TdlpackGridDef(mapProjection=3, nx=349, ny=198, latitudeLowerLeft=9.5803, longitudeLowerLeft=150.7806, standardLatitude=50.0, orientationLongitude=107.0, gridLength=32463.410156)})
def get_grid( name: str, *, lon_format: Literal['mos2k', 'standard', '0_360'] = 'mos2k') -> TdlpackGridDef:
228def get_grid(
229    name: str,
230    *,
231    lon_format: Literal["mos2k", "standard", "0_360"] = "mos2k",
232) -> TdlpackGridDef:
233    """
234    Return a grid definition by name.
235
236    Notes
237    -----
238    Internally, all grid definitions are stored using the MOS-2000
239    longitude convention (west longitude is positive).
240
241    Parameters
242    ----------
243    name : str
244        Grid name.
245    lon_format : {"mos2k", "standard", "0_360"}, optional
246        Longitude convention to return:
247
248        - "mos2k": West longitude positive (native storage format)
249        - "standard": East-positive, range [-180, 180]
250        - "0_360": East-positive, range [0, 360)
251
252    Returns
253    -------
254    TdlpackGridDef
255        Grid definition using the requested longitude convention.
256    """
257    try:
258        grid = GRIDS[name.lower()]
259    except KeyError as exc:
260        raise KeyError(f"Unknown grid: {name!r}") from exc
261
262    # Native format (no copy needed)
263    if lon_format == "mos2k":
264        return grid
265
266    if lon_format == "standard":
267        return replace(
268            grid,
269            longitudeLowerLeft=_mos2k_to_standard(grid.longitudeLowerLeft),
270            orientationLongitude=_mos2k_to_standard(grid.orientationLongitude),
271        )
272
273    if lon_format == "0_360":
274        return replace(
275            grid,
276            longitudeLowerLeft=_mos2k_to_0_360(grid.longitudeLowerLeft),
277            orientationLongitude=_mos2k_to_0_360(grid.orientationLongitude),
278        )
279
280    raise ValueError(f"Invalid lon_format: {lon_format!r}. Expected one of {{'mos2k', 'standard', '0_360'}}.")

Return a grid definition by name.

Notes

Internally, all grid definitions are stored using the MOS-2000 longitude convention (west longitude is positive).

Parameters
  • name (str): Grid name.
  • lon_format ({"mos2k", "standard", "0_360"}, optional): Longitude convention to return:

    • "mos2k": West longitude positive (native storage format)
    • "standard": East-positive, range [-180, 180]
    • "0_360": East-positive, range [0, 360)
Returns
  • TdlpackGridDef: Grid definition using the requested longitude convention.
def has_grid(name: str) -> bool:
283def has_grid(name: str) -> bool:
284    """Return True if a grid name is known."""
285    return name.lower() in GRIDS

Return True if a grid name is known.