From bd50af98b62628067e6a0b670c080944cbcc7004 Mon Sep 17 00:00:00 2001 From: XANTRONIX Industrial <xan@xantronix.com> Date: Sun, 23 Feb 2025 20:00:01 -0500 Subject: [PATCH] Initial implementation of IGRA data parser --- lib/xmet/igra.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 lib/xmet/igra.py diff --git a/lib/xmet/igra.py b/lib/xmet/igra.py new file mode 100644 index 0000000..6e9e531 --- /dev/null +++ b/lib/xmet/igra.py @@ -0,0 +1,149 @@ +import io +import re +import datetime +import shapely + +from xmet.coord import COORD_SYSTEM +from xmet.sounding import Sounding, SoundingSample + +RE_HEADER = re.compile(r''' + ^ (?P<headrec>\#) + (?P<id>[A-Z0-9]{11}) + [ ] (?P<year>\d{4}) + [ ] (?P<month>\d{2}) + [ ] (?P<day>\d{2}) + [ ] (?P<hour>\d{2}) + [ ] (?P<relhour>\d{2}) (?P<relmin>\d{2}) + [ ] (?P<numlev>[0-9\ ]{4}) + [ ] (?P<p_src>.{8}) + [ ] (?P<np_src>.{8}) + [ ] (?P<lat>[0-9 ]{7}) + [ ] (?P<lon>[0-9 ]{8}) + $ +''', re.X) + +RE_SAMPLE = re.compile(r''' + ^ (?P<lvltyp1>\d) + (?P<lvltyp2>\d) + [ ] (?P<etime>[0-9 \-]{5}) + (?P<press>[0-9 \-]{7}) + (?P<pflag>[AB ]) + (?P<gph>[0-9 \-]{5}) + (?P<zflag>[AB ]) + (?P<temp>[0-9 \-]{5}) + (?P<tflag>[AB ]) + (?P<rh>[0-9 \-]{5}) + [ ] (?P<dpdp>[0-9 \-]{5}) + [ ] (?P<wdir>[0-9 \-]{5}) + [ ] (?P<wspd>[0-9 \-]{5}) +''', re.X) + +def etime_to_seconds(etime: str) -> int: + if etime == '-8888' or etime == '-9999': + return + + return 60 * int(etime[0:2]) + int(etime[2:]) + +def parse_num(num: str, scale: float) -> float: + if num == '-8888' or num == '-9999': + return + + return int(num) * scale + +class IGRAReaderException(Exception): + ... + +class IGRAReader(): + def __init__(self, fh: io.TextIOBase): + self.fh = fh + + def read_sample(self) -> SoundingSample: + line = self.fh.readline() + + if line == '': + return + + match = RE_SAMPLE.match(line) + + if match is None: + raise IGRAReaderException(f"Invalid sample line {line}") + + print(f"Got etime '{match['etime']}'") + print(f"Got press '{match['press']}'") + print(f"Got pflag '{match['pflag']}'") + print(f"Got gph '{match['gph']}'") + print(f"Got zflag '{match['zflag']}'") + print(f"Got temp '{match['temp']}'") + print(f"Got rh '{match['rh']}'") + + sample = SoundingSample() + sample.elapsed = etime_to_seconds(match['etime']) + sample.pressure = parse_num(match['press'], 0.01) + sample.pressure_qa = match['pflag'] + sample.height = parse_num(match['gph'], 1.0) + sample.height_qa = match['zflag'] + sample.temp = parse_num(match['temp'], 0.1) + sample.temp_qa = match['tflag'] + sample.humidity = parse_num(match['rh'], 0.1) + sample.dewpoint = parse_num(match['dpdp'], 0.1) + sample.wind_dir = parse_num(match['wdir'], 1.0) + sample.wind_speed = parse_num(match['wspd'], 0.1) + + return sample + + def read(self) -> Sounding: + line = self.fh.readline() + + if line == '': + return + + line = line.rstrip() + + match = RE_HEADER.match(line) + + if match is None: + raise IGRAReaderException(f"Invalid record line {line}") + + sounding = Sounding() + sounding.station = match['id'] + + date = datetime.datetime( + year = int(match['year']), + month = int(match['month']), + day = int(match['day']) + ) + + timestamp = date.astimezone(datetime.UTC) + timestamp_release = date.astimezone(datetime.UTC) + + if match['hour'] != '99': + timestamp += datetime.timedelta(hours=int(match['hour'])) + + timestamp_release += datetime.timedelta(hours=int(match['relhour'])) + + if match['relmin'] != '99': + timestamp_release += datetime.timedelta(minutes=int(match['relmin'])) + + lat = int(match['lat']) / 1000.0 + lon = int(match['lon']) / 1000.0 + + sounding.timestamp_observed = timestamp + sounding.timestamp_released = timestamp_release + sounding.data_source_pressure = match['p_src'] + sounding.data_source_other = match['np_src'] + sounding.coord = shapely.Point(lon, lat, COORD_SYSTEM) + sounding.samples = list() + + count = int(match['numlev']) + + while count > 0: + sample = self.read_sample() + + if sample is None: + break + + sounding.samples.append(sample) + + count -= 1 + + return sounding