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.0b7'
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:
1158@dataclass
1159class TdlpackStationRecord:
1160    """
1161    TDLPACK Station Record class
1162    """
1163
1164    stations_in: InitVar[Optional[Iterable[str]]] = None
1165
1166    type: str = field(init=False, repr=False, default="station")
1167    stations: ClassVar[templates.Stations] = templates.Stations()
1168
1169    # Private class variable holding the list
1170    _stations: Optional[list[str]] = field(init=False, repr=False, default=None)
1171
1172    def __post_init__(self, stations_in):
1173        self._nsta_expected = 0
1174        self._recnum = -1
1175        self._source = None
1176        self._stations = None
1177        self._type = "station"
1178        self.id = TdlpackID([400001000, 0, 0, 0], self)
1179
1180        if stations_in is not None:
1181            self.stations = stations_in
1182
1183    def __hash__(self):
1184        return hash(tuple(self.stations))
1185
1186    def __str__(self):
1187        return f"{self._recnum}:d=0000000000:STATION CALL LETTER RECORD:{self.numberOfStations}"
1188
1189    @property
1190    def numberOfStations(self):
1191        if self._source is not None and (isinstance(self._stations, list) or self._stations is None):
1192            return self._nsta_expected
1193        return 0 if self.stations is None else len(self.stations)
1194
1195    @property
1196    def data(self):
1197        pass
1198
1199    def pack(self):
1200        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
1189    @property
1190    def numberOfStations(self):
1191        if self._source is not None and (isinstance(self._stations, list) or self._stations is None):
1192            return self._nsta_expected
1193        return 0 if self.stations is None else len(self.stations)
data
1195    @property
1196    def data(self):
1197        pass
def pack(self):
1199    def pack(self):
1200        pass
@dataclass
class TdlpackTrailerRecord:
1203@dataclass
1204class TdlpackTrailerRecord:
1205    """
1206    TDLPACK Trailer Record class
1207    """
1208
1209    type: str = field(init=False, repr=False, default="trailer")
1210
1211    def __post_init__(self):
1212        """"""
1213        self._recnum = -1
1214        self._type = "trailer"
1215        self.id = TdlpackID([0, 0, 0, 0], self)
1216
1217    def __hash__(self):
1218        return hash((""))
1219
1220    def __str__(self):
1221        """"""
1222        return f"{self._recnum}:d=0000000000:TRAILER RECORD"
1223
1224    @property
1225    def data(self):
1226        """"""
1227        pass
1228
1229    def pack(self):
1230        """"""
1231        pass

TDLPACK Trailer Record class

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

TDLPACK variable ID class

