def common_area_func(image_list): """""" common_nodata = 0 # Assume all Landsat images have same extent common_shape = gdc.raster_path_shape(image_list[0]) common_array = np.ones(common_shape, dtype=np.bool) for i, image in enumerate(image_list): image_array, image_nodata = gdc.raster_to_array(image) common_array &= (image_array != common_nodata) del image_array, image_nodata return common_array
def main(grb_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), variables=['pr'], landsat_ws=None, start_date=None, end_date=None, times_str='', extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False): """Extract NLDAS target variable(s) Args: grb_ws (str): folder of NLDAS GRB files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters variable (list): NLDAS variables to download ('ppt', 'srad', 'sph', 'tair', tmmn', 'tmmx', 'vs') landsat_ws (str): folder of Landsat scenes or tar.gz files start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) times (str): comma separated values and/or ranges of UTC hours (i.e. "1, 2, 5-8") Parsed with python_common.parse_int_set() extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nExtract NLDAS target variable(s)') # input_fmt = 'NLDAS_FORA0125_H.A{:04d}{:02d}{:02d}.{}.002.grb' input_re = re.compile( 'NLDAS_FORA0125_H.A(?P<YEAR>\d{4})(?P<MONTH>\d{2})' + '(?P<DAY>\d{2}).(?P<TIME>\d{4}).002.grb$') output_fmt = '{}_{:04d}{:02d}{:02d}_hourly_nldas.img' # output_fmt = '{}_{:04d}{:02d}{:02d}_{:04d}_nldas.img' # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Only process a specific hours if not times_str: time_list = range(0, 24, 1) else: time_list = list(parse_int_set(times_str)) time_list = ['{:02d}00'.format(t) for t in time_list] # Assume NLDAS is NAD83 # input_epsg = 'EPSG:4269' # NLDAS rasters to extract data_full_list = ['pr', 'srad', 'sph', 'tair', 'tmmn', 'tmmx', 'vs'] if not variables: logging.error('\nERROR: variables parameter is empty\n') sys.exit() elif type(variables) is not list: # DEADBEEF - I could try converting comma separated strings to lists? logging.warning('\nERROR: variables parameter must be a list\n') sys.exit() elif not set(variables).issubset(set(data_full_list)): logging.error('\nERROR: variables parameter is invalid\n {}'.format( variables)) sys.exit() # Ancillary raster paths mask_path = os.path.join(ancillary_ws, 'nldas_mask.img') # Build a date list from landsat_ws scene folders or tar.gz files date_list = [] if landsat_ws is not None and os.path.isdir(landsat_ws): logging.info('\nReading dates from Landsat IDs') logging.info(' {}'.format(landsat_ws)) landsat_re = re.compile( '^(?:LT04|LT05|LE07|LC08)_(?:\d{3})(?:\d{3})_' + '(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})') for root, dirs, files in os.walk(landsat_ws, topdown=True): # If root matches, don't explore subfolders try: landsat_match = landsat_re.match(os.path.basename(root)) date_list.append(dt.datetime.strptime( '_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) dirs[:] = [] except: pass for file in files: try: landsat_match = landsat_re.match(file) date_list.append(dt.datetime.strptime( '_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) except: pass date_list = sorted(list(set(date_list))) # elif landsat_ws is not None and os.path.isfile(landsat_ws): # with open(landsat_ws) as landsat_f: # This allows GDAL to throw Python Exceptions # gdal.UseExceptions() # mem_driver = gdal.GetDriverByName('MEM') # Get the NLDAS spatial reference from the mask raster nldas_ds = gdal.Open(mask_path) nldas_osr = gdc.raster_ds_osr(nldas_ds) nldas_proj = gdc.osr_proj(nldas_osr) nldas_cs = gdc.raster_ds_cellsize(nldas_ds, x_only=True) nldas_extent = gdc.raster_ds_extent(nldas_ds) nldas_geo = nldas_extent.geo(nldas_cs) nldas_x, nldas_y = nldas_extent.origin() nldas_ds = None logging.debug(' Projection: {}'.format(nldas_proj)) logging.debug(' Cellsize: {}'.format(nldas_cs)) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) nldas_extent = gdc.Extent(output_extent) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): nldas_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: nldas_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) nldas_extent = gdc.project_extent( nldas_extent, extent_osr, nldas_osr, extent_cs) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) logging.debug('') # Read the NLDAS mask array if present if mask_path and os.path.isfile(mask_path): mask_array, mask_nodata = gdc.raster_to_array( mask_path, mask_extent=nldas_extent, fill_value=0, return_nodata=True) mask_array = mask_array != mask_nodata else: mask_array = None # NLDAS band name dictionary nldas_band_dict = dict() nldas_band_dict['pr'] = 'Total precipitation [kg/m^2]' nldas_band_dict['srad'] = 'Downward shortwave radiation flux [W/m^2]' nldas_band_dict['sph'] = 'Specific humidity [kg/kg]' nldas_band_dict['tair'] = 'Temperature [C]' nldas_band_dict['tmmn'] = 'Temperature [C]' nldas_band_dict['tmmx'] = 'Temperature [C]' nldas_band_dict['vs'] = [ 'u-component of wind [m/s]', 'v-component of wind [m/s]'] # NLDAS band name dictionary # nldas_band_dict = dict() # nldas_band_dict['pr'] = 'precipitation_amount' # nldas_band_dict['srad'] = 'surface_downwelling_shortwave_flux_in_air' # nldas_band_dict['sph'] = 'specific_humidity' # nldas_band_dict['tmmn'] = 'air_temperature' # nldas_band_dict['tmmx'] = 'air_temperature' # nldas_band_dict['vs'] = 'wind_speed' # NLDAS band name dictionary (EarthEngine keys, GRID_ELEMENT values) # nldas_band_dict = dict() # nldas_band_dict['total_precipitation'] = 'Total precipitation [kg/m^2]' # nldas_band_dict['shortwave_radiation'] = 'Downward shortwave radiation flux [W/m^2]' # nldas_band_dict['specific_humidity'] = 'Specific humidity [kg/kg]' # nldas_band_dict['pressure'] = 'Pressure [Pa]' # nldas_band_dict['temperature'] = 'Temperature [C]' # nldas_band_dict['wind_u'] = 'u-component of wind [m/s]' # nldas_band_dict['wind_v'] = 'v-component of wind [m/s]' # Process each variable logging.info('\nReading NLDAS GRIBs') for input_var in variables: logging.info("Variable: {}".format(input_var)) # Build output folder var_ws = os.path.join(output_ws, input_var) if not os.path.isdir(var_ws): os.makedirs(var_ws) # Each sub folder in the main folde has all imagery for 1 day # The path for each subfolder is the /YYYY/DOY # This approach will process files for target dates # for input_dt in date_range(start_dt, end_dt + dt.timedelta(1)): # logging.info(input_dt.date()) # Iterate all available files and check dates if necessary for root, folders, files in os.walk(grb_ws): root_split = os.path.normpath(root).split(os.sep) # If the year/doy is outside the range, skip if (re.match('\d{4}', root_split[-2]) and re.match('\d{3}', root_split[-1])): root_dt = dt.datetime.strptime('{}_{}'.format( root_split[-2], root_split[-1]), '%Y_%j') logging.info('{}-{:02d}-{:02d}'.format( root_dt.year, root_dt.month, root_dt.day)) if ((start_dt is not None and root_dt < start_dt) or (end_dt is not None and root_dt > end_dt)): continue elif date_list and root_dt.date().isoformat() not in date_list: continue # If the year is outside the range, don't search subfolders elif re.match('\d{4}', root_split[-1]): root_year = int(root_split[-1]) logging.info('Year: {}'.format(root_year)) if ((start_dt is not None and root_year < start_dt.year) or (end_dt is not None and root_year > end_dt.year)): folders[:] = [] else: folders[:] = sorted(folders) continue else: continue # Create a single raster for each day with 24 bands # Each time step will be stored in a separate band output_name = output_fmt.format( input_var, root_dt.year, root_dt.month, root_dt.day) output_path = os.path.join( var_ws, str(root_dt.year), output_name) logging.debug(' {}'.format(output_path)) if os.path.isfile(output_path): if not overwrite_flag: logging.debug(' File already exists, skipping') continue else: logging.debug(' File already exists, removing existing') os.remove(output_path) logging.debug(' {}'.format(root)) if not os.path.isdir(os.path.dirname(output_path)): os.makedirs(os.path.dirname(output_path)) gdc.build_empty_raster( output_path, band_cnt=24, output_dtype=np.float32, output_proj=nldas_proj, output_cs=nldas_cs, output_extent=nldas_extent, output_fill_flag=True) # Iterate through hourly files for input_name in sorted(files): logging.info(' {}'.format(input_name)) input_path = os.path.join(root, input_name) input_match = input_re.match(input_name) if input_match is None: logging.debug( ' Regular expression didn\'t match, skipping') continue input_dt = dt.datetime( int(input_match.group('YEAR')), int(input_match.group('MONTH')), int(input_match.group('DAY'))) time_str = input_match.group('TIME') band_num = int(time_str[:2]) + 1 # if start_dt is not None and input_dt < start_dt: # continue # elif end_dt is not None and input_dt > end_dt: # continue # elif date_list and input_dt.date().isoformat() not in date_list: # continue if time_str not in time_list: logging.debug(' Time not in list, skipping') continue logging.debug(' Time: {} {}'.format( input_dt.date(), time_str)) logging.debug(' Band: {}'.format(band_num)) # Determine band numbering/naming input_band_dict = grib_band_names(input_path) # Extract array and save input_ds = gdal.Open(input_path) # Convert Kelvin to Celsius (old NLDAS files were in K i think) if input_var in ['tair', 'tmmx', 'tmmn']: # Temperature should be in C for et_common.refet_hourly_func() if 'Temperature [K]' in input_band_dict.keys(): temp_band_units = 'K' output_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Temperature [K]'], mask_extent=nldas_extent, return_nodata=False) elif 'Temperature [C]' in input_band_dict.keys(): temp_band_units = 'C' output_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Temperature [C]'], mask_extent=nldas_extent, return_nodata=False) else: logging.error('Unknown Temperature units, skipping') logging.error(' {}'.format(input_band_dict.keys())) continue # DEADBEEF - Having issue with T appearing to be C but labeled as K # Try to determine temperature units from values temp_mean = float(np.nanmean(output_array)) temp_units_dict = {20: 'C', 293: 'K'} temp_array_units = temp_units_dict[ min(temp_units_dict, key=lambda x:abs(x - temp_mean))] if temp_array_units == 'K' and temp_band_units == 'K': logging.debug(' Converting temperature from K to C') output_array -= 273.15 elif temp_array_units == 'C' and temp_band_units == 'C': pass elif temp_array_units == 'C' and temp_band_units == 'K': logging.debug( (' Temperature units are K in the GRB band name, ' + 'but values appear to be C\n Mean temperature: {:.2f}\n' + ' Values will NOT be adjusted').format(temp_mean)) elif temp_array_units == 'K' and temp_band_units == 'C': logging.debug( (' Temperature units are C in the GRB band name, ' + 'but values appear to be K\n Mean temperature: {:.2f}\n' + ' Values will be adjusted from K to C').format(temp_mean)) output_array -= 273.15 # Compute wind speed from vectors elif input_var == 'vs': wind_u_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['u-component of wind [m/s]'], mask_extent=nldas_extent, return_nodata=False) wind_v_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['v-component of wind [m/s]'], mask_extent=nldas_extent, return_nodata=False) output_array = np.sqrt( wind_u_array ** 2 + wind_v_array ** 2) # Read all other variables directly else: output_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict[nldas_band_dict[input_var]], mask_extent=nldas_extent, return_nodata=False) # Save the projected array as 32-bit floats gdc.array_to_comp_raster( output_array.astype(np.float32), output_path, band=band_num) # gdc.block_to_raster( # ea_array.astype(np.float32), output_path, band=band) # gdc.array_to_raster( # output_array.astype(np.float32), output_path, # output_geo=nldas_geo, output_proj=nldas_proj, # stats_flag=stats_flag) del output_array input_ds = None if stats_flag: gdc.raster_statistics(output_path) logging.debug('\nScript Complete')
def pixel_rating(image_ws, ini_path, stats_flag=False, overwrite_flag=None): """Calculate pixel rating Args: image_ws (str): Image folder path ini_path (str): Pixel regions config file path stats_flag (bool): if True, compute raster statistics ovewrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('Generating suggested hot/cold pixel regions') log_fmt = ' {:<18s} {}' env = gdc.env image = et_image.Image(image_ws, env) np.seterr(invalid='ignore') # # Check that image_ws is valid # image_re = re.compile( # '^(LT04|LT05|LE07|LC08)_(\d{3})(\d{3})_(\d{4})(\d{2})(\d{2})') # if not os.path.isdir(image_ws) or not image_re.match(scene_id): # logging.error('\nERROR: Image folder is invalid or does not exist\n') # return False # Folder Paths region_ws = os.path.join(image_ws, 'PIXEL_REGIONS') # Open config file config = open_ini(ini_path) # Get input parameters logging.debug(' Reading Input File') # Arrays are processed by block bs = read_param('block_size', 1024, config) logging.info(' {:<18s} {}'.format('Block Size:', bs)) # Raster pyramids/statistics pyramids_flag = read_param('pyramids_flag', False, config) if pyramids_flag: gdal.SetConfigOption('HFA_USE_RRD', 'YES') if stats_flag is None: stats_flag = read_param('statistics_flag', False, config) # Overwrite if overwrite_flag is None: overwrite_flag = read_param('overwrite_flag', True, config) # Check that common_area raster exists if not os.path.isfile(image.common_area_raster): logging.error( '\nERROR: A common area raster was not found.' + '\nERROR: Please rerun prep tool to build these files.\n' + ' {}\n'.format(image.common_area_raster)) sys.exit() # Use common_area to set mask parameters common_ds = gdal.Open(image.common_area_raster) # env.mask_proj = raster_ds_proj(common_ds) env.mask_geo = gdc.raster_ds_geo(common_ds) env.mask_rows, env.mask_cols = gdc.raster_ds_shape(common_ds) env.mask_extent = gdc.geo_extent(env.mask_geo, env.mask_rows, env.mask_cols) env.mask_array = gdc.raster_ds_to_array(common_ds)[0] env.mask_path = image.common_area_raster env.snap_osr = gdc.raster_path_osr(image.common_area_raster) env.snap_proj = env.snap_osr.ExportToWkt() env.cellsize = gdc.raster_path_cellsize(image.common_area_raster)[0] common_ds = None logging.debug(' {:<18s} {}'.format('Mask Extent:', env.mask_extent)) # Read Pixel Regions config file # Currently there is no code to support applying an NLCD mask apply_nlcd_mask = False # apply_nlcd_mask = read_param('apply_nlcd_mask', False, config) apply_cdl_ag_mask = read_param('apply_cdl_ag_mask', False, config) apply_field_mask = read_param('apply_field_mask', False, config) apply_ndwi_mask = read_param('apply_ndwi_mask', True, config) apply_ndvi_mask = read_param('apply_ndvi_mask', True, config) # Currently the code to apply a study area mask is commented out # apply_study_area_mask = read_param( # 'apply_study_area_mask', False, config) albedo_rating_flag = read_param('albedo_rating_flag', True, config) nlcd_rating_flag = read_param('nlcd_rating_flag', True, config) ndvi_rating_flag = read_param('ndvi_rating_flag', True, config) ts_rating_flag = read_param('ts_rating_flag', True, config) ke_rating_flag = read_param('ke_rating_flag', False, config) # if apply_study_area_mask: # study_area_path = config.get('INPUTS', 'study_area_path') if apply_nlcd_mask or nlcd_rating_flag: nlcd_raster = config.get('INPUTS', 'landuse_raster') if apply_cdl_ag_mask: cdl_ag_raster = config.get('INPUTS', 'cdl_ag_raster') cdl_buffer_cells = read_param('cdl_buffer_cells', 0, config) cdl_ag_eroded_name = read_param('cdl_ag_eroded_name', 'cdl_ag_eroded_{}.img', config) if apply_field_mask: field_raster = config.get('INPUTS', 'fields_raster') cold_rating_pct = read_param('cold_percentile', 99, config) hot_rating_pct = read_param('hot_percentile', 99, config) # min_cold_rating_score = read_param('min_cold_rating_score', 0.3, config) # min_hot_rating_score = read_param('min_hot_rating_score', 0.3, config) ts_bin_count = int(read_param('ts_bin_count', 10, config)) if 100 % ts_bin_count != 0: logging.warning( 'WARNING: ts_bins_count of {} is not a divisor ' + 'of 100. Using default ts_bins_count = 4'.format(ts_bin_count)) ts_bin_count = 10 bin_size = 1. / (ts_bin_count - 1) hot_rating_values = np.arange(0., 1. + bin_size, step=bin_size) cold_rating_values = hot_rating_values[::-1] # Input raster paths r_fmt = '.img' if 'Landsat' in image.type: albedo_raster = image.albedo_sur_raster ndvi_raster = image.ndvi_toa_raster ndwi_raster = image.ndwi_toa_raster ts_raster = image.ts_raster ke_raster = image.ke_raster # Check config file input paths # if apply_study_area_mask and not os.path.isfile(study_area_path): # logging.error( # ('\nERROR: The study area shapefile {} does ' + # 'not exist\n').format(study_area_path)) # sys.exit() if ((apply_nlcd_mask or nlcd_rating_flag) and not os.path.isfile(nlcd_raster)): logging.error(('\nERROR: The NLCD raster {} does ' + 'not exist\n').format(nlcd_raster)) sys.exit() if apply_cdl_ag_mask and not os.path.isfile(cdl_ag_raster): logging.error(('\nERROR: The CDL Ag raster {} does ' + 'not exist\n').format(cdl_ag_raster)) sys.exit() if apply_field_mask and not os.path.isfile(field_raster): logging.error(('\nERROR: The field raster {} does ' + 'not exist\n').format(field_raster)) sys.exit() if (not (isinstance(cold_rating_pct, (int, float)) and (0 <= cold_rating_pct <= 100))): logging.error( '\nERROR: cold_percentile must be a value between 0 and 100\n') sys.exit() if (not (isinstance(hot_rating_pct, (int, float)) and (0 <= hot_rating_pct <= 100))): logging.error( '\nERROR: hot_percentile must be a value between 0 and 100\n') sys.exit() # Set raster names raster_dict = dict() # Output Rasters raster_dict['region_mask'] = os.path.join(region_ws, 'region_mask' + r_fmt) raster_dict['cold_rating'] = os.path.join(region_ws, 'cold_pixel_rating' + r_fmt) raster_dict['hot_rating'] = os.path.join(region_ws, 'hot_pixel_rating' + r_fmt) raster_dict['cold_sugg'] = os.path.join(region_ws, 'cold_pixel_suggestion' + r_fmt) raster_dict['hot_sugg'] = os.path.join(region_ws, 'hot_pixel_suggestion' + r_fmt) # Read pixel region raster flags save_dict = dict() save_dict['region_mask'] = read_param('save_region_mask_flag', False, config) save_dict['cold_rating'] = read_param('save_rating_rasters_flag', False, config) save_dict['hot_rating'] = read_param('save_rating_rasters_flag', False, config) save_dict['cold_sugg'] = read_param('save_suggestion_rasters_flag', True, config) save_dict['hot_sugg'] = read_param('save_suggestion_rasters_flag', True, config) # Output folder if not os.path.isdir(region_ws): os.mkdir(region_ws) # Remove existing files if necessary region_ws_file_list = [ os.path.join(region_ws, item) for item in os.listdir(region_ws) ] if overwrite_flag and region_ws_file_list: for raster_path in raster_dict.values(): if raster_path in region_ws_file_list: remove_file(raster_path) # Check scene specific input paths if apply_ndwi_mask and not os.path.isfile(ndwi_raster): logging.error( 'ERROR: NDWI raster does not exist\n {}'.format(ndwi_raster)) sys.exit() elif apply_ndvi_mask and not os.path.isfile(ndvi_raster): logging.error( 'ERROR: NDVI raster does not exist\n {}'.format(ndvi_raster)) sys.exit() elif ke_rating_flag and not os.path.isfile(ke_raster): logging.error( ('ERROR: The Ke raster does not exist\n {}').format(ke_raster)) sys.exit() # Remove existing and build new empty rasters if necessary # If processing by block, rating rasters must be built logging.debug('\nBuilding empty rasters') for name, save_flag in sorted(save_dict.items()): if save_flag and 'rating' in name: gdc.build_empty_raster(raster_dict[name], 1, np.float32) elif save_flag: gdc.build_empty_raster(raster_dict[name], 1, np.uint8, output_nodata=0) if apply_cdl_ag_mask: logging.info('Building CDL ag mask') cdl_array = gdc.raster_to_array(cdl_ag_raster, mask_extent=env.mask_extent, return_nodata=False) if cdl_buffer_cells > 0: logging.info(' Eroding CDL by {} cells'.format(cdl_buffer_cells)) structure_array = np.ones((cdl_buffer_cells, cdl_buffer_cells), dtype=np.int) # Deadbeef - This could blow up in memory on bigger rasters cdl_array = ndimage.binary_erosion( cdl_array, structure_array).astype(structure_array.dtype) cdl_ag_eroded_raster = os.path.join( image.support_ws, cdl_ag_eroded_name.format(cdl_buffer_cells)) gdc.array_to_raster(cdl_array, cdl_ag_eroded_raster, output_geo=env.mask_geo, output_proj=env.snap_proj, mask_array=env.mask_array, output_nodata=0, stats_flag=False) cdl_array = None del cdl_array # Build region mask logging.debug('Building region mask') region_mask = np.copy(env.mask_array).astype(np.bool) if apply_field_mask: field_mask, field_nodata = gdc.raster_to_array( field_raster, mask_extent=env.mask_extent, return_nodata=True) region_mask &= field_mask != field_nodata del field_mask, field_nodata if apply_ndwi_mask: ndwi_array = gdc.raster_to_array(ndwi_raster, 1, mask_extent=env.mask_extent, return_nodata=False) region_mask &= ndwi_array > 0.0 del ndwi_array if apply_ndvi_mask: ndvi_array = gdc.raster_to_array(ndvi_raster, 1, mask_extent=env.mask_extent, return_nodata=False) region_mask &= ndvi_array > 0.12 del ndvi_array if apply_cdl_ag_mask: cdl_array, cdl_nodata = gdc.raster_to_array( cdl_ag_eroded_raster, mask_extent=env.mask_extent, return_nodata=True) region_mask &= cdl_array != cdl_nodata del cdl_array, cdl_nodata if save_dict['region_mask']: gdc.array_to_raster(region_mask, raster_dict['region_mask'], stats_flag=False) # Initialize rating arrays # This needs to be done before the ts_rating if block cold_rating_array = np.ones(env.mask_array.shape, dtype=np.float32) hot_rating_array = np.ones(env.mask_array.shape, dtype=np.float32) cold_rating_array[~region_mask] = np.nan hot_rating_array[~region_mask] = np.nan # Temperature pixel rating - grab the max and min value for the entire # Ts image in a memory safe way by using gdal_common blocks # The following is a percentile based approach if ts_rating_flag: logging.debug('Computing Ts percentile rating') ts_array = gdc.raster_to_array(ts_raster, mask_extent=env.mask_extent, return_nodata=False) ts_array[~region_mask] = np.nan percentiles = range(0, (100 + ts_bin_count), int(100 / ts_bin_count)) ts_score_value = 1. / (ts_bin_count - 1) hot_rating_values = np.arange(0, (1. + ts_score_value), step=ts_score_value)[:ts_bin_count] cold_rating_values = hot_rating_values[::-1] ts_percentile_array = stats.scoreatpercentile( ts_array[np.isfinite(ts_array)], percentiles) for bins_i in range(len(ts_percentile_array))[:-1]: bool_array = ((ts_array > ts_percentile_array[bins_i]) & (ts_array <= ts_percentile_array[bins_i + 1])) cold_rating_array[bool_array] = cold_rating_values[bins_i] hot_rating_array[bool_array] = hot_rating_values[bins_i] # gdc.array_to_raster(cold_rating_array, raster_dict['cold_rating']) # gdc.array_to_raster(hot_rating_array, raster_dict['hot_rating']) # Cleanup del ts_array, ts_percentile_array del cold_rating_values, hot_rating_values del ts_score_value, percentiles # Process by block logging.info('\nProcessing by block') logging.debug(' Mask cols/rows: {}/{}'.format(env.mask_cols, env.mask_rows)) for b_i, b_j in gdc.block_gen(env.mask_rows, env.mask_cols, bs): logging.debug(' Block y: {:5d} x: {:5d}'.format(b_i, b_j)) block_data_mask = gdc.array_to_block(env.mask_array, b_i, b_j, bs).astype(np.bool) # block_nodata_mask = ~block_data_mask block_rows, block_cols = block_data_mask.shape block_geo = gdc.array_offset_geo(env.mask_geo, b_j, b_i) block_extent = gdc.geo_extent(block_geo, block_rows, block_cols) logging.debug(' Block rows: {} cols: {}'.format( block_rows, block_cols)) # logging.debug(' Block extent: {}'.format(block_extent)) # logging.debug(' Block geo: {}'.format(block_geo)) # Don't skip empty blocks since block rating needs to be written # back to the array at the end of the block loop block_region_mask = gdc.array_to_block(region_mask, b_i, b_j, bs) if not np.any(block_region_mask): logging.debug(' Empty block') block_empty_flag = True else: block_empty_flag = False # New style continuous pixel weighting cold_rating_block = gdc.array_to_block(cold_rating_array, b_i, b_j, bs) hot_rating_block = gdc.array_to_block(hot_rating_array, b_i, b_j, bs) # Rating arrays already have region_mask set # cold_rating_block = np.ones(block_region_mask.shape, dtype=np.float32) # hot_rating_block = np.ones(block_region_mask.shape, dtype=np.float32) # cold_rating_block[~block_region_mask] = np.nan # hot_rating_block[~block_region_mask] = np.nan # del block_region_mask if ndvi_rating_flag and not block_empty_flag: # NDVI based rating ndvi_array = gdc.raster_to_array(ndvi_raster, 1, mask_extent=block_extent, return_nodata=False) # Don't let NDVI be negative ndvi_array.clip(0., 0.833, out=ndvi_array) # ndvi_array.clip(0.001, 0.833, out=ndvi_array) cold_rating_block *= ndvi_array cold_rating_block *= 1.20 ndvi_mask = (ndvi_array > 0) # DEADBEEF - Can this calculation be masked to only NDVI > 0? ndvi_mask = ndvi_array > 0 hot_rating_block[ndvi_mask] *= stats.norm.pdf( np.log(ndvi_array[ndvi_mask]), math.log(0.15), 0.5) hot_rating_block[ndvi_mask] *= 1.25 del ndvi_mask # hot_rating_block *= stats.norm.pdf( # np.log(ndvi_array), math.log(0.15), 0.5) # hot_rating_block *= 1.25 # cold_rating_block.clip(0., 1., out=cold_rating_block) # hot_rating_block.clip(0., 1., out=hot_rating_block) del ndvi_array if albedo_rating_flag and not block_empty_flag: # Albdo based rating albedo_array = gdc.raster_to_array(albedo_raster, 1, mask_extent=block_extent, return_nodata=False) albedo_cold_pdf = stats.norm.pdf(albedo_array, 0.21, 0.03) albedo_hot_pdf = stats.norm.pdf(albedo_array, 0.21, 0.06) del albedo_array cold_rating_block *= albedo_cold_pdf cold_rating_block *= 0.07 hot_rating_block *= albedo_hot_pdf hot_rating_block *= 0.15 # cold_rating_block.clip(0., 1., out=cold_rating_block) # hot_rating_block.clip(0., 1., out=hot_rating_block) del albedo_cold_pdf, albedo_hot_pdf if nlcd_rating_flag and not block_empty_flag: # NLCD based weighting, this could be CDL instead? nlcd_array = nlcd_rating( gdc.raster_to_array(nlcd_raster, 1, mask_extent=block_extent, return_nodata=False)) cold_rating_block *= nlcd_array hot_rating_block *= nlcd_array del nlcd_array if ke_rating_flag and not block_empty_flag: # SWB Ke based rating ke_array = gdc.raster_to_array(ke_raster, 1, mask_extent=block_extent, return_nodata=False) # Don't let NDVI be negative ke_array.clip(0., 1., out=ke_array) # Assumption, lower Ke is better for selecting the hot pixel # As the power (2) decreases and approaches 1, # the relationship gets more linear # cold_rating_block *= (1 - ke_array ** 2) # hot_rating_block *= (1 - ke_array ** 1.5) # Linear inverse # cold_rating_block *= (1. - ke_array) hot_rating_block *= (1. - ke_array) # cold_rating_block.clip(0., 1., out=cold_rating_block) # hot_rating_block.clip(0., 1., out=hot_rating_block) del ke_array # Clearness # clearness = 1.0 # cold_rating *= clearness # hot_rating *= clearness # Reset nan values # cold_rating_block[~region_mask] = np.nan # hot_rating_block[~region_mask] = np.nan # Save rating values cold_rating_array = gdc.block_to_array(cold_rating_block, cold_rating_array, b_i, b_j, bs) hot_rating_array = gdc.block_to_array(hot_rating_block, hot_rating_array, b_i, b_j, bs) # Save rating rasters if save_dict['cold_rating']: gdc.block_to_raster(cold_rating_block, raster_dict['cold_rating'], b_i, b_j, bs) if save_dict['hot_rating']: gdc.block_to_raster(hot_rating_block, raster_dict['hot_rating'], b_i, b_j, bs) # Save rating values cold_rating_array = gdc.block_to_array(cold_rating_block, cold_rating_array, b_i, b_j, bs) hot_rating_array = gdc.block_to_array(hot_rating_block, hot_rating_array, b_i, b_j, bs) del cold_rating_block, hot_rating_block # Select pixels above target percentile # Only build suggestion arrays if saving logging.debug('Building suggested pixel rasters') if save_dict['cold_sugg']: cold_rating_score = float( stats.scoreatpercentile( cold_rating_array[np.isfinite(cold_rating_array)], cold_rating_pct)) # cold_rating_array, cold_rating_nodata = gdc.raster_to_array( # raster_dict['cold_rating'], 1, mask_extent=env.mask_extent) # if cold_rating_score < float(min_cold_rating_score): # logging.error(('ERROR: The cold_rating_score ({}) is less ' + # 'than the min_cold_rating_score ({})').format( # cold_rating_score, min_cold_rating_score)) # sys.exit() cold_sugg_mask = cold_rating_array >= cold_rating_score gdc.array_to_raster(cold_sugg_mask, raster_dict['cold_sugg'], stats_flag=stats_flag) logging.debug(' Cold Percentile: {}'.format(cold_rating_pct)) logging.debug(' Cold Score: {:.6f}'.format(cold_rating_score)) logging.debug(' Cold Pixels: {}'.format(np.sum(cold_sugg_mask))) del cold_sugg_mask, cold_rating_array if save_dict['hot_sugg']: hot_rating_score = float( stats.scoreatpercentile( hot_rating_array[np.isfinite(hot_rating_array)], hot_rating_pct)) # hot_rating_array, hot_rating_nodata = gdc.raster_to_array( # raster_dict['hot_rating'], 1, mask_extent=env.mask_extent) # if hot_rating_score < float(min_hot_rating_score): # logging.error(('ERROR: The hot_rating_array ({}) is less ' + # 'than the min_hot_rating_score ({})').format( # hot_rating_array, min_hot_rating_score)) # sys.exit() hot_sugg_mask = hot_rating_array >= hot_rating_score gdc.array_to_raster(hot_sugg_mask, raster_dict['hot_sugg'], stats_flag=stats_flag) logging.debug(' Hot Percentile: {}'.format(hot_rating_pct)) logging.debug(' Hot Score: {:.6f}'.format(hot_rating_score)) logging.debug(' Hot Pixels: {}'.format(np.sum(hot_sugg_mask))) del hot_sugg_mask, hot_rating_array # Raster Statistics if stats_flag: logging.info('Calculating Statistics') for name, save_flag in save_dict.items(): if save_flag: gdc.raster_statistics(raster_dict[name]) # Raster Pyramids if pyramids_flag: logging.info('Building Pyramids') for name, save_flag in save_dict.items(): if save_flag: gdc.raster_pyramids(raster_dict[name])
def main(cimis_ws=os.getcwd(), gridmet_ws=None, ancillary_ws=os.getcwd(), etr_flag=False, eto_flag=False, start_date=None, end_date=None, stats_flag=True, overwrite_flag=False): """Fill missing CIMIS days with projected data from GRIDMET Currently missing (CGM 2014-08-15) 2010-11-16 -> 2010-11-23 Args: cimis_ws (str): root folder path of CIMIS data gridmet_ws (str): root folder path of GRIDMET data ancillary_ws (str): folder of ancillary rasters etr_flag (bool): if True, compute alfalfa reference ET (ETr) eto_flag (bool): if True, compute grass reference ET (ETo) start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nFilling CIMIS with GRIDMET') cimis_re = re.compile( '(?P<VAR>etr)_(?P<YYYY>\d{4})_daily_(?P<GRID>\w+).img$') # gridmet_re = re.compile( # '(?P<VAR>ppt)_(?P<YYY>\d{4})_daily_(?P<GRID>\w+).img$') gridmet_fmt = 'etr_{}_daily_gridmet.img' # Compute ETr and/or ETo if not etr_flag and not eto_flag: logging.info(' ETo/ETr flag(s) not set, defaulting to ETr') etr_flag = True logging.debug(' CIMIS: {}'.format(cimis_ws)) logging.debug(' GRIDMET: {}'.format(gridmet_ws)) # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Get GRIDMET spatial reference and cellsize from elevation raster # gridmet_elev_raster = os.path.join(ancillary_ws, 'gridmet_elev.img') # Get CIMIS spatial reference and cellsize from mask raster cimis_mask_raster = os.path.join(ancillary_ws, 'cimis_mask.img') # Resample type # 0 = GRA_NearestNeighbour, Nearest neighbour (select on one input pixel) # 1 = GRA_Bilinear,Bilinear (2x2 kernel) # 2 = GRA_Cubic, Cubic Convolution Approximation (4x4 kernel) # 3 = GRA_CubicSpline, Cubic B-Spline Approximation (4x4 kernel) # 4 = GRA_Lanczos, Lanczos windowed sinc interpolation (6x6 kernel) # 5 = GRA_Average, Average (computes the average of all non-NODATA contributing pixels) # 6 = GRA_Mode, Mode (selects the value which appears most often of all the sampled points) resample_type = gdal.GRA_Bilinear # ETo/ETr workspaces cimis_eto_ws = os.path.join(cimis_ws, 'eto') cimis_etr_ws = os.path.join(cimis_ws, 'etr') gridmet_eto_ws = os.path.join(gridmet_ws, 'eto') gridmet_etr_ws = os.path.join(gridmet_ws, 'etr') # This allows GDAL to throw Python Exceptions # gdal.UseExceptions() # mem_driver = gdal.GetDriverByName('MEM') # Get CIMIS grid properties from mask logging.info('\nCIMIS Properties') cimis_mask_ds = gdal.Open(cimis_mask_raster) cimis_osr = gdc.raster_ds_osr(cimis_mask_ds) cimis_proj = gdc.osr_proj(cimis_osr) cimis_cs = gdc.raster_ds_cellsize(cimis_mask_ds, x_only=True) cimis_extent = gdc.raster_ds_extent(cimis_mask_ds) cimis_geo = cimis_extent.geo(cimis_cs) cimis_mask_ds = None logging.debug(' Projection: {}'.format(cimis_proj)) logging.debug(' Cellsize: {}'.format(cimis_cs)) logging.debug(' Geo: {}'.format(cimis_geo)) logging.debug(' Extent: {}'.format(cimis_extent)) # Read the CIMIS mask array if present cimis_mask, cimis_mask_nodata = gdc.raster_to_array(cimis_mask_raster) cimis_mask = cimis_mask != cimis_mask_nodata # # Get extent/geo from elevation raster # logging.info('\nGRIDMET Properties') # gridmet_ds = gdal.Open(gridmet_elev_raster) # gridmet_osr = gdc.raster_ds_osr(gridmet_ds) # gridmet_proj = gdc.osr_proj(gridmet_osr) # gridmet_cs = gdc.raster_ds_cellsize(gridmet_ds, x_only=True) # gridmet_full_extent = gdc.raster_ds_extent(gridmet_ds) # gridmet_full_geo = gridmet_full_extent.geo(gridmet_cs) # gridmet_x, gridmet_y = gridmet_full_extent.origin() # gridmet_ds = None # logging.debug(' Projection: {}'.format(gridmet_proj)) # logging.debug(' Cellsize: {}'.format(gridmet_cs)) # logging.debug(' Geo: {}'.format(gridmet_full_geo)) # logging.debug(' Extent: {}'.format(gridmet_full_extent)) # # Project CIMIS extent to the GRIDMET spatial reference # logging.info('\nGet CIMIS extent in GRIDMET spat. ref.') # gridmet_sub_extent = gdc.project_extent( # cimis_extent, cimis_osr, gridmet_osr, cimis_cs) # gridmet_sub_extent.buffer_extent(4 * gridmet_cs) # gridmet_sub_extent.adjust_to_snap( # 'EXPAND', gridmet_x, gridmet_y, gridmet_cs) # gridmet_sub_geo = gridmet_sub_extent.geo(gridmet_cs) # logging.debug(' Geo: {}'.format(gridmet_sub_geo)) # logging.debug(' Extent: {}'.format(gridmet_sub_extent)) # Process Missing ETo if eto_flag: logging.info('\nETo') for cimis_name in sorted(os.listdir(cimis_eto_ws)): logging.debug("\n{}".format(cimis_name)) cimis_match = cimis_re.match(cimis_name) if not cimis_match: logging.debug(' Regular expression didn\'t match, skipping') continue year = int(cimis_match.group('YYYY')) logging.info(" {}".format(str(year))) if start_dt is not None and year < start_dt.year: logging.debug(' Before start date, skipping') continue elif end_dt is not None and year > end_dt.year: logging.debug(' After end date, skipping') continue cimis_path = os.path.join(cimis_eto_ws, cimis_name) gridmet_path = os.path.join(gridmet_eto_ws, gridmet_fmt.format(str(year))) if not os.path.isfile(gridmet_path): logging.debug(' GRIDMET raster does not exist, skipping') continue if not os.path.isfile(cimis_path): logging.error(' CIMIS raster does not exist, skipping') continue # Check all valid dates in the year year_dates = date_range(dt.datetime(year, 1, 1), dt.datetime(year + 1, 1, 1)) for date_dt in year_dates: if start_dt is not None and date_dt < start_dt: continue elif end_dt is not None and date_dt > end_dt: continue doy = int(date_dt.strftime('%j')) # Look for arrays that don't have data eto_array = gdc.raster_to_array(cimis_path, band=doy, return_nodata=False) if np.any(np.isfinite(eto_array)): logging.debug(' {} - no missing data, skipping'.format( date_dt.strftime('%Y-%m-%d'))) continue else: logging.info(' {}'.format(date_dt.strftime('%Y-%m-%d'))) # # This is much faster but doesn't apply the CIMIS mask # # Create an in memory dataset of the full ETo array # eto_full_rows, eto_full_cols = eto_full_array[:,:,doy_i].shape # eto_full_type, eto_full_nodata = numpy_to_gdal_type(np.float32) # eto_full_ds = mem_driver.Create( # '', eto_full_cols, eto_full_rows, 1, eto_full_type) # eto_full_ds.SetProjection(gridmet_proj) # eto_full_ds.SetGeoTransform(gridmet_full_geo) # eto_full_band = eto_full_ds.GetRasterBand(1) # # eto_full_band.Fill(eto_full_nodata) # eto_full_band.SetNoDataValue(eto_full_nodata) # eto_full_band.WriteArray(eto_full_array[:,:,doy_i], 0, 0) # # # Extract the subset # eto_sub_array, eto_sub_nodata = gdc.raster_ds_to_array( # eto_full_ds, 1, gridmet_sub_extent) # eto_sub_rows, eto_sub_cols = eto_sub_array.shape # eto_full_ds = None # # # Create projected raster # eto_sub_ds = mem_driver.Create( # '', eto_sub_cols, eto_sub_rows, 1, eto_full_type) # eto_sub_ds.SetProjection(gridmet_proj) # eto_sub_ds.SetGeoTransform(gridmet_sub_geo) # eto_sub_band = eto_sub_ds.GetRasterBand(1) # eto_sub_band.Fill(eto_sub_nodata) # eto_sub_band.SetNoDataValue(eto_sub_nodata) # eto_sub_band.WriteArray(eto_sub_array, 0, 0) # eto_sub_ds.FlushCache() # # # Project input DEM to CIMIS spat. ref. # gdc.project_raster_ds( # eto_sub_ds, gridmet_path, resample_type, # env.snap_proj, env.cellsize, cimis_extent) # eto_sub_ds = None # Extract the subset gridmet_ds = gdal.Open(gridmet_path) gridmet_extent = gdc.raster_ds_extent(gridmet_ds) gridmet_cs = gdc.raster_ds_cellsize(gridmet_ds, x_only=True) gridmet_osr = gdc.raster_ds_osr(gridmet_ds) eto_full_array = gdc.raster_ds_to_array(gridmet_ds, band=doy, return_nodata=False) gridmet_ds = None # Get the projected subset of the full ETo array # This is slower than projecting the subset above eto_sub_array = gdc.project_array(eto_full_array, resample_type, gridmet_osr, gridmet_cs, gridmet_extent, cimis_osr, cimis_cs, cimis_extent) # Save the projected array gdc.array_to_comp_raster(eto_sub_array, cimis_path, band=doy, stats_flag=False) # gdc.array_to_raster( # eto_sub_array, output_path, output_geo=cimis_geo, # output_proj=cimis_proj, stats_flag=False) # gdc.array_to_raster( # eto_sub_array, output_path, # output_geo=cimis_geo, output_proj=cimis_proj, # mask_array=cimis_mask, stats_flag=False) del eto_sub_array, eto_full_array if stats_flag: gdc.raster_statistics(cimis_path) # Process Missing ETr if etr_flag: logging.info('\nETr') for cimis_name in sorted(os.listdir(cimis_etr_ws)): cimis_match = cimis_re.match(cimis_name) if not cimis_match: continue year = int(cimis_match.group('YYYY')) if start_dt is not None and year < start_dt.year: continue elif end_dt is not None and year > end_dt.year: continue logging.info("{}".format(str(year))) cimis_path = os.path.join(cimis_etr_ws, cimis_name) gridmet_path = os.path.join(gridmet_etr_ws, gridmet_fmt.format(str(year))) if not os.path.isfile(gridmet_path): continue if not os.path.isfile(cimis_path): logging.error(' CIMIS raster does not exist') continue # Check all valid dates in the year year_dates = date_range(dt.datetime(year, 1, 1), dt.datetime(year + 1, 1, 1)) for date_dt in year_dates: if start_dt is not None and date_dt < start_dt: continue elif end_dt is not None and date_dt > end_dt: continue doy = int(date_dt.strftime('%j')) # Look for arrays that don't have data etr_array = gdc.raster_to_array(cimis_path, band=doy, return_nodata=False) if np.any(np.isfinite(etr_array)): logging.debug(' {} - skipping'.format( date_dt.strftime('%Y-%m-%d'))) continue else: logging.info(' {}'.format(date_dt.strftime('%Y-%m-%d'))) # Extract the subset gridmet_ds = gdal.Open(gridmet_path) gridmet_extent = gdc.raster_ds_extent(gridmet_ds) gridmet_cs = gdc.raster_ds_cellsize(gridmet_ds, x_only=True) gridmet_osr = gdc.raster_ds_osr(gridmet_ds) etr_full_array = gdc.raster_ds_to_array(gridmet_ds, band=doy, return_nodata=False) gridmet_ds = None # Get the projected subset of the full ETr array # This is slower than projecting the subset etr_sub_array = gdc.project_array(etr_full_array, resample_type, gridmet_osr, gridmet_cs, gridmet_extent, cimis_osr, cimis_cs, cimis_extent) # # Save the projected array gdc.array_to_comp_raster(etr_sub_array, cimis_path, band=doy, stats_flag=False) # gdc.array_to_raster( # etr_sub_array, output_path, # output_geo=cimis_geo, output_proj=cimis_proj, # mask_array=cimis_mask, stats_flag=False) del etr_sub_array, etr_full_array if stats_flag: gdc.raster_statistics(cimis_path) logging.debug('\nScript Complete')
def main(netcdf_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), start_date=None, end_date=None, extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False): """Extract GRIDMET temperature Args: netcdf_ws (str): folder of GRIDMET netcdf files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) extent_path (str): filepath a raster defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nExtracting GRIDMET vapor pressure') # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Save GRIDMET lat, lon, and elevation arrays elev_raster = os.path.join(ancillary_ws, 'gridmet_elev.img') output_fmt = '{}_{}_daily_gridmet.img' gridmet_re = re.compile('(?P<VAR>\w+)_(?P<YEAR>\d{4}).nc$') # GRIDMET band name dictionary gridmet_band_dict = dict() gridmet_band_dict['pr'] = 'precipitation_amount' gridmet_band_dict['srad'] = 'surface_downwelling_shortwave_flux_in_air' gridmet_band_dict['sph'] = 'specific_humidity' gridmet_band_dict['tmmn'] = 'air_temperature' gridmet_band_dict['tmmx'] = 'air_temperature' gridmet_band_dict['vs'] = 'wind_speed' # Get extent/geo from elevation raster gridmet_ds = gdal.Open(elev_raster) gridmet_osr = gdc.raster_ds_osr(gridmet_ds) gridmet_proj = gdc.osr_proj(gridmet_osr) gridmet_cs = gdc.raster_ds_cellsize(gridmet_ds, x_only=True) gridmet_extent = gdc.raster_ds_extent(gridmet_ds) gridmet_full_geo = gridmet_extent.geo(gridmet_cs) gridmet_x, gridmet_y = gridmet_extent.origin() gridmet_ds = None logging.debug(' Projection: {}'.format(gridmet_proj)) logging.debug(' Cellsize: {}'.format(gridmet_cs)) logging.debug(' Geo: {}'.format(gridmet_full_geo)) logging.debug(' Extent: {}'.format(gridmet_extent)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) gridmet_extent = gdc.Extent(output_extent) gridmet_extent.adjust_to_snap('EXPAND', gridmet_x, gridmet_y, gridmet_cs) gridmet_geo = gridmet_extent.geo(gridmet_cs) logging.debug(' Geo: {}'.format(gridmet_geo)) logging.debug(' Extent: {}'.format(gridmet_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') gridmet_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) gridmet_extent = gdc.project_extent(gridmet_extent, extent_osr, gridmet_osr, extent_cs) gridmet_extent.adjust_to_snap('EXPAND', gridmet_x, gridmet_y, gridmet_cs) gridmet_geo = gridmet_extent.geo(gridmet_cs) logging.debug(' Geo: {}'.format(gridmet_geo)) logging.debug(' Extent: {}'.format(gridmet_extent)) else: gridmet_geo = gridmet_full_geo # Get indices for slicing/clipping input arrays g_i, g_j = gdc.array_geo_offsets(gridmet_full_geo, gridmet_geo, cs=gridmet_cs) g_rows, g_cols = gridmet_extent.shape(cs=gridmet_cs) # Read the elevation array elev_array = gdc.raster_to_array(elev_raster, mask_extent=gridmet_extent, return_nodata=False) pair_array = et_common.air_pressure_func(elev_array) del elev_array # Process each variable input_var = 'sph' output_var = 'ea' logging.info("\nVariable: {}".format(input_var)) # Build output folder var_ws = os.path.join(output_ws, output_var) if not os.path.isdir(var_ws): os.makedirs(var_ws) # Process each file in the input workspace for input_name in sorted(os.listdir(netcdf_ws)): input_match = gridmet_re.match(input_name) if not input_match: logging.debug("{}".format(input_name)) logging.debug(' Regular expression didn\'t match, skipping') continue elif input_match.group('VAR') != input_var: logging.debug("{}".format(input_name)) logging.debug(' Variable didn\'t match, skipping') continue else: logging.info("{}".format(input_name)) year_str = input_match.group('YEAR') logging.info(" {}".format(year_str)) year_int = int(year_str) year_days = int(dt.datetime(year_int, 12, 31).strftime('%j')) if start_dt is not None and year_int < start_dt.year: logging.debug(' Before start date, skipping') continue elif end_dt is not None and year_int > end_dt.year: logging.debug(' After end date, skipping') continue # Build input file path input_raster = os.path.join(netcdf_ws, input_name) # if not os.path.isfile(input_raster): # logging.debug( # ' Input NetCDF doesn\'t exist, skipping {}'.format( # input_raster)) # continue # Create a single raster for each year with 365 bands # Each day will be stored in a separate band output_path = os.path.join(var_ws, output_fmt.format(output_var, year_str)) logging.debug(' {}'.format(output_path)) if os.path.isfile(output_path): if not overwrite_flag: logging.debug(' File already exists, skipping') continue else: logging.debug(' File already exists, removing existing') os.remove(output_path) gdc.build_empty_raster(output_path, band_cnt=366, output_dtype=np.float32, output_proj=gridmet_proj, output_cs=gridmet_cs, output_extent=gridmet_extent, output_fill_flag=True) # Read in the GRIDMET NetCDF file # Immediatly clip input array to save memory input_nc_f = netCDF4.Dataset(input_raster, 'r') input_nc = input_nc_f.variables[ gridmet_band_dict[input_var]][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() input_nc = np.transpose(input_nc, (0, 2, 1)) # A numpy array is returned when slicing a masked array # if there are no masked pixels # This is a hack to force the numpy array back to a masked array if type(input_nc) != np.ma.core.MaskedArray: input_nc = np.ma.core.MaskedArray( input_nc, np.zeros(input_nc.shape, dtype=bool)) # Check all valid dates in the year year_dates = date_range(dt.datetime(year_int, 1, 1), dt.datetime(year_int + 1, 1, 1)) for date_dt in year_dates: if start_dt is not None and date_dt < start_dt: # logging.debug(' before start date, skipping') continue elif end_dt is not None and date_dt > end_dt: # logging.debug(' after end date, skipping') continue logging.info(' {}'.format(date_dt.strftime('%Y_%m_%d'))) doy = int(date_dt.strftime('%j')) doy_i = range(1, year_days + 1).index(doy) # Arrays are being read as masked array with a fill value of -9999 # Convert to basic numpy array arrays with nan values try: input_full_ma = input_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue input_full_array = input_full_ma.data.astype(np.float32) input_full_nodata = float(input_full_ma.fill_value) input_full_array[input_full_array == input_full_nodata] = np.nan # Since inputs are netcdf, need to create GDAL raster # datasets in order to use gdal_common functions # Create an in memory dataset of the full ETo array input_full_ds = gdc.array_to_mem_ds(input_full_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) # Then extract the subset from the in memory dataset sph_array = gdc.raster_ds_to_array(input_full_ds, 1, mask_extent=gridmet_extent, return_nodata=False) # Compute ea [kPa] from specific humidity [kg/kg] ea_array = (sph_array * pair_array) / (0.622 + 0.378 * sph_array) # Save the projected array as 32-bit floats gdc.array_to_comp_raster(ea_array.astype(np.float32), output_path, band=doy, stats_flag=False) # gdc.array_to_raster( # ea_array.astype(np.float32), output_path, # output_geo=gridmet_geo, output_proj=gridmet_proj, # stats_flag=False) del sph_array, ea_array input_nc_f.close() del input_nc_f if stats_flag: gdc.raster_statistics(output_path) logging.debug('\nScript Complete')
def main(ancillary_ws=os.getcwd(), overwrite_flag=False): """Process CIMIS ancillary data Args: ancillary_ws (str): folder of ancillary rasters overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nProcess CIMIS ancillary data') # Site URL site_url = 'http://spatialcimis.water.ca.gov/cimis' # DEM for air pressure calculation # http://topotools.cr.usgs.gov/gmted_viewer/gmted2010_global_grids.php elev_full_url = 'http://edcintl.cr.usgs.gov/downloads/sciweb1/shared/topo/downloads/GMTED/Grid_ZipFiles/mn30_grd.zip' elev_full_zip = os.path.join(ancillary_ws, 'mn30_grd.zip') elev_full_raster = os.path.join(ancillary_ws, 'mn30_grd') # Get CIMIS grid properties from 2010/01/01 ETo raster # Grid of the spatial cimis input rasters # cimis_extent = gdc.Extent((-400000, -650000, 600000, 454000)) # cimis_cs = 2000 # cimis_geo = gdc.extent_geo(cimis_extent, cimis_cs) # Spatial reference parameters cimis_proj4 = ( "+proj=aea +lat_1=34 +lat_2=40.5 +lat_0=0 +lon_0=-120 +x_0=0 " + "+y_0=-4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs") cimis_osr = gdc.proj4_osr(cimis_proj4) # cimis_epsg = 3310 # NAD_1983_California_Teale_Albers # cimis_osr = gdc.epsg_osr(cimis_epsg) cimis_osr.MorphToESRI() cimis_proj = cimis_osr.ExportToWkt() # snap_xmin, snap_ymin = (0, 0) # Build output workspace if it doesn't exist if not os.path.isdir(ancillary_ws): os.makedirs(ancillary_ws) # File paths mask_url = site_url + '/2010/01/01/ETo.asc.gz' mask_gz = os.path.join(ancillary_ws, 'cimis_mask.asc.gz') mask_ascii = os.path.join(ancillary_ws, 'cimis_mask.asc') mask_raster = os.path.join(ancillary_ws, 'cimis_mask.img') elev_raster = os.path.join(ancillary_ws, 'cimis_elev.img') lat_raster = os.path.join(ancillary_ws, 'cimis_lat.img') lon_raster = os.path.join(ancillary_ws, 'cimis_lon.img') # Download an ETo ASCII raster to generate the mask raster if overwrite_flag or not os.path.isfile(mask_raster): logging.info('\nCIMIS mask') logging.debug(' Downloading') logging.debug(" {}".format(mask_url)) logging.debug(" {}".format(mask_gz)) url_download(mask_url, mask_gz) # try: # # This actually downloads the data # # urllib.urlretrieve(mask_url, mask_gz) # # This will work also, I don't know which is better # # f = open(mask_gz,'wb') # # f.write(urllib2.urlopen(mask_gz_url).read()) # # f.close() # except: # logging.error(" ERROR: {}\n FILE: {}".format( # sys.exc_info()[0], mask_gz)) # # Try to remove the file since it may not have completely downloaded # os.remove(mask_gz) # Uncompress '.gz' file to a new file logging.debug(' Uncompressing') logging.debug(' {}'.format(mask_ascii)) try: input_f = gzip.open(mask_gz, 'rb') output_f = open(mask_ascii, 'wb') output_f.write(input_f.read()) output_f.close() input_f.close() del input_f, output_f except: logging.error(" ERROR EXTRACTING FILE") os.remove(mask_gz) # # Set spatial reference of the ASCII files # if build_prj_flag: # prj_file = open(mask_asc.replace('.asc','.prj'), 'w') # prj_file.write(output_proj) # prj_file.close() # Convert the ASCII raster to a IMG raster logging.debug(' Computing mask') logging.debug(' {}'.format(mask_raster)) mask_array = gdc.raster_to_array(mask_ascii, return_nodata=False) cimis_geo = gdc.raster_path_geo(mask_ascii) cimis_extent = gdc.raster_path_extent(mask_ascii) logging.debug(' {}'.format(cimis_geo)) mask_array = np.isfinite(mask_array).astype(np.uint8) gdc.array_to_raster( mask_array, mask_raster, output_geo=cimis_geo, output_proj=cimis_proj, output_nodata=0) # gdc.ascii_to_raster( # mask_ascii, mask_raster, np.float32, cimis_proj) os.remove(mask_ascii) # Compute latitude/longitude rasters if ((overwrite_flag or not os.path.isfile(lat_raster) or not os.path.isfile(lat_raster)) and os.path.isfile(mask_raster)): logging.info('\nCIMIS latitude/longitude') logging.debug(' {}'.format(lat_raster)) lat_array, lon_array = gdc.raster_lat_lon_func(mask_raster) gdc.array_to_raster( lat_array, lat_raster, output_geo=cimis_geo, output_proj=cimis_proj) logging.debug(' {}'.format(lon_raster)) gdc.array_to_raster( lon_array, lon_raster, output_geo=cimis_geo, output_proj=cimis_proj) # Compute DEM raster if overwrite_flag or not os.path.isfile(elev_raster): logging.info('\nCIMIS DEM') logging.debug(' Downloading GMTED2010 DEM') logging.debug(" {}".format(elev_full_url)) logging.debug(" {}".format(elev_full_zip)) if overwrite_flag or not os.path.isfile(elev_full_zip): url_download(elev_full_url, elev_full_zip) # try: # # This actually downloads the data # urllib.urlretrieve(elev_full_url, elev_full_zip) # # This will work also, I don't know which is better # # f = open(mask_gz,'wb') # # f.write(urllib2.urlopen(mask_gz_url).read()) # # f.close() # except: # logging.error(" ERROR: {}\n FILE: {}".format( # sys.exc_info()[0], elev_full_zip)) # # Try to remove the file since it may not have completely downloaded # os.remove(elev_full_zip) # Uncompress '.gz' file to a new file logging.debug(' Uncompressing') logging.debug(' {}'.format(elev_full_raster)) if overwrite_flag or not os.path.isfile(elev_full_raster): try: with zipfile.ZipFile(elev_full_zip, "r") as z: z.extractall(ancillary_ws) except: logging.error(" ERROR EXTRACTING FILE") os.remove(elev_full_zip) # Get the extent and cellsize from the mask logging.debug(' Projecting to CIMIS grid') cimis_cs = gdc.raster_path_cellsize(mask_raster)[0] cimis_extent = gdc.raster_path_extent(mask_raster) logging.debug(' Extent: {}'.format(cimis_extent)) logging.debug(' Cellsize: {}'.format(cimis_cs)) logging.info(' {}'.format(mask_ascii)) if overwrite_flag and os.path.isfile(elev_raster): subprocess.call(['gdalmanage', 'delete', elev_raster]) if not os.path.isfile(elev_raster): subprocess.call( ['gdalwarp', '-r', 'average', '-t_srs', cimis_proj4, '-te', str(cimis_extent.xmin), str(cimis_extent.ymin), str(cimis_extent.xmax), str(cimis_extent.ymax), '-tr', str(cimis_cs), str(cimis_cs), '-of', 'HFA', '-co', 'COMPRESSED=TRUE', elev_full_raster, elev_raster], cwd=ancillary_ws) logging.debug('\nScript Complete')
def main(grb_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), etr_flag=False, eto_flag=False, landsat_ws=None, start_date=None, end_date=None, times_str='', extent_path=None, output_extent=None, daily_flag=True, stats_flag=True, overwrite_flag=False): """Compute hourly ETr/ETo from NLDAS data Args: grb_ws (str): folder of NLDAS GRB files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters etr_flag (bool): if True, compute alfalfa reference ET (ETr) eto_flag (bool): if True, compute grass reference ET (ETo) landsat_ws (str): folder of Landsat scenes or tar.gz files start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) times (str): comma separated values and/or ranges of UTC hours (i.e. "1, 2, 5-8") Parsed with python_common.parse_int_set() extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent daily_flag (bool): if True, save daily ETr/ETo sum raster. Default is True stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nComputing NLDAS hourly ETr/ETo') np.seterr(invalid='ignore') # Compute ETr and/or ETo if not etr_flag and not eto_flag: logging.info(' ETo/ETr flag(s) not set, defaulting to ETr') etr_flag = True # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Only process a specific hours if not times_str: time_list = range(0, 24, 1) else: time_list = list(parse_int_set(times_str)) time_list = ['{:02d}00'.format(t) for t in time_list] etr_folder = 'etr' eto_folder = 'eto' hour_fmt = '{}_{:04d}{:02d}{:02d}_hourly_nldas.img' # hour_fmt = '{}_{:04d}{:02d}{:02d}_{4:04d}_nldas.img' day_fmt = '{}_{:04d}{:02d}{:02d}_nldas.img' # input_fmt = 'NLDAS_FORA0125_H.A{:04d}{:02d}{:02d}.{}.002.grb' input_re = re.compile('NLDAS_FORA0125_H.A(?P<YEAR>\d{4})(?P<MONTH>\d{2})' + '(?P<DAY>\d{2}).(?P<TIME>\d{4}).002.grb$') # Assume NLDAS is NAD83 # input_epsg = 'EPSG:4269' # Ancillary raster paths mask_path = os.path.join(ancillary_ws, 'nldas_mask.img') elev_path = os.path.join(ancillary_ws, 'nldas_elev.img') lat_path = os.path.join(ancillary_ws, 'nldas_lat.img') lon_path = os.path.join(ancillary_ws, 'nldas_lon.img') # Build a date list from landsat_ws scene folders or tar.gz files date_list = [] if landsat_ws is not None and os.path.isdir(landsat_ws): logging.info('\nReading dates from Landsat IDs') logging.info(' {}'.format(landsat_ws)) landsat_re = re.compile( '^(?:LT04|LT05|LE07|LC08)_(?:\d{3})(?:\d{3})_' + '(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})') for root, dirs, files in os.walk(landsat_ws, topdown=True): # If root matches, don't explore subfolders try: landsat_match = landsat_re.match(os.path.basename(root)) date_list.append( dt.datetime.strptime('_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) dirs[:] = [] except: pass for file in files: try: landsat_match = landsat_re.match(file) date_list.append( dt.datetime.strptime('_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) except: pass date_list = sorted(list(set(date_list))) # elif landsat_ws is not None and os.path.isfile(landsat_ws): # with open(landsat_ws) as landsat_f: # This allows GDAL to throw Python Exceptions # gdal.UseExceptions() # mem_driver = gdal.GetDriverByName('MEM') # Get the NLDAS spatial reference from the mask raster nldas_ds = gdal.Open(mask_path) nldas_osr = gdc.raster_ds_osr(nldas_ds) nldas_proj = gdc.osr_proj(nldas_osr) nldas_cs = gdc.raster_ds_cellsize(nldas_ds, x_only=True) nldas_extent = gdc.raster_ds_extent(nldas_ds) nldas_geo = nldas_extent.geo(nldas_cs) nldas_x, nldas_y = nldas_extent.origin() nldas_ds = None logging.debug(' Projection: {}'.format(nldas_proj)) logging.debug(' Cellsize: {}'.format(nldas_cs)) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) nldas_extent = gdc.Extent(output_extent) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): nldas_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: nldas_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) nldas_extent = gdc.project_extent(nldas_extent, extent_osr, nldas_osr, extent_cs) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) logging.debug('') # Read the NLDAS mask array if present if mask_path and os.path.isfile(mask_path): mask_array, mask_nodata = gdc.raster_to_array(mask_path, mask_extent=nldas_extent, fill_value=0, return_nodata=True) mask_array = mask_array != mask_nodata else: mask_array = None # Read ancillary arrays (or subsets?) elev_array = gdc.raster_to_array(elev_path, mask_extent=nldas_extent, return_nodata=False) # pair_array = et_common.air_pressure_func(elev_array) lat_array = gdc.raster_to_array(lat_path, mask_extent=nldas_extent, return_nodata=False) lon_array = gdc.raster_to_array(lon_path, mask_extent=nldas_extent, return_nodata=False) # Hourly RefET functions expects lat/lon in radians lat_array *= (math.pi / 180) lon_array *= (math.pi / 180) # Build output folder etr_ws = os.path.join(output_ws, etr_folder) eto_ws = os.path.join(output_ws, eto_folder) if etr_flag and not os.path.isdir(etr_ws): os.makedirs(etr_ws) if eto_flag and not os.path.isdir(eto_ws): os.makedirs(eto_ws) # DEADBEEF - Instead of processing all available files, the following # code will process files for target dates # for input_dt in date_range(start_dt, end_dt + dt.timedelta(1)): # logging.info(input_dt.date()) # Iterate all available files and check dates if necessary # Each sub folder in the main folder has all imagery for 1 day # (in UTC time) # The path for each subfolder is the /YYYY/DOY errors = defaultdict(list) for root, folders, files in os.walk(grb_ws): root_split = os.path.normpath(root).split(os.sep) # If the year/doy is outside the range, skip if (re.match('\d{4}', root_split[-2]) and re.match('\d{3}', root_split[-1])): root_dt = dt.datetime.strptime( '{}_{}'.format(root_split[-2], root_split[-1]), '%Y_%j') logging.info('{}'.format(root_dt.date())) if ((start_dt is not None and root_dt < start_dt) or (end_dt is not None and root_dt > end_dt)): continue elif date_list and root_dt.date().isoformat() not in date_list: continue # If the year is outside the range, don't search subfolders elif re.match('\d{4}', root_split[-1]): root_year = int(root_split[-1]) logging.info('Year: {}'.format(root_year)) if ((start_dt is not None and root_year < start_dt.year) or (end_dt is not None and root_year > end_dt.year)): folders[:] = [] else: folders[:] = sorted(folders) continue else: continue logging.debug(' {}'.format(root)) # Start off assuming every file needs to be processed day_skip_flag = False # Build output folders if necessary etr_year_ws = os.path.join(etr_ws, str(root_dt.year)) eto_year_ws = os.path.join(eto_ws, str(root_dt.year)) if etr_flag and not os.path.isdir(etr_year_ws): os.makedirs(etr_year_ws) if eto_flag and not os.path.isdir(eto_year_ws): os.makedirs(eto_year_ws) # Build daily total paths etr_day_path = os.path.join( etr_year_ws, day_fmt.format('etr', root_dt.year, root_dt.month, root_dt.day)) eto_day_path = os.path.join( eto_year_ws, day_fmt.format('eto', root_dt.year, root_dt.month, root_dt.day)) etr_hour_path = os.path.join( etr_year_ws, hour_fmt.format('etr', root_dt.year, root_dt.month, root_dt.day)) eto_hour_path = os.path.join( eto_year_ws, hour_fmt.format('eto', root_dt.year, root_dt.month, root_dt.day)) # logging.debug(' {}'.format(etr_hour_path)) # If daily ETr/ETo files are present, day can be skipped if not overwrite_flag and daily_flag: if etr_flag and not os.path.isfile(etr_day_path): pass elif eto_flag and not os.path.isfile(eto_day_path): pass else: day_skip_flag = True # If the hour and daily files don't need to be made, skip the day if not overwrite_flag: if etr_flag and not os.path.isfile(etr_hour_path): pass elif eto_flag and not os.path.isfile(eto_hour_path): pass elif day_skip_flag: logging.debug(' File(s) already exist, skipping') continue # Create a single raster for each day with 24 bands # Each time step will be stored in a separate band if etr_flag: logging.debug(' {}'.format(etr_day_path)) gdc.build_empty_raster(etr_hour_path, band_cnt=24, output_dtype=np.float32, output_proj=nldas_proj, output_cs=nldas_cs, output_extent=nldas_extent, output_fill_flag=True) if eto_flag: logging.debug(' {}'.format(eto_day_path)) gdc.build_empty_raster(eto_hour_path, band_cnt=24, output_dtype=np.float32, output_proj=nldas_proj, output_cs=nldas_cs, output_extent=nldas_extent, output_fill_flag=True) # Sum all ETr/ETo images in each folder to generate a UTC day total etr_day_array = 0 eto_day_array = 0 # Process each hour file for input_name in sorted(files): logging.info(' {}'.format(input_name)) input_match = input_re.match(input_name) if input_match is None: logging.debug(' Regular expression didn\'t match, skipping') continue input_dt = dt.datetime(int(input_match.group('YEAR')), int(input_match.group('MONTH')), int(input_match.group('DAY'))) input_doy = int(input_dt.strftime('%j')) time_str = input_match.group('TIME') band_num = int(time_str[:2]) + 1 # if start_dt is not None and input_dt < start_dt: # continue # elif end_dt is not None and input_dt > end_dt: # continue # elif date_list and input_dt.date().isoformat() not in date_list: # continue if not daily_flag and time_str not in time_list: logging.debug(' Time not in list and not daily, skipping') continue input_path = os.path.join(root, input_name) logging.debug(' Time: {} {}'.format(input_dt.date(), time_str)) logging.debug(' Band: {}'.format(band_num)) # Determine band numbering/naming try: input_band_dict = grib_band_names(input_path) except RuntimeError as e: errors[input_path].append(e) logging.error(' RuntimeError: {} Skipping: {}'.format( e, input_path)) continue # Read input bands input_ds = gdal.Open(input_path) # Temperature should be in C for et_common.refet_hourly_func() if 'Temperature [K]' in input_band_dict.keys(): temp_band_units = 'K' temp_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Temperature [K]'], mask_extent=nldas_extent, return_nodata=False) elif 'Temperature [C]' in input_band_dict.keys(): temp_band_units = 'C' temp_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Temperature [C]'], mask_extent=nldas_extent, return_nodata=False) else: logging.error('Unknown Temperature units, skipping') logging.error(' {}'.format(input_band_dict.keys())) continue # DEADBEEF - Having issue with T appearing to be C but labeled as K # Try to determine temperature units from values temp_mean = float(np.nanmean(temp_array)) temp_units_dict = {20: 'C', 293: 'K'} temp_array_units = temp_units_dict[min( temp_units_dict, key=lambda x: abs(x - temp_mean))] if temp_array_units == 'K' and temp_band_units == 'K': logging.debug(' Converting temperature from K to C') temp_array -= 273.15 elif temp_array_units == 'C' and temp_band_units == 'C': pass elif temp_array_units == 'C' and temp_band_units == 'K': logging.debug(( ' Temperature units are K in the GRB band name, ' + 'but values appear to be C\n Mean temperature: {:.2f}\n' + ' Values will NOT be adjusted').format(temp_mean)) elif temp_array_units == 'K' and temp_band_units == 'C': logging.debug(( ' Temperature units are C in the GRB band name, ' + 'but values appear to be K\n Mean temperature: {:.2f}\n' + ' Values will be adjusted from K to C').format(temp_mean)) temp_array -= 273.15 try: sph_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Specific humidity [kg/kg]'], mask_extent=nldas_extent, return_nodata=False) rs_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict[ 'Downward shortwave radiation flux [W/m^2]'], mask_extent=nldas_extent, return_nodata=False) wind_u_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['u-component of wind [m/s]'], mask_extent=nldas_extent, return_nodata=False) wind_v_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['v-component of wind [m/s]'], mask_extent=nldas_extent, return_nodata=False) input_ds = None except KeyError as e: errors[input_path].append(e) logging.error(' KeyError: {} Skipping: {}'.format( e, input_ds.GetDescription())) continue rs_array *= 0.0036 # W m-2 to MJ m-2 hr-1 wind_array = np.sqrt(wind_u_array**2 + wind_v_array**2) del wind_u_array, wind_v_array # ETr if etr_flag: etr_array = et_common.refet_hourly_func(temp_array, sph_array, rs_array, wind_array, zw=10, elev=elev_array, lat=lat_array, lon=lon_array, doy=input_doy, time=int(time_str) / 100, ref_type='ETR') if daily_flag: etr_day_array += etr_array if time_str in time_list: gdc.array_to_comp_raster(etr_array.astype(np.float32), etr_hour_path, band=band_num, stats_flag=False) del etr_array # ETo if eto_flag: eto_array = et_common.refet_hourly_func(temp_array, sph_array, rs_array, wind_array, zw=10, elev=elev_array, lat=lat_array, lon=lon_array, doy=input_doy, time=int(time_str) / 100, ref_type='ETO') if eto_flag and daily_flag: eto_day_array += eto_array if eto_flag and time_str in time_list: gdc.array_to_comp_raster(eto_array.astype(np.float32), eto_hour_path, band=band_num, stats_flag=False) del eto_array del temp_array, sph_array, rs_array, wind_array if stats_flag and etr_flag: gdc.raster_statistics(etr_hour_path) if stats_flag and eto_flag: gdc.raster_statistics(eto_hour_path) # Save the projected ETr/ETo as 32-bit floats if not day_skip_flag and daily_flag: if etr_flag: try: gdc.array_to_raster(etr_day_array.astype(np.float32), etr_day_path, output_geo=nldas_geo, output_proj=nldas_proj, stats_flag=stats_flag) except AttributeError: pass if eto_flag: try: gdc.array_to_raster(eto_day_array.astype(np.float32), eto_day_path, output_geo=nldas_geo, output_proj=nldas_proj, stats_flag=stats_flag) except AttributeError: pass del etr_day_array, eto_day_array if len(errors) > 0: logging.info('\nThe following errors were encountered:') for key, value in errors.items(): logging.error(' Filepath: {}, error: {}'.format(key, value)) logging.debug('\nScript Complete')
def main(netcdf_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), start_date=None, end_date=None, extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False): """Extract DAYMET temperature Args: netcdf_ws (str): folder of DAYMET netcdf files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nExtracting DAYMET vapor pressure') # If a date is not set, process 2015 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2015, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2015, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Get DAYMET spatial reference from an ancillary raster mask_raster = os.path.join(ancillary_ws, 'daymet_mask.img') elev_raster = os.path.join(ancillary_ws, 'daymet_elev.img') daymet_re = re.compile('daymet_v3_(?P<VAR>\w+)_(?P<YEAR>\d{4})_na.nc4$') # DAYMET band name dictionary # daymet_band_dict = dict() # daymet_band_dict['prcp'] = 'precipitation_amount' # daymet_band_dict['srad'] = 'surface_downwelling_shortwave_flux_in_air' # daymet_band_dict['sph'] = 'specific_humidity' # daymet_band_dict['tmin'] = 'air_temperature' # daymet_band_dict['tmax'] = 'air_temperature' # Get extent/geo from mask raster daymet_ds = gdal.Open(mask_raster) daymet_osr = gdc.raster_ds_osr(daymet_ds) daymet_proj = gdc.osr_proj(daymet_osr) daymet_cs = gdc.raster_ds_cellsize(daymet_ds, x_only=True) daymet_extent = gdc.raster_ds_extent(daymet_ds) daymet_geo = daymet_extent.geo(daymet_cs) daymet_x, daymet_y = daymet_extent.origin() daymet_ds = None logging.debug(' Projection: {}'.format(daymet_proj)) logging.debug(' Cellsize: {}'.format(daymet_cs)) logging.debug(' Geo: {}'.format(daymet_geo)) logging.debug(' Extent: {}'.format(daymet_extent)) logging.debug(' Origin: {} {}'.format(daymet_x, daymet_y)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) # Assume input extent is in decimal degrees output_extent = gdc.project_extent(gdc.Extent(output_extent), gdc.epsg_osr(4326), daymet_osr, 0.001) output_extent = gdc.intersect_extents([daymet_extent, output_extent]) output_extent.adjust_to_snap('EXPAND', daymet_x, daymet_y, daymet_cs) output_geo = output_extent.geo(daymet_cs) logging.debug(' Geo: {}'.format(output_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): output_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: output_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) output_extent = gdc.project_extent(output_extent, extent_osr, daymet_osr, extent_cs) output_extent = gdc.intersect_extents([daymet_extent, output_extent]) output_extent.adjust_to_snap('EXPAND', daymet_x, daymet_y, daymet_cs) output_geo = output_extent.geo(daymet_cs) logging.debug(' Geo: {}'.format(output_geo)) logging.debug(' Extent: {}'.format(output_extent)) else: output_extent = daymet_extent.copy() output_geo = daymet_geo[:] # output_shape = output_extent.shape(cs=daymet_cs) xi, yi = gdc.array_geo_offsets(daymet_geo, output_geo, daymet_cs) output_rows, output_cols = output_extent.shape(daymet_cs) logging.debug(' Shape: {} {}'.format(output_rows, output_cols)) logging.debug(' Offsets: {} {} (x y)'.format(xi, yi)) # Read the elevation array elev_array = gdc.raster_to_array(elev_raster, mask_extent=output_extent, return_nodata=False) pair_array = et_common.air_pressure_func(elev_array) del elev_array # Process each variable input_var = 'vp' output_var = 'ea' logging.info("\nVariable: {}".format(input_var)) # Build output folder var_ws = os.path.join(output_ws, output_var) if not os.path.isdir(var_ws): os.makedirs(var_ws) # Process each file in the input workspace for input_name in sorted(os.listdir(netcdf_ws)): logging.debug("{}".format(input_name)) input_match = daymet_re.match(input_name) if not input_match: logging.debug(' Regular expression didn\'t match, skipping') continue elif input_match.group('VAR') != input_var: logging.debug(' Variable didn\'t match, skipping') continue year_str = input_match.group('YEAR') logging.info(" Year: {}".format(year_str)) year_int = int(year_str) year_days = int(dt.datetime(year_int, 12, 31).strftime('%j')) if start_dt is not None and year_int < start_dt.year: logging.debug(' Before start date, skipping') continue elif end_dt is not None and year_int > end_dt.year: logging.debug(' After end date, skipping') continue # Build input file path input_raster = os.path.join(netcdf_ws, input_name) # if not os.path.isfile(input_raster): # logging.debug( # ' Input raster doesn\'t exist, skipping {}'.format( # input_raster)) # continue # Build output folder output_year_ws = os.path.join(var_ws, year_str) if not os.path.isdir(output_year_ws): os.makedirs(output_year_ws) # Read in the DAYMET NetCDF file input_nc_f = netCDF4.Dataset(input_raster, 'r') # logging.debug(input_nc_f.variables) # Check all valid dates in the year year_dates = date_range(dt.datetime(year_int, 1, 1), dt.datetime(year_int + 1, 1, 1)) for date_dt in year_dates: if start_dt is not None and date_dt < start_dt: logging.debug(' {} - before start date, skipping'.format( date_dt.date())) continue elif end_dt is not None and date_dt > end_dt: logging.debug(' {} - after end date, skipping'.format( date_dt.date())) continue else: logging.info(' {}'.format(date_dt.date())) output_path = os.path.join( output_year_ws, '{}_{}_daymet.img'.format(output_var, date_dt.strftime('%Y%m%d'))) if os.path.isfile(output_path): logging.debug(' {}'.format(output_path)) if not overwrite_flag: logging.debug(' File already exists, skipping') continue else: logging.debug(' File already exists, removing existing') os.remove(output_path) doy = int(date_dt.strftime('%j')) doy_i = range(1, year_days + 1).index(doy) # Arrays are being read as masked array with a fill value of -9999 # Convert to basic numpy array arrays with nan values try: input_ma = input_nc_f.variables[input_var][doy_i, yi:yi + output_rows, xi:xi + output_cols] except IndexError: logging.info(' date not in netcdf, skipping') continue input_nodata = float(input_ma.fill_value) sph_array = input_ma.data.astype(np.float32) sph_array[sph_array == input_nodata] = np.nan # Compute ea [kPa] from specific humidity [kg/kg] ea_array = (sph_array * pair_array) / (0.622 + 0.378 * sph_array) # Save the array as 32-bit floats gdc.array_to_raster(ea_array.astype(np.float32), output_path, output_geo=output_geo, output_proj=daymet_proj, stats_flag=stats_flag) del input_ma, ea_array, sph_array input_nc_f.close() del input_nc_f logging.debug('\nScript Complete')
def main(grb_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), landsat_ws=None, start_date=None, end_date=None, times_str='', extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False): """Extract hourly NLDAS vapour pressure rasters Args: grb_ws (str): folder of NLDAS GRB files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters landsat_ws (str): folder of Landsat scenes or tar.gz files start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) times (str): comma separated values and/or ranges of UTC hours (i.e. "1, 2, 5-8") Parsed with python_common.parse_int_set() extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nExtracting NLDAS vapour pressure rasters') # input_fmt = 'NLDAS_FORA0125_H.A{:04d}{:02d}{:02d}.{}.002.grb' input_re = re.compile('NLDAS_FORA0125_H.A(?P<YEAR>\d{4})(?P<MONTH>\d{2})' + '(?P<DAY>\d{2}).(?P<TIME>\d{4}).002.grb$') output_folder = 'ea' output_fmt = 'ea_{:04d}{:02d}{:02d}_hourly_nldas.img' # output_fmt = 'ea_{:04d}{:02d}{:02d}_{:04d}_nldas.img' # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Only process a specific hours if not times_str: time_list = range(0, 24, 1) else: time_list = list(parse_int_set(times_str)) time_list = ['{:02d}00'.format(t) for t in time_list] # Assume NLDAS is NAD83 # input_epsg = 'EPSG:4269' # Ancillary raster paths mask_path = os.path.join(ancillary_ws, 'nldas_mask.img') elev_path = os.path.join(ancillary_ws, 'nldas_elev.img') # Build a date list from landsat_ws scene folders or tar.gz files date_list = [] if landsat_ws is not None and os.path.isdir(landsat_ws): logging.info('\nReading dates from Landsat IDs') logging.info(' {}'.format(landsat_ws)) landsat_re = re.compile( '^(?:LT04|LT05|LE07|LC08)_(?:\d{3})(?:\d{3})_' + '(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})') for root, dirs, files in os.walk(landsat_ws, topdown=True): # If root matches, don't explore subfolders try: landsat_match = landsat_re.match(os.path.basename(root)) date_list.append( dt.datetime.strptime('_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) dirs[:] = [] except: pass for file in files: try: landsat_match = landsat_re.match(file) date_list.append( dt.datetime.strptime('_'.join(landsat_match.groups()), '%Y_%m_%d').date().isoformat()) except: pass date_list = sorted(list(set(date_list))) # elif landsat_ws is not None and os.path.isfile(landsat_ws): # with open(landsat_ws) as landsat_f: # This allows GDAL to throw Python Exceptions # gdal.UseExceptions() # mem_driver = gdal.GetDriverByName('MEM') # Get the NLDAS spatial reference from the mask raster nldas_ds = gdal.Open(mask_path) nldas_osr = gdc.raster_ds_osr(nldas_ds) nldas_proj = gdc.osr_proj(nldas_osr) nldas_cs = gdc.raster_ds_cellsize(nldas_ds, x_only=True) nldas_extent = gdc.raster_ds_extent(nldas_ds) nldas_geo = nldas_extent.geo(nldas_cs) nldas_x, nldas_y = nldas_extent.origin() nldas_ds = None logging.debug(' Projection: {}'.format(nldas_proj)) logging.debug(' Cellsize: {}'.format(nldas_cs)) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) nldas_extent = gdc.Extent(output_extent) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): nldas_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: nldas_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) nldas_extent = gdc.project_extent(nldas_extent, extent_osr, nldas_osr, extent_cs) nldas_extent.adjust_to_snap('EXPAND', nldas_x, nldas_y, nldas_cs) nldas_geo = nldas_extent.geo(nldas_cs) logging.debug(' Geo: {}'.format(nldas_geo)) logging.debug(' Extent: {}'.format(nldas_extent)) logging.debug('') # Read the NLDAS mask array if present if mask_path and os.path.isfile(mask_path): mask_array, mask_nodata = gdc.raster_to_array(mask_path, mask_extent=nldas_extent, fill_value=0, return_nodata=True) mask_array = mask_array != mask_nodata else: mask_array = None # Read elevation arrays (or subsets?) elev_array = gdc.raster_to_array(elev_path, mask_extent=nldas_extent, return_nodata=False) pair_array = et_common.air_pressure_func(elev_array) # Build output folder var_ws = os.path.join(output_ws, output_folder) if not os.path.isdir(var_ws): os.makedirs(var_ws) # Each sub folder in the main folder has all imagery for 1 day # The path for each subfolder is the /YYYY/DOY # This approach will process files for target dates # for input_dt in date_range(start_dt, end_dt + dt.timedelta(1)): # logging.info(input_dt.date()) # Iterate all available files and check dates if necessary for root, folders, files in os.walk(grb_ws): root_split = os.path.normpath(root).split(os.sep) # If the year/doy is outside the range, skip if (re.match('\d{4}', root_split[-2]) and re.match('\d{3}', root_split[-1])): root_dt = dt.datetime.strptime( '{}_{}'.format(root_split[-2], root_split[-1]), '%Y_%j') logging.info('{}'.format(root_dt.date())) if ((start_dt is not None and root_dt < start_dt) or (end_dt is not None and root_dt > end_dt)): continue elif date_list and root_dt.date().isoformat() not in date_list: continue # If the year is outside the range, don't search subfolders elif re.match('\d{4}', root_split[-1]): root_year = int(root_split[-1]) logging.info('Year: {}'.format(root_year)) if ((start_dt is not None and root_year < start_dt.year) or (end_dt is not None and root_year > end_dt.year)): folders[:] = [] else: folders[:] = sorted(folders) continue else: continue # Create a single raster for each day with 24 bands # Each time step will be stored in a separate band output_name = output_fmt.format(root_dt.year, root_dt.month, root_dt.day) output_path = os.path.join(var_ws, str(root_dt.year), output_name) logging.debug(' {}'.format(output_path)) if os.path.isfile(output_path): if not overwrite_flag: logging.debug(' File already exists, skipping') continue else: logging.debug(' File already exists, removing existing') os.remove(output_path) logging.debug(' {}'.format(root)) if not os.path.isdir(os.path.dirname(output_path)): os.makedirs(os.path.dirname(output_path)) gdc.build_empty_raster(output_path, band_cnt=24, output_dtype=np.float32, output_proj=nldas_proj, output_cs=nldas_cs, output_extent=nldas_extent, output_fill_flag=True) # Iterate through hourly files for input_name in sorted(files): logging.info(' {}'.format(input_name)) input_path = os.path.join(root, input_name) input_match = input_re.match(input_name) if input_match is None: logging.debug(' Regular expression didn\'t match, skipping') continue input_dt = dt.datetime(int(input_match.group('YEAR')), int(input_match.group('MONTH')), int(input_match.group('DAY'))) time_str = input_match.group('TIME') band_num = int(time_str[:2]) + 1 # if start_dt is not None and input_dt < start_dt: # continue # elif end_dt is not None and input_dt > end_dt: # continue # elif date_list and input_dt.date().isoformat() not in date_list: # continue if time_str not in time_list: logging.debug(' Time not in list, skipping') continue logging.debug(' Time: {} {}'.format(input_dt.date(), time_str)) logging.debug(' Band: {}'.format(band_num)) # Determine band numbering/naming input_band_dict = grib_band_names(input_path) # Compute vapour pressure from specific humidity input_ds = gdal.Open(input_path) sph_array = gdc.raster_ds_to_array( input_ds, band=input_band_dict['Specific humidity [kg/kg]'], mask_extent=nldas_extent, return_nodata=False) ea_array = (sph_array * pair_array) / (0.622 + 0.378 * sph_array) # Save the projected array as 32-bit floats gdc.array_to_comp_raster(ea_array.astype(np.float32), output_path, band=band_num) # gdc.block_to_raster( # ea_array.astype(np.float32), output_path, band=band) # gdc.array_to_raster( # ea_array.astype(np.float32), output_path, # output_geo=nldas_geo, output_proj=nldas_proj, # stats_flag=stats_flag) del sph_array input_ds = None if stats_flag: gdc.raster_statistics(output_path) logging.debug('\nScript Complete')
def zonal_stats(ini_path=None, overwrite_flag=False): """Offline Zonal Stats Args: ini_path (str): overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nCompute Offline Zonal Stats') landsat_flag = True gridmet_flag = True pdsi_flag = False landsat_images_folder = 'landsat' landsat_tables_folder = 'landsat_tables' gridmet_images_folder = 'gridmet_monthly' # Regular expression to pull out Landsat scene_id landsat_image_re = re.compile('^\d{8}_\d{3}_\w+.\w+.tif$') gridmet_image_re = re.compile('^\d{6}_gridmet.(eto|ppt).tif$') # For now, hardcode snap, cellsize and spatial reference logging.info('\nHardcoding zone/output cellsize and snap') zone_cs = 30 zone_x, zone_y = 15, 15 logging.debug(' Snap: {} {}'.format(zone_x, zone_y)) logging.debug(' Cellsize: {}'.format(zone_cs)) logging.info('Hardcoding Landsat snap, cellsize and spatial reference') landsat_x, landsat_y = 15, 15 landsat_cs = 30 landsat_osr = gdc.epsg_osr(32611) logging.debug(' Snap: {} {}'.format(landsat_x, landsat_y)) logging.debug(' Cellsize: {}'.format(landsat_cs)) logging.debug(' OSR: {}'.format(landsat_osr)) logging.info('Hardcoding GRIDMET snap, cellsize and spatial reference') gridmet_x, gridmet_y = -124.79299639209513, 49.41685579737572 gridmet_cs = 0.041666001963701 # gridmet_cs = [0.041666001963701, 0.041666001489718] # gridmet_x, gridmet_y = -124.79166666666666666667, 25.04166666666666666667 # gridmet_cs = 1. / 24 gridmet_osr = gdc.epsg_osr(4326) # gridmet_osr = gdc.epsg_osr(4269) logging.debug(' Snap: {} {}'.format(gridmet_x, gridmet_y)) logging.debug(' Cellsize: {}'.format(gridmet_cs)) logging.debug(' OSR: {}'.format(gridmet_osr)) landsat_daily_fields = [ 'DATE', 'SCENE_ID', 'LANDSAT', 'PATH', 'ROW', 'YEAR', 'MONTH', 'DAY', 'DOY', 'PIXEL_COUNT', 'FMASK_COUNT', 'DATA_COUNT', 'CLOUD_SCORE', 'TS', 'ALBEDO_SUR', 'NDVI_TOA', 'NDVI_SUR', 'EVI_SUR', 'NDWI_GREEN_NIR_SUR', 'NDWI_GREEN_SWIR1_SUR', 'NDWI_NIR_SWIR1_SUR', # 'NDWI_GREEN_NIR_TOA', 'NDWI_GREEN_SWIR1_TOA', 'NDWI_NIR_SWIR1_TOA', # 'NDWI_SWIR1_GREEN_TOA', 'NDWI_SWIR1_GREEN_SUR', # 'NDWI_TOA', 'NDWI_SUR', 'TC_BRIGHT', 'TC_GREEN', 'TC_WET'] # gridmet_daily_fields = [ # 'DATE', 'YEAR', 'MONTH', 'DAY', 'DOY', 'WATER_YEAR', 'ETO', 'PPT'] gridmet_monthly_fields = [ 'DATE', 'YEAR', 'MONTH', 'WATER_YEAR', 'ETO', 'PPT'] pdsi_dekad_fields = [ 'DATE', 'YEAR', 'MONTH', 'DAY', 'DOY', 'PDSI'] landsat_int_fields = [ 'YEAR', 'MONTH', 'DAY', 'DOY', 'PIXEL_COUNT', 'FMASK_COUNT', 'CLOUD_SCORE'] gridmet_int_fields = ['YEAR', 'MONTH', 'WATER_YEAR'] # To figure out which Landsat and path, # Compare date to reference dates and look for even multiples of 16 ref_dates = { datetime.datetime(1985, 3, 31): ['LT5', '039'], datetime.datetime(1985, 4, 7): ['LT5', '040'], datetime.datetime(1999, 7, 4): ['LE7', '039'], datetime.datetime(1999, 7, 27): ['LE7', '040'], datetime.datetime(2013, 4, 13): ['LC8', '039'], datetime.datetime(2013, 4, 20): ['LC8', '040'] # datetime.datetime(1984, , ): ['LT4', '039'], # datetime.datetime(1984, , ): ['LT4', '040'], } # Open config file config = ConfigParser.ConfigParser() try: config.readfp(open(ini_path)) except: logging.error(('\nERROR: Input file could not be read, ' + 'is not an input file, or does not exist\n' + 'ERROR: ini_path = {}\n').format(ini_path)) sys.exit() logging.debug('\nReading Input File') # Read in config file zone_input_ws = config.get('INPUTS', 'zone_input_ws') zone_filename = config.get('INPUTS', 'zone_filename') zone_field = config.get('INPUTS', 'zone_field') zone_path = os.path.join(zone_input_ws, zone_filename) landsat_daily_fields.insert(0, zone_field) # gridmet_daily_fields.insert(0, zone_field) gridmet_monthly_fields.insert(0, zone_field) pdsi_dekad_fields.insert(0, zone_field) images_ws = config.get('INPUTS', 'images_ws') # Build and check file paths if not os.path.isdir(zone_input_ws): logging.error( '\nERROR: The zone workspace does not exist, exiting\n {}'.format( zone_input_ws)) sys.exit() elif not os.path.isfile(zone_path): logging.error( '\nERROR: The zone shapefile does not exist, exiting\n {}'.format( zone_path)) sys.exit() elif not os.path.isdir(images_ws): logging.error( '\nERROR: The image workspace does not exist, exiting\n {}'.format( images_ws)) sys.exit() # Final output folder try: output_ws = config.get('INPUTS', 'output_ws') if not os.path.isdir(output_ws): os.makedirs(output_ws) except: output_ws = os.getcwd() logging.debug(' Defaulting output workspace to {}'.format(output_ws)) # Start/end year try: start_year = int(config.get('INPUTS', 'start_year')) except: start_year = 1984 logging.debug(' Defaulting start_year={}'.format(start_year)) try: end_year = int(config.get('INPUTS', 'end_year')) except: end_year = datetime.datetime.today().year logging.debug(' Defaulting end year to {}'.format(end_year)) if start_year and end_year and end_year < start_year: logging.error( '\nERROR: End year must be >= start year, exiting') sys.exit() default_end_year = datetime.datetime.today().year + 1 if (start_year and start_year not in range(1984, default_end_year) or end_year and end_year not in range(1984, default_end_year)): logging.error( ('\nERROR: Year must be an integer from 1984-{}, ' + 'exiting').format(default_end_year - 1)) sys.exit() # Start/end month try: start_month = int(config.get('INPUTS', 'start_month')) except: start_month = None logging.debug(' Defaulting start_month=None') try: end_month = int(config.get('INPUTS', 'end_month')) except: end_month = None logging.debug(' Defaulting end_month=None') if start_month and start_month not in range(1, 13): logging.error( '\nERROR: Start month must be an integer from 1-12, exiting') sys.exit() elif end_month and end_month not in range(1, 13): logging.error( '\nERROR: End month must be an integer from 1-12, exiting') sys.exit() month_list = common.wrapped_range(start_month, end_month, 1, 12) # Start/end DOY try: start_doy = int(config.get('INPUTS', 'start_doy')) except: start_doy = None logging.debug(' Defaulting start_doy=None') try: end_doy = int(config.get('INPUTS', 'end_doy')) except: end_doy = None logging.debug(' Defaulting end_doy=None') if end_doy and end_doy > 273: logging.error( '\nERROR: End DOY must be in the same water year as start DOY, ' + 'exiting') sys.exit() if start_doy and start_doy not in range(1, 367): logging.error( '\nERROR: Start DOY must be an integer from 1-366, exiting') sys.exit() elif end_doy and end_doy not in range(1, 367): logging.error( '\nERROR: End DOY must be an integer from 1-366, exiting') sys.exit() # if end_doy < start_doy: # logging.error( # '\nERROR: End DOY must be >= start DOY') # sys.exit() doy_list = common.wrapped_range(start_doy, end_doy, 1, 366) # Control which Landsat images are used try: landsat5_flag = config.getboolean('INPUTS', 'landsat5_flag') except: landsat5_flag = False logging.debug(' Defaulting landsat5_flag=False') try: landsat4_flag = config.getboolean('INPUTS', 'landsat4_flag') except: landsat4_flag = False logging.debug(' Defaulting landsat4_flag=False') try: landsat7_flag = config.getboolean('INPUTS', 'landsat7_flag') except: landsat7_flag = False logging.debug(' Defaulting landsat7_flag=False') try: landsat8_flag = config.getboolean('INPUTS', 'landsat8_flag') except: landsat8_flag = False logging.debug(' Defaulting landsat8_flag=False') # Cloudmasking try: apply_mask_flag = config.getboolean('INPUTS', 'apply_mask_flag') except: apply_mask_flag = False logging.debug(' Defaulting apply_mask_flag=False') try: acca_flag = config.getboolean('INPUTS', 'acca_flag') except: acca_flag = False try: fmask_flag = config.getboolean('INPUTS', 'fmask_flag') except: fmask_flag = False # Intentionally don't apply scene_id skip/keep lists # Compute zonal stats for all available images # Filter by scene_id when making summary tables scene_id_keep_list = [] scene_id_skip_list = [] # # Only process specific Landsat scenes # try: # scene_id_keep_path = config.get('INPUTS', 'scene_id_keep_path') # with open(scene_id_keep_path) as input_f: # scene_id_keep_list = input_f.readlines() # scene_id_keep_list = [x.strip()[:16] for x in scene_id_keep_list] # except IOError: # logging.error('\nFileIO Error: {}'.format(scene_id_keep_path)) # sys.exit() # except: # scene_id_keep_list = [] # # Skip specific landsat scenes # try: # scene_id_skip_path = config.get('INPUTS', 'scene_id_skip_path') # with open(scene_id_skip_path) as input_f: # scene_id_skip_list = input_f.readlines() # scene_id_skip_list = [x.strip()[:16] for x in scene_id_skip_list] # except IOError: # logging.error('\nFileIO Error: {}'.format(scene_id_skip_path)) # sys.exit() # except: # scene_id_skip_list = [] # Only process certain Landsat path/rows try: path_keep_list = list( common.parse_int_set(config.get('INPUTS', 'path_keep_list'))) except: path_keep_list = [] # try: # row_keep_list = list( # common.parse_int_set(config.get('INPUTS', 'row_keep_list'))) # except: # row_keep_list = [] # Skip or keep certain FID try: fid_skip_list = list( common.parse_int_set(config.get('INPUTS', 'fid_skip_list'))) except: fid_skip_list = [] try: fid_keep_list = list( common.parse_int_set(config.get('INPUTS', 'fid_keep_list'))) except: fid_keep_list = [] # For now, output projection must be manually set above to match zones zone_osr = gdc.feature_path_osr(zone_path) zone_proj = gdc.osr_proj(zone_osr) logging.info('\nThe zone shapefile must be in a projected coordinate system!') logging.info(' Proj4: {}'.format(zone_osr.ExportToProj4())) logging.info('{}'.format(zone_osr)) # Read in zone shapefile logging.info('\nRasterizing Zone Shapefile') zone_name_dict = dict() zone_extent_dict = dict() zone_mask_dict = dict() # First get FIDs and extents zone_ds = ogr.Open(zone_path, 0) zone_lyr = zone_ds.GetLayer() zone_lyr.ResetReading() for zone_ftr in zone_lyr: zone_fid = zone_ftr.GetFID() if zone_field.upper() == 'FID': zone_name_dict[zone_fid] = str(zone_fid) else: zone_name_dict[zone_fid] = zone_ftr.GetField(zone_field) zone_extent = gdc.Extent( zone_ftr.GetGeometryRef().GetEnvelope()).ogrenv_swap() zone_extent.adjust_to_snap('EXPAND', zone_x, zone_y, zone_cs) zone_extent_dict[zone_fid] = list(zone_extent) # Rasterize each FID separately # The RasterizeLayer function wants a "layer" # There might be an easier way to select each feature as a layer for zone_fid, zone_extent in sorted(zone_extent_dict.items()): logging.debug('FID: {}'.format(zone_fid)) logging.debug(' Name: {}'.format(zone_name_dict[zone_fid])) zone_ds = ogr.Open(zone_path, 0) zone_lyr = zone_ds.GetLayer() zone_lyr.ResetReading() zone_lyr.SetAttributeFilter("{0} = {1}".format('FID', zone_fid)) zone_extent = gdc.Extent(zone_extent) zone_rows, zone_cols = zone_extent.shape(zone_cs) logging.debug(' Extent: {}'.format(str(zone_extent))) logging.debug(' Rows/Cols: {} {}'.format(zone_rows, zone_cols)) # zones_lyr.SetAttributeFilter("{0} = {1}".format('FID', zone_fid)) # Initialize the zone in memory raster mem_driver = gdal.GetDriverByName('MEM') zone_raster_ds = mem_driver.Create( '', zone_cols, zone_rows, 1, gdal.GDT_Byte) zone_raster_ds.SetProjection(zone_proj) zone_raster_ds.SetGeoTransform( gdc.extent_geo(zone_extent, cs=zone_cs)) zone_band = zone_raster_ds.GetRasterBand(1) zone_band.SetNoDataValue(0) # Clear the raster before rasterizing zone_band.Fill(0) gdal.RasterizeLayer(zone_raster_ds, [1], zone_lyr) # zones_ftr_ds = None zone_array = gdc.raster_ds_to_array( zone_raster_ds, return_nodata=False) zone_mask = zone_array != 0 logging.debug(' Pixel Count: {}'.format(np.sum(zone_mask))) # logging.debug(' Mask:\n{}'.format(zone_mask)) # logging.debug(' Array:\n{}'.format(zone_array)) zone_mask_dict[zone_fid] = zone_mask zone_raster_ds = None del zone_raster_ds, zone_array, zone_mask zone_ds = None del zone_ds, zone_lyr # Calculate zonal stats for each feature separately logging.info('') for fid, zone_str in sorted(zone_name_dict.items()): if fid_keep_list and fid not in fid_keep_list: continue elif fid_skip_list and fid in fid_skip_list: continue logging.info('ZONE: {} (FID: {})'.format(zone_str, fid)) if not zone_field or zone_field.upper() == 'FID': zone_str = 'fid_' + zone_str else: zone_str = zone_str.lower().replace(' ', '_') zone_output_ws = os.path.join(output_ws, zone_str) if not os.path.isdir(zone_output_ws): os.makedirs(zone_output_ws) zone_extent = gdc.Extent(zone_extent_dict[fid]) zone_mask = zone_mask_dict[fid] # logging.debug(' Extent: {}'.format(zone_extent)) if landsat_flag: logging.info(' Landsat') landsat_output_ws = os.path.join( zone_output_ws, landsat_tables_folder) if not os.path.isdir(landsat_output_ws): os.makedirs(landsat_output_ws) logging.debug(' {}'.format(landsat_output_ws)) # Project the zone extent to the image OSR clip_extent = gdc.project_extent( zone_extent, zone_osr, landsat_osr, zone_cs) # logging.debug(' Extent: {}'.format(clip_extent)) clip_extent.adjust_to_snap('EXPAND', landsat_x, landsat_y, landsat_cs) logging.debug(' Extent: {}'.format(clip_extent)) # Process date range by year for year in xrange(start_year, end_year + 1): images_year_ws = os.path.join( images_ws, landsat_images_folder, str(year)) if not os.path.isdir(images_year_ws): logging.debug( ' Landsat year folder doesn\'t exist, skipping\n {}'.format( images_year_ws)) continue else: logging.info(' Year: {}'.format(year)) # Create an empty dataframe output_path = os.path.join( landsat_output_ws, '{}_landsat_{}.csv'.format(zone_str, year)) if os.path.isfile(output_path): if overwrite_flag: logging.debug( ' Output CSV already exists, removing\n {}'.format( output_path)) os.remove(output_path) else: logging.debug( ' Output CSV already exists, skipping\n {}'.format( output_path)) continue output_df = pd.DataFrame(columns=landsat_daily_fields) output_df[landsat_int_fields] = output_df[ landsat_int_fields].astype(int) # Get list of all images year_image_list = [ image for image in os.listdir(images_year_ws) if landsat_image_re.match(image)] # Get list of all unique dates (multiple images per date) year_dt_list = sorted(set([ datetime.datetime.strptime(image[:8], '%Y%m%d') for image in year_image_list])) # Filter date lists if necessary if month_list: year_dt_list = [ image_dt for image_dt in year_dt_list if image_dt.month in month_list] if doy_list: year_dt_list = [ image_dt for image_dt in year_dt_list if int(image_dt.strftime('%j')) in doy_list] output_list = [] for image_dt in year_dt_list: image_str = image_dt.date().isoformat() logging.debug('{}'.format(image_dt.date())) # Get the list of available images image_list = [ image for image in year_image_list if image_dt.strftime('%Y%m%d') in image] # This conditional is probably impossible if not image_list: logging.debug(' No images, skipping date') continue # Use date offsets to determine the Landsat and Path ref_match = [ lp for ref_dt, lp in ref_dates.items() if (((ref_dt - image_dt).days % 16 == 0) and ((lp[0].upper() == 'LT5' and image_dt.year < 2012) or (lp[0].upper() == 'LC8' and image_dt.year > 2012) or (lp[0].upper() == 'LE7')))] if ref_match: landsat, path = ref_match[0] else: landsat, path = 'XXX', '000' # Get Landsat type from first image in list # image_dict['LANDSAT'] = image_list[0].split('.')[0].split('_')[2] image_name_fmt = '{}_{}.{}.tif'.format( image_dt.strftime('%Y%m%d_%j'), landsat.lower(), '{}') if not landsat4_flag and landsat.upper() == 'LT4': logging.debug(' Landsat 4, skipping image') continue elif not landsat5_flag and landsat.upper() == 'LT5': logging.debug(' Landsat 5, skipping image') continue elif not landsat7_flag and landsat.upper() == 'LE7': logging.debug(' Landsat 7, skipping image') continue elif not landsat8_flag and landsat.upper() == 'LC8': logging.debug(' Landsat 8, skipping image') continue # Load the "mask" image first if it is available # The zone_mask could be applied to the mask_array here # or below where it is used to select from the image_array mask_name = image_name_fmt.format('mask') mask_path = os.path.join(images_year_ws, mask_name) if apply_mask_flag and mask_name in image_list: logging.info(' Applying mask raster: {}'.format( mask_path)) mask_input_array, mask_nodata = gdc.raster_to_array( mask_path, band=1, mask_extent=clip_extent, fill_value=None, return_nodata=True) mask_array = gdc.project_array( mask_input_array, gdal.GRA_NearestNeighbour, landsat_osr, landsat_cs, clip_extent, zone_osr, zone_cs, zone_extent, output_nodata=None) # Assume 0 and nodata indicate unmasked pixels # All other pixels are "masked" mask_array = (mask_array == 0) | (mask_array == mask_nodata) # Assume 0 and nodata indicate masked pixels # mask_array = (mask_array != 0) & (mask_array != mask_nodata) if not np.any(mask_array): logging.info(' No unmasked values') else: mask_array = np.ones(zone_mask.shape, dtype=np.bool) # Save date specific properties image_dict = dict() # Get Fmask and Cloud score separately from other bands # FMask image_name = image_name_fmt.format('fmask') image_path = os.path.join(images_year_ws, image_name) if not os.path.isfile(image_path): logging.error( ' Image {} does not exist, skipping date'.format( image_name)) continue image_input_array, image_nodata = gdc.raster_to_array( image_path, band=1, mask_extent=clip_extent, fill_value=None, return_nodata=True) fmask_array = gdc.project_array( image_input_array, gdal.GRA_NearestNeighbour, landsat_osr, landsat_cs, clip_extent, zone_osr, zone_cs, zone_extent, output_nodata=None) fmask_mask = np.copy(zone_mask) & mask_array if fmask_array.dtype in [np.float32, np.float64]: fmask_mask &= np.isfinite(fmask_array) else: fmask_mask &= fmask_array != image_nodata if not np.any(fmask_mask): logging.debug(' Empty Fmask array, skipping') continue # Convert Fmask array into a mask (1 is cloudy, 0 is clear) fmask_array = (fmask_array > 1.5) & (fmask_array < 4.5) image_dict['FMASK_COUNT'] = int(np.sum(fmask_array[fmask_mask])) image_dict['PIXEL_COUNT'] = int(np.sum(fmask_mask)) # image_dict['PIXEL_COUNT'] = int(np.sum(fmask_mask)) image_dict['MASK_COUNT'] = int(np.sum(mask_array)) # Cloud Score image_name = image_name_fmt.format('cloud_score') image_path = os.path.join(images_year_ws, image_name) image_input_array, image_nodata = gdc.raster_to_array( image_path, band=1, mask_extent=clip_extent, fill_value=None, return_nodata=True) cloud_array = gdc.project_array( image_input_array, gdal.GRA_NearestNeighbour, landsat_osr, landsat_cs, clip_extent, zone_osr, zone_cs, zone_extent, output_nodata=None) cloud_mask = np.copy(zone_mask) & mask_array if cloud_array.dtype in [np.float32, np.float64]: cloud_mask &= np.isfinite(cloud_array) else: cloud_mask &= cloud_array != image_nodata if not np.any(cloud_mask): logging.debug(' Empty Cloud Score array, skipping') continue image_dict['CLOUD_SCORE'] = float(np.mean(cloud_array[cloud_mask])) # Workflow zs_list = [ ['ts', 1, 'TS'], ['albedo_sur', 1, 'ALBEDO_SUR'], ['ndvi_toa', 1, 'NDVI_TOA'], ['ndvi_sur', 1, 'NDVI_SUR'], ['evi_sur', 1, 'EVI_SUR'], ['ndwi_green_nir_sur', 1, 'NDWI_GREEN_NIR_SUR'], ['ndwi_green_swir1_sur', 1, 'NDWI_GREEN_SWIR1_SUR'], ['ndwi_nir_swir1_sur', 1, 'NDWI_NIR_SWIR1_SUR'], ['tasseled_cap', 1, 'TC_BRIGHT'], ['tasseled_cap', 2, 'TC_GREEN'], ['tasseled_cap', 3, 'TC_WET'] ] for band_name, band_num, field in zs_list: image_name = image_name_fmt.format(band_name) logging.debug(' {} {}'.format(image_name, field)) if image_name not in image_list: logging.debug(' Image doesn\'t exist, skipping') continue image_path = os.path.join(images_year_ws, image_name) # logging.debug(' {}'.format(image_path)) image_input_array, image_nodata = gdc.raster_to_array( image_path, band=band_num, mask_extent=clip_extent, fill_value=None, return_nodata=True) # GRA_NearestNeighbour, GRA_Bilinear, GRA_Cubic, # GRA_CubicSpline image_array = gdc.project_array( image_input_array, gdal.GRA_NearestNeighbour, landsat_osr, landsat_cs, clip_extent, zone_osr, zone_cs, zone_extent, output_nodata=None) image_mask = np.copy(zone_mask) & mask_array if image_array.dtype in [np.float32, np.float64]: image_mask &= np.isfinite(image_array) else: image_mask &= image_array != image_nodata del image_input_array if fmask_flag: # Fmask array was converted into a mask # 1 for cloud, 0 for clear image_mask &= (fmask_array == 0) if acca_flag: image_mask &= (cloud_array < 50) # Skip fully masked zones # This would not work for FMASK and CLOUD_SCORE if we # weren't using nearest neighbor for resampling if not np.any(image_mask): logging.debug(' Empty array, skipping') continue image_dict[field] = float(np.mean( image_array[image_mask])) # Should check "first" image instead of Ts specifically if band_name == 'ts': image_dict['DATA_COUNT'] = int(np.sum(image_mask)) del image_array, image_mask if not image_dict: logging.debug( ' {} - no image data in zone, skipping'.format( image_str)) continue # Save date specific properties # Change fid zone strings back to integer values if zone_str.startswith('fid_'): image_dict[zone_field] = int(zone_str[4:]) else: image_dict[zone_field] = zone_str image_dict['DATE'] = image_str image_dict['LANDSAT'] = landsat.upper() image_dict['PATH'] = path image_dict['ROW'] = '000' image_dict['SCENE_ID'] = '{}{}{}{}'.format( image_dict['LANDSAT'], image_dict['PATH'], image_dict['ROW'], image_dt.strftime('%Y%j')) image_dict['YEAR'] = image_dt.year image_dict['MONTH'] = image_dt.month image_dict['DAY'] = image_dt.day image_dict['DOY'] = int(image_dt.strftime('%j')) # image_dict['PIXEL_COUNT'] = int(np.sum(zone_mask & mask_array)) # Save each row to a list output_list.append(image_dict) # Append all rows for the year to a dataframe if not output_list: logging.debug(' Empty output list, skipping') continue output_df = output_df.append(output_list, ignore_index=True) output_df.sort_values(by=['DATE'], inplace=True) logging.debug(' {}'.format(output_path)) output_df.to_csv(output_path, index=False, columns=landsat_daily_fields) # Combine/merge annual files into a single CSV logging.debug('\n Merging annual Landsat CSV files') output_df = None for year in xrange(start_year, end_year + 1): # logging.debug(' {}'.format(year)) input_path = os.path.join( landsat_output_ws, '{}_landsat_{}.csv'.format(zone_str, year)) try: input_df = pd.read_csv(input_path) except: continue try: output_df = output_df.append(input_df) except: output_df = input_df.copy() if output_df is not None and not output_df.empty: output_path = os.path.join( zone_output_ws, '{}_landsat_daily.csv'.format(zone_str)) logging.debug(' {}'.format(output_path)) output_df.sort_values(by=['DATE', 'ROW'], inplace=True) output_df.to_csv( output_path, index=False, columns=landsat_daily_fields) if gridmet_flag: logging.info(' GRIDMET ETo/PPT') # Project the zone extent to the image OSR clip_extent = gdc.project_extent( zone_extent, zone_osr, gridmet_osr, zone_cs) logging.debug(' Extent: {}'.format(clip_extent)) # clip_extent.buffer_extent(gridmet_cs) # logging.debug(' Extent: {}'.format(clip_extent)) clip_extent.adjust_to_snap('EXPAND', gridmet_x, gridmet_y, gridmet_cs) logging.debug(' Extent: {}'.format(clip_extent)) gridmet_images_ws = os.path.join(images_ws, gridmet_images_folder) if not os.path.isdir(gridmet_images_ws): logging.debug( ' GRIDMET folder doesn\'t exist, skipping\n {}'.format( gridmet_images_ws)) continue else: logging.info(' {}'.format(gridmet_images_ws)) # Create an empty dataframe output_path = os.path.join( zone_output_ws, '{}_gridmet_monthly.csv'.format(zone_str)) if os.path.isfile(output_path): if overwrite_flag: logging.debug( ' Output CSV already exists, removing\n {}'.format( output_path)) os.remove(output_path) else: logging.debug( ' Output CSV already exists, skipping\n {}'.format( output_path)) continue output_df = pd.DataFrame(columns=gridmet_monthly_fields) output_df[gridmet_int_fields] = output_df[gridmet_int_fields].astype(int) # Get list of all images image_list = [ image for image in os.listdir(gridmet_images_ws) if gridmet_image_re.match(image)] dt_list = sorted(set([ datetime.datetime(int(image[:4]), int(image[4:6]), 1) for image in image_list])) output_list = [] for image_dt in dt_list: image_str = image_dt.date().isoformat() logging.debug('{}'.format(image_dt.date())) image_name_fmt = '{}_gridmet.{}.tif'.format( image_dt.strftime('%Y%m'), '{}') # Save date specific properties image_dict = dict() # Workflow zs_list = [ ['eto', 'ETO'], ['ppt', 'PPT'], ] for band_name, field in zs_list: image_name = image_name_fmt.format(band_name) logging.debug(' {} {}'.format(image_name, field)) if image_name not in image_list: logging.debug(' Image doesn\'t exist, skipping') continue image_path = os.path.join(gridmet_images_ws, image_name) # logging.debug(' {}'.format(image_path)) image_input_array, image_nodata = gdc.raster_to_array( image_path, band=1, mask_extent=clip_extent, fill_value=None, return_nodata=True) # GRA_NearestNeighbour, GRA_Bilinear, GRA_Cubic, # GRA_CubicSpline image_array = gdc.project_array( image_input_array, gdal.GRA_NearestNeighbour, gridmet_osr, gridmet_cs, clip_extent, zone_osr, zone_cs, zone_extent, output_nodata=None) del image_input_array # Skip fully masked zones if (np.all(np.isnan(image_array)) or np.all(image_array == image_nodata)): logging.debug(' Empty array, skipping') continue image_dict[field] = np.mean(image_array[zone_mask]) del image_array if not image_dict: logging.debug( ' {} - no image data in zone, skipping'.format( image_str)) continue # Save date specific properties # Change fid zone strings back to integer values if zone_str.startswith('fid_'): image_dict[zone_field] = int(zone_str[4:]) else: image_dict[zone_field] = zone_str image_dict['DATE'] = image_str image_dict['YEAR'] = image_dt.year image_dict['MONTH'] = image_dt.month image_dict['WATER_YEAR'] = (image_dt + relativedelta(months=3)).year # Save each row to a list output_list.append(image_dict) # Append all rows for the year to a dataframe if not output_list: logging.debug(' Empty output list, skipping') continue output_df = output_df.append(output_list, ignore_index=True) output_df.sort_values(by=['DATE'], inplace=True) logging.debug(' {}'.format(output_path)) output_df.to_csv( output_path, index=False, columns=gridmet_monthly_fields) if pdsi_flag: logging.info(' GRIDMET PDSI') logging.info(' Not currently implemented')
def main(netcdf_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), etr_flag=False, eto_flag=False, start_date=None, end_date=None, extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False): """Compute daily ETr/ETo from GRIDMET data Args: netcdf_ws (str): folder of GRIDMET netcdf files ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters etr_flag (bool): if True, compute alfalfa reference ET (ETr) eto_flag (bool): if True, compute grass reference ET (ETo) start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): if True, overwrite existing files Returns: None """ logging.info('\nComputing GRIDMET ETo/ETr') np.seterr(invalid='ignore') # Compute ETr and/or ETo if not etr_flag and not eto_flag: logging.info(' ETo/ETr flag(s) not set, defaulting to ETr') etr_flag = True # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) # Save GRIDMET lat, lon, and elevation arrays elev_raster = os.path.join(ancillary_ws, 'gridmet_elev.img') lat_raster = os.path.join(ancillary_ws, 'gridmet_lat.img') # Wind speed is measured at 2m zw = 10 etr_fmt = 'etr_{}_daily_gridmet.img' eto_fmt = 'eto_{}_daily_gridmet.img' # gridmet_re = re.compile('(?P<VAR>\w+)_(?P<YEAR>\d{4}).nc') # GRIDMET band name dictionary gridmet_band_dict = dict() gridmet_band_dict['pr'] = 'precipitation_amount' gridmet_band_dict['srad'] = 'surface_downwelling_shortwave_flux_in_air' gridmet_band_dict['sph'] = 'specific_humidity' gridmet_band_dict['tmmn'] = 'air_temperature' gridmet_band_dict['tmmx'] = 'air_temperature' gridmet_band_dict['vs'] = 'wind_speed' # Get extent/geo from elevation raster gridmet_ds = gdal.Open(elev_raster) gridmet_osr = gdc.raster_ds_osr(gridmet_ds) gridmet_proj = gdc.osr_proj(gridmet_osr) gridmet_cs = gdc.raster_ds_cellsize(gridmet_ds, x_only=True) gridmet_extent = gdc.raster_ds_extent(gridmet_ds) gridmet_full_geo = gridmet_extent.geo(gridmet_cs) gridmet_x, gridmet_y = gridmet_extent.origin() gridmet_ds = None logging.debug(' Projection: {}'.format(gridmet_proj)) logging.debug(' Cellsize: {}'.format(gridmet_cs)) logging.debug(' Geo: {}'.format(gridmet_full_geo)) logging.debug(' Extent: {}'.format(gridmet_extent)) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) gridmet_extent = gdc.Extent(output_extent) gridmet_extent.adjust_to_snap('EXPAND', gridmet_x, gridmet_y, gridmet_cs) gridmet_geo = gridmet_extent.geo(gridmet_cs) logging.debug(' Geo: {}'.format(gridmet_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): gridmet_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: gridmet_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) gridmet_extent = gdc.project_extent(gridmet_extent, extent_osr, gridmet_osr, extent_cs) gridmet_extent.adjust_to_snap('EXPAND', gridmet_x, gridmet_y, gridmet_cs) gridmet_geo = gridmet_extent.geo(gridmet_cs) logging.debug(' Geo: {}'.format(gridmet_geo)) logging.debug(' Extent: {}'.format(gridmet_extent)) else: gridmet_geo = gridmet_full_geo # Get indices for slicing/clipping input arrays g_i, g_j = gdc.array_geo_offsets(gridmet_full_geo, gridmet_geo, cs=gridmet_cs) g_rows, g_cols = gridmet_extent.shape(cs=gridmet_cs) # Read the elevation and latitude arrays elev_array = gdc.raster_to_array(elev_raster, mask_extent=gridmet_extent, return_nodata=False) lat_array = gdc.raster_to_array(lat_raster, mask_extent=gridmet_extent, return_nodata=False) lat_array *= math.pi / 180 # Check elevation and latitude arrays if np.all(np.isnan(elev_array)): logging.error('\nERROR: The elevation array is all nodata, exiting\n') sys.exit() elif np.all(np.isnan(lat_array)): logging.error('\nERROR: The latitude array is all nodata, exiting\n') sys.exit() # Build output folder etr_ws = os.path.join(output_ws, 'etr') eto_ws = os.path.join(output_ws, 'eto') if etr_flag and not os.path.isdir(etr_ws): os.makedirs(etr_ws) if eto_flag and not os.path.isdir(eto_ws): os.makedirs(eto_ws) # By default, try to process all possible years if start_dt.year == end_dt.year: year_list = [str(start_dt.year)] year_list = sorted(map(str, range((start_dt.year), (end_dt.year + 1)))) # Process each year separately for year_str in year_list: logging.info("\nYear: {}".format(year_str)) year_int = int(year_str) year_days = int(dt.datetime(year_int, 12, 31).strftime('%j')) if start_dt is not None and year_int < start_dt.year: logging.debug(' Before start date, skipping') continue elif end_dt is not None and year_int > end_dt.year: logging.debug(' After end date, skipping') continue # Build input file path tmin_path = os.path.join(netcdf_ws, 'tmmn_{}.nc'.format(year_str)) tmax_path = os.path.join(netcdf_ws, 'tmmx_{}.nc'.format(year_str)) sph_path = os.path.join(netcdf_ws, 'sph_{}.nc'.format(year_str)) rs_path = os.path.join(netcdf_ws, 'srad_{}.nc'.format(year_str)) wind_path = os.path.join(netcdf_ws, 'vs_{}.nc'.format(year_str)) # Check that all input files are present missing_flag = False for input_path in [tmin_path, tmax_path, sph_path, rs_path, wind_path]: if not os.path.isfile(input_path): logging.debug( ' Input NetCDF doesn\'t exist\n {}'.format(input_path)) missing_flag = True if missing_flag: logging.debug(' skipping') continue logging.debug(" {}".format(tmin_path)) logging.debug(" {}".format(tmax_path)) logging.debug(" {}".format(sph_path)) logging.debug(" {}".format(rs_path)) logging.debug(" {}".format(wind_path)) # Create a single raster for each year with 365 bands # Each day will be stored in a separate band etr_raster = os.path.join(etr_ws, etr_fmt.format(year_str)) eto_raster = os.path.join(eto_ws, eto_fmt.format(year_str)) if etr_flag and (overwrite_flag or not os.path.isfile(etr_raster)): logging.debug(' {}'.format(etr_raster)) gdc.build_empty_raster(etr_raster, band_cnt=366, output_dtype=np.float32, output_proj=gridmet_proj, output_cs=gridmet_cs, output_extent=gridmet_extent, output_fill_flag=True) if eto_flag and (overwrite_flag or not os.path.isfile(eto_raster)): logging.debug(' {}'.format(eto_raster)) gdc.build_empty_raster(eto_raster, band_cnt=366, output_dtype=np.float32, output_proj=gridmet_proj, output_cs=gridmet_cs, output_extent=gridmet_extent, output_fill_flag=True) # DEADBEEF - Need to find a way to test if both of these conditionals # did not pass and pass logging debug message to user # Read in the GRIDMET NetCDF file tmin_nc_f = netCDF4.Dataset(tmin_path, 'r') tmax_nc_f = netCDF4.Dataset(tmax_path, 'r') sph_nc_f = netCDF4.Dataset(sph_path, 'r') rs_nc_f = netCDF4.Dataset(rs_path, 'r') wind_nc_f = netCDF4.Dataset(wind_path, 'r') logging.info(' Reading NetCDFs into memory') # Immediatly clip input arrays to save memory tmin_nc = tmin_nc_f.variables[ gridmet_band_dict['tmmn']][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() tmax_nc = tmax_nc_f.variables[ gridmet_band_dict['tmmx']][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() sph_nc = sph_nc_f.variables[gridmet_band_dict['sph']][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() rs_nc = rs_nc_f.variables[gridmet_band_dict['srad']][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() wind_nc = wind_nc_f.variables[gridmet_band_dict['vs']][:, g_i:g_i + g_cols, g_j:g_j + g_rows].copy() # tmin_nc = tmin_nc_f.variables[gridmet_band_dict['tmmn']][:] # tmax_nc = tmax_nc_f.variables[gridmet_band_dict['tmmx']][:] # sph_nc = sph_nc_f.variables[gridmet_band_dict['sph']][:] # rs_nc = rs_nc_f.variables[gridmet_band_dict['srad']][:] # wind_nc = wind_nc_f.variables[gridmet_band_dict['vs']][:] # Transpose arrays back to row x col tmin_nc = np.transpose(tmin_nc, (0, 2, 1)) tmax_nc = np.transpose(tmax_nc, (0, 2, 1)) sph_nc = np.transpose(sph_nc, (0, 2, 1)) rs_nc = np.transpose(rs_nc, (0, 2, 1)) wind_nc = np.transpose(wind_nc, (0, 2, 1)) # A numpy array is returned when slicing a masked array # if there are no masked pixels # This is a hack to force the numpy array back to a masked array # For now assume all arrays need to be converted if type(tmin_nc) != np.ma.core.MaskedArray: tmin_nc = np.ma.core.MaskedArray( tmin_nc, np.zeros(tmin_nc.shape, dtype=bool)) if type(tmax_nc) != np.ma.core.MaskedArray: tmax_nc = np.ma.core.MaskedArray( tmax_nc, np.zeros(tmax_nc.shape, dtype=bool)) if type(sph_nc) != np.ma.core.MaskedArray: sph_nc = np.ma.core.MaskedArray(sph_nc, np.zeros(sph_nc.shape, dtype=bool)) if type(rs_nc) != np.ma.core.MaskedArray: rs_nc = np.ma.core.MaskedArray(rs_nc, np.zeros(rs_nc.shape, dtype=bool)) if type(wind_nc) != np.ma.core.MaskedArray: wind_nc = np.ma.core.MaskedArray( wind_nc, np.zeros(wind_nc.shape, dtype=bool)) # Check all valid dates in the year year_dates = date_range(dt.datetime(year_int, 1, 1), dt.datetime(year_int + 1, 1, 1)) for date_dt in year_dates: if start_dt is not None and date_dt < start_dt: logging.debug(' {} - before start date, skipping'.format( date_dt.date())) continue elif end_dt is not None and date_dt > end_dt: logging.debug(' {} - after end date, skipping'.format( date_dt.date())) continue else: logging.info(' {}'.format(date_dt.date())) doy = int(date_dt.strftime('%j')) doy_i = range(1, year_days + 1).index(doy) # Arrays are being read as masked array with a fill value of -9999 # Convert to basic numpy array arrays with nan values try: tmin_ma = tmin_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue tmin_array = tmin_ma.data.astype(np.float32) tmin_nodata = float(tmin_ma.fill_value) tmin_array[tmin_array == tmin_nodata] = np.nan try: tmax_ma = tmax_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue tmax_array = tmax_ma.data.astype(np.float32) tmax_nodata = float(tmax_ma.fill_value) tmax_array[tmax_array == tmax_nodata] = np.nan try: sph_ma = sph_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue sph_array = sph_ma.data.astype(np.float32) sph_nodata = float(sph_ma.fill_value) sph_array[sph_array == sph_nodata] = np.nan try: rs_ma = rs_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue rs_array = rs_ma.data.astype(np.float32) rs_nodata = float(rs_ma.fill_value) rs_array[rs_array == rs_nodata] = np.nan try: wind_ma = wind_nc[doy_i, :, :] except IndexError: logging.info(' date not in netcdf, skipping') continue wind_array = wind_ma.data.astype(np.float32) wind_nodata = float(wind_ma.fill_value) wind_array[wind_array == wind_nodata] = np.nan del tmin_ma, tmax_ma, sph_ma, rs_ma, wind_ma # Since inputs are netcdf, need to create GDAL raster # datasets in order to use gdal_common functions # Create an in memory dataset of the full ETo array tmin_ds = gdc.array_to_mem_ds( tmin_array, output_geo=gridmet_geo, # tmin_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) tmax_ds = gdc.array_to_mem_ds( tmax_array, output_geo=gridmet_geo, # tmax_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) sph_ds = gdc.array_to_mem_ds( sph_array, output_geo=gridmet_geo, # sph_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) rs_ds = gdc.array_to_mem_ds( rs_array, output_geo=gridmet_geo, # rs_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) wind_ds = gdc.array_to_mem_ds( wind_array, output_geo=gridmet_geo, # wind_array, output_geo=gridmet_full_geo, output_proj=gridmet_proj) # Then extract the subset from the in memory dataset tmin_array = gdc.raster_ds_to_array(tmin_ds, 1, mask_extent=gridmet_extent, return_nodata=False) tmax_array = gdc.raster_ds_to_array(tmax_ds, 1, mask_extent=gridmet_extent, return_nodata=False) sph_array = gdc.raster_ds_to_array(sph_ds, 1, mask_extent=gridmet_extent, return_nodata=False) rs_array = gdc.raster_ds_to_array(rs_ds, 1, mask_extent=gridmet_extent, return_nodata=False) wind_array = gdc.raster_ds_to_array(wind_ds, 1, mask_extent=gridmet_extent, return_nodata=False) del tmin_ds, tmax_ds, sph_ds, rs_ds, wind_ds # Adjust units tmin_array -= 273.15 tmax_array -= 273.15 rs_array *= 0.0864 # ETr/ETo if etr_flag: etr_array = et_common.refet_daily_func(tmin_array, tmax_array, sph_array, rs_array, wind_array, zw, elev_array, lat_array, doy, 'ETR') if eto_flag: eto_array = et_common.refet_daily_func(tmin_array, tmax_array, sph_array, rs_array, wind_array, zw, elev_array, lat_array, doy, 'ETO') # del tmin_array, tmax_array, sph_array, rs_array, wind_array # Save the projected array as 32-bit floats if etr_flag: gdc.array_to_comp_raster(etr_array.astype(np.float32), etr_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # etr_array.astype(np.float32), etr_raster, # output_geo=gridmet_geo, output_proj=gridmet_proj, # stats_flag=stats_flag) del etr_array if eto_flag: gdc.array_to_comp_raster(eto_array.astype(np.float32), eto_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # eto_array.astype(np.float32), eto_raster, # output_geo=gridmet_geo, output_proj=gridmet_proj, # stats_flag=stats_flag) del eto_array del tmin_nc del tmax_nc del sph_nc del rs_nc del wind_nc tmin_nc_f.close() tmax_nc_f.close() sph_nc_f.close() rs_nc_f.close() wind_nc_f.close() del tmin_nc_f, tmax_nc_f, sph_nc_f, rs_nc_f, wind_nc_f if stats_flag and etr_flag: gdc.raster_statistics(etr_raster) if stats_flag and eto_flag: gdc.raster_statistics(eto_raster) logging.debug('\nScript Complete')
def metric_weather_func(output_raster, input_ws, input_re, prev_dt, next_dt, resample_method=gdal.GRA_NearestNeighbour, rounding_flag=False): """Interpolate/project/clip METRIC hourly rasters""" logging.debug(' Output: {}'.format(output_raster)) if os.path.isfile(output_raster): if overwrite_flag: logging.debug(' Overwriting output') python_common.remove_file(output_raster) else: logging.debug(' Skipping, file already exists ' + 'and overwrite is False') return False prev_ws = os.path.join(input_ws, str(prev_dt.year)) next_ws = os.path.join(input_ws, str(next_dt.year)) # Technically previous and next could come from different days # or even years, although this won't happen in the U.S. try: prev_path = [ os.path.join(prev_ws, input_name) for input_name in os.listdir(prev_ws) for input_match in [input_re.match(input_name)] if (input_match and (prev_dt.strftime('%Y%m%d') == input_match.group('YYYYMMDD')))][0] logging.debug(' Input prev: {}'.format(prev_path)) except IndexError: logging.error(' No previous hourly file') logging.error(' {}'.format(prev_dt)) return False try: next_path = [ os.path.join(next_ws, input_name) for input_name in os.listdir(next_ws) for input_match in [input_re.match(input_name)] if (input_match and (next_dt.strftime('%Y%m%d') == input_match.group('YYYYMMDD')))][0] logging.debug(' Input next: {}'.format(next_path)) except IndexError: logging.error(' No next hourly file') logging.error(' {}'.format(next_dt)) return False # Band numbers are 1's based prev_band = int(prev_dt.strftime('%H')) + 1 next_band = int(next_dt.strftime('%H')) + 1 logging.debug(' Input prev band: {}'.format(prev_band)) logging.debug(' Input next band: {}'.format(next_band)) # Read arrays prev_array = gdc.raster_to_array( prev_path, band=prev_band, mask_extent=common_gcs_extent, return_nodata=False) next_array = gdc.raster_to_array( next_path, band=next_band, mask_extent=common_gcs_extent, return_nodata=False) if not np.any(prev_array) or not np.any(next_array): logging.warning('\nWARNING: Input NLDAS array is all nodata\n') return None output_array = hourly_interpolate_func( prev_array, next_array, prev_dt, next_dt, image.acq_datetime) output_array = gdc.project_array( output_array, resample_method, input_osr, input_cs, common_gcs_extent, common_osr, env.cellsize, common_extent, output_nodata=None) # Apply common area mask output_array[~common_array] = np.nan # Reduce the file size by rounding to the nearest n digits if rounding_flag: output_array = np.around(output_array, rounding_digits) # Force output to 32-bit float gdc.array_to_raster( output_array.astype(np.float32), output_raster, output_geo=common_geo, output_proj=common_proj, stats_flag=stats_flag) del output_array return True
def main(image_ws, ini_path, blocksize=2048, smooth_flag=False, stats_flag=False, overwrite_flag=False): """Prep a Landsat scene for METRIC Args: image_ws (str): the landsat scene folder that will be prepped ini_path (str): file path of the input parameters file blocksize (int): defines the size of blocks to process smooth_flag (bool): If True, dilate/erode image to remove fringe/edge pixels stats_flag (bool): if True, compute raster statistics overwrite_flag (bool): If True, overwrite existing files Returns: True is successful """ # Open config file config = python_common.open_ini(ini_path) # Get input parameters logging.debug(' Reading Input File') calc_refl_toa_flag = python_common.read_param( 'calc_refl_toa_flag', True, config, 'INPUTS') calc_refl_toa_qa_flag = python_common.read_param( 'calc_refl_toa_qa_flag', True, config, 'INPUTS') # calc_refl_sur_ledaps_flag = python_common.read_param( # 'calc_refl_sur_ledaps_flag', False, config, 'INPUTS') # calc_refl_sur_qa_flag = python_common.read_param( # 'calc_refl_sur_qa_flag', False, config, 'INPUTS') calc_ts_bt_flag = python_common.read_param( 'calc_ts_bt_flag', True, config, 'INPUTS') # Use QA band to set common area # Fmask cloud, shadow, & snow pixels will be removed from common area calc_fmask_common_flag = python_common.read_param( 'calc_fmask_common_flag', True, config, 'INPUTS') fmask_buffer_flag = python_common.read_param( 'fmask_buffer_flag', False, config, 'INPUTS') fmask_erode_flag = python_common.read_param( 'fmask_erode_flag', False, config, 'INPUTS') if fmask_erode_flag: fmask_erode_cells = int(python_common.read_param( 'fmask_erode_cells', 10, config, 'INPUTS')) if fmask_erode_cells == 0 and fmask_erode_flag: fmask_erode_flag = False # Number of cells to buffer Fmask clouds # For now use the same buffer radius and apply to if fmask_buffer_flag: fmask_buffer_cells = int(python_common.read_param( 'fmask_buffer_cells', 25, config, 'INPUTS')) if fmask_buffer_cells == 0 and fmask_buffer_flag: fmask_buffer_flag = False # Include hand made cloud masks cloud_mask_flag = python_common.read_param( 'cloud_mask_flag', False, config, 'INPUTS') cloud_mask_ws = "" if cloud_mask_flag: cloud_mask_ws = config.get('INPUTS', 'cloud_mask_ws') # Extract separate Fmask rasters calc_fmask_flag = python_common.read_param( 'calc_fmask_flag', True, config, 'INPUTS') calc_fmask_cloud_flag = python_common.read_param( 'calc_fmask_cloud_flag', True, config, 'INPUTS') calc_fmask_snow_flag = python_common.read_param( 'calc_fmask_snow_flag', True, config, 'INPUTS') calc_fmask_water_flag = python_common.read_param( 'calc_fmask_water_flag', True, config, 'INPUTS') # Keep Landsat DN, LEDAPS, and Fmask rasters keep_dn_flag = python_common.read_param( 'keep_dn_flag', True, config, 'INPUTS') # keep_sr_flag = python_common.read_param( # 'keep_sr_flag', True, config, 'INPUTS') # For this to work I would need to pass in the metric input file # calc_elev_flag = python_common.read_param( # 'calc_elev_flag', False, config, 'INPUTS') # calc_landuse_flag = python_common.read_param( # 'calc_landuse_flag', False, config, 'INPUTS') # calc_acca_cloud_flag = python_common.read_param( # 'calc_acca_cloud_flag', True, config, 'INPUTS') # calc_acca_snow_flag = python_common.read_param( # 'calc_acca_snow_flag', True, config, 'INPUTS') # calc_ledaps_dem_land_flag = python_common.read_param( # 'calc_ledaps_dem_land_flag', False, config, 'INPUTS') # calc_ledaps_veg_flag = python_common.read_param( # 'calc_ledaps_veg_flag', False, config, 'INPUTS') # calc_ledaps_snow_flag = python_common.read_param( # 'calc_ledaps_snow_flag', False, config, 'INPUTS') # calc_ledaps_land_flag = python_common.read_param( # 'calc_ledaps_land_flag', False, config, 'INPUTS') # calc_ledaps_cloud_flag = python_common.read_param( # 'calc_ledaps_cloud_flag', False, config, 'INPUTS') # Interpolate/clip/project hourly rasters for each Landsat scene # calc_metric_flag = python_common.read_param( # 'calc_metric_flag', False, config, 'INPUTS') calc_metric_ea_flag = python_common.read_param( 'calc_metric_ea_flag', False, config, 'INPUTS') calc_metric_wind_flag = python_common.read_param( 'calc_metric_wind_flag', False, config, 'INPUTS') calc_metric_etr_flag = python_common.read_param( 'calc_metric_etr_flag', False, config, 'INPUTS') calc_metric_tair_flag = python_common.read_param( 'calc_metric_tair_flag', False, config, 'INPUTS') # Interpolate/clip/project AWC and daily ETr/PPT rasters # to compute SWB Ke for each Landsat scene calc_swb_ke_flag = python_common.read_param( 'calc_swb_ke_flag', False, config, 'INPUTS') if cloud_mask_flag: spinup_days = python_common.read_param( 'swb_spinup_days', 30, config, 'INPUTS') min_spinup_days = python_common.read_param( 'swb_min_spinup_days', 5, config, 'INPUTS') # Round ea raster to N digits to save space rounding_digits = python_common.read_param( 'rounding_digits', 3, config, 'INPUTS') env = gdc.env image = et_image.Image(image_ws, env) np.seterr(invalid='ignore', divide='ignore') gdal.UseExceptions() # Input file paths dn_image_dict = et_common.landsat_band_image_dict( image.orig_data_ws, image.image_re) # # Open METRIC config file # if config_file: # logging.info( # log_f.format('METRIC INI File:', os.path.basename(config_file))) # config = configparser.ConfigParser() # try: # config.read(config_file) # except: # logging.error('\nERROR: Config file could not be read, ' + # 'is not an input file, or does not exist\n' + # 'ERROR: config_file = {}\n').format(config_file) # sys.exit() # # Overwrite # overwrite_flag = read_param('overwrite_flag', True, config) # # # Elevation and landuse parameters/flags from METRIC input file # calc_elev_flag = read_param('save_dem_raster_flag', True, config) # calc_landuse_flag = read_param( # 'save_landuse_raster_flag', True, config) # if calc_elev_flag: # elev_pr_path = config.get('INPUTS','dem_raster') # if calc_landuse_flag: # landuse_pr_path = config.get('INPUTS', 'landuse_raster') # else: # overwrite_flag = False # calc_elev_flag = False # calc_landuse_flag = False # # Elev raster must exist # if calc_elev_flag and not os.path.isfile(elev_pr_path): # logging.error('\nERROR: Elevation raster {} does not exist\n'.format( # elev_pr_path)) # return False # Landuse raster must exist # if calc_landuse_flag and not os.path.isfile(landuse_pr_path): # logging.error('\nERROR: Landuse raster {} does not exist\n'.format( # landuse_pr_path)) # return False # Removing ancillary files before checking for inputs if os.path.isdir(os.path.join(image.orig_data_ws, 'gap_mask')): shutil.rmtree(os.path.join(image.orig_data_ws, 'gap_mask')) for item in os.listdir(image.orig_data_ws): if (image.type == 'Landsat7' and (item.endswith('_B8.TIF') or item.endswith('_B6_VCID_2.TIF'))): os.remove(os.path.join(image.orig_data_ws, item)) elif (image.type == 'Landsat8' and (item.endswith('_B1.TIF') or item.endswith('_B8.TIF') or item.endswith('_B9.TIF') or item.endswith('_B11.TIF'))): os.remove(os.path.join(image.orig_data_ws, item)) elif (item.endswith('_VER.jpg') or item.endswith('_VER.txt') or item.endswith('_GCP.txt') or item == 'README.GTF'): os.remove(os.path.join(image.orig_data_ws, item)) # Check correction level (image must be L1T to process) if image.correction != 'L1TP': logging.debug(' Image is not L1TP corrected, skipping') return False # calc_fmask_common_flag = False # calc_refl_toa_flag = False # calc_ts_bt_flag = False # calc_metric_ea_flag = False # calc_metric_wind_flag = False # calc_metric_etr_flag = False # overwrite_flag = False # QA band must exist if (calc_fmask_common_flag and image.qa_band not in dn_image_dict.keys()): logging.warning( ('\nQA band does not exist but calc_fmask_common_flag=True' + '\n Setting calc_fmask_common_flag=False\n {}').format( os.path.basename(image.qa_input_raster))) calc_fmask_common_flag = False if cloud_mask_flag and not os.path.isdir(cloud_mask_ws): logging.warning( ('\ncloud_mask_ws is not a directory but cloud_mask_flag=True.' + '\n Setting cloud_mask_flag=False\n {}').format(cloud_mask_ws)) cloud_mask_flag = False # Check for Landsat TOA images if (calc_refl_toa_flag and (set(list(image.band_toa_dict.keys()) + [image.thermal_band, image.qa_band]) != set(dn_image_dict.keys()))): logging.warning( '\nMissing Landsat images but calc_refl_toa_flag=True' + '\n Setting calc_refl_toa_flag=False') calc_refl_toa_flag = False # Check for Landsat brightness temperature image if calc_ts_bt_flag and image.thermal_band not in dn_image_dict.keys(): logging.warning( '\nThermal band image does not exist but calc_ts_bt_flag=True' + '\n Setting calc_ts_bt_flag=False') calc_ts_bt_flag = False # DEADBEEF - Should the function return False if Ts doesn't exist? # return False # Check for METRIC hourly/daily input folders if calc_metric_ea_flag: metric_ea_input_ws = config.get('INPUTS', 'metric_ea_input_folder') if not os.path.isdir(metric_ea_input_ws): logging.warning( ('\nHourly Ea folder does not exist but calc_metric_ea_flag=True' + '\n Setting calc_metric_ea_flag=False\n {}').format( metric_ea_input_ws)) calc_metric_ea_flag = False if calc_metric_wind_flag: metric_wind_input_ws = config.get('INPUTS', 'metric_wind_input_folder') if not os.path.isdir(metric_wind_input_ws): logging.warning( ('\nHourly wind folder does not exist but calc_metric_wind_flag=True' + '\n Setting calc_metric_wind_flag=False\n {}').format( metric_wind_input_ws)) calc_metric_wind_flag = False if calc_metric_etr_flag: metric_etr_input_ws = config.get('INPUTS', 'metric_etr_input_folder') if not os.path.isdir(metric_etr_input_ws): logging.warning( ('\nHourly ETr folder does not exist but calc_metric_etr_flag=True' + '\n Setting calc_metric_etr_flag=False\n {}').format( metric_etr_input_ws)) calc_metric_etr_flag = False if calc_metric_tair_flag: metric_tair_input_ws = config.get('INPUTS', 'metric_tair_input_folder') if not os.path.isdir(metric_tair_input_ws): logging.warning( ('\nHourly Tair folder does not exist but calc_metric_tair_flag=True' + '\n Setting calc_metric_tair_flag=False\n {}').format( metric_tair_input_ws)) calc_metric_tair_flag = False if (calc_metric_ea_flag or calc_metric_wind_flag or calc_metric_etr_flag or calc_metric_tair_flag): metric_hourly_re = re.compile(config.get('INPUTS', 'metric_hourly_re')) metric_daily_re = re.compile(config.get('INPUTS', 'metric_daily_re')) if calc_swb_ke_flag: awc_input_path = config.get('INPUTS', 'awc_input_path') etr_input_ws = config.get('INPUTS', 'etr_input_folder') ppt_input_ws = config.get('INPUTS', 'ppt_input_folder') etr_input_re = re.compile(config.get('INPUTS', 'etr_input_re')) ppt_input_re = re.compile(config.get('INPUTS', 'ppt_input_re')) if not os.path.isfile(awc_input_path): logging.warning( ('\nAWC raster does not exist but calc_swb_ke_flag=True' + '\n Setting calc_swb_ke_flag=False\n {}').format( awc_input_path)) calc_swb_ke_flag = False if not os.path.isdir(etr_input_ws): logging.warning( ('\nDaily ETr folder does not exist but calc_swb_ke_flag=True' + '\n Setting calc_swb_ke_flag=False\n {}').format( etr_input_ws)) calc_swb_ke_flag = False if not os.path.isdir(ppt_input_ws): logging.warning( ('\nDaily PPT folder does not exist but calc_swb_ke_flag=True' + '\n Setting calc_swb_ke_flag=False\n {}').format( ppt_input_ws)) calc_swb_ke_flag = False # Build folders for support rasters if ((calc_fmask_common_flag or calc_refl_toa_flag or # calc_refl_sur_ledaps_flag or calc_ts_bt_flag or calc_metric_ea_flag or calc_metric_wind_flag or calc_metric_etr_flag or calc_metric_tair_flag or calc_swb_ke_flag) and not os.path.isdir(image.support_ws)): os.makedirs(image.support_ws) if calc_refl_toa_flag and not os.path.isdir(image.refl_toa_ws): os.makedirs(image.refl_toa_ws) # if calc_refl_sur_ledaps_flag and not os.path.isdir(image.refl_sur_ws): # os.makedirs(image.refl_sur_ws) # Apply overwrite flag if overwrite_flag: overwrite_list = [ image.fmask_cloud_raster, image.fmask_snow_raster, image.fmask_water_raster # image.elev_raster, image.landuse_raster # image.common_area_raster ] for overwrite_path in overwrite_list: try: python_common.remove_file(image.fmask_cloud_raster) except: pass # Use QA band to build common area rasters logging.info('\nCommon Area Raster') qa_ds = gdal.Open(dn_image_dict[image.qa_band], 0) common_geo = gdc.raster_ds_geo(qa_ds) common_extent = gdc.raster_ds_extent(qa_ds) common_proj = gdc.raster_ds_proj(qa_ds) common_osr = gdc.raster_ds_osr(qa_ds) # Initialize common_area as all non-fill QA values qa_array = gdc.raster_ds_to_array(qa_ds, return_nodata=False) common_array = qa_array != 1 common_rows, common_cols = common_array.shape del qa_ds # First try applying user defined cloud masks cloud_mask_path = os.path.join( cloud_mask_ws, image.folder_id + '_mask.shp') if cloud_mask_flag and os.path.isfile(cloud_mask_path): logging.info(' Applying cloud mask shapefile') feature_path = os.path.join( cloud_mask_ws, (image.folder_id + '_mask.shp')) logging.info(' {}'.format(feature_path)) cloud_mask_memory_ds = gdc.polygon_to_raster_ds( feature_path, nodata_value=0, burn_value=1, output_osr=common_osr, output_cs=30, output_extent=common_extent) cloud_array = gdc.raster_ds_to_array( cloud_mask_memory_ds, return_nodata=False) # DEADBEEF - If user sets a cloud mask, # it is probably better than Fmask # Eventually change "if" calc_fmask_common_flag: to "elif" common_array[cloud_array == 1] = 0 del cloud_mask_memory_ds, cloud_array if calc_fmask_common_flag: fmask_array = et_numpy.bqa_fmask_func(qa_array) fmask_mask = (fmask_array >= 2) & (fmask_array <= 4) if fmask_erode_flag: logging.info( (' Eroding and dilating Fmask clouds, shadows, and snow ' + '{} cells\n to remove errantly masked pixels.').format( fmask_erode_cells)) fmask_mask = ndimage.binary_erosion( fmask_mask, iterations=fmask_erode_cells, structure=ndimage.generate_binary_structure(2, 2)) fmask_mask = ndimage.binary_dilation( fmask_mask, iterations=fmask_erode_cells, structure=ndimage.generate_binary_structure(2, 2)) if fmask_buffer_flag: logging.info( (' Buffering Fmask clouds, shadows, and snow ' + '{} cells').format(fmask_buffer_cells)) # Only buffer clouds, shadow, and snow (not water or nodata) if fmask_mask is None: fmask_mask = (fmask_array >= 2) & (fmask_array <= 4) fmask_mask = ndimage.binary_dilation( fmask_mask, iterations=fmask_buffer_cells, structure=ndimage.generate_binary_structure(2, 2)) # Reset common_array for buffered cells common_array[fmask_mask] = 0 del fmask_array, fmask_mask if common_array is not None: # Erode and dilate to remove fringe on edge # Default is to not smooth, but user can force smoothing if smooth_flag: common_array = smooth_func(common_array) # Check that there are some cloud free pixels if not np.any(common_array): logging.error(' ERROR: There are no cloud/snow free pixels') return False # Always overwrite common area raster # if not os.path.isfile(image.common_area_raster): gdc.array_to_raster( common_array, image.common_area_raster, output_geo=common_geo, output_proj=common_proj, stats_flag=stats_flag) # Print common geo/extent logging.debug(' Common geo: {}'.format(common_geo)) logging.debug(' Common extent: {}'.format(common_extent)) # Extract Fmask components as separate rasters if (calc_fmask_flag or calc_fmask_cloud_flag or calc_fmask_snow_flag or calc_fmask_water_flag): logging.info('\nFmask') fmask_array = et_numpy.bqa_fmask_func(qa_array) # Save Fmask data as separate rasters if (calc_fmask_flag and not os.path.isfile(image.fmask_output_raster)): gdc.array_to_raster( fmask_array.astype(np.uint8), image.fmask_output_raster, output_geo=common_geo, output_proj=common_proj, mask_array=None, output_nodata=255, stats_flag=stats_flag) if (calc_fmask_cloud_flag and not os.path.isfile(image.fmask_cloud_raster)): fmask_cloud_array = (fmask_array == 2) | (fmask_array == 4) gdc.array_to_raster( fmask_cloud_array.astype(np.uint8), image.fmask_cloud_raster, output_geo=common_geo, output_proj=common_proj, mask_array=None, output_nodata=255, stats_flag=stats_flag) del fmask_cloud_array if (calc_fmask_snow_flag and not os.path.isfile(image.fmask_snow_raster)): fmask_snow_array = (fmask_array == 3) gdc.array_to_raster( fmask_snow_array.astype(np.uint8), image.fmask_snow_raster, output_geo=common_geo, output_proj=common_proj, mask_array=None, output_nodata=255, stats_flag=stats_flag) del fmask_snow_array if (calc_fmask_water_flag and not os.path.isfile(image.fmask_water_raster)): fmask_water_array = (fmask_array == 1) gdc.array_to_raster( fmask_water_array.astype(np.uint8), image.fmask_water_raster, output_geo=common_geo, output_proj=common_proj, mask_array=None, output_nodata=255, stats_flag=stats_flag) del fmask_water_array del fmask_array # # Calculate elevation # if calc_elev_flag and not os.path.isfile(elev_path): # logging.info('Elevation') # elev_array, elev_nodata = gdc.raster_to_array( # elev_pr_path, 1, common_extent) # gdc.array_to_raster( # elev_array, elev_raster, # output_geo=common_geo, output_proj=env.snap_proj, # mask_array=common_array, stats_flag=stats_flag) # del elev_array, elev_nodata, elev_path # # # Calculate landuse # if calc_landuse_flag and not os.path.isfile(landuse_raster): # logging.info('Landuse') # landuse_array, landuse_nodata = gdc.raster_to_array( # landuse_pr_path, 1, common_extent) # gdc.array_to_raster( # landuse_array, landuse_raster, # output_geo=common_geo, output_proj=env.snap_proj, # mask_array=common_array, stats_flag=stats_flag) # del landuse_array, landuse_nodata, landuse_raster # Calculate toa reflectance # f32_gtype, f32_nodata = numpy_to_gdal_type(np.float32) if calc_refl_toa_flag: logging.info('Top-of-Atmosphere Reflectance') if os.path.isfile(image.refl_toa_raster) and overwrite_flag: logging.debug(' Overwriting: {}'.format( image.refl_toa_raster)) python_common.remove_file(image.refl_toa_raster) if not os.path.isfile(image.refl_toa_raster): # First build empty composite raster gdc.build_empty_raster( image.refl_toa_raster, image.band_toa_cnt, np.float32, None, env.snap_proj, env.cellsize, common_extent) # cos_theta_solar_flt = et_common.cos_theta_solar_func( # image.sun_elevation) # Process by block logging.info('Processing by block') logging.debug(' Mask cols/rows: {}/{}'.format( common_cols, common_rows)) for b_i, b_j in gdc.block_gen(common_rows, common_cols, blocksize): logging.debug(' Block y: {:5d} x: {:5d}'.format(b_i, b_j)) block_data_mask = gdc.array_to_block( common_array, b_i, b_j, blocksize).astype(np.bool) block_rows, block_cols = block_data_mask.shape block_geo = gdc.array_offset_geo(common_geo, b_j, b_i) block_extent = gdc.geo_extent( block_geo, block_rows, block_cols) logging.debug(' Block rows: {} cols: {}'.format( block_rows, block_cols)) logging.debug(' Block extent: {}'.format(block_extent)) logging.debug(' Block geo: {}'.format(block_geo)) # Process each TOA band # for band, band_i in sorted(image.band_toa_dict.items()): for band, dn_image in sorted(dn_image_dict.items()): if band not in image.band_toa_dict.keys(): continue # thermal_band_flag = (band == image.thermal_band) # Set 0 as nodata value gdc.raster_path_set_nodata(dn_image, 0) # Calculate TOA reflectance dn_array, dn_nodata = gdc.raster_to_array( dn_image, 1, block_extent) dn_array = dn_array.astype(np.float64) # dn_array = dn_array.astype(np.float32) dn_array[dn_array == 0] = np.nan # if image.type in ['Landsat4', 'Landsat5', 'Landsat7']: refl_toa_array = et_numpy.l457_refl_toa_band_func( dn_array, image.cos_theta_solar, image.dr, image.esun_dict[band], image.lmin_dict[band], image.lmax_dict[band], image.qcalmin_dict[band], image.qcalmax_dict[band]) elif image.type in ['Landsat8']: refl_toa_array = et_numpy.l8_refl_toa_band_func( dn_array, image.cos_theta_solar, image.refl_mult_dict[band], image.refl_add_dict[band]) # if (image.type in ['Landsat4', 'Landsat5', 'Landsat7'] and # not thermal_band_flag): # refl_toa_array = et_numpy.l457_refl_toa_band_func( # dn_array, image.cos_theta_solar, # image.dr, image.esun_dict[band], # image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], # image.qcalmax_dict[band]) # # image.rad_mult_dict[band], # # image.rad_add_dict[band]) # elif (image.type in ['Landsat8'] and # not thermal_band_flag): # refl_toa_array = et_numpy.l8_refl_toa_band_func( # dn_array, image.cos_theta_solar, # image.refl_mult_dict[band], # image.refl_add_dict[band]) # elif (image.type in ['Landsat4', 'Landsat5', 'Landsat7'] and # thermal_band_flag): # refl_toa_array = et_numpy.l457_ts_bt_band_func( # dn_array, # image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], # image.qcalmax_dict[band], # # image.rad_mult_dict[band], # # image.rad_add_dict[band], # image.k1_dict[band], image.k2_dict[band]) # elif (image.type in ['Landsat8'] and # thermal_band_flag): # refl_toa_array = et_numpy.l8_ts_bt_band_func( # dn_array, # image.rad_mult_dict[band], # image.rad_add_dict[band], # image.k1_dict[band], image.k2_dict[band]) # refl_toa_array = et_numpy.refl_toa_band_func( # dn_array, cos_theta_solar_flt, # image.dr, image.esun_dict[band], # image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], image.qcalmax_dict[band], # thermal_band_flag) gdc.block_to_raster( refl_toa_array.astype(np.float32), image.refl_toa_raster, b_i, b_j, band=image.band_toa_dict[band]) # gdc.array_to_comp_raster( # refl_toa_array.astype(np.float32), # image.refl_toa_raster, # image.band_toa_dict[band], common_array) del refl_toa_array, dn_array if stats_flag: gdc.raster_statistics(image.refl_toa_raster) # # Process each TOA band # # for band, band_i in sorted(image.band_toa_dict.items()): # for band, dn_image in sorted(dn_image_dict.items()): # thermal_band_flag = (band == image.thermal_band) # # Set 0 as nodata value # gdc.raster_path_set_nodata(dn_image, 0) # # Calculate TOA reflectance # dn_array, dn_nodata = gdc.raster_to_array( # dn_image, 1, common_extent) # dn_array = dn_array.astype(np.float64) # # dn_array = dn_array.astype(np.float32) # dn_array[dn_array == 0] = np.nan # # # if (image.type in ['Landsat4', 'Landsat5', 'Landsat7'] and # not thermal_band_flag): # refl_toa_array = et_numpy.l457_refl_toa_band_func( # dn_array, image.cos_theta_solar, # image.dr, image.esun_dict[band], # image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], image.qcalmax_dict[band]) # # image.rad_mult_dict[band], image.rad_add_dict[band]) # elif (image.type in ['Landsat4', 'Landsat5', 'Landsat7'] and # thermal_band_flag): # refl_toa_array = et_numpy.l457_ts_bt_band_func( # dn_array, image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], image.qcalmax_dict[band], # # image.rad_mult_dict[band], image.rad_add_dict[band], # image.k1_dict[band], image.k2_dict[band]) # elif (image.type in ['Landsat8'] and # not thermal_band_flag): # refl_toa_array = et_numpy.l8_refl_toa_band_func( # dn_array, image.cos_theta_solar, # image.refl_mult_dict[band], image.refl_add_dict[band]) # elif (image.type in ['Landsat8'] and # thermal_band_flag): # refl_toa_array = et_numpy.l8_ts_bt_band_func( # dn_array, # image.rad_mult_dict[band], image.rad_add_dict[band], # image.k1_dict[band], image.k2_dict[band]) # # refl_toa_array = et_numpy.refl_toa_band_func( # # dn_array, cos_theta_solar_flt, # # image.dr, image.esun_dict[band], # # image.lmin_dict[band], image.lmax_dict[band], # # image.qcalmin_dict[band], image.qcalmax_dict[band], # # thermal_band_flag) # gdc.array_to_comp_raster( # refl_toa_array.astype(np.float32), image.refl_toa_raster, # image.band_toa_dict[band], common_array) # del refl_toa_array, dn_array # Calculate brightness temperature if calc_ts_bt_flag: logging.info('Brightness Temperature') if os.path.isfile(image.ts_bt_raster) and overwrite_flag: logging.debug(' Overwriting: {}'.format(image.ts_bt_raster)) python_common.remove_file(image.ts_bt_raster) if not os.path.isfile(image.ts_bt_raster): band = image.thermal_band thermal_dn_path = dn_image_dict[band] gdc.raster_path_set_nodata(thermal_dn_path, 0) thermal_dn_array, thermal_dn_nodata = gdc.raster_to_array( thermal_dn_path, 1, common_extent, return_nodata=True) thermal_dn_mask = thermal_dn_array != thermal_dn_nodata if image.type in ['Landsat4', 'Landsat5', 'Landsat7']: ts_bt_array = et_numpy.l457_ts_bt_band_func( thermal_dn_array, image.lmin_dict[band], image.lmax_dict[band], image.qcalmin_dict[band], image.qcalmax_dict[band], # image.rad_mult_dict[band], image.rad_add_dict[band], image.k1_dict[band], image.k2_dict[band]) elif image.type in ['Landsat8']: ts_bt_array = et_numpy.l8_ts_bt_band_func( thermal_dn_array, image.rad_mult_dict[band], image.rad_add_dict[band], image.k1_dict[band], image.k2_dict[band]) # thermal_rad_array = et_numpy.refl_toa_band_func( # thermal_dn_array, image.cos_theta_solar, # image.dr, image.esun_dict[band], # image.lmin_dict[band], image.lmax_dict[band], # image.qcalmin_dict[band], image.qcalmax_dict[band], # thermal_band_flag=True) # ts_bt_array = et_numpy.ts_bt_func( # thermal_rad_array, image.k1_dict[image.thermal_band], # image.k2_dict[image.thermal_band]) ts_bt_array[~thermal_dn_mask] = np.nan gdc.array_to_raster( ts_bt_array, image.ts_bt_raster, output_geo=common_geo, output_proj=env.snap_proj, # mask_array=common_array, stats_flag=stats_flag) # del thermal_dn_array, thermal_rad_array del thermal_dn_path, thermal_dn_array, ts_bt_array # Interpolate/project/clip METRIC hourly/daily rasters if (calc_metric_ea_flag or calc_metric_wind_flag or calc_metric_etr_flag): logging.info('METRIC hourly/daily rasters') # Get bracketing hours from image acquisition time image_prev_dt = image.acq_datetime.replace( minute=0, second=0, microsecond=0) image_next_dt = image_prev_dt + timedelta(seconds=3600) # Get NLDAS properties from one of the images input_ws = os.path.join( metric_etr_input_ws, str(image_prev_dt.year)) try: input_path = [ os.path.join(input_ws, file_name) for file_name in os.listdir(input_ws) for match in [metric_hourly_re.match(file_name)] if (match and (image_prev_dt.strftime('%Y%m%d') == match.group('YYYYMMDD')))][0] except IndexError: logging.error(' No hourly file for {}'.format( image_prev_dt.strftime('%Y-%m-%d %H00'))) return False try: input_ds = gdal.Open(input_path) input_osr = gdc.raster_ds_osr(input_ds) # input_proj = gdc.osr_proj(input_osr) input_extent = gdc.raster_ds_extent(input_ds) input_cs = gdc.raster_ds_cellsize(input_ds, x_only=True) # input_geo = input_extent.geo(input_cs) input_x, input_y = input_extent.origin() input_ds = None except: logging.error(' Could not get default input image properties') logging.error(' {}'.format(input_path)) return False # Project Landsat scene extent to NLDAS GCS common_gcs_osr = common_osr.CloneGeogCS() common_gcs_extent = gdc.project_extent( common_extent, common_osr, common_gcs_osr, cellsize=env.cellsize) common_gcs_extent.buffer_extent(0.1) common_gcs_extent.adjust_to_snap( 'EXPAND', input_x, input_y, input_cs) # common_gcs_geo = common_gcs_extent.geo(input_cs) def metric_weather_func(output_raster, input_ws, input_re, prev_dt, next_dt, resample_method=gdal.GRA_NearestNeighbour, rounding_flag=False): """Interpolate/project/clip METRIC hourly rasters""" logging.debug(' Output: {}'.format(output_raster)) if os.path.isfile(output_raster): if overwrite_flag: logging.debug(' Overwriting output') python_common.remove_file(output_raster) else: logging.debug(' Skipping, file already exists ' + 'and overwrite is False') return False prev_ws = os.path.join(input_ws, str(prev_dt.year)) next_ws = os.path.join(input_ws, str(next_dt.year)) # Technically previous and next could come from different days # or even years, although this won't happen in the U.S. try: prev_path = [ os.path.join(prev_ws, input_name) for input_name in os.listdir(prev_ws) for input_match in [input_re.match(input_name)] if (input_match and (prev_dt.strftime('%Y%m%d') == input_match.group('YYYYMMDD')))][0] logging.debug(' Input prev: {}'.format(prev_path)) except IndexError: logging.error(' No previous hourly file') logging.error(' {}'.format(prev_dt)) return False try: next_path = [ os.path.join(next_ws, input_name) for input_name in os.listdir(next_ws) for input_match in [input_re.match(input_name)] if (input_match and (next_dt.strftime('%Y%m%d') == input_match.group('YYYYMMDD')))][0] logging.debug(' Input next: {}'.format(next_path)) except IndexError: logging.error(' No next hourly file') logging.error(' {}'.format(next_dt)) return False # Band numbers are 1's based prev_band = int(prev_dt.strftime('%H')) + 1 next_band = int(next_dt.strftime('%H')) + 1 logging.debug(' Input prev band: {}'.format(prev_band)) logging.debug(' Input next band: {}'.format(next_band)) # Read arrays prev_array = gdc.raster_to_array( prev_path, band=prev_band, mask_extent=common_gcs_extent, return_nodata=False) next_array = gdc.raster_to_array( next_path, band=next_band, mask_extent=common_gcs_extent, return_nodata=False) if not np.any(prev_array) or not np.any(next_array): logging.warning('\nWARNING: Input NLDAS array is all nodata\n') return None output_array = hourly_interpolate_func( prev_array, next_array, prev_dt, next_dt, image.acq_datetime) output_array = gdc.project_array( output_array, resample_method, input_osr, input_cs, common_gcs_extent, common_osr, env.cellsize, common_extent, output_nodata=None) # Apply common area mask output_array[~common_array] = np.nan # Reduce the file size by rounding to the nearest n digits if rounding_flag: output_array = np.around(output_array, rounding_digits) # Force output to 32-bit float gdc.array_to_raster( output_array.astype(np.float32), output_raster, output_geo=common_geo, output_proj=common_proj, stats_flag=stats_flag) del output_array return True # Ea - Project to Landsat scene after clipping if calc_metric_ea_flag: logging.info(' Hourly vapor pressure (Ea)') metric_weather_func( image.metric_ea_raster, metric_ea_input_ws, metric_hourly_re, image_prev_dt, image_next_dt, gdal.GRA_Bilinear, rounding_flag=True) # Wind - Project to Landsat scene after clipping if calc_metric_wind_flag: logging.info(' Hourly windspeed') metric_weather_func( image.metric_wind_raster, metric_wind_input_ws, metric_hourly_re, image_prev_dt, image_next_dt, gdal.GRA_NearestNeighbour, rounding_flag=False) # ETr - Project to Landsat scene after clipping if calc_metric_etr_flag: logging.info(' Hourly reference ET (ETr)') metric_weather_func( image.metric_etr_raster, metric_etr_input_ws, metric_hourly_re, image_prev_dt, image_next_dt, gdal.GRA_NearestNeighbour, rounding_flag=False) # ETr 24hr - Project to Landsat scene after clipping if calc_metric_etr_flag: logging.info(' Daily reference ET (ETr)') logging.debug(' Output: {}'.format( image.metric_etr_24hr_raster)) if (os.path.isfile(image.metric_etr_24hr_raster) and overwrite_flag): logging.debug(' Overwriting output') os.remove(image.metric_etr_24hr_raster) if not os.path.isfile(image.metric_etr_24hr_raster): etr_prev_ws = os.path.join( metric_etr_input_ws, str(image_prev_dt.year)) try: input_path = [ os.path.join(etr_prev_ws, file_name) for file_name in os.listdir(etr_prev_ws) for match in [metric_daily_re.match(file_name)] if (match and (image_prev_dt.strftime('%Y%m%d') == match.group('YYYYMMDD')))][0] logging.debug(' Input: {}'.format(input_path)) except IndexError: logging.error(' No daily file for {}'.format( image_prev_dt.strftime('%Y-%m-%d'))) return False output_array = gdc.raster_to_array( input_path, mask_extent=common_gcs_extent, return_nodata=False) output_array = gdc.project_array( output_array, gdal.GRA_NearestNeighbour, input_osr, input_cs, common_gcs_extent, common_osr, env.cellsize, common_extent, output_nodata=None) # Apply common area mask output_array[~common_array] = np.nan # Reduce the file size by rounding to the nearest n digits # output_array = np.around(output_array, rounding_digits) gdc.array_to_raster( output_array, image.metric_etr_24hr_raster, output_geo=common_geo, output_proj=common_proj, stats_flag=stats_flag) del output_array del input_path # Tair - Project to Landsat scene after clipping if calc_metric_tair_flag: logging.info(' Hourly air temperature (Tair)') metric_weather_func( image.metric_tair_raster, metric_tair_input_ws, metric_hourly_re, image_prev_dt, image_next_dt, gdal.GRA_NearestNeighbour, rounding_flag=False) # Cleanup del image_prev_dt, image_next_dt # Soil Water Balance if calc_swb_ke_flag: logging.info('Daily soil water balance') # Check if output file already exists logging.debug(' Ke: {}'.format(image.ke_raster)) if os.path.isfile(image.ke_raster): if overwrite_flag: logging.debug(' Overwriting output') python_common.remove_file(image.ke_raster) else: logging.debug(' Skipping, file already ' + 'exists and overwrite is False') return False ke_array = et_common.raster_swb_func( image.acq_datetime, common_osr, env.cellsize, common_extent, awc_input_path, etr_input_ws, etr_input_re, ppt_input_ws, ppt_input_re, spinup_days=spinup_days, min_spinup_days=min_spinup_days) # Apply common area mask ke_array[~common_array] = np.nan # Reduce the file size by rounding to the nearest 2 digits np.around(ke_array, 2, out=ke_array) # Force output to 32-bit float gdc.array_to_raster( ke_array.astype(np.float32), image.ke_raster, output_geo=common_geo, output_proj=common_proj, stats_flag=stats_flag) # Remove Landsat TOA rasters if not keep_dn_flag: for landsat_item in python_common.build_file_list( image.orig_data_ws, image.image_re): os.remove(os.path.join(image.orig_data_ws, landsat_item)) return True
def monte_carlo(image_ws, metric_ini_path, mc_ini_path, mc_iter=None, cold_tgt_pct=None, hot_tgt_pct=None, groupsize=64, blocksize=4096, multipoint_flag=False, shapefile_flag=False, stats_flag=False, overwrite_flag=False, debug_flag=False, no_etrf_final_plots=None, no_etrf_temp_plots=None): """METRIC Monte Carlo Args: image_ws (str): the workspace (path) of the landsat scene folder metric_ini_path (str): the METRIC config file (path) mc_ini_path (str): the Monte Carlo config file (path) mc_iter (int): iteration number for Monte Carlo processing cold_tgt_pct (float): target percentage of pixels with ETrF > than cold Kc hot_tgt_pct (float): target percentage of pixels with ETrF < than hot Kc groupsize (int): Script will try to place calibration point randomly into a labeled group of clustered values with at least n pixels. -1 = In the largest group 0 = Anywhere in the image (not currently implemented) 1 >= In any group with a pixel count greater or equal to n blocksize (int): processing block size shapefile_flag (bool): if True, save calibration points to shapefile multipoint_flag (bool): if True, save cal. points to multipoint shapefile stats_flag (bool): if True, compute raster statistics ovewrite_flag (bool): if True, overwrite existing files debug_flag (bool): if True, enable debug level logging no_final_plots (bool): if True, don't save final ETrF histograms This will override the flag in the INI file no_temp_plots (bool): if True, don't save temp ETrF histograms This will override the flag in the INI file Returns: None """ logging.info('METRIC Automated Calibration') # Open config file config = open_ini(mc_ini_path) # Get input parameters logging.debug(' Reading Input File') etrf_training_path = config.get('INPUTS', 'etrf_training_path') # Adjust Kc cold target value based on day of year # etrf_doy_adj_path = read_param( # 'etrf_doy_adj_path', None, config, 'INPUTS') # Intentionally set default to None, to trigger error in eval call kc_cold_doy_dict = read_param('kc_cold_doy_dict', None, config, 'INPUTS') kc_hot_doy_dict = read_param('kc_hot_doy_dict', None, config, 'INPUTS') # If the "no_" flags were set True, honor them and set the flag False # If the "no_" flags were not set by the user, use the INI flag values # If not set in the INI, default to False (don't save any plots) if no_etrf_temp_plots: save_etrf_temp_plots = False else: save_etrf_temp_plots = read_param('save_etrf_temp_plots', False, config, 'INPUTS') if no_etrf_final_plots: save_etrf_final_plots = False else: save_etrf_final_plots = read_param('save_etrf_final_plots', False, config, 'INPUTS') save_ndvi_plots = read_param('save_ndvi_plots', False, config, 'INPUTS') max_cal_iter = read_param('max_cal_iterations', 5, config, 'INPUTS') max_point_iter = read_param('max_point_iterations', 10, config, 'INPUTS') ts_diff_threshold = read_param('ts_diff_threshold', 4, config, 'INPUTS') etr_ws = config.get('INPUTS', 'etr_ws') ppt_ws = config.get('INPUTS', 'ppt_ws') etr_re = re.compile(config.get('INPUTS', 'etr_re')) ppt_re = re.compile(config.get('INPUTS', 'ppt_re')) awc_path = config.get('INPUTS', 'awc_path') spinup_days = read_param('swb_spinup_days', 5, config, 'INPUTS') min_spinup_days = read_param('swb_min_spinup_days', 30, config, 'INPUTS') log_fmt = ' {:<18s} {}' break_line = '\n{}'.format('#' * 80) env = gdc.env image = et_image.Image(image_ws, env) logging.info(log_fmt.format('Image:', image.folder_id)) # Check inputs for file_path in [awc_path]: if not os.path.isfile(file_path): logging.error('\nERROR: File {} does not exist'.format(file_path)) sys.exit() for folder in [etr_ws, ppt_ws]: if not os.path.isdir(folder): logging.error('\nERROR: Folder {} does not exist'.format(folder)) sys.exit() # if (etrf_doy_adj_path and not # os.path.isfile(etrf_doy_adj_path)): # logging.error( # '\nERROR: File {} does not exist.'.format( # etrf_doy_adj_path)) # sys.exit() # Use iteration number to file iteration string if mc_iter is None: mc_str = '' mc_fmt = '.img' elif int(mc_iter) < 0: logging.error('\nERROR: Iteration number must be a positive integer') return False else: mc_str = 'MC{:02d}_'.format(int(mc_iter)) mc_fmt = '_{:02d}.img'.format(int(mc_iter)) logging.info(' {:<18s} {}'.format('Iteration:', mc_iter)) # Folder names etrf_ws = os.path.join(image_ws, 'ETRF') # indices_ws = image.indices_ws region_ws = os.path.join(image_ws, 'PIXEL_REGIONS') pixels_ws = os.path.join(image_ws, 'PIXELS') plots_ws = os.path.join(image_ws, 'PLOTS') if shapefile_flag and not os.path.isdir(pixels_ws): os.mkdir(pixels_ws) if not os.path.isdir(plots_ws): os.mkdir(plots_ws) # File names r_fmt = '.img' etrf_path = os.path.join(etrf_ws, 'et_rf' + mc_fmt) region_path = os.path.join(region_ws, 'region_mask' + r_fmt) # Initialize calibration parameters dictionary logging.info(break_line) logging.info('Calibration Parameters') cal_dict = dict() logging.debug(' Reading target cold/hot Kc from INI') # Using eval is potentially a really bad way of reading this in try: kc_cold_doy_dict = eval('{' + kc_cold_doy_dict + '}') except: kc_cold_doy_dict = {1: 1.05, 366: 1.05} logging.info( ' ERROR: kc_cold_doy_dict was not parsed, using default values') try: kc_hot_doy_dict = eval('{' + kc_hot_doy_dict + '}') except: kc_hot_doy_dict = {1: 0.1, 366: 0.1} logging.info( ' ERROR: kc_hot_doy_dict was not parsed, using default values') logging.debug(' Kc cold dict: {}'.format(kc_cold_doy_dict)) logging.debug(' Kc hot dict: {}\n'.format(kc_hot_doy_dict)) # doy_cold, kc_cold = zip(*sorted(kc_cold_doy_dict.items())) cal_dict['cold_tgt_kc'] = np.interp( image.acq_doy, *zip(*sorted(kc_cold_doy_dict.items())), left=1.05, right=1.05) # doy_hot, kc_hot = zip(*sorted(kc_hot_doy_dict.items())) cal_dict['hot_tgt_kc'] = np.interp(image.acq_doy, *zip(*sorted(kc_hot_doy_dict.items())), left=0.1, right=0.1) # if etrf_doy_adj_path: # doy_adj_df = pd.read_csv(etrf_doy_adj_path) # doy_adj = float( # doy_adj_df[doy_adj_df['DOY'] == image.acq_doy]['ETRF_ADJ']) # cal_dict['cold_tgt_kc'] = cal_dict['cold_tgt_kc'] + doy_adj # Get hot/cold etrf fraction sizes if cold_tgt_pct is None or hot_tgt_pct is None: logging.info('ETrF Tail Size Percentages') logging.info(' Reading target tail size from file') cold_tgt_pct, hot_tgt_pct = auto_calibration.etrf_fractions( etrf_training_path) if cold_tgt_pct is None or hot_tgt_pct is None: logging.error('\nERROR: Tail sizes were not mannually set or ' 'read from the the file\n') return False cal_dict['cold_tgt_pct'] = cold_tgt_pct cal_dict['hot_tgt_pct'] = hot_tgt_pct logging.info(pixel_str_fmt.format('', 'Cold Pixel', 'Hot Pixel')) logging.info( pixel_flt_fmt.format('Target kc:', cal_dict['cold_tgt_kc'], cal_dict['hot_tgt_kc'])) logging.info( pixel_pct_fmt.format('Tail Size:', cal_dict['cold_tgt_pct'], cal_dict['hot_tgt_pct'])) # # Create calibration database # # Set overwrite false to use existing database if it exists # cal_ws = os.path.join(image_ws, cal_folder) # if not os.path.isdir(cal_ws): # os.mkdir(cal_ws) # cal_path = os.path.join(cal_ws, cal_name) # logging.info('{:<20s} {}\{}'.format( # 'Calibration DB:', cal_folder, cal_name)) # calibration_database.create_calibration_database( # image_ws, cal_path, overwrite_db_flag) # del cal_ws # Remove previous calibrations from database # logging.info(break_line) # calibration_database.remove_calibration_points( # image_ws, cal_path, cal_initials, mc_iter) # Get ETrF and region mask (from pixel rating) # Assume they have identical extents try: region_mask = gdc.raster_to_array(region_path, return_nodata=False) region_mask = region_mask.astype(np.bool) except: logging.error( '\nERROR: Pixel regions mask does not exist or could not be read.\n' ' Please try re-running the METRIC Pixel Rating tool.') logging.debug(' {} '.format(region_path)) return False # Remove previous plots logging.info(break_line) auto_calibration.remove_histograms(plots_ws, mc_iter) # Generate the NDVI histogram if save_ndvi_plots: logging.info(break_line) logging.info('NDVI Histograms') if os.path.isfile(image.ndvi_toa_raster): ndvi_array = gdc.raster_to_array(image.ndvi_toa_raster, return_nodata=False) else: logging.error( '\nERROR: NDVI raster does not exist. METRIC Model 1 may not ' 'have run successfully.') logging.debug(' {} '.format(image.ndvi_toa_raster)) # Only process ag. ETrF pixels ndvi_array[~region_mask] = np.nan ndvi_sub_array = ndvi_array[region_mask] if np.any(ndvi_sub_array): auto_calibration.save_ndvi_histograms(ndvi_sub_array, plots_ws, mc_iter) else: logging.error( '\nERROR: Empty NDVI array, histogram was not built\n') # Place points in suggested region allowing for a number of iterations # dependent on whether or not Ts meets certain criteria logging.info(break_line) pixel_point_iters = 0 while pixel_point_iters <= max_point_iter: if pixel_point_iters == max_point_iter: logging.error('\nERROR: Suitable hot and cold pixels could not be ' 'determined. The scene will not calibrate.\n') return False cold_xy, hot_xy = pixel_points.pixel_points( image_ws, groupsize=groupsize, blocksize=blocksize, mc_iter=mc_iter, shapefile_flag=shapefile_flag, multipoint_flag=multipoint_flag, overwrite_flag=overwrite_flag, pixel_point_iters=pixel_point_iters) if any(x is None for x in cold_xy) or any(x is None for x in hot_xy): logging.error(('\nPixel points coordinates are invalid. ' 'The scene will not calibrate.' '\n Cold: {}\n Hot: {}').format(cold_xy, hot_xy)) return False cold_ts = gdc.raster_value_at_xy(image.ts_raster, cold_xy) hot_ts = gdc.raster_value_at_xy(image.ts_raster, hot_xy) if cold_ts > hot_ts: logging.info( '\nThe cold pixel is hotter than the hot pixel. Placing ' 'the points again.\n') logging.info(break_line) pixel_point_iters += 1 elif abs(hot_ts - cold_ts) < ts_diff_threshold: logging.info(( '\nThere is less than a {} degree difference in Ts hot and cold. ' 'Placing the points again.\n').format(ts_diff_threshold)) logging.info(break_line) pixel_point_iters += 1 # raise et_common.TemperatureError else: break # Adjust Kc hot for soil water balance logging.info(break_line) cal_dict = auto_calibration.hot_kc_swb_adjust(cal_dict, hot_xy, env.snap_osr, image.acq_date, awc_path, etr_ws, etr_re, ppt_ws, ppt_re, spinup_days, min_spinup_days) # Adjust Kc cold based on NDVI # cal_dict['tgt_c_kc'] = auto_calibration.kc_ndvi_adjust( # cal_dict['tgt_c_kc'], cold_xy, ndvi_path, 'Cold') # Check that Kc hot (Ke) is not too high? if cal_dict['hot_tgt_kc'] >= 1.0: logging.error('\nERROR: Target Kc hot is too high for automated ' 'calibration\n ETrF will not be computed') return False elif (cal_dict['cold_tgt_kc'] - cal_dict['hot_tgt_kc']) <= 0.05: logging.error('\nERROR: Target Kc hot and Kc cold are too close for ' 'automated calibration\n ETrF will not be computed') return False # Initialize Kc values at targets cal_dict['kc_cold'] = cal_dict['cold_tgt_kc'] cal_dict['kc_hot'] = cal_dict['hot_tgt_kc'] # Iterate until max calibrations is reached or error is small cal_flag = False cal_iter = 1 while not cal_flag: logging.info(break_line) logging.info('Calibration Iteration: {}'.format(cal_iter)) # Run METRIC Model2 for initial ETrF map logging.info(break_line) metric_model2.metric_model2(image_ws, metric_ini_path, mc_iter=mc_iter, kc_cold=cal_dict['kc_cold'], kc_hot=cal_dict['kc_hot'], cold_xy=cold_xy, hot_xy=hot_xy, overwrite_flag=overwrite_flag) # Read in ETrF array if os.path.isfile(etrf_path): etrf_array = gdc.raster_to_array(etrf_path, return_nodata=False) else: logging.warning( ('WARNING: ETrF raster does not exist. METRIC Model 2 ' 'may not have run successfully.\n {}').format(etrf_path)) break etrf_geo = gdc.raster_path_geo(etrf_path) # Only process ag. ETrF pixels etrf_array[~region_mask] = np.nan etrf_sub_array = etrf_array[np.isfinite(etrf_array)] if not np.any(etrf_sub_array): logging.error( '\nERROR: Empty ETrF array, scene cannot be calibrated\n') break # Calculate calibration parameters logging.debug(break_line) cal_dict = auto_calibration.calibration_params(cal_dict, etrf_sub_array) # Plot intermediates calibration histograms if save_etrf_temp_plots: logging.debug(break_line) auto_calibration.save_etrf_histograms(etrf_sub_array, plots_ws, cal_dict, mc_iter, cal_iter) # Check calibration logging.debug(break_line) cal_flag = auto_calibration.check_calibration(cal_dict) # Don't re-calibrate if initial calibration was suitable if cal_flag: break # Limit calibration attempts # cal_iter index is 1 based for Monte Carlo # cal_iter is 0 for stand alone mode elif cal_iter >= max_cal_iter: logging.info(break_line) logging.info( ('All {} iteration attempts were made, ' 'the scene will not calibrate.').format(max_cal_iter)) if os.path.isfile(etrf_path): os.remove(etrf_path) return False # break # Adjust Kc value of calibration points (instead of moving them) cal_dict = auto_calibration.kc_calibration_adjust( cal_dict, etrf_sub_array) # # Select new calibration points based on ETrF distribution # logging.info(break_line) # cold_xy, hot_xy = auto_calibration.build_pixel_points( # etrf_array, etrf_geo, cal_dict, # shapefile_flag=shapefile_flag, pixels_ws=pixels_ws) del etrf_array, etrf_geo # Increment calibration iteration counter cal_iter += 1 # Only save 'final' results if the scene was calibrated if cal_flag and save_etrf_final_plots: # Plot final ETrF distribution # logging.info(break_line) auto_calibration.save_etrf_histograms(etrf_sub_array, plots_ws, cal_dict, mc_iter, None) # Save final calibration points to database # logging.info(break_line) # calibration_database.save_calibration_points( # image_ws, cal_path, cal_dict, mc_iter, 0) return True
def main(img_ws=os.getcwd(), ancillary_ws=os.getcwd(), output_ws=os.getcwd(), etr_flag=False, eto_flag=False, start_date=None, end_date=None, extent_path=None, output_extent=None, stats_flag=True, overwrite_flag=False, use_cimis_eto_flag=False): """Compute daily ETr/ETo from CIMIS data Args: img_ws (str): root folder of GRIDMET data ancillary_ws (str): folder of ancillary rasters output_ws (str): folder of output rasters etr_flag (bool): if True, compute alfalfa reference ET (ETr) eto_flag (bool): if True, compute grass reference ET (ETo) start_date (str): ISO format date (YYYY-MM-DD) end_date (str): ISO format date (YYYY-MM-DD) extent_path (str): file path defining the output extent output_extent (list): decimal degrees values defining output extent stats_flag (bool): if True, compute raster statistics. Default is True. overwrite_flag (bool): If True, overwrite existing files use_cimis_eto_flag (bool): if True, use CIMIS ETo raster if one of the component rasters is missing and ETo/ETr cannot be computed Returns: None """ logging.info('\nComputing CIMIS ETo/ETr') np.seterr(invalid='ignore') # Use CIMIS ETo raster directly instead of computing from components # Currently this will only be applied if one of the inputs is missing use_cimis_eto_flag = True # Compute ETr and/or ETo if not etr_flag and not eto_flag: logging.info(' ETo/ETr flag(s) not set, defaulting to ETr') etr_flag = True # If a date is not set, process 2017 try: start_dt = dt.datetime.strptime(start_date, '%Y-%m-%d') logging.debug(' Start date: {}'.format(start_dt)) except: start_dt = dt.datetime(2017, 1, 1) logging.info(' Start date: {}'.format(start_dt)) try: end_dt = dt.datetime.strptime(end_date, '%Y-%m-%d') logging.debug(' End date: {}'.format(end_dt)) except: end_dt = dt.datetime(2017, 12, 31) logging.info(' End date: {}'.format(end_dt)) etr_folder = 'etr' eto_folder = 'eto' etr_fmt = 'etr_{}_daily_cimis.img' eto_fmt = 'eto_{}_daily_cimis.img' # DEM for air pressure calculation mask_raster = os.path.join(ancillary_ws, 'cimis_mask.img') dem_raster = os.path.join(ancillary_ws, 'cimis_elev.img') lat_raster = os.path.join(ancillary_ws, 'cimis_lat.img') # lon_raster = os.path.join(ancillary_ws, 'cimis_lon.img') # Interpolate zero windspeed pixels # interpolate_zero_u2_flag = False # Interpolate edge and coastal cells # interpolate_edge_flag = False # Resample type # 0 = GRA_NearestNeighbour, Nearest neighbour (select on one input pixel) # 1 = GRA_Bilinear,Bilinear (2x2 kernel) # 2 = GRA_Cubic, Cubic Convolution Approximation (4x4 kernel) # 3 = GRA_CubicSpline, Cubic B-Spline Approximation (4x4 kernel) # 4 = GRA_Lanczos, Lanczos windowed sinc interpolation (6x6 kernel) # 5 = GRA_Average, Average (computes the average of all non-NODATA contributing pixels) # 6 = GRA_Mode, Mode (selects the value which appears most often of all the sampled points) resample_type = gdal.GRA_CubicSpline # Wind speed is measured at 2m zw = 2 # Output workspaces etr_ws = os.path.join(output_ws, etr_folder) eto_ws = os.path.join(output_ws, eto_folder) if etr_flag and not os.path.isdir(etr_ws): os.makedirs(etr_ws) if eto_flag and not os.path.isdir(eto_ws): os.makedirs(eto_ws) # Check ETr/ETo functions test_flag = False # Check that the daily_refet_func produces the correct values if test_flag: doy_test = 245 elev_test = 1050.0 lat_test = 39.9396 * math.pi / 180 tmin_test = 11.07 tmax_test = 34.69 rs_test = 22.38 u2_test = 1.94 zw_test = 2.5 tdew_test = -3.22 ea_test = et_common.saturation_vapor_pressure_func(tdew_test) pair_test = 101.3 * np.power((285 - 0.0065 * elev_test) / 285, 5.26) q_test = 0.622 * ea_test / (pair_test - (0.378 * ea_test)) etr = float( et_common.daily_refet_func(tmin_test, tmax_test, q_test, rs_test, u2_test, zw_test, elev_test, doy_test, lat_test, 'ETR')) eto = float( et_common.daily_refet_func(tmin_test, tmax_test, q_test, rs_test, u2_test, zw_test, elev_test, doy_test, lat_test, 'ETO')) print('ETr: 8.89', etr) print('ETo: 6.16', eto) sys.exit() # Get CIMIS grid properties from mask cimis_mask_ds = gdal.Open(mask_raster) cimis_osr = gdc.raster_ds_osr(cimis_mask_ds) cimis_proj = gdc.osr_proj(cimis_osr) cimis_cs = gdc.raster_ds_cellsize(cimis_mask_ds, x_only=True) cimis_extent = gdc.raster_ds_extent(cimis_mask_ds) cimis_full_geo = cimis_extent.geo(cimis_cs) cimis_x, cimis_y = cimis_extent.origin() cimis_mask_ds = None logging.debug(' Projection: {}'.format(cimis_proj)) logging.debug(' Cellsize: {}'.format(cimis_cs)) logging.debug(' Geo: {}'.format(cimis_full_geo)) logging.debug(' Extent: {}'.format(cimis_extent)) # Manually set CIMIS grid properties # cimis_extent = gdc.Extent((-400000, -650000, 600000, 454000)) # cimis_cs = 2000 # cimis_geo = gdc.extent_geo(cimis_extent, cellsize) # cimis_epsg = 3310 # NAD_1983_California_Teale_Albers # cimis_x, cimis_y = (0,0) # Subset data to a smaller extent if output_extent is not None: logging.info('\nComputing subset extent & geo') logging.debug(' Extent: {}'.format(output_extent)) cimis_extent = gdc.Extent(output_extent) cimis_extent.adjust_to_snap('EXPAND', cimis_x, cimis_y, cimis_cs) cimis_geo = cimis_extent.geo(cimis_cs) logging.debug(' Geo: {}'.format(cimis_geo)) logging.debug(' Extent: {}'.format(output_extent)) elif extent_path is not None: logging.info('\nComputing subset extent & geo') if extent_path.lower().endswith('.shp'): cimis_extent = gdc.feature_path_extent(extent_path) extent_osr = gdc.feature_path_osr(extent_path) extent_cs = None else: cimis_extent = gdc.raster_path_extent(extent_path) extent_osr = gdc.raster_path_osr(extent_path) extent_cs = gdc.raster_path_cellsize(extent_path, x_only=True) cimis_extent = gdc.project_extent(cimis_extent, extent_osr, cimis_osr, extent_cs) cimis_extent.adjust_to_snap('EXPAND', cimis_x, cimis_y, cimis_cs) cimis_geo = cimis_extent.geo(cimis_cs) logging.debug(' Geo: {}'.format(cimis_geo)) logging.debug(' Extent: {}'.format(cimis_extent)) else: cimis_geo = cimis_full_geo # Latitude lat_array = gdc.raster_to_array(lat_raster, mask_extent=cimis_extent, return_nodata=False) lat_array = lat_array.astype(np.float32) lat_array *= math.pi / 180 # Elevation data elev_array = gdc.raster_to_array(dem_raster, mask_extent=cimis_extent, return_nodata=False) elev_array = elev_array.astype(np.float32) # Process each year in the input workspace logging.info("") for year_str in sorted(os.listdir(img_ws)): logging.debug('{}'.format(year_str)) if not re.match('^\d{4}$', year_str): logging.debug(' Not a 4 digit year folder, skipping') continue year_ws = os.path.join(img_ws, year_str) year_int = int(year_str) # year_days = int(dt.datetime(year_int, 12, 31).strftime('%j')) if start_dt is not None and year_int < start_dt.year: logging.debug(' Before start date, skipping') continue elif end_dt is not None and year_int > end_dt.year: logging.debug(' After end date, skipping') continue logging.info('{}'.format(year_str)) # Output paths etr_raster = os.path.join(etr_ws, etr_fmt.format(year_str)) eto_raster = os.path.join(eto_ws, eto_fmt.format(year_str)) if etr_flag and (overwrite_flag or not os.path.isfile(etr_raster)): logging.debug(' {}'.format(etr_raster)) gdc.build_empty_raster(etr_raster, band_cnt=366, output_dtype=np.float32, output_proj=cimis_proj, output_cs=cimis_cs, output_extent=cimis_extent, output_fill_flag=True) if eto_flag and (overwrite_flag or not os.path.isfile(eto_raster)): logging.debug(' {}'.format(eto_raster)) gdc.build_empty_raster(eto_raster, band_cnt=366, output_dtype=np.float32, output_proj=cimis_proj, output_cs=cimis_cs, output_extent=cimis_extent, output_fill_flag=True) # Process each date in the year for date_str in sorted(os.listdir(year_ws)): logging.debug('{}'.format(date_str)) try: date_dt = dt.datetime.strptime(date_str, '%Y_%m_%d') except ValueError: logging.debug( ' Invalid folder date format (YYYY_MM_DD), skipping') continue if start_dt is not None and date_dt < start_dt: logging.debug(' Before start date, skipping') continue elif end_dt is not None and date_dt > end_dt: logging.debug(' After end date, skipping') continue logging.info(date_str) date_ws = os.path.join(year_ws, date_str) doy = int(date_dt.strftime('%j')) # Set file paths tmax_path = os.path.join(date_ws, 'Tx.img') tmin_path = os.path.join(date_ws, 'Tn.img') tdew_path = os.path.join(date_ws, 'Tdew.img') rso_path = os.path.join(date_ws, 'Rso.img') rs_path = os.path.join(date_ws, 'Rs.img') u2_path = os.path.join(date_ws, 'U2.img') eto_path = os.path.join(date_ws, 'ETo.img') # k_path = os.path.join(date_ws, 'K.img') # rnl_path = os.path.join(date_ws, 'Rnl.img') input_list = [ tmin_path, tmax_path, tdew_path, u2_path, rs_path, rso_path ] # If any input raster is missing, skip the day # Unless ETo is present (and use_cimis_eto_flag is True) day_skip_flag = False for t_path in input_list: if not os.path.isfile(t_path): logging.info(' {} is missing'.format(t_path)) day_skip_flag = True if (day_skip_flag and use_cimis_eto_flag and os.path.isfile(eto_path)): logging.info(' Using CIMIS ETo directly') eto_array = gdc.raster_to_array(eto_path, 1, cimis_extent, return_nodata=False) eto_array = eto_array.astype(np.float32) if not np.any(eto_array): logging.info(' {} is empty or missing'.format(eto_path)) logging.info(' Skipping date') continue # ETr if etr_flag: gdc.array_to_comp_raster(1.2 * eto_array, etr_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # 1.2 * eto_array, etr_raster, # output_geo=cimis_geo, output_proj=cimis_proj, # stats_flag=stats_flag) # ETo if eto_flag: gdc.array_to_comp_raster(eto_array, eto_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # eto_array, eto_raster, # output_geo=cimis_geo, output_proj=cimis_proj, # stats_flag=stats_flag) del eto_array continue elif not day_skip_flag: # Read in rasters # DEADBEEF - Read with extent since some arrays are too big # i.e. 2012-03-21, 2013-03-20, 2014-02-27 tmin_array = gdc.raster_to_array(tmin_path, 1, cimis_extent, return_nodata=False) tmax_array = gdc.raster_to_array(tmax_path, 1, cimis_extent, return_nodata=False) tdew_array = gdc.raster_to_array(tdew_path, 1, cimis_extent, return_nodata=False) rso_array = gdc.raster_to_array(rso_path, 1, cimis_extent, return_nodata=False) rs_array = gdc.raster_to_array(rs_path, 1, cimis_extent, return_nodata=False) u2_array = gdc.raster_to_array(u2_path, 1, cimis_extent, return_nodata=False) # k_array = gdc.raster_to_array( # k_path, 1, cimis_extent, return_nodata=False) # rnl_array = gdc.raster_to_array( # rnl_path, 1, cimis_extent, return_nodata=False) # Check that all input arrays have data for t_name, t_array in [[tmin_path, tmin_array], [tmax_path, tmax_array], [tdew_path, tdew_array], [u2_path, u2_array], [rs_path, rs_array]]: if not np.any(t_array): logging.warning( ' {} is empty or missing'.format(t_name)) day_skip_flag = True if day_skip_flag: logging.warning(' Skipping date') continue # DEADBEEF - Some arrays have a 500m cellsize # i.e. 2011-07-25, 2010-01-01 -> 2010-07-27 tmin_array = rescale_array_func(tmin_array, elev_array, 'tmin') tmax_array = rescale_array_func(tmax_array, elev_array, 'tmax') tdew_array = rescale_array_func(tdew_array, elev_array, 'tdew') rso_array = rescale_array_func(rso_array, elev_array, 'rso') rs_array = rescale_array_func(rs_array, elev_array, 'rs') u2_array = rescale_array_func(u2_array, elev_array, 'u2') # k_array = rescale_array_func(k_array, elev_array, 'k') # rnl_array = rescale_array_func(rnl_array, elev_array, 'rnl') # Back calculate q from tdew by first calculating ea from tdew es_array = et_common.saturation_vapor_pressure_func(tdew_array) pair_array = et_common.air_pressure_func(elev_array) q_array = 0.622 * es_array / (pair_array - (0.378 * es_array)) del es_array, pair_array, tdew_array # Back calculate rhmin/rhmax from tdew # ea_tmax = et_common.saturation_vapor_pressure_func(tmax_array) # ea_tmin = et_common.saturation_vapor_pressure_func(tmin_array) # rhmin = ea_tdew * 2 / (ea_tmax + ea_tmin); # rhmax = ea_tdew * 2 / (ea_tmax + ea_tmin); # del ea_tmax, ea_tmin # ETr if etr_flag: etr_array = et_common.refet_daily_func(tmin_array, tmax_array, q_array, rs_array, u2_array, zw, elev_array, lat_array, doy, ref_type='ETR', rso_type='ARRAY', rso=rso_array) gdc.array_to_comp_raster(etr_array.astype(np.float32), etr_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # etr_array.astype(np.float32), etr_raster, # output_geo=cimis_geo, output_proj=cimis_proj, # stats_flag=stats_flag) del etr_array # ETo if eto_flag: eto_array = et_common.refet_daily_func(tmin_array, tmax_array, q_array, rs_array, u2_array, zw, elev_array, lat_array, doy, ref_type='ETO', rso_type='ARRAY', rso=rso_array) gdc.array_to_comp_raster(eto_array.astype(np.float32), eto_raster, band=doy, stats_flag=False) # gdc.array_to_raster( # eto_array.astype(np.float32), eto_raster, # output_geo=cimis_geo, output_proj=cimis_proj, # stats_flag=stats_flag) del eto_array # Cleanup del tmin_array, tmax_array, u2_array, rs_array, q_array # del rnl, rs, rso else: logging.info(' Skipping date') continue if stats_flag and etr_flag: gdc.raster_statistics(etr_raster) if stats_flag and eto_flag: gdc.raster_statistics(eto_raster) logging.debug('\nScript Complete')