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}")
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.
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
158 @property 159 def size(self): 160 """Return the file size.""" 161 return os.path.getsize(self.name)
Return the file size.
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:
dataortrailer:int32array.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).
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.
TdlpackStationRecord: Station record. Station identifiers are padded toNCHAR._TdlpackRecord: Packed data record.TdlpackTrailerRecord: Trailer record.list: List of supported record types.
Returns
- None
Notes
Updates bytes_written, records_written, records, and
_type_lastrecord_written.
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.
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
TdlpackRecordattributes 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.
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
int32array of sizeND7. - is1 (numpy.ndarray, optional):
Section 1 array. Default is zero-initialized
int32array of sizeND7. - is2 (numpy.ndarray, optional):
Section 2 array. Default is zero-initialized
int32array of sizeND7. - is4 (numpy.ndarray, optional):
Section 4 array. Default is zero-initialized
int32array of sizeND7. - *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". Ifis2contains any non-zero values, the type is automatically set to"grid".grid: str, optional Predefined grid. Valid values can be found intdlpackio.grids.GRIDS. This will force the type to be"grid"and override values in theis2argument.
Returns
- _TdlpackRecord: Instance of a dynamically generated subclass of
_TdlpackRecord.
Notes
- Record subclasses are created dynamically and cached in
_record_class_storeusing the record type as the key. - For
"grid"records,templates.GridDefinitionSectionis added as a base class andis1[1]is set to indicate the presence of a grid definition section.
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
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
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
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.
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.
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 withF13.6formatting.
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'
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.
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.
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.
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.
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.
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.
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
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
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.
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
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
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
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
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
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
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
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
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
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
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
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
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.
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.
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.
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.
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.