TdlpackID(id=[0, 0, 0, 0], linked_rec=None)
1241    def __init__(self, id=[0, 0, 0, 0], linked_rec=None):
1242        """
1243        Initialize TDLPACK variable ID
1244
1245        Parameters
1246        ----------
1247        id : list of ints
1248            The 4-word TDLPACK variable ID
1249        linked_rec : TdlpackRecord, optional
1250            TDLPACK record object. This optional argument provides a mechanism
1251            for updating the TDLPACK variable ID in the TdlpackRecord is1 array.
1252        """
1253        self._id = utils.parse_id(id)
1254        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):
1276    @classmethod
1277    def from_string(cls, idstr):
1278        """
1279        Create a TdlpackID object from a TDLPACK ID string.
1280
1281        Parameters
1282        ----------
1283        idstr : str
1284            String containing the TDLPACK ID with leading zeros and delimited
1285            by a non-numeric character.
1286
1287        Returns
1288        -------
1289            An instance of TdlpackID.
1290        """
1291        delim = idstr[9]
1292        if {idstr[19], idstr[29]} != {delim, delim}:
1293            raise ValueError(f"Invalid TDLPACK ID string format")
1294        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:
1296    def format(self, style: str = "basic") -> str:
1297        """
1298        Format the TDLPACK ID identifier as a string.
1299
1300        This method returns a string representation of the identifier in one
1301        of several supported formats commonly used in MOS-2000 workflows.
1302
1303        Parameters
1304        ----------
1305        style : {'basic', 'b', 'mos', 'm', 'parsed', 'p'}, optional
1306            Output format style (case-insensitive):
1307
1308            - ``"basic"`` or ``"b"``
1309                Four-word identifier. Each word is printed as a zero-padded
1310                integer field.
1311
1312            - ``"mos"`` or ``"m"``
1313                MOS-style identifier consisting of the first three words
1314                followed by the ISG components and the threshold value
1315                formatted in scientific notation (``.0000e±00``).
1316
1317            - ``"parsed"`` or ``"p"``
1318                Identifier parsed into its individual components as defined
1319                by the internal ID mapping. All components are printed as
1320                zero-padded integers except the threshold value, which is
1321                printed as a floating-point value with ``F13.6`` formatting.
1322
1323        Returns
1324        -------
1325        str
1326            String representation of the identifier in the requested format.
1327
1328        Notes
1329        -----
1330        The MOS-style threshold representation removes the leading zero
1331        from scientific notation (e.g., ``0.0000e+00`` → ``.0000e+00``)
1332        to match legacy MOS-2000 formatting conventions.
1333
1334        Examples
1335        --------
1336        Using the ``format`` method:
1337
1338        >>> rec.format("basic")
1339        '001000008 000000500 000000000 0000000000'
1340
1341        >>> rec.format("mos")
1342        '001000008 000000500 000000000 000 .0000e+00'
1343
1344        >>> rec.format("parsed")
1345        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1346
1347        Using Python's format protocol (``__format__``):
1348
1349        >>> f"{rec.id:basic}"
1350        '001000008 000000500 000000000 0000000000'
1351
1352        >>> f"{rec.id:mos}"
1353        '001000008 000000500 000000000 000 .0000e+00'
1354
1355        >>> f"{rec.id:parsed}"
1356        '001 000 0 08 0 0000 0500 0 00 0 00 000 0 0 0      0.000000'
1357        """
1358        style = style.lower()
1359
1360        if style in {"basic", "b"}:
1361            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {str(self.word4).zfill(10)}"
1362        elif style in {"mos", "m"}:
1363            thresh = f"{self.thresh:.4e}"
1364            if thresh.startswith("0"):
1365                thresh = thresh[1:]
1366            elif thresh.startswith("-0"):
1367                thresh = "-" + thresh[2:]
1368            return f"{str(self.word1).zfill(9)} {str(self.word2).zfill(9)} {str(self.word3).zfill(9)} {self.i}{self.s}{self.g} {thresh}"
1369        elif style in {"parsed", "p"}:
1370            parsed = ""
1371            for k, v in self._id.items():
1372                if "thresh" not in k:
1373                    parsed += f"{str(v).zfill(len(k))} "
1374                else:
1375                    parsed += f"{v:13.6f}"
1376            return parsed
1377
1378        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):
1380    def to_dict(self):
1381        """
1382        Return TDLPACK variable ID as dict.
1383
1384        Returns
1385        -------
1386            String of the 4-word TDLPACK variable ID.
1387        """
1388        return self._id

Return TDLPACK variable ID as dict.

Returns
  • String of the 4-word TDLPACK variable ID.
def to_string(self, delim=' '):
1390    def to_string(self, delim=" "):
1391        """
1392        Return TDLPACK variable ID as string.
1393
1394        Parameters
1395        ----------
1396        delim : str, optional
1397            Delimiter character between each TDLPACK variable ID word.
1398
1399        Returns
1400        -------
1401            String of the 4-word TDLPACK variable ID.
1402        """
1403        strlen = (9, 9, 9, 10)
1404        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
1406    @property
1407    def word1(self):
1408        """
1409        First ID word.
1410
1411        Returns
1412        -------
1413        int
1414            First parsed ID word.
1415        """
1416        return utils.unparse_id(self._id)[0]

First ID word.

Returns
  • int: First parsed ID word.
word2
1438    @property
1439    def word2(self):
1440        """
1441        Second ID word.
1442
1443        Returns
1444        -------
1445        int
1446            Second parsed ID word.
1447        """
1448        return utils.unparse_id(self._id)[1]

Second ID word.

Returns
  • int: Second parsed ID word.
word3
1470    @property
1471    def word3(self):
1472        """
1473        Third ID word.
1474
1475        Returns
1476        -------
1477        int
1478            Third parsed ID word.
1479        """
1480        return utils.unparse_id(self._id)[2]

Third ID word.

Returns
  • int: Third parsed ID word.
