Source code for pyiem.nws.products.fd

"""Parser for the FD (Temp Wind Aloft Forecasts)."""

import re
from datetime import timedelta

import numpy as np
import pandas as pd

from pyiem.nws.product import TextProduct

BASED_ON_RE = re.compile("^DATA BASED ON ([0-9]{6})Z", re.M)
VALID_RE = re.compile("^VALID ([0-9]{6})Z", re.M)


[docs] def parse_encoding(text): """Convert the encoded text into drct, sknt, tmpc.""" tmpc = np.nan drct = np.nan sknt = np.nan if len(text) not in [4, 6, 7]: return drct, sknt, tmpc drct = int(text[:2]) * 10 sknt = int(text[2:4]) # NWSI 10-812 section 6 if text.startswith("9900"): drct = 0 sknt = 0 if len(text) > 4: tmpc = int(text[-2:]) if text[4] == "-" or len(text) == 6: tmpc *= -1 # Fun if drct >= 500: drct -= 500 sknt += 100 return drct, sknt, tmpc
[docs] def compute_time(valid, tokens): """Figure out the timestamp of interest here.""" dd, hh, mi = int(tokens[0][:2]), int(tokens[0][2:4]), int(tokens[0][4:6]) valid2 = valid.replace(hour=hh, minute=mi) if valid.day > 25 and dd < 5: valid2 += timedelta(days=10) if valid.day < 5 and dd > 25: valid2 -= timedelta(days=10) valid2 = valid2.replace(day=dd) # In theory, we should not be far apart if abs((valid2 - valid).days) > 5: raise ValueError(f"Timestamp {valid2} too far from {valid}") return valid2
[docs] def make4(station, afos): """Make this 3 character station, 4!""" ccode = afos[3:5] if ccode == "US": return f"K{station}" if ccode == "CN": return f"C{station}" return f"P{station}"
[docs] class FDProduct(TextProduct): """ Represents a FD Product """ def __init__( self, text, utcnow=None, ugc_provider=None, nwsli_provider=None ): """constructor""" text = text.replace("\x1e", "") # Aviation Control super().__init__(text, utcnow, ugc_provider, nwsli_provider) self.df = None self.obtime = compute_time( self.valid, BASED_ON_RE.findall(self.unixtext) ) self.ftime = compute_time(self.valid, VALID_RE.findall(self.unixtext)) self.parser()
[docs] def parser(self): """Do the parsing we need to do!""" rows = [] levels = [] for line in self.unixtext.split("\n"): if line.startswith("FT ") and not rows: levels = line[3:].strip().split() continue if not levels: continue if len(line) < 10: continue tokens = line.strip().split(" ") data = {"station": make4(tokens[0], self.afos)} # fill right to left for i in range(-1, -1 - len(levels), -1): ( data[f"drct{levels[i]}"], data[f"sknt{levels[i]}"], data[f"tmpc{levels[i]}"], ) = parse_encoding(tokens[i]) rows.append(data) if rows: self.df = pd.DataFrame(rows).set_index("station")
[docs] def sql(self, cursor): """Send the data to the database.""" if self.df is None or self.df.empty: return # Prevent NaN numbers from going to the database. sql = ", ".join([f"{c} = %s" for c in self.df.columns]) for row in self.df.itertuples(index=True): # Need upsert as data is split over products cursor.execute( "SELECT station from alldata_tempwind_aloft " "where ftime = %s and station = %s and obtime = %s", (self.ftime, row[0], self.obtime), ) if cursor.rowcount == 0: cursor.execute( "INSERT into alldata_tempwind_aloft" "(ftime, station, obtime) VALUES (%s, %s, %s)", (self.ftime, row[0], self.obtime), ) # np.nan + float64 columns + psycopg life is fun here def _fint(val): """Force an int.""" if np.isnan(val): return None return int(val) cursor.execute( f""" UPDATE alldata_tempwind_aloft SET {sql} WHERE ftime = %s and station = %s and obtime = %s """, ( *[_fint(x) for x in row[1:]], self.ftime, row[0], self.obtime, ), )
[docs] def parser(text, utcnow=None, ugc_provider=None, nwsli_provider=None): """Provide back FD objects based on the parsing of this text""" return FDProduct(text, utcnow, ugc_provider, nwsli_provider)