acgc.hysplit
Package of functions to read HYSPLIT output
Assumed directory structure for meteorological data. All directories are archived meteorology except for "forecast/" directory.
metroot/
forecast/ (All forecasts in subdirectories for each initialization date)
YYYYMMDD/ (forecasts initialized on YYYYMMDD)
gdas1/
gdas0p5/
gfs0p25/
hrrr/
nam12/ (what ARL calls nams)
nam3/ (pieced forecast of NAM CONUS nest (3 km))
1#!/usr/bin/env python3 2'''Package of functions to read HYSPLIT output 3 4Assumed directory structure for meteorological data. 5All directories are archived meteorology except for "forecast/" directory. 6``` 7metroot/ 8 forecast/ (All forecasts in subdirectories for each initialization date) 9 YYYYMMDD/ (forecasts initialized on YYYYMMDD) 10 gdas1/ 11 gdas0p5/ 12 gfs0p25/ 13 hrrr/ 14 nam12/ (what ARL calls nams) 15 nam3/ (pieced forecast of NAM CONUS nest (3 km)) 16``` 17''' 18 19import os 20import glob 21import datetime as dt 22import warnings 23import numpy as np 24import pandas as pd 25from . import netcdf as nct 26 27METROOT = '/data/MetData/ARL/' 28'''Default location for ARL met data''' 29 30def check_METROOT(): 31 '''Check if METROOT is a valid directory path''' 32 if not os.path.isdir(METROOT): 33 warnings.warn('\n' 34 +'Directory with ARL meteorology data not found. ' 35 +f'METROOT is currently set to {METROOT}.\n' 36 +'Use set_METROOT(path) to set the correct directory path.' 37 ) 38 39def set_METROOT(path='/data/MetData/ARL/'): 40 '''Set METROOT, the directory for ARL meteorology data 41 42 Parameters 43 ---------- 44 path : str or path 45 absolute path''' 46 # Enable access to module variable 47 global METROOT 48 49 # Ensure that path contains a trailing '/' 50 path = os.path.join(path,'') 51 52 # Ensure that path is a valid directory 53 if not os.path.isdir(path): 54 raise NotADirectoryError(f'{path} is not a directory. ' 55 +'It should be the path for ARL meteorology data' ) 56 # Set the path 57 METROOT=path 58 59def tdump2nc( inFile, outFile, clobber=False, globalAtt=None, 60 altIsMSL=False, dropOneTime=False, pack=False ): 61 '''Convert a HYSPLIT tdump file to netCDF 62 Works with single point or ensemble trajectories 63 64 Parameters 65 ---------- 66 inFile : str 67 name/path of HYSPLIT tdump file 68 outFile : str 69 name/path of netCDF file that will be created 70 clobber : bool, default=False 71 determines whether outFile will be overwrite any previous file 72 globalAtt : dict, default=None 73 If present, dict keys will be added to outFile as global attributes 74 altIsMSL : bool, default=False 75 Determines whether altitude in HYSPLIT tdump file is treated as altitude above sea level 76 (altIsMSL=True) or altitude above ground (altIsMSL=False). In either case, the netCDF 77 file will contain both altitude variables. 78 dropOneTime : bool, default=False 79 Kludge to address back trajectories that start 1 minute after the hour, 80 due to CONTROL files created with write_control(... exacttime=False ) 81 set True only for trajectories using this setup. 82 pack : bool, default=False 83 NOT IMPLEMENTED 84 determines whether variables in the netCDF file should be compressed with *lossy* 85 integer packing. 86 ''' 87 # Return if the file already exists and not set to clobber 88 if os.path.exists(outFile) and clobber==False: 89 return 90 91 # Trajectory points 92 traj = read_tdump( inFile ) 93 94 # Trajectory numbers; convert to int32 95 tnums = traj.tnum.unique().astype('int32') 96 97 # Number of trajectories (usually 1 or 27) 98 ntraj = len( tnums ) 99 100 # Trajectory start time 101 starttime = traj.time[0] 102 103 # Time along trajectory, hours since trajectory start 104 ttime = traj.thour.unique().astype('f4') 105 106 # Number of times along trajectory 107 nttime = len( ttime ) 108 109 # Empty arrays 110 lat = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 111 lon = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 112 alt = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 113 altTerr= np.zeros( (ntraj, nttime), np.float32 ) * np.nan 114 p = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 115 T = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 116 Q = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 117 U = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 118 V = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 119 precip = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 120 zmix = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 121 inBL = np.zeros( (ntraj, nttime), np.int8 ) * -9 122 123 # Check if optional variables are present 124 doP = ('PRESSURE' in traj.columns) 125 doTerr = ('TERR_MSL' in traj.columns) 126 doBL = ('MIXDEPTH' in traj.columns) 127 doT = ('AIR_TEMP' in traj.columns) 128 doQ = ('SPCHUMID' in traj.columns) 129 doU = ('UWIND' in traj.columns) 130 doV = ('VWIND' in traj.columns) 131 doPrecip = ('RAINFALL' in traj.columns) 132 133 for t in tnums: 134 135 # Find entries for this trajectory 136 idx = traj.tnum==t 137 138 # Number of times in this trajectory 139 nt = np.sum(idx) 140 141 if dropOneTime: 142 # Drop the second time element (at minute 0) to retain one point per hour 143 # Find entries and Drop the second element 144 tmpidx = np.where(traj.tnum==t)[0] 145 idx = [tmpidx[0]] 146 idx.extend(tmpidx[2:]) 147 148 # Save the coordinates 149 lat[t-1,:nt] = traj.lat[idx] 150 lon[t-1,:nt] = traj.lon[idx] 151 alt[t-1,:nt] = traj.alt[idx] 152 153 # Add optional variables 154 if doP: 155 p[t-1,:nt] = traj.PRESSURE[idx] 156 if doT: 157 T[t-1,:nt] = traj.AIR_TEMP[idx] 158 if doQ: 159 Q[t-1,:nt] = traj.SPCHUMID[idx] 160 if doU: 161 U[t-1,:nt] = traj.UWIND[idx] 162 if doV: 163 V[t-1,:nt] = traj.VWIND[idx] 164 if doPrecip: 165 precip[t-1,:nt] = traj.RAINFALL[idx] 166 if doTerr: 167 altTerr[t-1,:nt]= traj.TERR_MSL[idx] 168 if doBL: 169 inBL[t-1,:nt] = traj.alt[idx] < traj.MIXDEPTH[idx] 170 zmix[t-1,:nt] = traj.MIXDEPTH[idx] 171 172 if altIsMSL: 173 altName= 'altMSL' 174 altLongName= 'altitude above mean sea level' 175 if doTerr: 176 alt2Name= 'altAGL' 177 alt2LongName= 'altitude above ground level' 178 alt2= alt-altTerr 179 else: 180 altName= 'altAGL' 181 altLongName= 'altitude above ground level' 182 if doTerr: 183 alt2Name= 'altMSL' 184 alt2LongName= 'altitude above mean sea level' 185 alt2= alt+altTerr 186 187 # Put output variables into a list 188 variables = [ 189 {'name':'lat', 190 'long_name':'latitude of trajectory', 191 'units':'degrees_north', 192 'value':np.expand_dims(lat,axis=0), 193 'fill_value':np.float32(np.nan)}, 194 {'name':'lon', 195 'long_name':'longitude of trajectory', 196 'units':'degrees_east', 197 'value':np.expand_dims(lon, axis=0), 198 'fill_value':np.float32(np.nan)}, 199 {'name':altName, 200 'long_name':altLongName, 201 'units':'m', 202 'value':np.expand_dims(alt, axis=0), 203 'fill_value':np.float32(np.nan)} ] 204 205 # Add optional variables to output list 206 if doTerr: 207 variables.append( 208 {'name':'altTerr', 209 'long_name':'altitude of terrain', 210 'units':'m', 211 'value':np.expand_dims(altTerr,axis=0), 212 'fill_value':np.float32(np.nan)} ) 213 variables.append( 214 {'name':alt2Name, 215 'long_name':alt2LongName, 216 'units':'m', 217 'value':np.expand_dims(alt2,axis=0), 218 'fill_value':np.float32(np.nan)} ) 219 if doP: 220 variables.append( 221 {'name':'p', 222 'long_name':'pressure', 223 'units':'hPa', 224 'value':np.expand_dims(p,axis=0), 225 'fill_value':np.float32(np.nan)} ) 226 if doT: 227 variables.append( 228 {'name':'T', 229 'long_name':'temperature', 230 'units':'K', 231 'value':np.expand_dims(T,axis=0), 232 'fill_value':np.float32(np.nan)} ) 233 if doQ: 234 variables.append( 235 {'name':'q', 236 'long_name':'specific humidity', 237 'units':'g/kg', 238 'value':np.expand_dims(Q,axis=0), 239 'fill_value':np.float32(np.nan)} ) 240 if doU: 241 variables.append( 242 {'name':'U', 243 'long_name':'eastward wind speed', 244 'units':'m/s', 245 'value':np.expand_dims(U,axis=0), 246 'fill_value':np.float32(np.nan)} ) 247 if doV: 248 variables.append( 249 {'name':'V', 250 'long_name':'northward wind speed', 251 'units':'m/s', 252 'value':np.expand_dims(V,axis=0), 253 'fill_value':np.float32(np.nan)} ) 254 if doPrecip: 255 variables.append( 256 {'name':'precipitation', 257 'long_name':'precipitation', 258 'units':'mm/hr', 259 'value':np.expand_dims(precip,axis=0), 260 'fill_value':np.float32(np.nan)} ) 261 if doBL: 262 variables.append( 263 {'name':'inBL', 264 'long_name':'trajectory in boundary layer flag', 265 'units':'unitless', 266 'value':np.expand_dims(inBL,axis=0), 267 'fill_value':-9} ) 268 variables.append( 269 {'name':'mixdepth', 270 'long_name':'boundary layer mixing depth', 271 'units':'m', 272 'value':np.expand_dims(zmix,axis=0), 273 'fill_value':np.float32(np.nan)} ) 274 275 # Add dimension information to all variables 276 for v in range(len(variables)): 277 variables[v]['dim_names'] = ['time','trajnum','trajtime'] 278 279 # Construct global attributes 280 # Start with default and add any provided by user input 281 gAtt = {'Content': 'HYSPLIT trajectory'} 282 if isinstance(globalAtt, dict): 283 gAtt.update(globalAtt) 284 285 # Create the output file 286 nct.write_geo_nc( outFile, variables, 287 xDim={'name':'trajnum', 288 'long_name':'trajectory number', 289 'units':'unitless', 290 'value':tnums}, 291 yDim={'name':'trajtime', 292 'long_name':'time since trajectory start', 293 'units':'hours', 294 'value':ttime}, 295 tDim={'name':'time', 296 'long_name':'time of trajectory start', 297 'units':'hours since 2000-01-01 00:00:00', 298 'calendar':'standard', 299 'value':np.array([starttime]), 300 'unlimited':True}, 301 globalAtt=gAtt, 302 nc4=True, classic=True, clobber=clobber ) 303 304def read_tdump(file): 305 '''Read trajectory file output from HYSPLIT 306 307 Parameters 308 ---------- 309 file : str 310 name of trajectory file to read 311 312 Returns 313 ------- 314 pandas.DataFrame 315 DataFrame contains columns: 316 - time : datetime object 317 - year, month, day, hour, minute : floats, same as time 318 - lat, lon, alt : trajectory location 319 - thour : hours since trajectory initialization, 320 negative for back trajectories 321 - tnum : trajectory number tnum=1 for single trajectory, 322 tnum=1-27 for trajectory ensemble 323 - metnum : index number of met file used at this point in trajectory, 324 see tdump file for corresponding file paths 325 - fcasthr : hours since the meteorological dataset was initialized 326 ''' 327 328 # Open the file 329 with open(file,"r",encoding="ascii") as fid: 330 331 # First line gives number of met fields 332 nmet = int(fid.readline().split()[0]) 333 334 # Skip the met file lines 335 for i in range(nmet): 336 next(fid) 337 338 # Number of trajectories 339 ntraj = int(fid.readline().split()[0]) 340 341 # Skip the next lines 342 for i in range(ntraj): 343 next(fid) 344 345 # Read the variables that are included 346 line = fid.readline() 347 nvar = int(line.split()[0]) 348 vnames = line.split()[1:] 349 350 # Read the data 351 df = pd.read_csv( fid, delim_whitespace=True, 352 header=None, index_col=False, na_values=['NaN','********'], 353 names=['tnum','metnum', 354 'year','month','day','hour','minute','fcasthr', 355 'thour','lat','lon','alt']+vnames ) 356 357 # Convert 2-digit year to 4-digits 358 df.loc[:,'year'] += 2000 359 360 # Convert to time 361 df['time'] = pd.to_datetime( df[['year','month','day','hour','minute']] ) 362 363 #print(df.iloc[-1,:]) 364 return df 365 366def _get_gdas1_filename( time ): 367 '''Directory and File names for GDAS 1 degree meteorology, for given date''' 368 369 # Directory for GDAS 1 degree 370 dirname = METROOT+'gdas1/' 371 372 # Filename template 373 filetmp = 'gdas1.{mon:s}{yy:%y}.w{week:d}' 374 375 # week number in the month 376 wnum = ((time.day-1) // 7) + 1 377 378 # GDAS 1 degree file 379 filename = dirname + filetmp.format( mon=time.strftime("%b").lower(), yy=time, week=wnum ) 380 381 return filename 382 383def _get_hrrr_filename( time ): 384 '''Directory and File names for HRRR meteorology, for given date''' 385 386 # Directory 387 dirname = METROOT+'hrrr/' 388 389 # There are four files per day containing these hours 390 hourstrings = ['00-05','06-11','12-17','18-23'] 391 392 filenames = [ '{:s}/{:%Y%m%d}_{:s}_hrrr'.format( dirname, time, hstr ) 393 for hstr in hourstrings ] 394 dirnames = [ dirname for i in range(4) ] 395 396 # Return 397 return filenames 398 399def _get_met_filename( metmodel, time ): 400 '''Directory and file names for given meteorology and date 401 402 Parameters 403 ---------- 404 metmodel : str 405 met model wanted: gdas1, gdas0p5, gfs0p25, nam12, nam3, hrrr 406 time : pandas.Timestamp, datetime.date, datetime.datetime 407 day of desired meteorology 408 409 Returns 410 ------- 411 filename : str or list 412 name of met file or files containing met data for input date 413 ''' 414 415 # Ensure that time is a datetime object 416 if not isinstance( time, dt.date) : 417 raise TypeError( "_get_met_filename: time must be a datetime object" ) 418 419 # Directory and filename template 420 doFormat=True 421 if metmodel == 'gdas1': 422 # Special treatment 423 filename = _get_gdas1_filename( time ) 424 elif metmodel == 'gdas0p5': 425 dirname = METROOT+'gdas0p5/' 426 filename = dirname+'{date:%Y%m%d}_gdas0p5' 427 elif metmodel == 'gfs0p25': 428 dirname = METROOT+'gfs0p25/' 429 filename = dirname+'{date:%Y%m%d}_gfs0p25' 430 elif metmodel == 'nam3': 431 dirname = METROOT+'nam3/' 432 filename = dirname+'{date:%Y%m%d}_hysplit.namsa.CONUS' 433 elif metmodel == 'nam12': 434 dirname = METROOT+'nam12/' 435 filename = dirname+'{date:%Y%m%d}_hysplit.t00z.namsa' 436 elif metmodel == 'hrrr': 437 # Special treatment 438 filename = _get_hrrr_filename( time ) 439 doFormat=False 440 else: 441 raise NotImplementedError( 442 f"_get_met_filename: {metmodel} unrecognized" ) 443 444 # Build the filename 445 if doFormat: 446 filename = filename.format( date=time ) 447 448 return filename 449 450def _get_archive_filelist( metmodels, time, useAll=True ): 451 '''Get a list of met directories and files 452 When there are two met version provided, the first result will be used 453 454 455 Parameters 456 ---------- 457 metmodels : list or str 458 met models wanted: gdas1, gdas0p5, gfs0p25, nam12, nam3, hrrr 459 commonly provide a list with both regional and global models 460 e.g. ['hrrr','gfs0p25'] 461 time : pandas.Timestamp, datetime.date, datetime.datetime 462 day of desired meteorology 463 useAll : bool, default=True 464 if True, then return files for all metmodels found 465 if False, then return files only for the first metmodel found 466 467 Returns 468 ------- 469 filename : list 470 names of files containing met data for input date 471 ''' 472 473 if isinstance( metmodels, str ): 474 475 # If metmodels is a single string, then get value from appropriate function 476 filename = _get_met_filename( metmodels, time ) 477 478 # Convert to list, if isn't already 479 if isinstance(filename, str): 480 filename = [filename] 481 482 elif isinstance( metmodels, list ): 483 484 # If metmodels is a list, then get files for each met version in list 485 486 filename = [] 487 488 # Loop over all the metmodelss 489 # Use the first one with a file present 490 for met in metmodels: 491 492 # Find filename for this met version 493 f = _get_met_filename( met, time ) 494 495 if useAll: 496 497 # Ensure that directory and file are lists so that we can use extend below 498 if isinstance(f, str): 499 f = [f] 500 elif isinstance(f, list): 501 pass 502 else: 503 raise NotImplementedError( 504 'Variable expected to be string or list but is actually ',type(f) ) 505 506 # Append to the list so that all can be used 507 filename.extend(f) 508 509 else: 510 # Use just the first met file that exists 511 filename = f 512 break 513 # If the file exists, use this and exit; 514 # otherwise keep looking 515 #if isinstance( f, str ): 516 # if ( os.path.isfile( d+f ) ): 517 # dirname = d 518 # filename = f 519 # break 520 #elif isinstance( f, list ): 521 # if ( os.path.isfile( d[0]+f[0] ) ): 522 # dirname = d 523 # filename = f 524 # break 525 526 # Raise an error if 527 if filename is []: 528 raise FileNotFoundError( 529 "_get_archive_filename: no files found for " + ','.join(metmodels) ) 530 531 else: 532 raise TypeError( "_get_archive_filelist: metmodels must be a string or list" ) 533 534 return filename 535 536def _get_hybrid_filelist( metmodels, time ): 537 '''Get list of met files combining archive and forecast 538 539 Starting 1 day before 'time' and extending 10 days ahead, find all archived analysis 540 files, then add forecast files 541 542 Parameters 543 ---------- 544 metmodels : str 545 IGNORED 546 Current implementation uses nam3 for past meteorology and namsfCONUS for forecast 547 time : datetime.datetime or pandas.Timestamp 548 date of interest, should be near present. 549 If farther in past, then use archived met. Distance future won't have any forecst available. 550 551 Returns 552 ------- 553 metfiles : list 554 file path names that are found 555 ''' 556 557 print( "Hybrid Archive/Forecast meteorology using NAM3 CONUS nest" ) 558 559 archivemet = 'nam3' 560 forecastmet = 'namsfCONUS' 561 562 # List of met directories and met files that will be used 563 metfiles = [] 564 565 # Loop over 10 days, because that's probably enough for FIREX-AQ 566 for d in range(-1,10): 567 568 # date of met data 569 metdate = time.date() + pd.Timedelta( d, "D" ) 570 571 #dirname, filename = _get_archive_filelist( metmodel, metdate ) 572 filename = _get_met_filename( archivemet, metdate ) 573 574 # Check if the file exists 575 if os.path.isfile( filename ): 576 577 # Add the file, if it isn't already in the list 578 if filename not in metfiles: 579 metfiles.append( filename ) 580 581 else: 582 583 # We need to get forecast meteorology 584 585 # Loop over forecast cycles 586 for hr in [0,6,12,18]: 587 588 cy = dt.datetime.combine( metdate, dt.time( hour=hr ) ) 589 590 try: 591 # If this works, then the file exists 592 f = _get_forecast_filename( forecastmet, cy, partial=True ) 593 # Add the first 6-hr forecast period 594 metfiles.append( f[0] ) 595 # If we have a full forecast cycle, save it 596 if len(f)==8: 597 flast = f 598 except FileNotFoundError: 599 # We have run out of forecast meteorology, 600 # So add the remainder of the prior forecast cycle and we're done 601 metfiles.extend( flast[1:] ) 602 603 return metfiles 604 605 return metfiles 606 607def _get_forecast_template( metmodel ): 608 '''Get filename template for forecast meteorology 609 610 Parameters 611 ---------- 612 metmodel : str 613 name of forecast model: namsfCONUS, namf, gfs0p25, hrrr 614 615 Returns 616 ------- 617 template : str 618 template filename with {:%H} field for initialization time and ?? for forecast hour 619 nexpected : int 620 number of forecast files expected for each forecast initialization 621 ''' 622 623 if metmodel == 'namsfCONUS': 624 # Filename template 625 filetemplate = 'hysplit.t{:%H}z.namsf??.CONUS' 626 nexpected = 8 627 else: 628 filetemplate = 'hysplit.t{:%H}z.'+metmodel 629 nexpected = 1 630 631 return filetemplate, nexpected 632 633def _get_forecast_filename( metmodel, cycle, partial=False ): 634 '''Find files for a particular met model and forecast cycle 635 636 Parameters 637 ---------- 638 metmodel : str 639 name of forecast model: namsfCONUS, namf, gfs0p25, hrrr 640 cycle : datetime.datetime or pandas.Timestamp 641 forecast initialization time UTC (date and hour) 642 partial : bool, default=True 643 With partial=True, function will raise error if some forecast files are missing 644 With partial=False, function will return all forecast files that are found 645 646 Returns 647 ------- 648 filenames : list 649 file paths to all files that are found 650 ''' 651 652 dirname = METROOT + f'forecast/{cycle:%Y%m%d}/' 653 filename, nexpected = _get_forecast_template( metmodel ) 654 655 # Filename for this cycle 656 filename = filename.format(cycle) 657 658 # Find all the files that match these criteria 659 files = glob.glob( dirname + filename ) 660 661 # Check if we found the expected number of files 662 if (len(files) == nexpected) or (partial and len(files) >=1): 663 664 # When we find then, sort and combine into one list 665 filenames = sorted( files ) 666 667 # Return 668 return filenames 669 670 # Raise an error if no forecasts are found 671 raise FileNotFoundError('ARL forecast meteorology found' ) 672 673def _get_forecast_filename_latest( metmodel ): 674 '''Find files for the latest available forecast cycle for the requested met version. 675 676 Requires a complete set of forecast files for a cycle 677 678 Parameter 679 --------- 680 metmodel : str 681 name of forecast model: namsfCONUS, namf, gfs0p25, hrrr 682 683 Returns 684 ------- 685 filenames : list 686 file paths to all files that are found 687 ''' 688 689 # Filename template for forecast files 690 filetemplate, nexpected = _get_forecast_template( metmodel ) 691 692 # Find all of the forecast directories, most recent first 693 dirs = [item for item 694 in sorted( glob.glob( METROOT+'forecast/????????' ), reverse=True ) 695 if os.path.isdir(item) ] 696 697 for d in dirs: 698 699 # Loop backwards over the forecast cycles 700 for hh in [18,12,6,0]: 701 702 # Check if the forecast files exist 703 files = glob.glob(d+'/'+filetemplate.format(dt.time(hh))) 704 #'hysplit.t{:02d}z.{:s}'.format(hh,metsearch)) 705 706 # Check if we found the expected number of files 707 if len(files) == nexpected: 708 709 # When we find then, sort and combine into one list 710 filenames = sorted( files ) 711 712 # Return 713 return filenames 714 715 # Raise an error if no forecasts are found 716 raise FileNotFoundError('No ARL forecast meteorology found' ) 717 718def _get_forecast_filelist( metmodels=None, cycle=None ): 719 '''Get list of filenames for forecast cycle 720 721 This function calls itself recursively 722 723 Parameters 724 ---------- 725 metmodels : str or list, default=['namsfCONUS','namf'] 726 name of forecast model: namsfCONUS, namf, gfs0p25, hrrr 727 cycle : datetime.datetime or pandas.Timestamp or None 728 forecast initialization time UTC (date and hour) 729 if cycle=None, then will use the latest available forecast cycle 730 731 Returns 732 ------- 733 filenames : list 734 file paths to all files that are found 735 ''' 736 737 if metmodels is None: 738 metmodels = ['namsfCONUS','namf'] 739 740 # If metmodel is a single string, then get value from appropriate function 741 if isinstance( metmodels, str ): 742 743 if cycle is None: 744 filenames = _get_forecast_filename_latest( metmodels ) 745 else: 746 filenames = _get_forecast_filename( metmodels, cycle ) 747 748 else: 749 750 filenames = [] 751 752 # Loop over the list of met types, combine them all 753 for met in metmodels: 754 755 # Get directory and file names for one version 756 f = _get_forecast_filelist( met, cycle ) 757 758 # Combine them into one list 759 filenames = filenames + f 760 761 return filenames 762 763def find_arl_metfiles( start_time, ndays, back=False, metmodels=None, 764 forecast=False, forecastcycle=None, hybrid=False ): 765 '''Find ARL meteorology files for specified dates, models, forecast and archive 766 767 Files will be located for start_time and extending ndays forward or backward 768 769 Parameters 770 ---------- 771 start_time : datetime.datetime or pandas.Timestamp 772 start date and time for finding meteorology data files 773 ndays : int 774 number of days of files to retrieve 775 back : bool, default=False 776 specifies files should go ndays backward (back=True) from start_time 777 metmodels : list or str, default=['gdas0p5','gdas1'] 778 meteorological models that will be used, in order of decreasing resolution and priority 779 forecast : bool, default=False 780 set forecast=True to use forecast meteorology for trajectory computation 781 set forecast=False to use archived (past) meteorology for trajectory computation 782 Note: hybrid=True will supercede forecast=True 783 forecastcycle : datetime.datetime, pandas.Timestamp, or None, default=None 784 if forecast=True, this sets the forecast initialization cycle that will be used 785 set forecastcycle=None to use the latest available cycle for which files are found 786 if forecast=False, this parameter has no effect 787 hybrid : bool, default=False 788 set hybrid=True for trajectories that use a combination of past archive and 789 forecast meteorlogy. This supercedes forecast=True 790 791 Returns 792 ------- 793 metfiles : list 794 path to meteorology files meeting the criteria 795 ''' 796 797 check_METROOT() 798 799 if metmodels is None: 800 metmodels = ['gdas0p5','gdas1'] 801 802 # Find the meteorology files 803 if hybrid is True: 804 805 # "Hybrid" combines past (archived) and future (forecast) meteorology 806 if back: 807 raise NotImplementedError( "Combined Analysis/Forecast meteorology " 808 + "not supported for back trajectories" ) 809 metfiles = _get_hybrid_filelist( metmodels, start_time ) 810 811 elif forecast is True: 812 # Get the forecast meteorology 813 metfiles = _get_forecast_filelist( metmodels, forecastcycle ) 814 815 # Check if the forecast meteorology covers the entire trajectory duration 816 else: 817 # Use archived (past) meteorology 818 819 # Relative to trajectory start day, 820 # we need meteorology for days d0 through d1 821 if back: 822 d0 = -ndays+1 823 d1 = 1 824 # For trajectories that start 23-0Z (nam) or 21-0Z (gfs0p25), 825 # also need the next day to bracket first time step 826 if (('nam3' in metmodels) and (start_time.hour==23)) \ 827 or (('gfs0p25' in metmodels) and (start_time.hour>=21)): 828 d1 = 2 829 else: 830 d0 = 0 831 d1 = ndays 832 # For trajectories that start 00-01Z (nam) or 00-03Z (gfs0p25), 833 # also need the prior day to bracket first time step 834 if start_time.hour==0: 835 d0 = -1 836 837 # Debug output 838 # print('Initial Date Range',d0,d1) 839 # datelist = np.unique( ( time + 840 # pd.TimedeltaIndex( np.sign(trajhours) * np.arange(0,np.abs(trajhours)+1),"H" ) ).date ) 841 # print(datelist) 842 843 # List of met directories and met files that will be used 844 metfiles = [] 845 for d in range(d0,d1): 846 847 # date of met data 848 metdate = start_time.date() + pd.Timedelta( d, "D" ) 849 850 # Met data for a single day 851 filename = _get_archive_filelist( metmodels, metdate ) 852 853 # Add the files to the list 854 metfiles.extend( filename ) 855 # Keep only the unique files 856 metfiles = np.unique(metfiles) 857 858 if len(metfiles) <= 0: 859 raise ValueError( 'Meteorology files not found for ' + str( metmodels ) ) 860 861 return metfiles 862 863def write_control( time, lat, lon, alt, trajhours, 864 fname='CONTROL.000', clobber=False, 865 maxheight=15000., outdir='./', tfile='tdump', 866 metfiles=None, exacttime=True, **kwargs ): 867 '''Write HYSPLIT control file for trajectory starting at designated time and coordinates 868 869 Parameters 870 ---------- 871 time : datetime.datetime or pandas.Timestamp 872 trajectory initialization time 873 lat, lon : float or list 874 trajectory initialization latitude and longitude in degrees 875 alt : float or list 876 trajectory initialization altitude in meters 877 The setup.cfg determines whether this is above ground or above mean sea level 878 trajhours : int 879 desired trajectory duration in hours. Use negative for back trajectories 880 fname : str, default='CONTROL.000' 881 path and name for the file that will be written 882 clobber : bool, default=False 883 if clobber=True, then fname will be overwritten 884 maxheight : float, default=15000 885 terminate trajectories that exceed maxheight altitude in meters 886 outdir : str, default='./' 887 directory path where HYSPLIT output will be written 888 tfile : str, default='tdump' 889 name of the trajectory file that HYSPLIT will write 890 metfiles : list or str, default=None 891 paths to ARL meteorology files needed for the trajectory computation 892 If metfiles=None, then find_arl_metfiles will be used to locate necessary files 893 exacttime : bool, default=True 894 It is not recommended to change this default, but keyword is retained for backward 895 compatibility with some scripts. Setting exacttime=False will shift the actual 896 start time of trajectories that begin at 00:00 UTC to 00:01 UTC to avoid reading 897 an additional day of meteorological data. 898 **kwargs 899 kwargs will be passed to find_arl_metfiles to locate ARL meteorlogy files if metfiles=None 900 These keywords should include metmodels and possibly forecast, forecastcycle, or hybrid 901 See find_arl_metfiles for definitions of these parameters 902 ''' 903 904 check_METROOT() 905 906 if os.path.isfile( fname ) and (clobber is False): 907 raise OSError( f'File exists. Set clobber=True to overwrite: {fname:s}' ) 908 909 # Ensure that lat, lon, and alt are lists 910 try: 911 nlat = len( lat ) 912 except TypeError: 913 lat = [ lat ] 914 nlat = len( lat ) 915 try: 916 nlon = len( lon ) 917 except TypeError: 918 lon = [ lon ] 919 nlon = len( lon ) 920 try: 921 nalt = len( alt ) 922 except TypeError: 923 alt = [ alt ] 924 nalt = len( alt ) 925 926 # Should add some type checking to ensure that lon, lat, alt 927 # have conformable lengths 928 if ( (nlat != nlon) or (nlat != nalt) ): 929 raise ValueError( "lon, lat, alt must be conformable" ) 930 931 # Number of days of met data 932 ndays = int( np.ceil( np.abs( trajhours ) / 24 ) + 1 ) 933 934 # Get met file names, if not provided 935 if (metfiles is None) or (len(metfiles)==0): 936 metfiles = find_arl_metfiles( time, ndays, back=trajhours<0, **kwargs ) 937 938 # Number of met files 939 nmet = len( metfiles ) 940 941 # Runs will fail if the initial time is not bracketed by met data. 942 # When met resolution changes on the first day, this condition may not be met. 943 # Unless exacttime==True, the starting time will be shifted 1 minute to 00:01 944 # to avoid reading in an entire extra day of met data. 945 if (time.hour==0 and time.minute == 0 and not exacttime): 946 time = time + pd.Timedelta( 1, "m" ) 947 948 # Start date-time, formatted as YY MM DD HH {mm} 949 startdate = time.strftime( "%y %m %d %H %M" ) 950 951 # Write the CONTROL file 952 with open( fname, 'w', encoding='ascii' ) as f: 953 954 f.write( startdate+'\n' ) 955 f.write( f'{nlat:d}\n' ) 956 for i in range(nlat): 957 f.write( f'{lat[i]:<10.4f} {lon[i]:<10.4f} {alt[i]:<10.4f}\n' ) 958 f.write( f'{trajhours:d}\n' ) 959 f.write( "0\n" ) 960 f.write( f"{maxheight:<10.1f}\n" ) 961 f.write( f'{nmet:d}\n' ) 962 for file in metfiles: 963 f.write( os.path.dirname( file ) + '/\n' ) 964 f.write( os.path.basename( file ) + '\n' ) 965 f.write( f'{outdir:s}\n' ) 966 f.write( f'{tfile:s}\n' ) 967 968# Check METROOT path at import 969check_METROOT()
METROOT =
'/data/MetData/ARL/'
Default location for ARL met data
def
check_METROOT():
31def check_METROOT(): 32 '''Check if METROOT is a valid directory path''' 33 if not os.path.isdir(METROOT): 34 warnings.warn('\n' 35 +'Directory with ARL meteorology data not found. ' 36 +f'METROOT is currently set to {METROOT}.\n' 37 +'Use set_METROOT(path) to set the correct directory path.' 38 )
Check if METROOT is a valid directory path
def
set_METROOT(path='/data/MetData/ARL/'):
40def set_METROOT(path='/data/MetData/ARL/'): 41 '''Set METROOT, the directory for ARL meteorology data 42 43 Parameters 44 ---------- 45 path : str or path 46 absolute path''' 47 # Enable access to module variable 48 global METROOT 49 50 # Ensure that path contains a trailing '/' 51 path = os.path.join(path,'') 52 53 # Ensure that path is a valid directory 54 if not os.path.isdir(path): 55 raise NotADirectoryError(f'{path} is not a directory. ' 56 +'It should be the path for ARL meteorology data' ) 57 # Set the path 58 METROOT=path
Set METROOT, the directory for ARL meteorology data
Parameters
- path (str or path): absolute path
def
tdump2nc( inFile, outFile, clobber=False, globalAtt=None, altIsMSL=False, dropOneTime=False, pack=False):
60def tdump2nc( inFile, outFile, clobber=False, globalAtt=None, 61 altIsMSL=False, dropOneTime=False, pack=False ): 62 '''Convert a HYSPLIT tdump file to netCDF 63 Works with single point or ensemble trajectories 64 65 Parameters 66 ---------- 67 inFile : str 68 name/path of HYSPLIT tdump file 69 outFile : str 70 name/path of netCDF file that will be created 71 clobber : bool, default=False 72 determines whether outFile will be overwrite any previous file 73 globalAtt : dict, default=None 74 If present, dict keys will be added to outFile as global attributes 75 altIsMSL : bool, default=False 76 Determines whether altitude in HYSPLIT tdump file is treated as altitude above sea level 77 (altIsMSL=True) or altitude above ground (altIsMSL=False). In either case, the netCDF 78 file will contain both altitude variables. 79 dropOneTime : bool, default=False 80 Kludge to address back trajectories that start 1 minute after the hour, 81 due to CONTROL files created with write_control(... exacttime=False ) 82 set True only for trajectories using this setup. 83 pack : bool, default=False 84 NOT IMPLEMENTED 85 determines whether variables in the netCDF file should be compressed with *lossy* 86 integer packing. 87 ''' 88 # Return if the file already exists and not set to clobber 89 if os.path.exists(outFile) and clobber==False: 90 return 91 92 # Trajectory points 93 traj = read_tdump( inFile ) 94 95 # Trajectory numbers; convert to int32 96 tnums = traj.tnum.unique().astype('int32') 97 98 # Number of trajectories (usually 1 or 27) 99 ntraj = len( tnums ) 100 101 # Trajectory start time 102 starttime = traj.time[0] 103 104 # Time along trajectory, hours since trajectory start 105 ttime = traj.thour.unique().astype('f4') 106 107 # Number of times along trajectory 108 nttime = len( ttime ) 109 110 # Empty arrays 111 lat = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 112 lon = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 113 alt = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 114 altTerr= np.zeros( (ntraj, nttime), np.float32 ) * np.nan 115 p = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 116 T = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 117 Q = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 118 U = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 119 V = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 120 precip = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 121 zmix = np.zeros( (ntraj, nttime), np.float32 ) * np.nan 122 inBL = np.zeros( (ntraj, nttime), np.int8 ) * -9 123 124 # Check if optional variables are present 125 doP = ('PRESSURE' in traj.columns) 126 doTerr = ('TERR_MSL' in traj.columns) 127 doBL = ('MIXDEPTH' in traj.columns) 128 doT = ('AIR_TEMP' in traj.columns) 129 doQ = ('SPCHUMID' in traj.columns) 130 doU = ('UWIND' in traj.columns) 131 doV = ('VWIND' in traj.columns) 132 doPrecip = ('RAINFALL' in traj.columns) 133 134 for t in tnums: 135 136 # Find entries for this trajectory 137 idx = traj.tnum==t 138 139 # Number of times in this trajectory 140 nt = np.sum(idx) 141 142 if dropOneTime: 143 # Drop the second time element (at minute 0) to retain one point per hour 144 # Find entries and Drop the second element 145 tmpidx = np.where(traj.tnum==t)[0] 146 idx = [tmpidx[0]] 147 idx.extend(tmpidx[2:]) 148 149 # Save the coordinates 150 lat[t-1,:nt] = traj.lat[idx] 151 lon[t-1,:nt] = traj.lon[idx] 152 alt[t-1,:nt] = traj.alt[idx] 153 154 # Add optional variables 155 if doP: 156 p[t-1,:nt] = traj.PRESSURE[idx] 157 if doT: 158 T[t-1,:nt] = traj.AIR_TEMP[idx] 159 if doQ: 160 Q[t-1,:nt] = traj.SPCHUMID[idx] 161 if doU: 162 U[t-1,:nt] = traj.UWIND[idx] 163 if doV: 164 V[t-1,:nt] = traj.VWIND[idx] 165 if doPrecip: 166 precip[t-1,:nt] = traj.RAINFALL[idx] 167 if doTerr: 168 altTerr[t-1,:nt]= traj.TERR_MSL[idx] 169 if doBL: 170 inBL[t-1,:nt] = traj.alt[idx] < traj.MIXDEPTH[idx] 171 zmix[t-1,:nt] = traj.MIXDEPTH[idx] 172 173 if altIsMSL: 174 altName= 'altMSL' 175 altLongName= 'altitude above mean sea level' 176 if doTerr: 177 alt2Name= 'altAGL' 178 alt2LongName= 'altitude above ground level' 179 alt2= alt-altTerr 180 else: 181 altName= 'altAGL' 182 altLongName= 'altitude above ground level' 183 if doTerr: 184 alt2Name= 'altMSL' 185 alt2LongName= 'altitude above mean sea level' 186 alt2= alt+altTerr 187 188 # Put output variables into a list 189 variables = [ 190 {'name':'lat', 191 'long_name':'latitude of trajectory', 192 'units':'degrees_north', 193 'value':np.expand_dims(lat,axis=0), 194 'fill_value':np.float32(np.nan)}, 195 {'name':'lon', 196 'long_name':'longitude of trajectory', 197 'units':'degrees_east', 198 'value':np.expand_dims(lon, axis=0), 199 'fill_value':np.float32(np.nan)}, 200 {'name':altName, 201 'long_name':altLongName, 202 'units':'m', 203 'value':np.expand_dims(alt, axis=0), 204 'fill_value':np.float32(np.nan)} ] 205 206 # Add optional variables to output list 207 if doTerr: 208 variables.append( 209 {'name':'altTerr', 210 'long_name':'altitude of terrain', 211 'units':'m', 212 'value':np.expand_dims(altTerr,axis=0), 213 'fill_value':np.float32(np.nan)} ) 214 variables.append( 215 {'name':alt2Name, 216 'long_name':alt2LongName, 217 'units':'m', 218 'value':np.expand_dims(alt2,axis=0), 219 'fill_value':np.float32(np.nan)} ) 220 if doP: 221 variables.append( 222 {'name':'p', 223 'long_name':'pressure', 224 'units':'hPa', 225 'value':np.expand_dims(p,axis=0), 226 'fill_value':np.float32(np.nan)} ) 227 if doT: 228 variables.append( 229 {'name':'T', 230 'long_name':'temperature', 231 'units':'K', 232 'value':np.expand_dims(T,axis=0), 233 'fill_value':np.float32(np.nan)} ) 234 if doQ: 235 variables.append( 236 {'name':'q', 237 'long_name':'specific humidity', 238 'units':'g/kg', 239 'value':np.expand_dims(Q,axis=0), 240 'fill_value':np.float32(np.nan)} ) 241 if doU: 242 variables.append( 243 {'name':'U', 244 'long_name':'eastward wind speed', 245 'units':'m/s', 246 'value':np.expand_dims(U,axis=0), 247 'fill_value':np.float32(np.nan)} ) 248 if doV: 249 variables.append( 250 {'name':'V', 251 'long_name':'northward wind speed', 252 'units':'m/s', 253 'value':np.expand_dims(V,axis=0), 254 'fill_value':np.float32(np.nan)} ) 255 if doPrecip: 256 variables.append( 257 {'name':'precipitation', 258 'long_name':'precipitation', 259 'units':'mm/hr', 260 'value':np.expand_dims(precip,axis=0), 261 'fill_value':np.float32(np.nan)} ) 262 if doBL: 263 variables.append( 264 {'name':'inBL', 265 'long_name':'trajectory in boundary layer flag', 266 'units':'unitless', 267 'value':np.expand_dims(inBL,axis=0), 268 'fill_value':-9} ) 269 variables.append( 270 {'name':'mixdepth', 271 'long_name':'boundary layer mixing depth', 272 'units':'m', 273 'value':np.expand_dims(zmix,axis=0), 274 'fill_value':np.float32(np.nan)} ) 275 276 # Add dimension information to all variables 277 for v in range(len(variables)): 278 variables[v]['dim_names'] = ['time','trajnum','trajtime'] 279 280 # Construct global attributes 281 # Start with default and add any provided by user input 282 gAtt = {'Content': 'HYSPLIT trajectory'} 283 if isinstance(globalAtt, dict): 284 gAtt.update(globalAtt) 285 286 # Create the output file 287 nct.write_geo_nc( outFile, variables, 288 xDim={'name':'trajnum', 289 'long_name':'trajectory number', 290 'units':'unitless', 291 'value':tnums}, 292 yDim={'name':'trajtime', 293 'long_name':'time since trajectory start', 294 'units':'hours', 295 'value':ttime}, 296 tDim={'name':'time', 297 'long_name':'time of trajectory start', 298 'units':'hours since 2000-01-01 00:00:00', 299 'calendar':'standard', 300 'value':np.array([starttime]), 301 'unlimited':True}, 302 globalAtt=gAtt, 303 nc4=True, classic=True, clobber=clobber )
Convert a HYSPLIT tdump file to netCDF Works with single point or ensemble trajectories
Parameters
- inFile (str): name/path of HYSPLIT tdump file
- outFile (str): name/path of netCDF file that will be created
- clobber (bool, default=False): determines whether outFile will be overwrite any previous file
- globalAtt (dict, default=None): If present, dict keys will be added to outFile as global attributes
- altIsMSL (bool, default=False): Determines whether altitude in HYSPLIT tdump file is treated as altitude above sea level (altIsMSL=True) or altitude above ground (altIsMSL=False). In either case, the netCDF file will contain both altitude variables.
- dropOneTime (bool, default=False): Kludge to address back trajectories that start 1 minute after the hour, due to CONTROL files created with write_control(... exacttime=False ) set True only for trajectories using this setup.
- pack (bool, default=False): NOT IMPLEMENTED determines whether variables in the netCDF file should be compressed with lossy integer packing.
def
read_tdump(file):
305def read_tdump(file): 306 '''Read trajectory file output from HYSPLIT 307 308 Parameters 309 ---------- 310 file : str 311 name of trajectory file to read 312 313 Returns 314 ------- 315 pandas.DataFrame 316 DataFrame contains columns: 317 - time : datetime object 318 - year, month, day, hour, minute : floats, same as time 319 - lat, lon, alt : trajectory location 320 - thour : hours since trajectory initialization, 321 negative for back trajectories 322 - tnum : trajectory number tnum=1 for single trajectory, 323 tnum=1-27 for trajectory ensemble 324 - metnum : index number of met file used at this point in trajectory, 325 see tdump file for corresponding file paths 326 - fcasthr : hours since the meteorological dataset was initialized 327 ''' 328 329 # Open the file 330 with open(file,"r",encoding="ascii") as fid: 331 332 # First line gives number of met fields 333 nmet = int(fid.readline().split()[0]) 334 335 # Skip the met file lines 336 for i in range(nmet): 337 next(fid) 338 339 # Number of trajectories 340 ntraj = int(fid.readline().split()[0]) 341 342 # Skip the next lines 343 for i in range(ntraj): 344 next(fid) 345 346 # Read the variables that are included 347 line = fid.readline() 348 nvar = int(line.split()[0]) 349 vnames = line.split()[1:] 350 351 # Read the data 352 df = pd.read_csv( fid, delim_whitespace=True, 353 header=None, index_col=False, na_values=['NaN','********'], 354 names=['tnum','metnum', 355 'year','month','day','hour','minute','fcasthr', 356 'thour','lat','lon','alt']+vnames ) 357 358 # Convert 2-digit year to 4-digits 359 df.loc[:,'year'] += 2000 360 361 # Convert to time 362 df['time'] = pd.to_datetime( df[['year','month','day','hour','minute']] ) 363 364 #print(df.iloc[-1,:]) 365 return df
Read trajectory file output from HYSPLIT
Parameters
- file (str): name of trajectory file to read
Returns
- pandas.DataFrame: DataFrame contains columns:
- time : datetime object
- year, month, day, hour, minute : floats, same as time
- lat, lon, alt : trajectory location
- thour : hours since trajectory initialization, negative for back trajectories
- tnum : trajectory number tnum=1 for single trajectory, tnum=1-27 for trajectory ensemble
- metnum : index number of met file used at this point in trajectory, see tdump file for corresponding file paths
- fcasthr : hours since the meteorological dataset was initialized
def
find_arl_metfiles( start_time, ndays, back=False, metmodels=None, forecast=False, forecastcycle=None, hybrid=False):
764def find_arl_metfiles( start_time, ndays, back=False, metmodels=None, 765 forecast=False, forecastcycle=None, hybrid=False ): 766 '''Find ARL meteorology files for specified dates, models, forecast and archive 767 768 Files will be located for start_time and extending ndays forward or backward 769 770 Parameters 771 ---------- 772 start_time : datetime.datetime or pandas.Timestamp 773 start date and time for finding meteorology data files 774 ndays : int 775 number of days of files to retrieve 776 back : bool, default=False 777 specifies files should go ndays backward (back=True) from start_time 778 metmodels : list or str, default=['gdas0p5','gdas1'] 779 meteorological models that will be used, in order of decreasing resolution and priority 780 forecast : bool, default=False 781 set forecast=True to use forecast meteorology for trajectory computation 782 set forecast=False to use archived (past) meteorology for trajectory computation 783 Note: hybrid=True will supercede forecast=True 784 forecastcycle : datetime.datetime, pandas.Timestamp, or None, default=None 785 if forecast=True, this sets the forecast initialization cycle that will be used 786 set forecastcycle=None to use the latest available cycle for which files are found 787 if forecast=False, this parameter has no effect 788 hybrid : bool, default=False 789 set hybrid=True for trajectories that use a combination of past archive and 790 forecast meteorlogy. This supercedes forecast=True 791 792 Returns 793 ------- 794 metfiles : list 795 path to meteorology files meeting the criteria 796 ''' 797 798 check_METROOT() 799 800 if metmodels is None: 801 metmodels = ['gdas0p5','gdas1'] 802 803 # Find the meteorology files 804 if hybrid is True: 805 806 # "Hybrid" combines past (archived) and future (forecast) meteorology 807 if back: 808 raise NotImplementedError( "Combined Analysis/Forecast meteorology " 809 + "not supported for back trajectories" ) 810 metfiles = _get_hybrid_filelist( metmodels, start_time ) 811 812 elif forecast is True: 813 # Get the forecast meteorology 814 metfiles = _get_forecast_filelist( metmodels, forecastcycle ) 815 816 # Check if the forecast meteorology covers the entire trajectory duration 817 else: 818 # Use archived (past) meteorology 819 820 # Relative to trajectory start day, 821 # we need meteorology for days d0 through d1 822 if back: 823 d0 = -ndays+1 824 d1 = 1 825 # For trajectories that start 23-0Z (nam) or 21-0Z (gfs0p25), 826 # also need the next day to bracket first time step 827 if (('nam3' in metmodels) and (start_time.hour==23)) \ 828 or (('gfs0p25' in metmodels) and (start_time.hour>=21)): 829 d1 = 2 830 else: 831 d0 = 0 832 d1 = ndays 833 # For trajectories that start 00-01Z (nam) or 00-03Z (gfs0p25), 834 # also need the prior day to bracket first time step 835 if start_time.hour==0: 836 d0 = -1 837 838 # Debug output 839 # print('Initial Date Range',d0,d1) 840 # datelist = np.unique( ( time + 841 # pd.TimedeltaIndex( np.sign(trajhours) * np.arange(0,np.abs(trajhours)+1),"H" ) ).date ) 842 # print(datelist) 843 844 # List of met directories and met files that will be used 845 metfiles = [] 846 for d in range(d0,d1): 847 848 # date of met data 849 metdate = start_time.date() + pd.Timedelta( d, "D" ) 850 851 # Met data for a single day 852 filename = _get_archive_filelist( metmodels, metdate ) 853 854 # Add the files to the list 855 metfiles.extend( filename ) 856 # Keep only the unique files 857 metfiles = np.unique(metfiles) 858 859 if len(metfiles) <= 0: 860 raise ValueError( 'Meteorology files not found for ' + str( metmodels ) ) 861 862 return metfiles
Find ARL meteorology files for specified dates, models, forecast and archive
Files will be located for start_time and extending ndays forward or backward
Parameters
- start_time (datetime.datetime or pandas.Timestamp): start date and time for finding meteorology data files
- ndays (int): number of days of files to retrieve
- back (bool, default=False): specifies files should go ndays backward (back=True) from start_time
- metmodels (list or str, default=['gdas0p5','gdas1']): meteorological models that will be used, in order of decreasing resolution and priority
- forecast (bool, default=False): set forecast=True to use forecast meteorology for trajectory computation set forecast=False to use archived (past) meteorology for trajectory computation Note: hybrid=True will supercede forecast=True
- forecastcycle (datetime.datetime, pandas.Timestamp, or None, default=None): if forecast=True, this sets the forecast initialization cycle that will be used set forecastcycle=None to use the latest available cycle for which files are found if forecast=False, this parameter has no effect
- hybrid (bool, default=False): set hybrid=True for trajectories that use a combination of past archive and forecast meteorlogy. This supercedes forecast=True
Returns
- metfiles (list): path to meteorology files meeting the criteria
def
write_control( time, lat, lon, alt, trajhours, fname='CONTROL.000', clobber=False, maxheight=15000.0, outdir='./', tfile='tdump', metfiles=None, exacttime=True, **kwargs):
864def write_control( time, lat, lon, alt, trajhours, 865 fname='CONTROL.000', clobber=False, 866 maxheight=15000., outdir='./', tfile='tdump', 867 metfiles=None, exacttime=True, **kwargs ): 868 '''Write HYSPLIT control file for trajectory starting at designated time and coordinates 869 870 Parameters 871 ---------- 872 time : datetime.datetime or pandas.Timestamp 873 trajectory initialization time 874 lat, lon : float or list 875 trajectory initialization latitude and longitude in degrees 876 alt : float or list 877 trajectory initialization altitude in meters 878 The setup.cfg determines whether this is above ground or above mean sea level 879 trajhours : int 880 desired trajectory duration in hours. Use negative for back trajectories 881 fname : str, default='CONTROL.000' 882 path and name for the file that will be written 883 clobber : bool, default=False 884 if clobber=True, then fname will be overwritten 885 maxheight : float, default=15000 886 terminate trajectories that exceed maxheight altitude in meters 887 outdir : str, default='./' 888 directory path where HYSPLIT output will be written 889 tfile : str, default='tdump' 890 name of the trajectory file that HYSPLIT will write 891 metfiles : list or str, default=None 892 paths to ARL meteorology files needed for the trajectory computation 893 If metfiles=None, then find_arl_metfiles will be used to locate necessary files 894 exacttime : bool, default=True 895 It is not recommended to change this default, but keyword is retained for backward 896 compatibility with some scripts. Setting exacttime=False will shift the actual 897 start time of trajectories that begin at 00:00 UTC to 00:01 UTC to avoid reading 898 an additional day of meteorological data. 899 **kwargs 900 kwargs will be passed to find_arl_metfiles to locate ARL meteorlogy files if metfiles=None 901 These keywords should include metmodels and possibly forecast, forecastcycle, or hybrid 902 See find_arl_metfiles for definitions of these parameters 903 ''' 904 905 check_METROOT() 906 907 if os.path.isfile( fname ) and (clobber is False): 908 raise OSError( f'File exists. Set clobber=True to overwrite: {fname:s}' ) 909 910 # Ensure that lat, lon, and alt are lists 911 try: 912 nlat = len( lat ) 913 except TypeError: 914 lat = [ lat ] 915 nlat = len( lat ) 916 try: 917 nlon = len( lon ) 918 except TypeError: 919 lon = [ lon ] 920 nlon = len( lon ) 921 try: 922 nalt = len( alt ) 923 except TypeError: 924 alt = [ alt ] 925 nalt = len( alt ) 926 927 # Should add some type checking to ensure that lon, lat, alt 928 # have conformable lengths 929 if ( (nlat != nlon) or (nlat != nalt) ): 930 raise ValueError( "lon, lat, alt must be conformable" ) 931 932 # Number of days of met data 933 ndays = int( np.ceil( np.abs( trajhours ) / 24 ) + 1 ) 934 935 # Get met file names, if not provided 936 if (metfiles is None) or (len(metfiles)==0): 937 metfiles = find_arl_metfiles( time, ndays, back=trajhours<0, **kwargs ) 938 939 # Number of met files 940 nmet = len( metfiles ) 941 942 # Runs will fail if the initial time is not bracketed by met data. 943 # When met resolution changes on the first day, this condition may not be met. 944 # Unless exacttime==True, the starting time will be shifted 1 minute to 00:01 945 # to avoid reading in an entire extra day of met data. 946 if (time.hour==0 and time.minute == 0 and not exacttime): 947 time = time + pd.Timedelta( 1, "m" ) 948 949 # Start date-time, formatted as YY MM DD HH {mm} 950 startdate = time.strftime( "%y %m %d %H %M" ) 951 952 # Write the CONTROL file 953 with open( fname, 'w', encoding='ascii' ) as f: 954 955 f.write( startdate+'\n' ) 956 f.write( f'{nlat:d}\n' ) 957 for i in range(nlat): 958 f.write( f'{lat[i]:<10.4f} {lon[i]:<10.4f} {alt[i]:<10.4f}\n' ) 959 f.write( f'{trajhours:d}\n' ) 960 f.write( "0\n" ) 961 f.write( f"{maxheight:<10.1f}\n" ) 962 f.write( f'{nmet:d}\n' ) 963 for file in metfiles: 964 f.write( os.path.dirname( file ) + '/\n' ) 965 f.write( os.path.basename( file ) + '\n' ) 966 f.write( f'{outdir:s}\n' ) 967 f.write( f'{tfile:s}\n' )
Write HYSPLIT control file for trajectory starting at designated time and coordinates
Parameters
- time (datetime.datetime or pandas.Timestamp): trajectory initialization time
- lat, lon (float or list): trajectory initialization latitude and longitude in degrees
- alt (float or list): trajectory initialization altitude in meters The setup.cfg determines whether this is above ground or above mean sea level
- trajhours (int): desired trajectory duration in hours. Use negative for back trajectories
- fname (str, default='CONTROL.000'): path and name for the file that will be written
- clobber (bool, default=False): if clobber=True, then fname will be overwritten
- maxheight (float, default=15000): terminate trajectories that exceed maxheight altitude in meters
- outdir (str, default='./'): directory path where HYSPLIT output will be written
- tfile (str, default='tdump'): name of the trajectory file that HYSPLIT will write
- metfiles (list or str, default=None): paths to ARL meteorology files needed for the trajectory computation If metfiles=None, then find_arl_metfiles will be used to locate necessary files
- exacttime (bool, default=True): It is not recommended to change this default, but keyword is retained for backward compatibility with some scripts. Setting exacttime=False will shift the actual start time of trajectories that begin at 00:00 UTC to 00:01 UTC to avoid reading an additional day of meteorological data.
- **kwargs: kwargs will be passed to find_arl_metfiles to locate ARL meteorlogy files if metfiles=None These keywords should include metmodels and possibly forecast, forecastcycle, or hybrid See find_arl_metfiles for definitions of these parameters