word4
1502    @property
1503    def word4(self):
1504        """
1505        Fourth ID word.
1506
1507        Returns
1508        -------
1509        int
1510            Fourth parsed ID word.
1511        """
1512        return utils.unparse_id(self._id)[3]

Fourth ID word.

Returns
  • int: Fourth parsed ID word.
ccc
1534    @property
1535    def ccc(self):
1536        """
1537        CCC identifier component.
1538
1539        Returns
1540        -------
1541        int
1542        """
1543        return self._id["ccc"]

CCC identifier component.

Returns
  • int
fff
1562    @property
1563    def fff(self):
1564        """
1565        FFF identifier component.
1566
1567        Returns
1568        -------
1569        int
1570        """
1571        return self._id["fff"]

FFF identifier component.

Returns
  • int
cccfff
1590    @property
1591    def cccfff(self):
1592        """
1593        Combined CCCFFF identifier.
1594
1595        Returns
1596        -------
1597        int
1598            Integer representation of ``word1 / 1000``.
1599        """
1600        return int(self.word1 / 1000)

Combined CCCFFF identifier.

Returns
  • int: Integer representation of word1 / 1000.
b
1602    @property
1603    def b(self):
1604        """
1605        B identifier component.
1606
1607        Returns
1608        -------
1609        int
1610        """
1611        return self._id["b"]

B identifier component.

Returns
  • int
dd
1630    @property
1631    def dd(self):
1632        """
1633        DD identifier component.
1634
1635        Returns
1636        -------
1637        int
1638        """
1639        return self._id["dd"]

DD identifier component.

Returns
  • int
v
1659    @property
1660    def v(self):
1661        """
1662        V identifier component.
1663
1664        Returns
1665        -------
1666        int
1667        """
1668        return self._id["v"]

V identifier component.

Returns
  • int
llll
1687    @property
1688    def llll(self):
1689        """
1690        LLLL identifier component.
1691
1692        Returns
1693        -------
1694        int
1695        """
1696        return self._id["llll"]

LLLL identifier component.

Returns
  • int
uuuu
1715    @property
1716    def uuuu(self):
1717        """
1718        UUUU identifier component.
1719
1720        Returns
1721        -------
1722        int
1723        """
1724        return self._id["uuuu"]

UUUU identifier component.

Returns
  • int
t
1743    @property
1744    def t(self):
1745        """
1746        T identifier component.
1747
1748        Returns
1749        -------
1750        int
1751        """
1752        return self._id["t"]

T identifier component.

Returns
  • int
rr
1771    @property
1772    def rr(self):
1773        """
1774        RR identifier component.
1775
1776        Returns
1777        -------
1778        int
1779        """
1780        return self._id["rr"]

RR identifier component.

Returns
  • int
o
1799    @property
1800    def o(self):
1801        """
1802        O identifier component.
1803
1804        Returns
1805        -------
1806        int
1807        """
1808        return self._id["o"]

O identifier component.

Returns
  • int
hh
1827    @property
1828    def hh(self):
1829        """
1830        HH identifier component.
1831
1832        Returns
1833        -------
1834        int
1835        """
1836        return self._id["hh"]

HH identifier component.

Returns
  • int
tau
1855    @property
1856    def tau(self):
1857        """
1858        Forecast hour (tau).
1859
1860        Returns
1861        -------
1862        int
1863        """
1864        return self._id["tau"]

Forecast hour (tau).

Returns
  • int
thresh
1884    @property
1885    def thresh(self):
1886        """
1887        Threshold identifier component.
1888
1889        Returns
1890        -------
1891        int
1892        """
1893        return self._id["thresh"]

Threshold identifier component.

Returns
  • int
i
1912    @property
1913    def i(self):
1914        """
1915        I identifier component.
1916
1917        Returns
1918        -------
1919        int
1920        """
1921        return self._id["i"]

I identifier component.

Returns
  • int
s
1940    @property
1941    def s(self):
1942        """
1943        S identifier component.
1944
1945        Returns
1946        -------
1947        int
1948        """
1949        return self._id["s"]

S identifier component.

Returns
  • int
g
1968    @property
1969    def g(self):
1970        """
1971        G identifier component.
1972
1973        Returns
1974        -------
1975        int
1976        """
1977        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.