#!/usr/bin/env python ############################################################################### # PROGRAM: meteogramObs.TDWGP.py # # MAR 2017 NETTESHEIM MDL INITIAL SET UP. REPLICATE U660 OBS PROCESSING # HANDLES CIG, VIS, TEMP, DEW POINT, WIND # INTRODUCE 'OBSRPT' VARIABLE FOR lmp.v2.1.0 # OBSRPT = 1: METAR # OBSRPT = 2: SPECI # MAR 2017 NETTESHEIM MDL ADDED PRECIP, SKY, OBS VISION # JUN 2017 NETTESHEIM MDL POINT TO CHENJIE'S SCP'D DATA # REQUIRED MODIFICATION TO TIMING LOGIC. THESE # CHANES ARE COMMENTED WITH A PRECEEDING # '#JJN 06/19/17' COMMENT # JUL 2017 NETTESHEIM MDL PREPARE FOR HAND-OFF # UPDATED TMP AND FINAL DIRECTORY LOCATION # JUL 2019 HUANG MDL TRANSITIONED TO WCOSS PHASE 3 # JUN 2022 HUANG MDL TRANSITIONED TO WCOSS2 # FEB 2023 SHAFER MDL MODIFIED TO REQUIRE PDY+HH+MIN AS ARG # JUL 2024 SHAFER MDL REMOVAL OF EXTRANEOUS COMMENTED LINES; # MODIFIED TO NOT READ OR WRITE DIRECTLY # TO COMOUT (VIOLATES IMPL STANDARD) # # PURPOSE: # TO READ SFCTBLS OBSERVATIONS FOR USE IN METEOGRAMS. CREATED AS A # REPLACEMENT FOR OBS HANDLING PROCESS OF U660 DUE TO LIMITATIONS # WITH THE 15 MIN REPORTING FREQUENCY # # USAGE: # python meteogramObs.TDWGP.py # # COMMAND LINE ARGUMENTS: # $1 = DATE+HOUR+MIN (PDY+HH+mm) # ############################################################################### from datetime import datetime, timedelta import subprocess import glob import os import sys comoutbase = os.environ.get('COMOUTbase') data = os.environ.get('DATA') # Function: parseHandle - Parse variables from lines of surface tables def parseHandle(split): obstype = split[1].replace(' ','') obstmp = split[5].replace(' ','') obsdew = split[6].replace(' ','') prwx1 = split[7].replace(' ','') prwx2 = split[8].replace(' ','') prwx3 = split[9].replace(' ','') obsvis = split[10].replace(' ','') obsdir = split[11].replace(' ','') obsspd = split[12].replace(' ','') obsgst = split[13].replace(' ','') CA1 = split[16].replace(' ','') CH1 = split[17].replace(' ','') CA2 = split[18].replace(' ','') CH2 = split[19].replace(' ','') CA3 = split[20].replace(' ','') CH3 = split[21].replace(' ','') CA4 = split[22].replace(' ','') CH4 = split[23].replace(' ','') CA5 = split[24].replace(' ','') CH5 = split[25].replace(' ','') CA6 = split[26].replace(' ','') CH6 = split[27].replace(' ','') PCP1 = split[28].replace(' ','') sun = split[32] return obstype, obstmp, obsdew, prwx1, prwx2, prwx3, obsvis, obsdir, obsspd, obsgst, CA1, CH1, CA2, CH2, CA3, CH3, CA4, CH4, CA5, CH5, CA6, CH6, PCP1, sun # Function: cigHandle - Assign ceiling height/category based on lowest level reported as 'broken' or 'overcast'_ def CigHandle(CA1,CA2,CA3,CA4,CA5,CA6,CH1,CH2,CH3,CH4,CH5,CH6): if CA1 == "CLR": return CA1, 888 if CA1 == '': return 9999.0,9999.0 if CA1 == "BKN" or CA1 == "OVC" or CA1 == "OB": return CA1,CH1 elif CA2 == '': return CA1,888 if CA2 == "BKN" or CA2 == "OVC": return CA2,CH2 elif CA3 == '': return CA2,888 if CA3 == "BKN" or CA3 == "OVC": return CA3,CH3 elif CA4 == '': return CA3,888 if CA4 == "BKN" or CA4 == "OVC": return CA4,CH4 elif CA5 == '': return CA4,888 if CA5 == "BKN" or CA5 == "OVC": return CA5,CH5 elif CA6 == '': return CA5,888 if CA6 == "BKN" or CA6 == "OVC": return CA6,CH6 else: return 9999.0,9999.0 # Function: OpaqueSkyHandle - Returns most 'cloudy' layer def OpaqueSkyHandle(CA1,CA2,CA3,CA4,CA5,CA6): if CA1 == '': return 9999.0 elif CA2 == '': return CA1 elif CA3 == '': return CA2 elif CA4 == '': return CA3 elif CA5 == '': return CA4 elif CA6 == '': return CA5 else: return CA6 # Function: SkyHandle - Maps most 'cloudy' layer (OpaqueSkyHandle) to values Mike expects def SkyHandle(obssky): if obssky == "OVC": return 8.0 elif obssky == "OB": return 10.0 elif obssky == "BKN": return 6.0 elif obssky == "SCT": return 3.0 elif obssky == "FEW": return 2.0 elif obssky == 9999.0: return 9999.0 elif obssky == "POB": return 0.0 else: return 1.0 # Function: ObsVisHandle - Handles/Maps Obstruction to Vision, based on obsobvis.f (lamp_shared), obsobvbin.f (lamp_shared), and cnvtwx.f (Felicia's lmp_obsprep.fd/) def ObsVisHandle(obstype,prwx1,prwx2,prwx3): prwx1 = prwx1.replace('-','') # Ignore intensity prwx2 = prwx2.replace('-','') prwx3 = prwx3.replace('-','') prwx1 = prwx1.replace('+','') prwx2 = prwx2.replace('+','') prwx3 = prwx3.replace('+','') obstypeReject = ["AO1","AO1A"] # No Obs Vis for AO1, AO1A stations if any(x in obstype for x in obstypeReject): return 9999.0 obstypeRejectCondition = ["AUTO"] # No Obs Vis for AUTO stations without prwx1 if any(x in obstype for x in obstypeRejectCondition): if not prwx1: return 9999.0 obstypeAccept = ["AO2","AO2A","MANU"] # Acceptable station types if any(x in obstype for x in obstypeAccept): if not prwx1: return 1.0 # Return NONE from trusted station types without wx element report allNoneElements = ["PO","VCDS","PY","VCSH","TS","SQ","FC","VCFG","DZ","FZDZ","RADZ","RA","FZRA","IC","SG","PE","SHRA","GS","GR","TSRA","TSGR","UP","PL","VA","BLSP","RASN","SN","SHRASN","SHSN"] # Elements which do not count as Obs Vis BlowingOBVElements = ["BLDU","DRDU","DS","TSDS","BLSN","DRSN"] FogDenseFogElements = ["FG","BR"] HazeSmokeElements = ["FU","HZ","DU"] if any(x in prwx1 for x in allNoneElements) or any(x in prwx2 for x in allNoneElements) or any(x in prwx3 for x in allNoneElements): if any(y in prwx1 for y in BlowingOBVElements) or any(y in prwx2 for y in BlowingOBVElements): return 5.0 elif any(y in prwx1 for y in FogDenseFogElements) or any(y in prwx2 for y in FogDenseFogElements): standardFog = ["BC","PR","BR"] skipVC = ["VC"] if any(z in prwx1 for z in standardFog) or any(z in prwx2 for z in standardFog): # BC and PR fog not considered dense (as per cnvtwx.f) return 3.0 elif any(z in prwx1 for z in skipVC) or any(z in prwx2 for z in skipVC): return 1.0 else: return 4.0 elif any(y in prwx1 for y in HazeSmokeElements) or any(y in prwx2 for y in HazeSmokeElements): return 2.0 else: return 1.0 elif any(x in prwx1 for x in BlowingOBVElements) or any(x in prwx2 for x in BlowingOBVElements): return 5.0 elif any(x in prwx1 for x in FogDenseFogElements) or any(x in prwx2 for x in FogDenseFogElements): standardFog = ["BC","PR","BR"] if any(y in prwx1 for y in standardFog) or any(y in prwx2 for y in standardFog): # BC and PR fog not considered dense (as per cnvtwx.f) return 3.0 else: return 4.0 elif any(x in prwx1 for x in HazeSmokeElements) or any(x in prwx2 for x in HazeSmokeElements): return 2.0 else: return 9999.0 # Function: PrecipOccurHandle - Handles/Maps Precip, based on obspopo.f (lamp_shared), obstype.f (lamp_shared), and cnvtwx.f (Felicia's lmp_obsprep.fd/) def PrecipOccurHandle(obstype,prwx1,prwx2,prwx3): precipElements = ["DZ","RA","SN","SG","IC","PL","GR","GS","UP"] prwx1 = prwx1.replace('-','') # Ignore intensity prwx2 = prwx2.replace('-','') prwx3 = prwx3.replace('-','') prwx1 = prwx1.replace('+','') prwx2 = prwx2.replace('+','') prwx3 = prwx3.replace('+','') obstypeReject = ["AO1","AO1A"] if any(x in obstype for x in obstypeReject): return 9999.0,9999.0 obstypeRejectCondition = ["AUTO"] if any(x in obstype for x in obstypeRejectCondition): if not prwx1: return 9999.0,9999.0 obstypeAccept = ["AO2","AO2A","MANU"] if any(x in obstype for x in obstypeAccept): if not prwx1: return 0.0,9999.0 if any(x in prwx1 for x in precipElements) or any(x in prwx2 for x in precipElements) or any(x in prwx3 for x in precipElements): rainElements = ["RA","DZ"] snowElements = ["SN","SG"] drblSnow = ["BL","DR"] iceElements = ["PL"] if any(y in prwx1 for y in rainElements) or any(y in prwx2 for y in rainElements) or any(y in prwx3 for y in rainElements): if ("SN") in prwx1: obsppt = 5.0 # rain/snow --> 5.0 return 1.0,obsppt elif ("TS") in prwx1: obsppt = 7.0 # t-storm + rain --> 7.0 return 1.0,obsppt elif ("FZ") in prwx1: obsppt = 1.0 # freezing rain/drizzle --> 1.0 return 1.0,obsppt else: obsppt = 6.0 # Rain --> 6.0 return 1.0,obsppt elif any(y in prwx1 for y in snowElements): if any(z in prwx1 for z in drblSnow): # Blowing/drifting snow not considered (as per cnvtwx.f) obsppt = 9999.0 return 0.0,obsppt else: obsppt = 4.0 # snow --> 4.0 return 1.0,obsppt elif any(y in prwx1 for y in iceElements): # Ice pellets obsppt = 3.0 # Ice pellets --> 3.0 return 1.0,obsppt else: obsppt = 9999.0 return 1.0,obsppt else: obsppt = 9999.0 return 0.0,obsppt # Reported a non-precip wx element, such as IC, GR def tryFloat(elements): for ele in range(len(elements)): try: elements[ele] = float(elements[ele]) except ValueError: elements[ele] = 9999.0 return elements # Handle arguments if len(sys.argv) == 2: date1 = sys.argv[1] date1=datetime.strptime(date1,'%Y%m%d%H%M') else: sys.exit() YYYY=str(date1.year).zfill(4) MM=str(date1.month).zfill(2) DD=str(date1.day).zfill(2) HH=str(date1.hour).zfill(2) YYYYMMDD = YYYY+MM+DD YYYYMMDDHH=YYYY+MM+DD+HH # Dir for 15min updated sfctbls sfctbldir15min = comoutbase + '/lmp.' + YYYYMMDD # Store cat'd/sorted sfctbls #sfctbldir= comoutbase + '/lmp.' + YYYYMMDD + '/tmp_sfctbls' #if not os.path.exists(sfctbldir): # os.makedirs(sfctbldir) # Create working directory workdir= data + '/meteo_obs15m' if not os.path.exists(workdir): os.makedirs(workdir) # Final product directory output = comoutbase + '/lmp.' + YYYYMMDD if not os.path.exists(output): os.makedirs(output) # Check if HH:30 surface table exists. If it does not, cat together current # hour and previous hours surface tables, since late obs make it into HH:15 # sfctbls. Handle previous hour 23z from previous day sfctbl = '/lmp_sfctbl_early.' + HH if not os.path.exists(sfctbldir15min + sfctbl + '30'): HHm1 = str(int(HH) - 1).zfill(2) # Define previous hour, pad with 0 if 00 <= HH <= 09, type STR if int(HHm1) < 0: # Check for HHm1 = previous day 23z date1m1=date1+timedelta(days=-1) YYYYm1=str(date1m1.year).zfill(4) MMm1=str(date1m1.month).zfill(2) DDm1=str(date1m1.day).zfill(2) YYYYMMDDmd1=YYYYm1+MMm1+DDm1 # PDY previous day sfctbldir15minmd1 = comoutbase + '/lmp.' + YYYYMMDDmd1 # Prev day directory HHm1=str(23) # Force assign HHm1 to previous day 23z sfctblm1 = '/lmp_sfctbl_early.' +HHm1 YYYYMMDDHHm1=YYYYMMDDmd1+HHm1 # Set concatenation commands, current hour and previous day 23z # catCMDprev='cat '+sfctbldir15min+sfctbl+'* ' +sfctbldir15minmd1+sfctblm1+'* '+' > '+sfctbldir+sfctbl+'.prev.all' catCMDprev='cat '+sfctbldir15min+sfctbl+'* ' +sfctbldir15minmd1+sfctblm1+'* '+' > '+workdir+sfctbl+'.prev.all' # catCMD='cat '+sfctbldir15min+sfctbl+'* > '+sfctbldir+sfctbl+'.all' catCMD='cat '+sfctbldir15min+sfctbl+'* > '+workdir+sfctbl+'.all' else: sfctblm1 = '/lmp_sfctbl_early.' + HHm1 YYYYMMDDmd1=YYYYMMDD YYYYMMDDHHm1=YYYYMMDD+HHm1 # Set concatenation commands, current hour and previous hour # catCMDprev='cat '+sfctbldir15min+sfctbl+'* ' +sfctbldir15min+sfctblm1+'* '+' > '+sfctbldir+sfctbl+'.prev.all' catCMDprev='cat '+sfctbldir15min+sfctbl+'* ' +sfctbldir15min+sfctblm1+'* '+' > '+workdir+sfctbl+'.prev.all' # catCMD='cat '+sfctbldir15min+sfctbl+'* > '+sfctbldir+sfctbl+'.all' catCMD='cat '+sfctbldir15min+sfctbl+'* > '+workdir+sfctbl+'.all' # Cat current hour sfctbls; also cat previous hour for HH:00 and HH:15 runs os.system(catCMD) os.system(catCMDprev) # Define and issue the Sorting command; also sort previous hour cat'd sfctbl file for HH:00 and HH:15 runs # sortCMD='sort -u '+sfctbldir+sfctbl+'.all > '+sfctbldir+sfctbl sortCMD='sort -u '+workdir+sfctbl+'.all > '+workdir+sfctbl # sortCMDprev='sort -u '+sfctbldir+sfctbl+'.prev.all > '+sfctbldir+sfctblm1 sortCMDprev='sort -u '+workdir+sfctbl+'.prev.all > '+workdir+sfctblm1 os.system(sortCMD) os.system(sortCMDprev) # Read lines from sfctbl, ignore the leading 2 lines; repeat for previous hour for HH:00 and HH:15 runs # f = open(sfctbldir + sfctbl,'r') f = open(workdir + sfctbl,'r') tbl = f.readlines()[2:] f.close() # fm1 = open(sfctbldir + sfctblm1,'r') fm1 = open(workdir + sfctblm1,'r') tblm1= fm1.readlines()[2:] fm1.close() else: sfctbl = '/lmp_sfctbl_early.' + HH # Set concatenation command, current hour only (for HH:30 and HH:45 sfctbls) # catCMD='cat '+sfctbldir15min+sfctbl+'* > '+sfctbldir+sfctbl+'.all' catCMD='cat '+sfctbldir15min+sfctbl+'* > '+workdir+sfctbl+'.all' # Cat current hour sfctbls os.system(catCMD) # Define and issue the Sorting command # sortCMD='sort -u '+sfctbldir+sfctbl+'.all > '+sfctbldir+sfctbl sortCMD='sort -u '+workdir+sfctbl+'.all > '+workdir+sfctbl os.system(sortCMD) # Read lines from sfctbl, ignore the leading 2 lines # f = open(sfctbldir + sfctbl,'r') f = open(workdir + sfctbl,'r') tbl = f.readlines()[2:] f.close() # Write a Meteogram Obs file, formatted as u660 would # OBSRPT = 1: METAR; OBSRPT = 2: SPECIAL #o = open(output + '/lmp_meteo.15min.obs.' + YYYYMMDDHH,'w') o = open(workdir + '/lmp_meteo.15min.obs.' + YYYYMMDDHH,'w') o.write('%35s %9s %3s \n\n' % ("DATE/TIME:", YYYYMMDD, HH)) o.write(' DATA FOR %13s\n\n' % (YYYYMMDDHH)) # Header formatting o.write(' VARIABLE %7s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s\n' % ("OBTIME", "OBSRPT", "OBSTMP", "OBSDEW", "OBSDIR", "OBSSPD", "OBSGST", "OBSPPO", "OBSPPT", "OBSSKY", "OBSCIG", "OBSVIS", "OBSOBV")) # Loop through lines, assigning values based on element-specific functions defined at top of script ObCounter = 0 # Counter for obs that are within the hour prevLine = "" for idx, line in enumerate(tbl): if 'END-OF-HOUR' in line: # Do not read lines with 'END-OF-HOUR', 'PRWX1', or 'MDL HOURLY METAR TABLE' continue if 'PRWX1' in line: continue if 'MDL HOURLY METAR TABLE' in line: continue split = line.split(':') station = split[0].replace(' ','') time = split[4].replace(' ','') # Gather data reported during the HOUR if time[:2] == HH: obstype, obstmp, obsdew, prwx1, prwx2, prwx3, obsvis, obsdir, obsspd, obsgst, CA1, CH1, CA2, CH2, CA3, CH3, CA4, CH4, CA5, CH5, CA6, CH6, PCP1, sun = parseHandle(split) # Call to parseHandle function obssky,obscig = CigHandle(CA1,CA2,CA3,CA4,CA5,CA6,CH1,CH2,CH3,CH4,CH5,CH6) # Call to CigHandle function obssky = OpaqueSkyHandle(CA1,CA2,CA3,CA4,CA5,CA6) # Call to OpaqueSkyHandle function obssky = SkyHandle(obssky) # Call to SkyHandle function obsobv = ObsVisHandle(obstype,prwx1,prwx2,prwx3) # Call to ObsVisHandle function obsppo,obsppt = PrecipOccurHandle(obstype,prwx1,prwx2,prwx3) # Call to PrecipOccurHandle function elements = [obstmp, obsdew, obsdir, obsspd, obsgst, obsppo, obsppt, obssky, obscig, obsvis, obsobv] # Array holding final data to write # For missing info, replace whitespace with 9999.0 elements = tryFloat(elements) if sun == 'REG': obsrpt = 1 elif sun == 'SPE': obsrpt = 2 else: obsrpt = 3 # Add format for the other variables formatOutput = '%5s %-5s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s\n' variableListContent = (station, time, obsrpt, elements[0], elements[1], elements[2], elements[3], elements[4], elements[5], elements[6], elements[7], elements[8], elements[9], elements[10]) # Check to ensure duplicated lines are ignored if prevLine != variableListContent: # JOE 07/2017 Still needs to be added: add check that if the contents are different, but the station and time are the same, only write newer one... This is relevant if an observation is corrected ObCounter += 1 variableList = (ObCounter, station, time, obsrpt, elements[0], elements[1], elements[2], elements[3], elements[4], elements[5], elements[6], elements[7], elements[8], elements[9], elements[10]) o.write(formatOutput % variableList) prevLine = variableListContent continue o.close() # Define and issue copy command to copy Meteogram Obs file to COMOUT meteofile = '/lmp_meteo.15min.obs.' + YYYYMMDDHH cpCMD='cp '+workdir+meteofile+' ' +output+meteofile os.system(cpCMD) # If :00 or :15 run, repeat for previous hour if not os.path.exists(sfctbldir15min + sfctbl + '30'): #JJN 2017.07.31 if 21 <= int(mm) <= 26: # Write a Meteogram Obs file for previous hour, formatted as u660 would # OBSRPT = 1: METAR; OBSRPT = 2: SPECIAL # om1 = open(output + '/lmp_meteo.15min.obs.' + YYYYMMDDHHm1,'w') om1 = open(workdir + '/lmp_meteo.15min.obs.' + YYYYMMDDHHm1,'w') om1.write('%35s %9s %3s \n\n' % ("DATE/TIME:", YYYYMMDDmd1, HHm1)) om1.write(' DATA FOR %13s\n\n' % (YYYYMMDDHHm1)) om1.write(' VARIABLE %7s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s\n' % ("OBTIME", "OBSRPT", "OBSTMP", "OBSDEW", "OBSDIR", "OBSSPD", "OBSGST", "OBSPPO", "OBSPPT", "OBSSKY", "OBSCIG", "OBSVIS", "OBSOBV")) ObCounter2 = 0 # Counter for obs that are within the hour prevLine = "" for idx, line in enumerate(tblm1): if 'END-OF-HOUR' in line: continue if 'PRWX1' in line: continue if 'MDL HOURLY METAR TABLE' in line: continue split = line.split(':') station = split[0].replace(' ','') time = split[4].replace(' ','') # Gather data reported during the previous HOUR if time[:2] == HHm1: obstype, obstmp, obsdew, prwx1, prwx2, prwx3, obsvis, obsdir, obsspd, obsgst, CA1, CH1, CA2, CH2, CA3, CH3, CA4, CH4, CA5, CH5, CA6, CH6, PCP1, sun = parseHandle(split) obssky, obscig = CigHandle(CA1,CA2,CA3,CA4,CA5,CA6,CH1,CH2,CH3,CH4,CH5,CH6) obssky = OpaqueSkyHandle(CA1,CA2,CA3,CA4,CA5,CA6) obssky = SkyHandle(obssky) obsobv = ObsVisHandle(obstype,prwx1,prwx2,prwx3) obsppo,obsppt = PrecipOccurHandle(obstype,prwx1,prwx2,prwx3) elements = [obstmp, obsdew, obsdir, obsspd, obsgst, obsppo, obsppt, obssky, obscig, obsvis, obsobv] # For missing info, replace whitespace with 9999.0 elements = tryFloat(elements) if sun == 'REG': obsrpt = 1 elif sun == 'SPE': obsrpt = 2 else: obsrpt = 3 # Add format for elements formatOutput = '%5s %-5s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s %6s\n' variableListContent = (station, time, obsrpt, elements[0], elements[1], elements[2], elements[3], elements[4], elements[5], elements[6], elements[7], elements[8], elements[9], elements[10]) if prevLine != variableListContent: ObCounter2 += 1 variableList = (ObCounter2, station, time, obsrpt, elements[0], elements[1], elements[2], elements[3], elements[4], elements[5], elements[6], elements[7], elements[8], elements[9], elements[10]) om1.write(formatOutput % variableList) prevLine = variableListContent continue om1.close() # Define and issue copy command to copy Meteogram Obs file to COMOUT meteofile = '/lmp_meteo.15min.obs.' + YYYYMMDDHHm1 cpCMD='cp '+workdir+meteofile+' ' +output+meteofile os.system(cpCMD)