#!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------# # # Scripts for processing of WRF and CAMx files to PALM dynamic driver # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Copyright 2018-2020 Institute of Computer Science # of the Czech Academy of Sciences, Prague # Authors: Krystof Eben, Jaroslav Resler, Pavel Krc # #------------------------------------------------------------------------------# '''Scripts for processing of WRF and CAMx files to PALM dynamic driver. Usage: palm_dynamic -c [-w] Script requires name of the configuration on command line. The corresponding case configuration files are placed in subdirectory "configuration" and the are named .conf. The template of the config file is in the file palm_dynamic_defaults.py, the values which agree with defaults need not be present in the user config. The file palm_dynamic_init.py contains setting and calculation of standard initialization values for particular system and can be adjusted. The optional parameter -w allows to skip horizontal and vertical interpolation in case it is already done. ''' __version__ = '3.1' import sys import getopt import os.path from datetime import datetime, timedelta import numpy as np from pyproj import Proj, transform import glob import netCDF4 import palm_dynamic_config ################################## # read configuration from command # line and parse it def print_help(): print() print(__doc__) print('Version: '+__version__) # set commandline variables configname = '' wrf_interpolation = True if sys.argv[0].endswith('pydevconsole.py'): # we are in Pycharm developlent console - set testing data configname = 'evropska_12s_basecase' else: try: opts, args = getopt.getopt(sys.argv[1:],"hc:w)",["help=","config=","write="]) #print(opts) except getopt.GetoptError as err: print("Error:", err) print_help() sys.exit(2) # parse options for opt, arg in opts: if opt in ("-h", "--help"): print_help() sys.exit(0) elif opt in ("-c", "--config"): configname = arg elif opt in ("-w", "--write"): wrf_interpolation = False if configname == '': # script requires config name print("This script requires input parameter -c ") print_help() exit(2) # Load config defaults, config file and standard init into the config module palm_dynamic_config.configure(configname) # Import values (including loaded) from config module into globals from palm_dynamic_config import * # Other modules are imported *AFTER* the config module has been filled with # values (so that they can correctly import * from palm_dynamic_config). import palm_wrf_utils from palm_dynamic_output import palm_dynamic_output import palm_dynamic_camx print('Domain name: ', domain) print('Resolution name: ', resolution) print('Scenario name: ', scenario) print('Read domain from static:', grid_from_static) if grid_from_static: print('STATIC driver input file: ', static_driver_file) print('WRF and CAMx file path:', wrf_dir_name) print('WRF file mask:', wrf_file_mask) print('CAMx file mask:', camx_file_mask) print('Simulation start time:', origin_time) print('Simulation hours:', simulation_hours) ########################### # domain parameters if grid_from_static: # get parameters of the horizontal domain from PALM STATIC driver try: ncs = netCDF4.Dataset(static_driver_file, "r", format="NETCDF4") except Exception as err: print("Error opening static driver file:") print(static_driver_file) print(err) sys.exit(1) # get horizontal structure of the domain nx = ncs.dimensions['x'].size ny = ncs.dimensions['y'].size dx = ncs.variables['x'][:][1] - ncs.variables['x'][:][0] dy = ncs.variables['y'][:][1] - ncs.variables['y'][:][0] origin_x = ncs.getncattr('origin_x') origin_y = ncs.getncattr('origin_y') origin_z = ncs.getncattr('origin_z') if origin_time == '': # origin_time is not prvided in configuration - read it from static driver origin_time = ncs.getncattr('origin_time') # create vertical structure of the domain # calculate dz in case dz is not supplied (dz<=0) if dz <= 0.0: print("dz = 0.0: set dz = dx") dz = dx if nz <= 0: print("nz > 0 needs to be set in config") sys.exit(1) # read terrain height (relative to origin_z) and # calculate and check the height of the surface canopy layer if 'zt' in ncs.variables.keys(): terrain_rel = ncs.variables['zt'][:] else: terrain_rel = np.zeros([ny,nx]) th = np.ceil(terrain_rel / dz) # building height if 'buildings_3d' in ncs.variables.keys(): print(np.argmax(a != 0, axis=0)) bh3 = ncs.variables['buildings_3d'][:] # minimum index of nonzeo value along inverted z bh = np.argmax(bh3[::-1], axis=0) # inversion back and masking grids with no buildings bh = bh3.shape[0] - bh bh[np.max(bh3, axis=0) == 0] = 0 elif 'buildings_2d' in ncs.variables.keys(): bh = ncs.variables['buildings_2d'][:] bh[bh.mask] = 0 bh = np.ceil(bh / dz) else: bh = np.zeros([ny,nx]) # plant canopy height if 'lad' in ncs.variables.keys(): lad3 = ncs.variables['lad'][:] # replace non-zero values with 1 lad3[lad3 != 0] = 1 # minimum index of nonzeo value along inverted z lad = np.argmax(lad3[::-1], axis=0) # inversion back and masking grids with no buildings lad = lad3.shape[0] - lad lad[np.max(lad3, axis=0) == 0] = 0 else: lad = np.zeros([ny,nx]) # calculate maximum of surface canopy layer nscl = max(np.amax(th+bh),np.amax(th+lad)) # check nz with ncl if nz < nscl + nscl_free: print('nz has to be higher than ', nscl + nscl_free) print('nz=', nz, ', dz=', dz, ', number of scl=', nscl, ',nscl_free=', nscl_free) if dz_stretch_level <= (nscl + nscl_free) * dz: print('stretching has to start in higher level than ', (nscl + nscl_free) * dz) print('dz_stretch_level=', dz_stretch_level, ', nscl=', nscl, ', nscl_free=', nscl_free, ', dz=', dz) if 'soil_moisture_adjust' in ncs.variables.keys(): soil_moisture_adjust = ncs.variables['soil_moisture_adjust'][:] else: soil_moisture_adjust = np.ones(shape=(ny, nx), dtype=float) # close static driver nc file ncs.close() else: #TODO check all necessary parameters are set and are correct # # initialize necessary arrays try: terrain = np.zeros(shape=(ny, nx), dtype=float) soil_moisture_adjust = np.ones(shape=(ny, nx), dtype=float) except: print('domain parameters nx, ny have to be set in configuration') sys.exit(1) # absolute terrain needed for vertical interpolation of wrf data terrain = terrain_rel + origin_z # print domain parameters and check ist existence in caso of setup from config try: print('Domain parameters:') print('nx, ny, nz:', nx, ny, nz) print('dx, dy, dz:', dx, dy, dz) print('origin_x, origin_y:', origin_x, origin_y) print('Base of domain is in level origin_z:', origin_z) except: print('domain parameters have to be read from static driver or set in configuration') sys.exit(1) # centre of the domain (needed for ug,vg calculation) xcent = origin_x + nx * dx / 2.0 ycent = origin_y + ny * dy / 2.0 # WGS84 projection for transformation to lat-lon inproj = Proj('+init='+proj_palm) print('inproj', inproj) lonlatproj = Proj('+init='+proj_wgs84) print('lonlatproj', lonlatproj) cent_lon, cent_lat = transform(inproj, lonlatproj, xcent, ycent) print('xcent, ycent:',xcent, ycent) print('cent_lon, cent_lat:', cent_lon, cent_lat) # prepare target grid irange = origin_x + dx * (np.arange(nx, dtype='f8') + .5) jrange = origin_y + dy * (np.arange(ny, dtype='f8') + .5) palm_grid_y, palm_grid_x = np.meshgrid(jrange, irange, indexing='ij') palm_grid_lon, palm_grid_lat = transform(inproj, lonlatproj, palm_grid_x, palm_grid_y) ###################################### # build structure of vertical layers # remark: # PALM input requires nz=ztop in PALM # but the output file in PALM has max z higher than z in PARIN. # The highest levels in PALM are wrongly initialized !!! ##################################### # fill out z_levels z_levels = np.zeros(nz,dtype=float) z_levels_stag = np.zeros(nz-1,dtype=float) dzs = dz z_levels[0] = dzs/2.0 for i in range(nz-1): z_levels[i+1] = z_levels[i] + dzs z_levels_stag[i] = (z_levels[i+1]+z_levels[i])/2.0 dzso = dzs if z_levels[i+1] + dzs >= dz_stretch_level: dzs = min(dzs * dz_stretch_factor, dz_max) ztop = z_levels[-1] + dzs / 2. print('z:',z_levels) print('zw:',z_levels_stag) ###################################### # get time extent of the PALM simulation ##################################### # get complete list of wrf files wrf_file_list = glob.glob(os.path.join(wrf_dir_name, wrf_file_mask)) # get simulation origin and final time as datetime start_time = datetime.strptime(origin_time, '%Y-%m-%d %H:%M:%S') end_time = start_time + timedelta(hours=simulation_hours) end_time_rad = end_time print('PALM simulation extent', start_time, end_time, simulation_hours) if nested_domain: print('Nested domain - process only initialization.') print('Set end_time = start_time') end_time = start_time # get wrf times and sort wrf files by time print('Analyse WRF files dates:') file_times = [] for wrf_file in wrf_file_list: # get real time from wrf file nc_wrf = netCDF4.Dataset(wrf_file, "r", format="NETCDF4") ta = nc_wrf.variables['Times'][:] t = ta.tobytes().decode("utf-8") td = datetime.strptime(t, '%Y-%m-%d_%H:%M:%S') print(os.path.basename(wrf_file), ': ', td) file_times.append((td,wrf_file)) file_times.sort() times = [] wrf_files = [] for tf in file_times: if end_time is None or tf[0] <= end_time: times.append(tf[0]) wrf_files.append(tf[1]) #print('PALM output times:') #print('\n'.join('{}'.format(t) for t in times)) print('PALM output times:', ', '.join('{}'.format(t) for t in times)) if not times.__contains__(start_time): print('WRF files does not contain PALM origin_time timestep - cannot process!') exit(1) if not times.__contains__(end_time): print('WRF files does not contain PALM end_time timestep - cannot process!') exit(1) start_index = times.index(start_time) end_index = times.index(end_time) if not nested_domain and end_index-start_index < simulation_hours: print('Number of WRF files does not aggre with number of simulation hours') exit(1) print('PALM simulation timestep number are from ', start_index, ' to ', end_index, ' WRF file.') # create list of processed wrf files wrf_files_proc = [] for wf in wrf_files[start_index:end_index+1]: wrf_files_proc.append(wf) # id of process files simul_id = domain + "_d" + resolution ###################################### #VERTICAL AND HORIZONTAL INTERPOLATION ###################################### # get hydro and soil variables contained in wrf files shvars = [] z_soil_levels = [] nc_wrf = netCDF4.Dataset(wrf_files_proc[0], "r", format="NETCDF4") try: # hydro variables shvars = sorted(set(['QVAPOR', 'QCLOUD', 'QRAIN', 'QICE', 'QSNOW', 'QGRAUP']).intersection(nc_wrf.variables.keys())) # soil layers if 'ZS' in nc_wrf.variables.keys(): z_soil_levels = nc_wrf.variables['ZS'][0,:].data.tolist() finally: nc_wrf.close() print('Hydro variables in wrf files:', '+'.join(shvars)) print('Start of vertical and horizontal interpolation of inputs to the PALM domain.') interp_files = [] regridder = None for wrf_file in wrf_files_proc: print ("Input wrf file: ", wrf_file) # hinterp_file = wrf_file+"_"+simul_id+'.hinterp' hinterp_log = wrf_file + "_" + simul_id + '.hinterp.log' vinterp_file = wrf_file+"_"+simul_id+'.interp' interp_files.append(vinterp_file) if wrf_interpolation: try: os.remove(vinterp_file) os.remove(hinterp_file) os.remove(hinterp_log) except: pass print('Horizontal interpolation...') f_wrf = netCDF4.Dataset(wrf_file, 'r') try: if not regridder: trans_wrf = palm_wrf_utils.WRFCoordTransform(f_wrf) palm_in_wrf_y, palm_in_wrf_x = trans_wrf.latlon_to_ji( palm_grid_lat, palm_grid_lon) regridder = palm_wrf_utils.BilinearRegridder( palm_in_wrf_x, palm_in_wrf_y, preloaded=True) regridder_u = palm_wrf_utils.BilinearRegridder( palm_in_wrf_x+.5, palm_in_wrf_y, preloaded=True) regridder_v = palm_wrf_utils.BilinearRegridder( palm_in_wrf_x, palm_in_wrf_y+.5, preloaded=True) f_out = netCDF4.Dataset(hinterp_file, 'w', format='NETCDF4') try: # dimensions f_out.createDimension('Time', None) for d in 'bottom_top bottom_top_stag soil_layers_stag'.split(): f_out.createDimension(d, len(f_wrf.dimensions[d])) f_out.createDimension('west_east', len(irange)) f_out.createDimension('south_north', len(jrange)) # copied vars for varname in 'PH PHB HGT T W TSLB SMOIS MU MUB P PB PSFC'.split(): v_wrf = f_wrf.variables[varname] v_out = f_out.createVariable(varname, 'f4', v_wrf.dimensions) v_out[:] = regridder.regrid(v_wrf[...,regridder.ys,regridder.xs]) # U and V have special treatment (unstaggering) v_out = f_out.createVariable('U', 'f4', ('Time', 'bottom_top', 'south_north', 'west_east')) v_out[:] = regridder_u.regrid(f_wrf.variables['U'][...,regridder_u.ys,regridder_u.xs]) v_out = f_out.createVariable('V', 'f4', ('Time', 'bottom_top', 'south_north', 'west_east')) v_out[:] = regridder_v.regrid(f_wrf.variables['V'][...,regridder_v.ys,regridder_v.xs]) # calculated SPECHUM v_out = f_out.createVariable('SPECHUM', 'f4', ('Time', 'bottom_top', 'south_north', 'west_east')) vdata = regridder.regrid(f_wrf.variables[shvars[0]][...,regridder.ys,regridder.xs]) for vname in shvars[1:]: vdata += regridder.regrid(f_wrf.variables[vname][...,regridder.ys,regridder.xs]) v_out[:] = vdata finally: f_out.close() finally: f_wrf.close() print('Vertical interpolation...') palm_wrf_utils.palm_wrf_vertical_interp(hinterp_file, vinterp_file, wrf_file, z_levels, z_levels_stag, z_soil_levels, origin_z, terrain, wrf_hybrid_levs, vinterp_terrain_smoothing) if radiation_from_wrf: print('Start processing of radiation inputs from the WRF radiation files.') # get available times from wrf radiation output files and sort files by time # get complete list of wrf radiation files rad_file_list = glob.glob(os.path.join(wrf_dir_name, wrf_rad_file_mask)) rad_file_times = [] print('Analyse WRF radiation files dates:') for rad_file in rad_file_list: # get real time from wrf radiation file nc_rad = netCDF4.Dataset(rad_file, "r", format="NETCDF4") ta = nc_rad.variables['Times'][:] t = ta.tobytes().decode("utf-8") td = datetime.strptime(t, '%Y-%m-%d_%H:%M:%S') print(os.path.basename(rad_file), ': ', td) rad_file_times.append((td, rad_file)) rad_file_times.sort() rad_times = [] rad_files = [] for tf in rad_file_times: if end_time_rad is None or tf[0] <= end_time_rad: rad_times.append(tf[0]) rad_files.append(tf[1]) # check list of available times if not rad_times.__contains__(start_time): print('WRF radiation files does not contain PALM origin_time timestep - cannot process!') exit(1) if not rad_times.__contains__(end_time_rad): print('WRF radiation files does not contain PALM end_time_rad timestep - cannot process!') exit(1) rad_start_index = rad_times.index(start_time) rad_end_index = rad_times.index(end_time_rad) print('PALM radiation timestep numbers are from ', rad_start_index, ' to ', rad_end_index, ' WRF radiation file.') # get radiation timestep rad_timestep = (rad_times[rad_start_index+1] - rad_times[rad_start_index]).total_seconds() print('Timestep of the wrf radiation data is ', rad_timestep,' seconds.') # check consistency of the series for i in range(rad_start_index, rad_end_index): if (rad_times[i+1] - rad_times[i]).total_seconds() != rad_timestep: print('Inconsistent timeline of radiation inputs! Cannot process.') print(rad_times[rad_start_index:rad_end_index]) exit(1) # create list of processed wrf radiation files and times rad_files_proc = [] rad_times_proc = [] for i in range(rad_start_index, rad_end_index + 1): rad_files_proc.append(rad_files[i]) rad_times_proc.append((rad_times[i]-rad_times[rad_start_index]).total_seconds()) print("Radiation times are ", rad_times_proc) # process radiation inputs rad_swdown = [] rad_lwdown = [] rad_swdiff = [] nfiles = 0 for rad_file in rad_files_proc: print ("Input wrf radiation file: ", rad_file) ncf = netCDF4.Dataset(rad_file, "r", format="NETCDF4") try: if nfiles == 0: # create list of (i,j) indices used for smoothing of the radiation print('Build list of indices for radiation smoothig.') palmproj = Proj(init=proj_palm) lonlatproj = Proj(init=proj_wgs84) rad_ind = [] for i in range(ncf.dimensions['west_east'].size): for j in range(ncf.dimensions['south_north'].size): # transfrom lat-lon to PALM domain in meters lonij = ncf.variables['XLONG'][0,j,i] latij = ncf.variables['XLAT'][0,j,i] xij, yij = transform(lonlatproj, palmproj, lonij, latij) if abs(xij-xcent)<=radiation_smoothing_distance and \ abs(yij-ycent)<=radiation_smoothing_distance: rad_ind.append([i,j]) ngrids = len(rad_ind) # process wrf radiation file nfiles += 1 swdown = 0.0 lwd = 0.0 swddif = 0.0 for ij in rad_ind: swdown = swdown + ncf.variables['SWDOWN'][0,ij[1],ij[0]] lwd = lwd + ncf.variables['GLW'][0,ij[1],ij[0]] swddif = swddif + ncf.variables['SWDDIF'][0,ij[1],ij[0]] rad_swdown.append(swdown / ngrids) rad_lwdown.append(lwd / ngrids) rad_swdiff.append(swddif / ngrids) finally: ncf.close() # create list of all radiation values rad_values_proc = [rad_swdown, rad_lwdown, rad_swdiff] else: rad_times_proc = [] rad_values_proc = [] camx_interp_fname = None if not nested_domain: # process camx files camx_file_list = glob.glob(os.path.join(wrf_dir_name, camx_file_mask)) if camx_file_list: print('Processing CAMx input files: {0}'.format(', '.join(camx_file_list))) camx_interp_fname = os.path.join(wrf_dir_name, 'CAMx_{0}_interp.nc'.format(simul_id)) palm_dynamic_camx.process_files(camx_file_list, camx_interp_fname, palm_grid_lat, palm_grid_lon, terrain_rel, z_levels, times[start_index:end_index+1], species_names, camx_conversions, camx_helpers) # ===================== CREATE NETCDF DRIVER ============================== # calculate relative times from simulation start times_sec = [] for t in times[start_index:end_index+1]: times_sec.append((t-start_time).total_seconds()) # collect dimension sizes dimensions = {'zdim': nz, 'zwdim': nz-1, 'zsoildim': len(z_soil_levels), 'xdim': nx, 'xudim': nx-1, 'ydim': ny, 'yvdim': ny-1} # process interpolated files to dynamic driver palm_dynamic_output(wrf_files, interp_files, camx_interp_fname, dynamic_driver_file, times_sec, dimensions, z_levels, z_levels_stag, ztop, z_soil_levels, dx, dy, cent_lon, cent_lat, rad_times_proc, rad_values_proc, soil_moisture_adjust, nested_domain) print('Creation of dynamic driver finished.')