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