878 lines
33 KiB
Python
878 lines
33 KiB
Python
##
|
|
# This software was developed and / or modified by Raytheon Company,
|
|
# pursuant to Contract DG133W-05-CQ-1067 with the US Government.
|
|
#
|
|
# U.S. EXPORT CONTROLLED TECHNICAL DATA
|
|
# This software product contains export-restricted data whose
|
|
# export/transfer/disclosure is restricted by U.S. law. Dissemination
|
|
# to non-U.S. persons whether in the United States or abroad requires
|
|
# an export license or other authorization.
|
|
#
|
|
# Contractor Name: Raytheon Company
|
|
# Contractor Address: 6825 Pine Street, Suite 340
|
|
# Mail Stop B8
|
|
# Omaha, NE 68106
|
|
# 402.291.0100
|
|
#
|
|
# See the AWIPS II Master Rights File ("Master Rights File.pdf") for
|
|
# further licensing information.
|
|
##
|
|
#
|
|
# Name:
|
|
# %PM%
|
|
# %PID%
|
|
#
|
|
# Status:
|
|
# %PS%
|
|
#
|
|
# History:
|
|
# %PL%
|
|
#
|
|
# Change Document History:
|
|
# %PIRC%
|
|
#
|
|
# SOFTWARE HISTORY
|
|
#
|
|
# Date Ticket# Engineer Description
|
|
# ------------- -------- ----------- ------------------------------------------
|
|
# Oct 28, 2015 15464 zhao Modified mkTempo & mkProb30 to handle case
|
|
# of "TS"+"SKC"
|
|
# Feb 02, 2018 7001 tgurney Clean up NameErrors
|
|
# Mar 07, 2020 21314 mporricelli Updated to allow LAMP data fields to have
|
|
# extended forecast hours
|
|
# Feb 01, 2021 8316 randerso Added missing default value in
|
|
# LampProjection.mkProb30(). Cleaned up
|
|
# Python 2 style -inf values.
|
|
# Mar 10, 2022 8808 randerso Update ConfigParser to better work with
|
|
# Java commons.configuration
|
|
#
|
|
##
|
|
# This is a base file that is not intended to be overridden.
|
|
##
|
|
|
|
import logging
|
|
import math
|
|
|
|
import Avn
|
|
import AvnConfigParser
|
|
import AvnLib
|
|
import AvnParser
|
|
|
|
_Logger = logging.getLogger(Avn.CATEGORY)
|
|
|
|
|
|
# moved here from GridData.py
|
|
def fixPcp(plist):
|
|
if not plist or plist == [' ']:
|
|
return ''
|
|
# skip the first one if it is an empty string
|
|
if plist[0] == ' ':
|
|
plist = plist[1:]
|
|
|
|
# TS is always first, set in makeRec()
|
|
if plist[0] == 'TS':
|
|
plist = plist[1:]
|
|
ts = 'TS'
|
|
else:
|
|
ts = None
|
|
# push FZ to the top of the list
|
|
for p in plist:
|
|
if 'FZ' in p:
|
|
fz = p
|
|
plist.remove(p)
|
|
break
|
|
else:
|
|
fz = ''
|
|
if ts:
|
|
if fz:
|
|
pcp = 'TS %s' % fz
|
|
elif plist:
|
|
if plist[0][0] in ['+', '-']:
|
|
i, p = plist[0][0], plist[0][1:]
|
|
else:
|
|
i, p = '', plist[0]
|
|
# replace SH by TS
|
|
if p[:2] == 'SH':
|
|
pcp = '%sTS%s' % (i, p[2:])
|
|
else:
|
|
pcp = '%sTS%s' % (i, p)
|
|
plist[0] = pcp
|
|
else:
|
|
return 'TS'
|
|
elif fz:
|
|
pcp = fz
|
|
else:
|
|
pcp = plist[0]
|
|
plist.pop(0)
|
|
for p in plist:
|
|
if p[0] in ('+', '-'):
|
|
p = p[1:]
|
|
if pcp.find(p) == -1:
|
|
pcp = pcp + p
|
|
return pcp
|
|
|
|
|
|
class Config:
|
|
|
|
def __init__(self, siteIdent, model):
|
|
self.grpTafFile = Avn.getTafPath(siteIdent, 'grp_taf.cfg')
|
|
self.fltCatFile = Avn.getTafPath(siteIdent, 'flt_cat.cfg')
|
|
self.model = model
|
|
|
|
def grpTaf(self):
|
|
config = AvnConfigParser.AvnConfigParser()
|
|
prdCfg = {}
|
|
try:
|
|
config.read(self.grpTafFile)
|
|
if config.has_section(self.model):
|
|
prdCfg['prev_pop'] = config.getint(self.model, 'prev_precip')
|
|
prdCfg['prev_tstm'] = config.getint(self.model, 'prev_tstm')
|
|
prdCfg['tempo_tstm'] = config.getint(self.model, 'tempo_tstm')
|
|
prdCfg['tempo_pop'] = config.getint(self.model, 'tempo_precip')
|
|
prdCfg['prob30_tstm'] = config.getint(self.model, 'prob30_tstm')
|
|
prdCfg['prob30_pop'] = config.getint(self.model, 'prob30_precip')
|
|
else: #use mos configuration as a default
|
|
prdCfg['prev_pop'] = config.getint('mos', 'prev_precip')
|
|
prdCfg['prev_tstm'] = config.getint('mos', 'prev_tstm')
|
|
prdCfg['tempo_tstm'] = config.getint('mos', 'tempo_tstm')
|
|
prdCfg['tempo_pop'] = config.getint('mos', 'tempo_precip')
|
|
prdCfg['prob30_tstm'] = config.getint('mos', 'prob30_tstm')
|
|
prdCfg['prob30_pop'] = config.getint('mos', 'prob30_precip')
|
|
|
|
prdCfg['prob30'] = config.getboolean('prob30', 'value')
|
|
prdCfg['short_dt'] = config.getint('def_singleton', 'short_dt')
|
|
prdCfg['long_dt'] = config.getint('def_singleton', 'long_dt')
|
|
prdCfg['low_p'] = config.getint('def_singleton', 'low_p')
|
|
prdCfg['vis_cat'] = {'dc':config.getint('vis_diff', 'dc_vis')}
|
|
prdCfg['vis_cat']['low_c'] = config.getint('vis_diff', 'low_c_vis')
|
|
prdCfg['vis_cat']['high_c'] = config.getint('vis_diff', 'high_c_vis')
|
|
prdCfg['cig_cat'] = {'dc':config.getint('cig_diff', 'dc_cig')}
|
|
prdCfg['cig_cat']['low_c'] = config.getint('cig_diff', 'low_c_cig')
|
|
prdCfg['cig_cat']['high_c'] = config.getint('cig_diff', 'high_c_cig')
|
|
prdCfg['cbhight'] = config.getint('cb_hi', 'value')
|
|
prdCfg['calmwd'] = config.getint('wind', 'calm_wind')
|
|
prdCfg['wdthrsh'] = config.getint('wind', 'high_wind')
|
|
prdCfg['wddt'] = config.getint('wind', 'wind_chg')
|
|
return prdCfg
|
|
except:
|
|
_Logger.exception('Cannot read taf config file,%s.', self.grpTafFile)
|
|
|
|
def fltCat(self):
|
|
cat_cfg = AvnConfigParser.AvnConfigParser()
|
|
vc, cc = [], []
|
|
try:
|
|
cat_cfg.read(self.fltCatFile)
|
|
vc = [float(v) for v in cat_cfg.get('vis', 'thresholds').split(',')]
|
|
cc = [int(v) for v in cat_cfg.get('cig', 'thresholds').split(',')]
|
|
return {'vis':vc, 'cig':cc}
|
|
except:
|
|
_Logger.exception('Cannot read flight category config file,%s.', self.fltCatFile)
|
|
|
|
|
|
class Projection:
|
|
|
|
def __init__(self, siteId, grpTaf, fltCat, fcstData, itime):
|
|
self.siteId = siteId
|
|
self.startTime = fcstData.get('time').get('from')
|
|
self.endTime = fcstData.get('time').get('to')
|
|
self.itime = itime
|
|
self.wind = fcstData.get('wind')
|
|
self.vis = fcstData.get('vsby')
|
|
self.sky = fcstData.get('sky')
|
|
self.obv = fcstData.get('obv')
|
|
self.grpTaf = grpTaf
|
|
self.fltCat = fltCat
|
|
self.visCat = self.mkVisCat()
|
|
self.cigCat = self.mkCigCat()
|
|
self.pcp = self.mkPcp(fcstData.get('pcp'))
|
|
self.tstm = self.mkTstm(fcstData.get('pcp'))
|
|
self.tempo = self.mkTempo(fcstData.get('pcp'))
|
|
#mkProb30 needs to be called after mkTempo since some
|
|
#consolidation is done in mkProb30
|
|
self.prob30 = self.mkProb30(fcstData.get('pcp'))
|
|
|
|
def getCat(self, val, thresholds):
|
|
if thresholds[-1] == 5:#MVFR limit
|
|
thresholds[-1] = 5.1 #so that 5 SM will be MVFR
|
|
nlen = len(thresholds)
|
|
if val < 0:
|
|
return 0
|
|
#VLIFR
|
|
elif val < thresholds[0]:
|
|
return 1
|
|
#VFR or the last category
|
|
elif val >= thresholds[-1]:
|
|
return nlen + 1
|
|
#LIFR, IFR , MVFR, VFR(when more than one VFR categories exist)
|
|
else:
|
|
for i in range(nlen - 1):
|
|
if thresholds[i] <= val < thresholds[i + 1]:
|
|
return i + 2
|
|
|
|
def mkVisCat(self):
|
|
if self.vis:
|
|
return self.getCat(self.vis['value'], self.fltCat['vis'])
|
|
else:
|
|
return 0
|
|
|
|
def mkCigCat(self):
|
|
if self.sky and self.sky.get('cig'):
|
|
return self.getCat(self.sky['cig'], self.fltCat['cig'])
|
|
else:
|
|
return 0
|
|
|
|
def mkPcp(self, pcpn):
|
|
if not pcpn:
|
|
return
|
|
pop = pcpn.get('pop', -math.inf)
|
|
if pop >= self.grpTaf['prev_pop']:
|
|
return pcpn['str']
|
|
|
|
def mkTstm(self, pcpn):
|
|
if not pcpn:
|
|
return
|
|
pot = pcpn.get('pot', -math.inf)
|
|
if pot >= self.grpTaf['prev_tstm']:
|
|
self.sky['str'] += 'CB'
|
|
return 'TS'
|
|
|
|
def mkTempo(self, pcpn):
|
|
tmpStr = []
|
|
if pcpn:
|
|
if (self.grpTaf['prev_tstm'] > pcpn.get('pot', -math.inf) >=
|
|
self.grpTaf['tempo_tstm']):
|
|
tmpStr.append('TS')
|
|
if (self.grpTaf['prev_pop'] > pcpn.get('pop', -math.inf) >=
|
|
self.grpTaf['tempo_pop']):
|
|
tmpStr.append(pcpn['str'])
|
|
if tmpStr == ['TS'] and self.pcp:
|
|
tmpStr.append(self.pcp)
|
|
if tmpStr:
|
|
skyStr = self.sky['str']
|
|
if 'TS' in tmpStr:
|
|
# make sure cig is below threshold
|
|
try:
|
|
cig = int(self.sky['str'][3:])
|
|
except ValueError:
|
|
cig = 250
|
|
if cig > self.grpTaf['cbhight']:
|
|
cig = self.grpTaf['cbhight']
|
|
skyStr = "%s%03d%s" % ('BKN', cig, 'CB')
|
|
return {'wxStr':fixPcp(tmpStr), 'visStr':self.vis['str'],
|
|
'skyStr':skyStr}
|
|
|
|
def mkProb30(self, pcpn):
|
|
try:
|
|
if (self.startTime - self.itime >= 9 * 3600 and
|
|
self.grpTaf['prob30'] and pcpn):
|
|
tmpStr = []
|
|
if not pcpn.get('str'):
|
|
pcpn['str'] = []
|
|
if (self.grpTaf['prob30_tstm'] <= pcpn.get('pot', -math.inf) <
|
|
self.grpTaf['tempo_tstm']):
|
|
tmpStr.append('TS')
|
|
if (self.grpTaf['prob30_pop'] <= pcpn.get('pop', -math.inf) <
|
|
self.grpTaf['tempo_pop']):
|
|
tmpStr.append(pcpn['str'])
|
|
if tmpStr:
|
|
# Consolidate 'TS' & precip in tempo and prob30 seperately
|
|
if self.tempo and self.tempo['wxStr'].find('TS') >= 0:
|
|
tmpStr.insert(0, self.tempo['wxStr'])
|
|
self.tempo['wxStr'] = fixPcp(tmpStr)
|
|
return None
|
|
elif 'TS' in tmpStr:
|
|
self.tempo = None
|
|
if tmpStr == ['TS'] and self.pcp:
|
|
tmpStr.append(self.pcp)
|
|
skyStr = self.sky['str']
|
|
if 'TS' in tmpStr:
|
|
# make sure cig is below threshold
|
|
cig = int(self.sky['str'][3:])
|
|
if cig > self.grpTaf['cbhight']:
|
|
cig = self.grpTaf['cbhight']
|
|
skyStr = "%s%03d%s" % ('BKN', cig, 'CB')
|
|
return {'wxStr':fixPcp(tmpStr), 'visStr':self.vis['str'],
|
|
'skyStr':skyStr}
|
|
except (ValueError, IndexError, KeyError):
|
|
return None
|
|
|
|
def getData(self):
|
|
return {'visCat':self.visCat, 'cigCat':self.cigCat, 'tempo':self.tempo,
|
|
'prob30':self.prob30, 'startTime':self.startTime, 'endTime':self.endTime,
|
|
'vis':self.vis, 'sky':self.sky, 'wind':self.wind, 'obv':self.obv,
|
|
'tstm':self.tstm, 'pcp':self.pcp}
|
|
|
|
|
|
class LampProjection(Projection):
|
|
|
|
def __init__(self, siteId, grpTaf, fltCat, fcstData, itime):
|
|
self.siteId = siteId
|
|
self.startTime = fcstData.get('time').get('from')
|
|
self.endTime = fcstData.get('time').get('to')
|
|
self.itime = itime
|
|
self.wind = fcstData.get('wind')
|
|
self.vis = fcstData.get('vsby')
|
|
self.sky = fcstData.get('sky')
|
|
self.obv = fcstData.get('obv')
|
|
self.grpTaf = grpTaf
|
|
self.fltCat = fltCat
|
|
self.pcat = fcstData.get('pcp', {}).get('pcat')
|
|
self.tcat = fcstData.get('pcp', {}).get('tcat')
|
|
self.csky = fcstData.get('csky')
|
|
self.cvis = fcstData.get('cvsby')
|
|
self.pcp = self.mkPcp(fcstData['pcp'])
|
|
if self.pcp: # If precip,use conditional cig/vis
|
|
self.vis = self.cvis
|
|
self.sky = self.csky
|
|
self.visCat = self.mkVisCat()
|
|
self.cigCat = self.mkCigCat()
|
|
self.tstm = self.mkTstm(fcstData['pcp'])
|
|
self.tempo = self.mkTempo(fcstData['pcp'])
|
|
# mkProb30 needs to be called after mkTempo since some
|
|
# consolidation is done in mkProb30
|
|
self.prob30 = self.mkProb30(fcstData['pcp'])
|
|
|
|
def mkPcp(self, pcpn):
|
|
try:
|
|
if self.pcat or pcpn['pop'] > self.grpTaf['prev_pop']:
|
|
self.sky = self.csky
|
|
self.vis = self.cvis
|
|
self.visCat = self.mkcVisCat()
|
|
self.cigCat = self.mkcCigCat()
|
|
return pcpn['str']
|
|
except Exception as e:
|
|
_Logger.info('mkPcp: Field not yet available in LAMP data for this forecast hour. ' + str(e) + ' Skip this calculation.')
|
|
return None
|
|
|
|
def mkcVisCat(self):
|
|
return self.getCat(self.cvis['value'], self.fltCat['vis'])
|
|
|
|
def mkcCigCat(self):
|
|
return self.getCat(self.csky['cig'], self.fltCat['cig'])
|
|
|
|
def mkTempo(self, pcpn):
|
|
tmpStr = []
|
|
if self.grpTaf['prev_tstm'] > pcpn.get('pot', -math.inf) >= self.grpTaf['tempo_tstm']:
|
|
tmpStr.append('TS')
|
|
if self.pcat != 1 and self.grpTaf['prev_pop'] > pcpn.get('pop', -math.inf) >= self.grpTaf['tempo_pop']:
|
|
tmpStr.append(pcpn['str'])
|
|
if tmpStr:
|
|
skyStr = self.csky['str']
|
|
visStr = self.cvis['str']
|
|
# dry thunder
|
|
if tmpStr == ['TS'] and self.pcp:
|
|
tmpStr.append(self.pcp)
|
|
skyStr = self.sky['str']
|
|
visStr = self.vis['str']
|
|
if 'TS' in tmpStr:
|
|
# make sure cig is below threshold
|
|
if skyStr != 'SKC':
|
|
cig = int(skyStr[3:])
|
|
if cig > self.grpTaf['cbhight']:
|
|
cig = self.grpTaf['cbhight']
|
|
skyStr = "%s%03d%s" % ('BKN', cig, 'CB')
|
|
return {'wxStr': fixPcp(tmpStr), 'visStr': visStr, 'skyStr': skyStr}
|
|
|
|
def mkProb30(self, pcpn):
|
|
if self.startTime - self.itime >= 9 * 3600 and self.grpTaf['prob30']:
|
|
tmpStr = []
|
|
if not pcpn.get('str'):
|
|
pcpn['str'] = []
|
|
if self.grpTaf['prob30_tstm'] <= pcpn.get('pot', 0) < \
|
|
self.grpTaf['tempo_tstm']:
|
|
tmpStr.append('TS')
|
|
if self.pcat != 1 and self.grpTaf['prob30_pop'] <= \
|
|
pcpn.get('pop', -math.inf) <= self.grpTaf['tempo_pop']:
|
|
tmpStr.append(pcpn['str'])
|
|
if tmpStr:
|
|
#Consolidate tempo and prob30
|
|
if self.tempo and self.tempo['wxStr'].find('TS') >= 0:
|
|
tmpStr.insert(0, self.tempo['wxStr'])
|
|
self.tempo['wxStr'] = fixPcp(tmpStr)
|
|
return None
|
|
elif 'TS' in tmpStr:
|
|
self.tempo = None
|
|
skyStr = self.csky['str']
|
|
visStr = self.cvis['str']
|
|
if tmpStr == ['TS'] and self.pcp:
|
|
tmpStr.append(self.pcp)
|
|
skyStr = self.sky['str']
|
|
visStr = self.vis['str']
|
|
if 'TS' in tmpStr:
|
|
#make sure cig is below threshold
|
|
if skyStr != 'SKC':
|
|
cig = int(skyStr[3:])
|
|
if cig > self.grpTaf['cbhight']:
|
|
cig = self.grpTaf['cbhight']
|
|
skyStr = "%s%03d%s" % ('BKN', cig, 'CB')
|
|
return {'wxStr':fixPcp(tmpStr), 'visStr':visStr, \
|
|
'skyStr':skyStr}
|
|
|
|
|
|
class Comparison:
|
|
|
|
def __init__(self, proj1, proj2, grpTaf):
|
|
self.dataA = proj1
|
|
self.dataB = proj2
|
|
self.grpTaf = grpTaf
|
|
|
|
def compVis(self):
|
|
if self.dataA['visCat'] == self.dataB['visCat']:
|
|
return 1
|
|
|
|
def compCig(self):
|
|
if self.dataA['cigCat'] == self.dataB['cigCat']:
|
|
return 1
|
|
|
|
def compWind(self):
|
|
if self.dataA['wind'] and self.dataB['wind']:
|
|
aff = self.dataA.get('wind', {}).get('ff', 0)
|
|
bff = self.dataB.get('wind', {}).get('ff', 0)
|
|
add = self.dataA.get('wind', {}).get('dd', 0)
|
|
bdd = self.dataB.get('wind', {}).get('dd', 0)
|
|
agg = self.dataA.get('wind', {}).get('gg', 0)
|
|
bgg = self.dataB.get('wind', {}).get('gg', 0)
|
|
if (agg > 10 or bgg > 10) and abs(agg - bgg) > 5:
|
|
return None
|
|
if aff < self.grpTaf['wdthrsh'] and bff < self.grpTaf['wdthrsh']:
|
|
return 1
|
|
ddir = abs(add - bdd)
|
|
dspd = abs(aff - bff)
|
|
if ddir > 180:
|
|
ddir = 360 - ddir
|
|
if ddir < 30 and dspd < self.grpTaf['wddt']:
|
|
return 1
|
|
elif not self.dataA['wind'] and not self.dataB['wind']:
|
|
return 1
|
|
|
|
def compPcp(self):
|
|
if self.dataA.get('pcp') == self.dataB.get('pcp'):
|
|
return 1
|
|
|
|
def compTstm(self):
|
|
if self.dataA.get('tstm') == self.dataB.get('tstm'):
|
|
return 1
|
|
|
|
def compTempo(self):
|
|
if (self.dataA.get('tempo') and self.dataB.get('prob30')) or \
|
|
(self.dataA.get('prob30') and self.dataB.get('tempo')):
|
|
return None
|
|
if not self.dataA.get('tempo') or not self.dataB.get('tempo'):
|
|
return 1
|
|
else:
|
|
if self.dataA.get('tempo').get('wxStr') == \
|
|
self.dataB.get('tempo').get('wxStr') and \
|
|
self.dataA.get('tempo').get('skyStr')[3:] == \
|
|
self.dataB.get('tempo').get('skyStr')[3:] and \
|
|
self.dataA.get('tempo').get('visStr')[-3:] == \
|
|
self.dataB.get('tempo').get('visStr')[-3:]:
|
|
return 1
|
|
|
|
def compProb30(self):
|
|
if not self.dataA.get('prob30') or not self.dataB.get('prob30'):
|
|
return 1
|
|
else:
|
|
if self.dataA.get('prob30').get('wxStr') == \
|
|
self.dataB.get('prob30').get('wxStr') \
|
|
and self.dataA.get('prob30').get('skyStr')[3:] == \
|
|
self.dataB.get('prob30').get('skyStr')[3:] \
|
|
and self.dataA.get('prob30').get('visStr')[-3:] == \
|
|
self.dataB.get('prob30').get('visStr')[-3:]:
|
|
return 1
|
|
|
|
def isSame(self):
|
|
return self.compVis() and self.compCig() and self.compWind() and \
|
|
self.compPcp() and self.compTempo() and self.compProb30() and \
|
|
self.compTstm()
|
|
|
|
|
|
class Summarize:
|
|
|
|
def __init__(self, grpTaf, list):
|
|
self.startTimes = [v['startTime'] for v in list]
|
|
self.endTimes = [v['endTime'] for v in list]
|
|
self.grpTaf = grpTaf
|
|
self.vis = [v['vis'] for v in list]
|
|
self.sky = [v['sky'] for v in list]
|
|
self.wdDir = []
|
|
self.wdSpd = []
|
|
self.wdGst = []
|
|
for v in list:
|
|
if v['wind']:
|
|
self.wdDir.append(v['wind'].get('dd'))
|
|
self.wdSpd.append(v['wind'].get('ff'))
|
|
self.wdGst.append(v['wind'].get('gg'))
|
|
else:
|
|
self.wdDir.append(None)
|
|
self.wdSpd.append(None)
|
|
self.wdGst.append(None)
|
|
self.obv = [v['obv'] for v in list]
|
|
#for the following elements,they should be identical in the list
|
|
self.pcp = list[0]['pcp']
|
|
self.tstm = list[0]['tstm']
|
|
self.tempo = [t['tempo'] for t in list]
|
|
self.prob30 = [p['prob30'] for p in list]
|
|
|
|
def summTime(self):
|
|
return {'from':self.startTimes[0], 'to':self.endTimes[-1]}
|
|
|
|
def summVis(self):
|
|
if [_f for _f in self.vis if _f]:
|
|
vsby = min([v['value'] for v in self.vis if v is not None])
|
|
else:
|
|
vsby = -1
|
|
return AvnLib.fixTafVsby(vsby)
|
|
|
|
def summSky(self):
|
|
d = {}
|
|
if [_f for _f in self.sky if _f]:
|
|
for s in self.sky:
|
|
d[s['cig']] = s['str']
|
|
minCig = min(d.keys())
|
|
return {'str':d[minCig]}
|
|
|
|
def summObv(self):
|
|
d = {}
|
|
for s, v in zip(self.obv, self.vis):
|
|
if s:
|
|
if s['str'] == 'FG' and v['value'] > 0.625:
|
|
s['str'] = 'BR'
|
|
elif s['str'] == 'BR' and v['value'] <= 0.625:
|
|
s['str'] = 'FG'
|
|
d[s['str']] = 1
|
|
return {'str': ' '.join(d)}
|
|
|
|
def varWind(self, wdir, wspd):
|
|
wdir = [w for w in self.wdDir if w is not None]
|
|
wspd = [w for w in self.wdSpd if w is not None]
|
|
if wdir and wspd and max(wspd) <= 6 and min(wspd) > self.grpTaf['calmwd']:
|
|
for d1 in wdir:
|
|
for d2 in wdir[1:]:
|
|
dDir = abs(d1 - d2)
|
|
if dDir > 180:
|
|
dDir = 360 - dDir
|
|
if dDir > 30:
|
|
varW = 'VRB%02dKT' % (sum(wspd) / len(wspd))
|
|
return {'str': varW}
|
|
|
|
def summWind(self):
|
|
rt = self.varWind(self.wdDir, self.wdSpd)
|
|
if not rt:
|
|
wdir = [a for a in self.wdDir if a != None]
|
|
wspd = [a for a in self.wdSpd if a != None]
|
|
if wdir and wspd:
|
|
u = sum(math.cos(math.radians(270 - dir)) * spd
|
|
for dir, spd in zip(wdir, wspd))
|
|
v = sum(math.sin(math.radians(270 - dir)) * spd
|
|
for dir, spd in zip(wdir, wspd))
|
|
dd = math.degrees(math.atan2(v, u))
|
|
else:
|
|
#missing dir or spd, ski
|
|
return ''
|
|
if dd >= -90:
|
|
dd = 270 - dd
|
|
else:
|
|
dd = -90 - dd
|
|
dd = int(10 * ((dd + 5) // 10))
|
|
if dd == 0:
|
|
dd = 360
|
|
ff = sum(wspd) // len(wspd)
|
|
if ff < self.grpTaf['calmwd']:
|
|
dd, ff = 0, 0
|
|
gst = [_f for _f in self.wdGst if _f]
|
|
if gst:
|
|
# calculate average wgust
|
|
gg = sum(gst) / len(gst)
|
|
if gg - ff > 5:
|
|
return {'str':'%03d%02dG%02dKT' % (dd, ff, gg)}
|
|
else:
|
|
return {'str':'%03d%02dKT' % (dd, ff)}
|
|
else:
|
|
return {'str':'%03d%02dKT' % (dd, ff)}
|
|
else:
|
|
return rt
|
|
|
|
def summPcp(self):
|
|
if self.tstm:
|
|
if self.pcp:
|
|
return {'str':fixPcp(['TS', self.pcp])}
|
|
else:
|
|
return {'str':'TS'}
|
|
else:
|
|
return {'str':self.pcp}
|
|
|
|
def summOcnlTime(self):
|
|
tp = [_f for _f in self.tempo if _f]
|
|
pb = [_f for _f in self.prob30 if _f]
|
|
tList = []
|
|
if tp:
|
|
for t, ind in zip(self.tempo, range(30)):
|
|
if t:
|
|
tList.append(ind)
|
|
elif pb:
|
|
for t, ind in zip(self.prob30, range(30)):
|
|
if t:
|
|
tList.append(ind)
|
|
b = self.startTimes[tList[0]]
|
|
e = self.endTimes[tList[-1]]
|
|
return {'from':b, 'to':e}
|
|
|
|
def summOcnl(self):
|
|
#The ocnl str should be identical, so pick up the first one
|
|
tempo, prob30 = None, None
|
|
if [_f for _f in self.tempo if _f]:
|
|
tempo = [_f for _f in self.tempo if _f][0]
|
|
if [_f for _f in self.prob30 if _f]:
|
|
prob30 = [_f for _f in self.prob30 if _f][0]
|
|
|
|
if tempo:
|
|
return {'type':'TEMPO', 'pcp':{'str':tempo['wxStr']},
|
|
'time':self.summOcnlTime(),
|
|
'sky':{'str':tempo['skyStr']},
|
|
'vsby':{'str':tempo['visStr']}}
|
|
elif prob30:
|
|
return {'type':'PROB', 'pcp':{'str':prob30['wxStr']},
|
|
'time':self.summOcnlTime(),
|
|
'sky':{'str':prob30['skyStr']},
|
|
'vsby':{'str':prob30['visStr']}}
|
|
|
|
def returnPrd(self):
|
|
prd = {'time':self.summTime()}
|
|
if self.summWind() and self.summWind().get('str'):
|
|
prd['wind'] = self.summWind()
|
|
if self.summPcp() and self.summPcp().get('str'):
|
|
prd['pcp'] = self.summPcp()
|
|
if self.summObv() and self.summObv().get('str'):
|
|
prd['obv'] = self.summObv()
|
|
if self.summVis() and self.summVis().get('str'):
|
|
if ('pcp' not in prd and 'obv' not in prd and
|
|
self.summVis()['value'] < 6):
|
|
prd['vsby'] = {'str':'6SM', 'value':6.0}
|
|
else:
|
|
prd['vsby'] = self.summVis()
|
|
if self.summSky() and self.summSky().get('str'):
|
|
prd['sky'] = self.summSky()
|
|
if self.summOcnl():
|
|
return {'prev':prd, 'ocnl':self.summOcnl()}
|
|
else:
|
|
return {'prev':prd}
|
|
|
|
|
|
class TafGen:
|
|
|
|
def __init__(self, modelId, allFcst, tafType=' ', tafTime=None):
|
|
self.model = modelId
|
|
self.ident = allFcst['ident']['str']
|
|
self.fcst = allFcst['group']
|
|
self.startTimes = [t['time']['from'] for t in self.fcst]
|
|
self.endTimes = [t['time']['to'] for t in self.fcst]
|
|
self.grpTaf = Config(self.ident, modelId).grpTaf()
|
|
self.fltCat = Config(self.ident, modelId).fltCat()
|
|
self.tafType = tafType
|
|
self.formatTafTime = tafTime
|
|
if tafTime:
|
|
self.tafTime = tafTime
|
|
else:
|
|
self.tafTime = AvnLib.getValidTime(tafType)
|
|
#get TAF's start and end projections
|
|
|
|
try:
|
|
self.tafDuration = int(AvnParser.getTafSiteCfg(self.ident)['thresholds']['tafduration'])
|
|
except:
|
|
self.tafDuration = 24
|
|
|
|
nBeg, nEnd = self.getTafPrd(self.tafDuration)
|
|
if self.model == 'gfslamp':
|
|
self.projData = [LampProjection(self.ident, self.grpTaf, self.fltCat,
|
|
dat, self.tafTime).getData() for dat in self.fcst[nBeg:nEnd]]
|
|
else:
|
|
self.projData = [Projection(self.ident, self.grpTaf, self.fltCat, dat,
|
|
self.tafTime).getData() for dat in self.fcst[nBeg:nEnd]]
|
|
#create a subset time series
|
|
self.subStartTimes = self.startTimes[nBeg:nEnd]
|
|
self.subEndTimes = self.endTimes[nBeg:nEnd]
|
|
|
|
def getTafPrd(self, duration=24):
|
|
endtm = ((self.tafTime + duration * 3600) // 21600) * 21600
|
|
times = self.startTimes
|
|
n_st = 0
|
|
n_ed = len(times)
|
|
for i in range(1, len(times)):
|
|
if times[i - 1] <= self.tafTime < times[i]:
|
|
n_st = i - 1
|
|
break
|
|
for i in range(1, len(times)):
|
|
if times[i - 1] < endtm <= times[i]:
|
|
n_ed = i
|
|
break
|
|
return (n_st, n_ed)
|
|
|
|
def meetThrshds(self, short, long, dict):
|
|
longPrd = self.projData[dict[long]]
|
|
if long > 1:
|
|
lgdt = (self.subEndTimes[dict[long]] -
|
|
self.subEndTimes[dict[long - 1]])
|
|
else:
|
|
lgdt = (self.subEndTimes[dict[long]] -
|
|
self.subStartTimes[dict[long]])
|
|
longCigCat = longPrd['cigCat']
|
|
longVisCat = longPrd['visCat']
|
|
shrtPrd = self.projData[dict[short]]
|
|
shrtCigCat = shrtPrd['cigCat']
|
|
shrtVisCat = shrtPrd['visCat']
|
|
if (lgdt > self.grpTaf['long_dt'] * 3600 and
|
|
abs(shrtVisCat - longVisCat) < self.grpTaf['vis_cat'] and
|
|
abs(shrtCigCat - longCigCat) < self.grpTaf['cig_cat'] and
|
|
longVisCat < self.grpTaf['vis_cat']['high_c'] and
|
|
shrtVisCat < self.grpTaf['vis_cat']['high_c'] and
|
|
longVisCat > self.grpTaf['vis_cat']['low_c'] and
|
|
shrtVisCat > self.grpTaf['vis_cat']['low_c'] and
|
|
longCigCat < self.grpTaf['cig_cat']['high_c'] and
|
|
shrtCigCat < self.grpTaf['cig_cat']['high_c'] and
|
|
longCigCat > self.grpTaf['cig_cat']['low_c'] and
|
|
shrtCigCat > self.grpTaf['cig_cat']['low_c']):
|
|
return 1
|
|
|
|
def isSingleton(self, ref, dict):
|
|
if (self.projData[dict[ref]]['tempo'] or
|
|
self.projData[dict[ref]]['prob30']):
|
|
return None
|
|
#check if the period is short enough to be a singleton
|
|
dt = self.subEndTimes[dict[ref]] - self.subEndTimes[dict[ref - 1]]
|
|
pTime = self.subEndTimes[dict[ref]] - self.subStartTimes[0]
|
|
if dt < self.grpTaf['short_dt'] * 3600 and pTime > self.grpTaf['low_p'] * 3600:
|
|
if (self.meetThrshds(ref, ref - 1, dict) and
|
|
Comparison(self.projData[dict[ref]], self.projData[dict[ref - 1]],
|
|
self.grpTaf).isSame()):
|
|
return ref - 1
|
|
elif (self.meetThrshds(ref, ref + 1, dict) and
|
|
Comparison(self.projData[dict[ref]], self.projData[dict[ref + 1]],
|
|
self.grpTaf).isSame()):
|
|
return ref + 1
|
|
|
|
def filterSingleton(self, grpList):
|
|
dic = {}
|
|
for g, ind in zip(grpList, range(100)):
|
|
dic[g] = ind
|
|
|
|
for k in list(dic.keys())[1:]:
|
|
neighbor = self.isSingleton(k, dic)
|
|
if neighbor and neighbor < k:
|
|
dic[neighbor] = dic[k]
|
|
dic[k] = None
|
|
elif neighbor and neighbor > k:
|
|
dic[k] = None
|
|
return dic
|
|
|
|
def Brk4Tempo(self, gList, fcstD):
|
|
#fcstD is the current proj to be determined if it can be combined with
|
|
#last group in gList.
|
|
#find the starting proj for the last group in the gList
|
|
startInd = gList.index(gList[-1])
|
|
dt = fcstD['endTime'] - self.subStartTimes[startInd]
|
|
if fcstD['tempo'] and dt > 4 * 3600:
|
|
return 1
|
|
|
|
def checkOcnl(self, grpInd):
|
|
#create the indexes that mark the locations of tempo & prob30
|
|
pstr = ''.join('1' if 'prob30' in x else '0' for x in self.projData)
|
|
tstr = ''.join('1' if 'tempo' in x else '0' for x in self.projData)
|
|
|
|
# the dic that marks the end of each group
|
|
grpDic = {}
|
|
for g, i in zip(grpInd, range(30)):
|
|
grpDic[g] = i
|
|
|
|
# create an empty list to hold
|
|
binary = []
|
|
for i in range(len(pstr)):
|
|
binary.append(0)
|
|
|
|
beg = 0
|
|
for s in grpDic:
|
|
if s > 1:
|
|
beg = grpDic[s - 1] + 1
|
|
e = grpDic[s]
|
|
|
|
# if the group has only one projection, skip it
|
|
if e - beg <= 0:
|
|
continue
|
|
|
|
pp = pstr[beg:e + 1].find('1')
|
|
pt = tstr[beg:e + 1].find('1')
|
|
pp1, pt1 = -1, -1
|
|
if pp >= 0:
|
|
pp1 = pstr[beg + pp + 1:e + 1].find('1')
|
|
if pt >= 0:
|
|
pt1 = tstr[beg + pt + 1:e + 1].find('1')
|
|
if (pp >= 0 and pp1 > 0) or \
|
|
(pt >= 0 and pt1 > 0) or \
|
|
(pp >= 0 and pt >= 0):
|
|
brks = [pp, pt, pp1, pt1]
|
|
brks.sort()
|
|
brkPts = [a for a in brks if a >= 0]
|
|
# we need to break from the 2nd
|
|
for b in brkPts[1:]:
|
|
# if the first group,b is identical to index of grpInd
|
|
if s == 1:
|
|
binary[b] = 1
|
|
else: # grpDic[s-1]+1 is the grp begining index
|
|
binary[b + grpDic[s - 1] + 1] = 1
|
|
|
|
#merge grpInd and binary
|
|
lev = 0
|
|
newInd = []
|
|
newInd.append(grpInd[0])
|
|
for g, b in zip(grpInd[1:], binary[1:]):
|
|
if b == 0:
|
|
newInd.append(g + lev)
|
|
else:
|
|
#if this one was in the same level as the previous,break
|
|
if g == newInd[-1]:
|
|
lev += 1
|
|
newInd.append(g + lev)
|
|
return newInd
|
|
|
|
def formNewDic(self, noGrp):
|
|
period = []
|
|
if noGrp:
|
|
for p in self.projData:
|
|
period.append(Summarize(self.grpTaf, [p]).returnPrd())
|
|
else:
|
|
#Create index strings for prob30 and tempo
|
|
grpInd = []
|
|
grpDic = {}
|
|
count = 1
|
|
grpInd.append(count)
|
|
for d1, d2 in zip(self.projData, self.projData[1:]):
|
|
if Comparison(d1, d2, self.grpTaf).isSame() and not self.Brk4Tempo(grpInd, d2):
|
|
grpInd.append(count)
|
|
else:
|
|
count += 1
|
|
grpInd.append(count)
|
|
|
|
newGrpInd = self.checkOcnl(grpInd)
|
|
#filter-singleton
|
|
grpDic = self.filterSingleton(newGrpInd)
|
|
|
|
#summarize period element values
|
|
grpDicKeys = sorted(grpDic.keys())
|
|
for i in grpDicKeys:
|
|
endProj = grpDic[i]
|
|
if i == 1:
|
|
fcstList = self.projData[:endProj + 1]
|
|
period.append(Summarize(self.grpTaf, fcstList).returnPrd())
|
|
elif i == grpDicKeys[-1]:
|
|
fcstList = self.projData[grpDic[i - 1] + 1:]
|
|
period.append(Summarize(self.grpTaf, fcstList).returnPrd())
|
|
else:
|
|
fcstList = self.projData[grpDic[i - 1] + 1:endProj + 1]
|
|
period.append(Summarize(self.grpTaf, fcstList).returnPrd())
|
|
return period
|
|
|
|
def createTaf(self, noGrp=1):
|
|
taf = AvnLib.indentTaf(AvnLib.makeTafFromPeriods(self.ident, self.tafType,
|
|
self.formNewDic(noGrp),
|
|
self.formatTafTime,
|
|
self.tafDuration))
|
|
return taf
|