|
| 1 | +#!/usr/bin/env python3 |
| 2 | +import argparse |
| 3 | +import numpy as np |
| 4 | +import numpy.ma as ma |
| 5 | +from pyiodaconv import bufr |
| 6 | +import calendar |
| 7 | +import json |
| 8 | +import time |
| 9 | +import math |
| 10 | +import datetime |
| 11 | +import os |
| 12 | +from datetime import datetime |
| 13 | +from pyioda import ioda_obs_space as ioda_ospace |
| 14 | +from wxflow import Logger |
| 15 | + |
| 16 | +# Define and initialize global variables |
| 17 | +global float32_fill_value |
| 18 | +global int32_fill_value |
| 19 | +global int64_fill_value |
| 20 | + |
| 21 | +float32_fill_value = np.float32(0) |
| 22 | +int32_fill_value = np.int32(0) |
| 23 | +int64_fill_value = np.int64(0) |
| 24 | + |
| 25 | + |
| 26 | +def bufr_to_ioda(config, logger): |
| 27 | + |
| 28 | + subsets = config["subsets"] |
| 29 | + logger.debug(f"Checking subsets = {subsets}") |
| 30 | + |
| 31 | + # Get parameters from configuration |
| 32 | + subsets = config["subsets"] |
| 33 | + data_format = config["data_format"] |
| 34 | + data_type = config["data_type"] |
| 35 | + data_description = config["data_description"] |
| 36 | + data_provider = config["data_provider"] |
| 37 | + cycle_type = config["cycle_type"] |
| 38 | + dump_dir = config["dump_directory"] |
| 39 | + ioda_dir = config["ioda_directory"] |
| 40 | + cycle = config["cycle_datetime"] |
| 41 | + yyyymmdd = cycle[0:8] |
| 42 | + hh = cycle[8:10] |
| 43 | + |
| 44 | + satellite_info_array = config["satellite_info"] |
| 45 | + sensor_name = config["sensor_info"]["sensor_name"] |
| 46 | + sensor_full_name = config["sensor_info"]["sensor_full_name"] |
| 47 | + sensor_id = config["sensor_info"]["sensor_id"] |
| 48 | + |
| 49 | + # Get derived parameters |
| 50 | + yyyymmdd = cycle[0:8] |
| 51 | + hh = cycle[8:10] |
| 52 | + reference_time = datetime.strptime(cycle, "%Y%m%d%H") |
| 53 | + reference_time = reference_time.strftime("%Y-%m-%dT%H:%M:%SZ") |
| 54 | + |
| 55 | + # General informaton |
| 56 | + converter = 'BUFR to IODA Converter' |
| 57 | + process_level = 'Level-2' |
| 58 | + platform_description = 'NOAA Series of Geostationary Operational Environmental Satellites - 3rd generation since 2016' |
| 59 | + sensor_description = '16 channels, balaned visible, near IR, short-wave IR, mid-wave IR, and thermal IR; \ |
| 60 | + central wavelentgh ranges from 470 nm to 13.3 micron' |
| 61 | + |
| 62 | + logger.info(f'sensor_name = {sensor_name}') |
| 63 | + logger.info(f'sensor_full_name = {sensor_full_name}') |
| 64 | + logger.info(f'sensor_id = {sensor_id}') |
| 65 | + logger.info(f'reference_time = {reference_time}') |
| 66 | + |
| 67 | + bufrfile = f"{cycle_type}.t{hh}z.{data_type}.tm00.{data_format}" |
| 68 | + DATA_PATH = os.path.join(dump_dir, f"{cycle_type}.{yyyymmdd}", str(hh), 'atmos', bufrfile) |
| 69 | + |
| 70 | + # ============================================ |
| 71 | + # Make the QuerySet for all the data we want |
| 72 | + # ============================================ |
| 73 | + start_time = time.time() |
| 74 | + |
| 75 | + logger.info('Making QuerySet') |
| 76 | + q = bufr.QuerySet(subsets) |
| 77 | + |
| 78 | + # MetaData |
| 79 | + q.add('latitude', '*/CLATH') |
| 80 | + q.add('longitude', '*/CLONH') |
| 81 | + q.add('satelliteId', '*/SAID') |
| 82 | + q.add('year', '*/YEAR') |
| 83 | + q.add('month', '*/MNTH') |
| 84 | + q.add('day', '*/DAYS') |
| 85 | + q.add('hour', '*/HOUR') |
| 86 | + q.add('minute', '*/MINU') |
| 87 | + q.add('second', '*/SECO') |
| 88 | +#azadeh added |
| 89 | + q.add('sensorId', '*/SIID[1]') |
| 90 | + q.add('sensorZenithAngle', '*/SAZA') |
| 91 | + q.add('sensorCentralFrequency', '*/SCCF') |
| 92 | + |
| 93 | +# azadeh added: |
| 94 | + q.add('solarZenithAngle', '*/SOZA') |
| 95 | + q.add('cloudFree', '*/NCLDMNT') |
| 96 | + # ObsValue |
| 97 | + q.add('brightnessTemperature', '*/TMBRST') |
| 98 | + # ClearSkyStdDev |
| 99 | + q.add('ClearSkyStdDev', '*/SDTB/TMBRST') |
| 100 | + |
| 101 | + |
| 102 | + end_time = time.time() |
| 103 | + running_time = end_time - start_time |
| 104 | + logger.debug(f'Processing time for making QuerySet : {running_time} seconds') |
| 105 | + |
| 106 | + # ============================================================== |
| 107 | + # Open the BUFR file and execute the QuerySet to get ResultSet |
| 108 | + # Use the ResultSet returned to get numpy arrays of the data |
| 109 | + # ============================================================== |
| 110 | + start_time = time.time() |
| 111 | + |
| 112 | + logger.info('Executing QuerySet to get ResultSet') |
| 113 | + with bufr.File(DATA_PATH) as f: |
| 114 | + r = f.execute(q) |
| 115 | + |
| 116 | + # MetaData |
| 117 | + satid = r.get('satelliteId') |
| 118 | + instid = r.get('sensorId') |
| 119 | + year = r.get('year') |
| 120 | + month = r.get('month') |
| 121 | + day = r.get('day') |
| 122 | + hour = r.get('hour') |
| 123 | + minute = r.get('minute') |
| 124 | + second = r.get('second') |
| 125 | + lat = r.get('latitude') |
| 126 | + lon = r.get('longitude') |
| 127 | + satzenang = r.get('sensorZenithAngle') |
| 128 | + chanfreq = r.get('sensorCentralFrequency', type='float') |
| 129 | + BT= r.get('brightnessTemperature') |
| 130 | + clrStdDev= r.get('ClearSkyStdDev') |
| 131 | + cldFree= r.get('cloudFree') |
| 132 | + solzenang=r.get('solarZenithAngle') |
| 133 | + # DateTime: seconds since Epoch time |
| 134 | + # IODA has no support for numpy datetime arrays dtype=datetime64[s] |
| 135 | + timestamp = r.get_datetime('year', 'month', 'day', 'hour', 'minute', 'second').astype(np.int64) |
| 136 | + |
| 137 | + # Check BUFR variable generic dimension and type |
| 138 | + |
| 139 | + # Global variables declaration |
| 140 | + # Set global fill values |
| 141 | + float32_fill_value = satzenang.fill_value |
| 142 | + int32_fill_value = satid.fill_value |
| 143 | + int64_fill_value = timestamp.fill_value.astype(np.int64) |
| 144 | + |
| 145 | + end_time = time.time() |
| 146 | + running_time = end_time - start_time |
| 147 | + logger.info(f'Processing time for executing QuerySet to get ResultSet : {running_time} seconds') |
| 148 | + |
| 149 | + # ========================= |
| 150 | + # Create derived variables |
| 151 | + # ========================= |
| 152 | + start_time = time.time() |
| 153 | +#azadeh added |
| 154 | + if np.ma.is_masked(satzenang[0]): |
| 155 | + # Handle the masked value |
| 156 | + scanpos = np.array([int32_fill_value], dtype=np.int64) |
| 157 | + |
| 158 | + else: |
| 159 | + # Convert the non-masked value to an integer |
| 160 | + scanpos = np.array([int(satzenang[0]) + 1], dtype=np.int32) |
| 161 | + |
| 162 | + cloudAmount=1-cldFree |
| 163 | + channum=12 |
| 164 | + |
| 165 | + logger.info('Creating derived variables') |
| 166 | + |
| 167 | + end_time = time.time() |
| 168 | + running_time = end_time - start_time |
| 169 | + logger.info(f'Processing time for creating derived variables : {running_time} seconds') |
| 170 | + |
| 171 | + # ===================================== |
| 172 | + # Split output based on satellite id |
| 173 | + # Create IODA ObsSpace |
| 174 | + # Write IODA output |
| 175 | + # ===================================== |
| 176 | + logger.info('Create IODA ObsSpace and Write IODA output based on satellite ID') |
| 177 | + |
| 178 | + # Find unique satellite identifiers in data to process |
| 179 | + unique_satids = np.unique(satid) |
| 180 | + logger.info(f'Number of Unique satellite identifiers: {len(unique_satids)}') |
| 181 | + logger.info(f'Unique satellite identifiers: {unique_satids}') |
| 182 | + |
| 183 | + logger.debug(f'Loop through unique satellite identifier {unique_satids}') |
| 184 | + total_ob_processed = 0 |
| 185 | + for sat in unique_satids.tolist(): |
| 186 | + start_time = time.time() |
| 187 | + |
| 188 | + matched = False |
| 189 | + for satellite_info in satellite_info_array: |
| 190 | + if (satellite_info["satellite_id"] == sat): |
| 191 | + matched = True |
| 192 | + satellite_id = satellite_info["satellite_id"] |
| 193 | + satellite_name = satellite_info["satellite_name"] |
| 194 | + satinst = sensor_name.lower()+'_'+satellite_name.lower() |
| 195 | + logger.debug(f'Split data for {satinst} satid = {sat}') |
| 196 | + |
| 197 | + if matched: |
| 198 | + |
| 199 | + # Define a boolean mask to subset data from the original data object |
| 200 | + mask = satid == sat |
| 201 | + # MetaData |
| 202 | + lon2 = lon[mask] |
| 203 | + lat2 = lat[mask] |
| 204 | + timestamp2 = timestamp[mask] |
| 205 | + satid2 = satid[mask] |
| 206 | + instid2 = instid[mask] |
| 207 | + satzenang2 = satzenang[mask] |
| 208 | + chanfreq2 = chanfreq[mask] |
| 209 | + # obstype2 = obstype[mask] |
| 210 | +#azadeh added |
| 211 | + solzenang2 = solzenang[mask] |
| 212 | + cldFree2 = cldFree[mask] |
| 213 | + cloudAmount2 = cloudAmount[mask] |
| 214 | + BT2= BT[mask] |
| 215 | + clrStdDev2= clrStdDev[mask] |
| 216 | + |
| 217 | + # Timestamp Range |
| 218 | + timestamp2_min = datetime.fromtimestamp(timestamp2.min()) |
| 219 | + timestamp2_max = datetime.fromtimestamp(timestamp2.max()) |
| 220 | + |
| 221 | + # Check unique observation time |
| 222 | + unique_timestamp2 = np.unique(timestamp2) |
| 223 | + logger.debug(f'Processing output for satid {sat}') |
| 224 | + |
| 225 | + # Define Channel dimension for channels 4 to 11 since the other channel values are missing.( AZADEH) |
| 226 | + channel_start = 4 |
| 227 | + channel_end = 11 |
| 228 | + |
| 229 | + # Create the dimensions |
| 230 | + dims = { |
| 231 | + 'Location': np.arange(0, BT2.shape[0]), |
| 232 | + 'Channel' : np.arange(channel_start, channel_end + 1)} |
| 233 | + |
| 234 | + # Create IODA ObsSpace |
| 235 | + iodafile = f"{cycle_type}.t{hh}z.{satinst}.tm00.nc" |
| 236 | + OUTPUT_PATH = os.path.join(ioda_dir, iodafile) |
| 237 | + logger.info(f'Create output file : {OUTPUT_PATH}') |
| 238 | + obsspace = ioda_ospace.ObsSpace(OUTPUT_PATH, mode='w', dim_dict=dims) |
| 239 | + |
| 240 | + # Create Global attributes |
| 241 | + logger.debug('Write global attributes') |
| 242 | + obsspace.write_attr('Converter', converter) |
| 243 | + obsspace.write_attr('sourceFiles', bufrfile) |
| 244 | +# obsspace.write_attr('dataProviderOrigin', data_provider) |
| 245 | + obsspace.write_attr('description', data_description) |
| 246 | + obsspace.write_attr('datetimeReference', reference_time) |
| 247 | + obsspace.write_attr('datetimeRange', [str(timestamp2_min), str(timestamp2_max)]) |
| 248 | + obsspace.write_attr('sensor', sensor_id) |
| 249 | + obsspace.write_attr('platform', satellite_id) |
| 250 | + obsspace.write_attr('platformCommonName', satellite_name) |
| 251 | + obsspace.write_attr('sensorCommonName', sensor_name) |
| 252 | + obsspace.write_attr('processingLevel', process_level) |
| 253 | + obsspace.write_attr('platformLongDescription', platform_description) |
| 254 | + obsspace.write_attr('sensorLongDescription', sensor_description) |
| 255 | + |
| 256 | + # Create IODA variables |
| 257 | + logger.debug('Write variables: name, type, units, and attributes') |
| 258 | + # Longitude |
| 259 | + obsspace.create_var('MetaData/longitude', dtype=lon2.dtype, fillval=lon2.fill_value) \ |
| 260 | + .write_attr('units', 'degrees_east') \ |
| 261 | + .write_attr('valid_range', np.array([-180, 180], dtype=np.float32)) \ |
| 262 | + .write_attr('long_name', 'Longitude') \ |
| 263 | + .write_data(lon2) |
| 264 | + |
| 265 | + |
| 266 | + # Latitude |
| 267 | + obsspace.create_var('MetaData/latitude', dtype=lat.dtype, fillval=lat2.fill_value) \ |
| 268 | + .write_attr('units', 'degrees_north') \ |
| 269 | + .write_attr('valid_range', np.array([-90, 90], dtype=np.float32)) \ |
| 270 | + .write_attr('long_name', 'Latitude') \ |
| 271 | + .write_data(lat2) |
| 272 | + |
| 273 | + # Datetime |
| 274 | + obsspace.create_var('MetaData/dateTime', dtype=np.int64, fillval=int64_fill_value) \ |
| 275 | + .write_attr('units', 'seconds since 1970-01-01T00:00:00Z') \ |
| 276 | + .write_attr('long_name', 'Datetime') \ |
| 277 | + .write_data(timestamp2) |
| 278 | + |
| 279 | + # Satellite Identifier |
| 280 | + obsspace.create_var('MetaData/satelliteIdentifier', dtype=satid2.dtype, fillval=satid2.fill_value) \ |
| 281 | + .write_attr('long_name', 'Satellite Identifier') \ |
| 282 | + .write_data(satid2) |
| 283 | + |
| 284 | + # Instrument Identifier |
| 285 | + obsspace.create_var('MetaData/instrumentIdentifier', dtype=instid2.dtype, fillval=instid2.fill_value) \ |
| 286 | + .write_attr('long_name', 'Satellite Instrument Identifier') \ |
| 287 | + .write_data(instid2) |
| 288 | + |
| 289 | + |
| 290 | + # Scan Position (derived variable, need to specified fill value explicitly)Azadeh |
| 291 | + obsspace.create_var('MetaData/sensorScanPosition', dtype=scanpos.dtype, fillval=int32_fill_value) \ |
| 292 | + .write_attr('long_name', 'Sensor Scan Position') \ |
| 293 | + .write_data(scanpos) |
| 294 | + |
| 295 | + |
| 296 | + # Sensor Zenith Angle |
| 297 | + obsspace.create_var('MetaData/sensorZenithAngle', dtype=satzenang2.dtype, fillval=satzenang2.fill_value) \ |
| 298 | + .write_attr('units', 'degree') \ |
| 299 | + .write_attr('valid_range', np.array([0, 90], dtype=np.float32)) \ |
| 300 | + .write_attr('long_name', 'Sensor Zenith Angle') \ |
| 301 | + .write_data(satzenang2) |
| 302 | + |
| 303 | + |
| 304 | + # Sensor Centrall Frequency |
| 305 | + obsspace.create_var('MetaData/sensorCentralFrequency', dtype=chanfreq2.dtype, fillval=chanfreq2.fill_value) \ |
| 306 | + .write_attr('units', 'Hz') \ |
| 307 | + .write_attr('long_name', 'Satellite Channel Center Frequency') \ |
| 308 | + .write_data(chanfreq2) |
| 309 | + |
| 310 | + # Solar Zenith Angle azadeh |
| 311 | + obsspace.create_var('MetaData/solarZenithAngle', dtype=solzenang2.dtype, fillval=solzenang2.fill_value) \ |
| 312 | + .write_attr('units', 'degree') \ |
| 313 | + .write_attr('valid_range', np.array([0, 180], dtype=np.float32)) \ |
| 314 | + .write_attr('long_name', 'Solar Zenith Angle') \ |
| 315 | + .write_data(solzenang2) |
| 316 | + |
| 317 | + # Cloud free Azadeh |
| 318 | + obsspace.create_var('MetaData/cloudFree', dtype=cldFree2.dtype, fillval=cldFree2.fill_value) \ |
| 319 | + .write_attr('units', '1') \ |
| 320 | + .write_attr('valid_range', np.array([0, 1], dtype=np.float32)) \ |
| 321 | + .write_attr('long_name', 'Amount Segment Cloud Free') \ |
| 322 | + .write_data(cldFree2) |
| 323 | + |
| 324 | + |
| 325 | + # Cloud amount based on computation Azadeh |
| 326 | + obsspace.create_var('MetaData/cloudAmount', dtype=cloudAmount2.dtype, fillval=cloudAmount2.fill_value) \ |
| 327 | + .write_attr('units', '1') \ |
| 328 | + .write_attr('valid_range', np.array([0, 1], dtype=np.float32)) \ |
| 329 | + .write_attr('long_name', 'Amount of cloud coverage in layer') \ |
| 330 | + .write_data(cloudAmount2) |
| 331 | + |
| 332 | + # ObsType based on computation method/spectral band |
| 333 | + obsspace.create_var('ObsValue/brightnessTemperature', dim_list=['Location','Channel'],dtype=np.float32, fillval=float32_fill_value) \ |
| 334 | + .write_attr('units', 'k') \ |
| 335 | + .write_attr('long_name', 'Brightness Temperature') \ |
| 336 | + .write_data(BT2) |
| 337 | + |
| 338 | + |
| 339 | + obsspace.create_var('ClearSkyStdDev/brightnessTemperature', dim_list=['Location','Channel'],dtype=np.float32, fillval=float32_fill_value) \ |
| 340 | + .write_attr('long_name', 'Standard Deviation Brightness Temperature') \ |
| 341 | + .write_data(clrStdDev2) |
| 342 | + |
| 343 | + |
| 344 | + |
| 345 | + end_time = time.time() |
| 346 | + running_time = end_time - start_time |
| 347 | + total_ob_processed += len(satid2) |
| 348 | + logger.debug(f'Number of observation processed : {len(satid2)}') |
| 349 | + logger.debug(f'Processing time for splitting and output IODA for {satinst} : {running_time} seconds') |
| 350 | + |
| 351 | + else: |
| 352 | + logger.info(f"Do not find this satellite id in the configuration: satid = {sat}") |
| 353 | + |
| 354 | + logger.info("All Done!") |
| 355 | + logger.info(f'Total number of observation processed : {total_ob_processed}') |
| 356 | + |
| 357 | + |
| 358 | +if __name__ == '__main__': |
| 359 | + |
| 360 | + start_time = time.time() |
| 361 | + |
| 362 | + parser = argparse.ArgumentParser() |
| 363 | + parser.add_argument('-c', '--config', type=str, help='Input JSON configuration', required=True) |
| 364 | + parser.add_argument('-v', '--verbose', help='print debug logging information', |
| 365 | + action='store_true') |
| 366 | + args = parser.parse_args() |
| 367 | + |
| 368 | + log_level = 'DEBUG' if args.verbose else 'INFO' |
| 369 | + logger = Logger('bufr2ioda_sevcsr.py', level=log_level, colored_log=True) |
| 370 | + |
| 371 | + with open(args.config, "r") as json_file: |
| 372 | + config = json.load(json_file) |
| 373 | + |
| 374 | + bufr_to_ioda(config, logger) |
| 375 | + |
| 376 | + end_time = time.time() |
| 377 | + running_time = end_time - start_time |
| 378 | + logger.info(f"Total running time: {running_time} seconds") |
0 commit comments