From 79ccce1b79e8ee7e179214d20d5782fe7e93fad3 Mon Sep 17 00:00:00 2001 From: huikyole Date: Wed, 10 May 2017 11:19:00 -0700 Subject: [PATCH 01/11] CLIMATE-913 - Bugs in CLI All of the options shown in the run screen work after applying this fix. --- RCMES/cli_app.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/RCMES/cli_app.py b/RCMES/cli_app.py index 58e8cb20..06165714 100644 --- a/RCMES/cli_app.py +++ b/RCMES/cli_app.py @@ -1185,8 +1185,6 @@ def settings_screen(header): screen.addstr(11, x/2, "6 - Change Target dataset/s") screen.addstr(12, x/2, "7 - Change Metric") screen.addstr(13, x/2, "8 - Change Working Directory") - #screen.addstr(14, x/2, "9 - Change Plot Title [Coming Soon....]") - #screen.addstr(15, x/2, "10 - Save the processed data [Coming Soon....]") screen.addstr(14, x/2, "9 - Show Temporal Boundaries") screen.addstr(15, x/2, "10 - Show Spatial Boundaries") screen.addstr(16, x/2, "0 - Return to Main Menu") @@ -1377,19 +1375,12 @@ def settings_screen(header): else: note = "Working directory has not changed" - if option == '9': - screen.addstr(25, x/2, "Please enter plot title:") - plot_title = screen.getstr() - - #if option == '10': - # screen.addstr(25, x/2, "Please enter plot title:") - # plot_title = screen.getstr() - if option == '9': models_start_time, models_end_time = get_models_temp_bound() line = 25 for i, model in enumerate(model_datasets): - mode_name = models_info[i]['directory'].split("/")[-1] + #mode_name = models_info[i]['directory'].split("/")[-1] + mode_name = 'model %d' %(i+1) line += 1 screen.addstr(line, x/2, "{0}".format(mode_name)) line += 1 @@ -1407,7 +1398,8 @@ def settings_screen(header): models_bound = get_models_spatial_bound() line = 25 for i, model in enumerate(model_datasets): - mode_name = models_info[i]['directory'].split("/")[-1] + #mode_name = models_info[i]['directory'].split("/")[-1] + mode_name = 'model %d' %(i+1) line += 1 screen.addstr(line, x/2, "{0}".format(mode_name)) line += 1 From 6cd5c1d4ef2177750d259943f369a5e2928040fc Mon Sep 17 00:00:00 2001 From: huikyole Date: Wed, 10 May 2017 16:23:05 -0700 Subject: [PATCH 02/11] CLIMATE-914 - Update dataset_processor.spatial_regrid module - map_coordinates has been replaced with scipy.interpolate.griddata. --- ocw/dataset_processor.py | 90 ++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/ocw/dataset_processor.py b/ocw/dataset_processor.py index 917419ea..2097cc42 100755 --- a/ocw/dataset_processor.py +++ b/ocw/dataset_processor.py @@ -21,7 +21,7 @@ import datetime import numpy as np import numpy.ma as ma -import scipy.interpolate +from scipy.interpolate import griddata import scipy.ndimage from scipy.stats import rankdata from scipy.ndimage import map_coordinates @@ -222,10 +222,7 @@ def spatial_regrid(target_dataset, new_latitudes, new_longitudes, # Make masked array of shape (times, new_latitudes,new_longitudes) new_values = ma.zeros([len(target_dataset.times), - ny_new, nx_new]) - # Make masked array of shape (times, new_latitudes,new_longitudes) - new_values = ma.zeros([len(target_dataset.times), - ny_new, nx_new]) + ny_new, nx_new])+1.e+20 # Boundary vertices of target_dataset vertices = [] @@ -249,81 +246,54 @@ def spatial_regrid(target_dataset, new_latitudes, new_longitudes, for ix in np.arange(nx_old)[::-1]: vertices.append([lons[0, ix], lats[0, ix]]) path = Path(vertices) - - # Convert new_lats and new_lons to float indices - new_lons_indices = np.zeros(new_lons.shape) - new_lats_indices = np.zeros(new_lats.shape) + new_xy_mask = np.ones(new_lats.shape) for iy in np.arange(ny_new): for ix in np.arange(nx_new): if path.contains_point([new_lons[iy, ix], new_lats[iy, ix]]) or not boundary_check: - if regular_grid: - mn = lats.min() - mx = lats.max() - new_lats_indices[iy, ix] = ( - ny_old - 1.) * (new_lats[iy, ix] - mn) / (mx - mn) - mn = lons.min() - mx = lons.max() - new_lons_indices[iy, ix] = ( - nx_old - 1.) * (new_lons[iy, ix] - mn) / (mx - mn) - else: - distance_from_original_grids = ( - (lons - new_lons[iy, ix])**2. + - (lats - new_lats[iy, ix])**2.)**0.5 - if np.min(distance_from_original_grids) == 0.: - new_lats_indices[iy, ix], new_lons_indices[ - iy, ix] = np.where( - distance_from_original_grids == 0) - else: - distance_rank = rankdata( - distance_from_original_grids.flatten(), - method='ordinal').reshape(lats.shape) - # the nearest grid point's indices - iy1, ix1 = np.where(distance_rank == 1) - # point [iy2, ix] is diagonally across from [iy1, ix1] - iy2, ix2 = np.where(distance_rank == 4) - dist1 = distance_from_original_grids[iy1, ix1] - dist2 = distance_from_original_grids[iy2, ix2] - new_lats_indices[iy, ix] = ( - dist1 * iy2 + dist2 * iy1) / (dist1 + dist2) - new_lons_indices[iy, ix] = ( - dist1 * ix2 + dist2 * ix1) / (dist1 + dist2) - else: - new_lats_indices[iy, ix] = -999. - new_lats_indices[iy, ix] = -999. - new_lats_indices = ma.masked_less(new_lats_indices, 0.) - new_lons_indices = ma.masked_less(new_lons_indices, 0.) - + new_xy_mask[iy, ix] = 0. + + new_index = np.where(new_xy_mask == 0.) # Regrid the data on each time slice for i in range(len(target_dataset.times)): if len(target_dataset.times) == 1 and target_dataset.values.ndim == 2: values_original = ma.array(target_dataset.values) else: values_original = ma.array(target_dataset.values[i]) + new_mask = np.copy(values_original.mask) for shift in (-1, 1): for axis in (0, 1): q_shifted = np.roll(values_original, shift=shift, axis=axis) - idx = ~q_shifted.mask * values_original.mask - indices = np.where(idx)[0] - values_original.data[indices] = q_shifted[indices] - new_values[i] = map_coordinates(values_original, - [new_lats_indices.flatten(), - new_lons_indices.flatten()], - order=1).reshape(new_lats.shape) - new_values[i] = ma.array(new_values[i], mask=new_lats_indices.mask) + if (np.where((values_original.mask == True) & (q_shifted.mask == False)))[0].size !=0: + index1 =np.where((values_original.mask == True) & (q_shifted.mask == False)) + n_indices = len(index1[0]) + values_original.data[index1] = q_shifted[index1] + new_mask[index1] = np.repeat(False, n_indices) + mask_index = np.where(~new_mask) + if new_mask.size != 1: + mask_index = np.where(~new_mask) + else: + mask_index = np.where(~np.isnan(values_original)) + new_values_temp = griddata((lons[mask_index], lats[mask_index]), values_original[mask_index], + (new_lons[new_index], + new_lats[new_index]), + method='linear') # Make a masking map using nearest neighbour interpolation -use this to # determine locations with MDI and mask these qmdi = np.zeros_like(values_original) - values_true_indices = np.where(values_original.mask == True)[0] - values_false_indices = np.where(values_original.mask == False)[0] + values_true_indices = np.where(values_original.mask == True) + values_false_indices = np.where(values_original.mask == False) qmdi[values_true_indices] = 1. qmdi[values_false_indices] = 0. - qmdi_r = map_coordinates(qmdi, [new_lats_indices.flatten( - ), new_lons_indices.flatten()], order=1).reshape(new_lats.shape) - mdimask = (qmdi_r != 0.0) + qmdi_r = griddata((lons.flatten(), lats.flatten()), qmdi.flatten(), + (new_lons[new_index], + new_lats[new_index]), + method='nearest') + new_values_temp = ma.masked_where(qmdi_r != 0.0, new_values_temp) # Combine missing data mask, with outside domain mask define above. - new_values[i].mask = np.logical_or(mdimask, new_values[i].mask) + new_values[i, new_index[0], new_index[1]] = new_values_temp[:] + new_values[i,:] = ma.masked_equal(new_values[i,:], 1.e+20) # TODO: # This will call down to the _congrid() function and the lat and lon From ee5f3c26fb1d4009325028bda4b29536b14a0319 Mon Sep 17 00:00:00 2001 From: huikyole Date: Wed, 10 May 2017 21:45:06 -0700 Subject: [PATCH 03/11] the unreasonable reshaping of latitude and longitude was replaced by the numpy.meshgrid --- ocw/tests/test_dataset_processor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ocw/tests/test_dataset_processor.py b/ocw/tests/test_dataset_processor.py index 32fa42df..27af9514 100644 --- a/ocw/tests/test_dataset_processor.py +++ b/ocw/tests/test_dataset_processor.py @@ -391,10 +391,8 @@ def test_variable_propagation(self): self.regridded_dataset.variable) def test_two_dimensional_lats_lons(self): - self.input_dataset.lats = np.array(range(-89, 90, 2)) - self.input_dataset.lons = np.array(range(-179, 180, 4)) - self.input_dataset.lats = self.input_dataset.lats.reshape(2, 45) - self.input_dataset.lons = self.input_dataset.lons.reshape(2, 45) + self.input_dataset.lons, self.input_dataset.lats = np.meshgrid( + np.array(range(-179, 180, 2)), np.array(range(-89, 90, 2))) new_dataset = dp.spatial_regrid( self.input_dataset, self.new_lats, self.new_lons) np.testing.assert_array_equal(new_dataset.lats, self.new_lats) From e84bd1839dbbeb7bd64a9cd837e0895a0c86e092 Mon Sep 17 00:00:00 2001 From: huikyole Date: Thu, 11 May 2017 13:22:11 -0700 Subject: [PATCH 04/11] CLIMATE-915 - Updates for the NARCCAP example configuration files - Fig14_and_Fig15.yaml, Fig16_summer.yaml, and Fig16_winter.yaml have been updated. --- .../NARCCAP_examples/Fig14_and_Fig15.yaml | 4 +++- RCMES/configuration_files/NARCCAP_examples/Fig16_summer.yaml | 5 +++-- RCMES/configuration_files/NARCCAP_examples/Fig16_winter.yaml | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/RCMES/configuration_files/NARCCAP_examples/Fig14_and_Fig15.yaml b/RCMES/configuration_files/NARCCAP_examples/Fig14_and_Fig15.yaml index 83a2e325..11410a29 100644 --- a/RCMES/configuration_files/NARCCAP_examples/Fig14_and_Fig15.yaml +++ b/RCMES/configuration_files/NARCCAP_examples/Fig14_and_Fig15.yaml @@ -22,9 +22,11 @@ regrid: regrid_dlat: 0.50 regrid_dlon: 0.50 +# generic_dataset_name: If false, data filenames must include the elements of dataset_name list. datasets: - loader_name: local - name: SRB + generic_dataset_name: True + dataset_name: ['SRB'] file_path: ./data/NARCCAP_data/srb_rel3.0_shortwave_from_1983_to_2007.nc variable_name: sw_sfc_dn diff --git a/RCMES/configuration_files/NARCCAP_examples/Fig16_summer.yaml b/RCMES/configuration_files/NARCCAP_examples/Fig16_summer.yaml index dca3c00a..05cda15b 100644 --- a/RCMES/configuration_files/NARCCAP_examples/Fig16_summer.yaml +++ b/RCMES/configuration_files/NARCCAP_examples/Fig16_summer.yaml @@ -21,10 +21,11 @@ regrid: regrid_on_reference: False regrid_dlat: 0.50 regrid_dlon: 0.50 - +# generic_dataset_name: If false, data filenames must include the elements of dataset_name list. datasets: - loader_name: local - name: SRB + generic_dataset_name: True + dataset_name: ['SRB'] file_path: ./data/NARCCAP_data/srb_rel3.0_shortwave_from_1983_to_2007.nc variable_name: sw_sfc_dn diff --git a/RCMES/configuration_files/NARCCAP_examples/Fig16_winter.yaml b/RCMES/configuration_files/NARCCAP_examples/Fig16_winter.yaml index d6e647f5..622bbe32 100644 --- a/RCMES/configuration_files/NARCCAP_examples/Fig16_winter.yaml +++ b/RCMES/configuration_files/NARCCAP_examples/Fig16_winter.yaml @@ -22,9 +22,11 @@ regrid: regrid_dlat: 0.50 regrid_dlon: 0.50 +# generic_dataset_name: If false, data filenames must include the elements of dataset_name list. datasets: - loader_name: local - name: SRB + generic_dataset_name: True + dataset_name: ['SRB'] file_path: ./data/NARCCAP_data/srb_rel3.0_shortwave_from_1983_to_2007.nc variable_name: sw_sfc_dn From 8d0f458bce87a3fafa59578ba3201682f9a7b495 Mon Sep 17 00:00:00 2001 From: huikyole Date: Fri, 19 May 2017 08:36:05 -0700 Subject: [PATCH 05/11] CLIMATE-915 - Checking netcdf files' file format - ocw.utils.decode_time_value has been fixed --- ocw/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocw/utils.py b/ocw/utils.py index e9d8d751..db4b9268 100755 --- a/ocw/utils.py +++ b/ocw/utils.py @@ -54,12 +54,12 @@ def decode_time_values(dataset, time_var_name): time_format = time_format[:-3] time_units = parse_time_units(time_format) - time_base = parse_time_base(time_format) times = [] if time_units == 'months': # datetime.timedelta doesn't support a 'months' option. To remedy # this, a month == 30 days for our purposes. + time_base = parse_time_base(time_format) for time_val in time_data: times.append(time_base + relativedelta(months=int(time_val))) else: From 60ec96ad92a9b9284d03370a715644f03e1a337c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B1=A1=E9=81=93?= Date: Tue, 30 May 2017 10:13:01 -0400 Subject: [PATCH 06/11] optimizing code --- .../run_statistical_downscaling.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/RCMES/statistical_downscaling/run_statistical_downscaling.py b/RCMES/statistical_downscaling/run_statistical_downscaling.py index 9aae6184..62bc1a4c 100644 --- a/RCMES/statistical_downscaling/run_statistical_downscaling.py +++ b/RCMES/statistical_downscaling/run_statistical_downscaling.py @@ -76,9 +76,9 @@ def extract_data_at_nearest_grid_point(target_dataset, longitude, latitude): :rtype: Open Climate Workbench Dataset Object """ - if target_dataset.lons.ndim == 1 and target_dataset.lats.ndim == 1: + if target_dataset.lons.ndim == target_dataset.lats.ndim == 1: new_lon, new_lat = np.meshgrid(target_dataset.lons, target_dataset.lats) - elif target_dataset.lons.ndim == 2 and target_dataset.lats.ndim == 2: + elif target_dataset.lons.ndim == target_dataset.lats.ndim == 2: new_lon = target_datasets.lons new_lat = target_datasets.lats distance = (new_lon - longitude)**2. + (new_lat - latitude)**2. @@ -155,16 +155,9 @@ def extract_data_at_nearest_grid_point(target_dataset, longitude, latitude): print(downscale_option_names[DOWNSCALE_OPTION]+": Downscaling model output") -if DOWNSCALE_OPTION == 1: - downscaled_model_present, downscaled_model_future = downscale.Delta_addition() -elif DOWNSCALE_OPTION == 2: - downscaled_model_present, downscaled_model_future = downscale.Delta_correction() -elif DOWNSCALE_OPTION == 3: - downscaled_model_present, downscaled_model_future = downscale.Quantile_mapping() -elif DOWNSCALE_OPTION == 4: - downscaled_model_present, downscaled_model_future = downscale.Asynchronous_regression() -else: - sys.exit("DOWNSCALE_OPTION must be an integer between 1 and 4") +xdownscale = [downscale.Delta_addition, downscale.Delta_correction, downscale.Quantile_mapping, downscale.Asynchronous_regression] +if 0 < DOWNSCALE_OPTION <= len(xdownscale): xdownscale[DOWNSCALE_OPTION - 1]() +else: sys.exit("DOWNSCALE_OPTION must be an integer between 1 and " + len(xdownscale)) """ Step 5: Create plots and spreadsheet """ From 8b2f8160185afb1b70dd57a18aed95a790b1a4fa Mon Sep 17 00:00:00 2001 From: huikyole Date: Tue, 20 Jun 2017 21:33:37 -0700 Subject: [PATCH 07/11] CLIMATE-918 - Revise LAT_NAMES and LON_NAMES - LAT_NAMES and LON_NAMES in ocw.data_source.local have been revised to prevent loading wrong variables for longitudes and latitudes when using OCW loaders --- ocw/data_source/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ocw/data_source/local.py b/ocw/data_source/local.py index adc89b6d..0c5c07e0 100644 --- a/ocw/data_source/local.py +++ b/ocw/data_source/local.py @@ -31,8 +31,8 @@ import numpy import numpy.ma as ma -LAT_NAMES = [b'y', b'rlat', b'rlats', b'lat', b'lats', b'latitude', b'latitudes'] -LON_NAMES = [b'x', b'rlon', b'rlons', b'lon', b'lons', b'longitude', b'longitudes'] +LAT_NAMES = [b'lat', b'lats', b'latitude', b'latitudes'] +LON_NAMES = [b'lon', b'lons', b'longitude', b'longitudes'] TIME_NAMES = [b'time', b'times', b'date', b'dates', b'julian'] From e5630163f6d587039d144ae2725b9cf9428b907c Mon Sep 17 00:00:00 2001 From: huikyole Date: Wed, 21 Jun 2017 11:21:01 -0700 Subject: [PATCH 08/11] more latitude and longitude name options have been added --- ocw/data_source/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ocw/data_source/local.py b/ocw/data_source/local.py index 0c5c07e0..be23bb26 100644 --- a/ocw/data_source/local.py +++ b/ocw/data_source/local.py @@ -31,8 +31,8 @@ import numpy import numpy.ma as ma -LAT_NAMES = [b'lat', b'lats', b'latitude', b'latitudes'] -LON_NAMES = [b'lon', b'lons', b'longitude', b'longitudes'] +LAT_NAMES = [b'lat', b'lats', b'latitude', b'latitudes',b'rlat'] +LON_NAMES = [b'lon', b'lons', b'longitude', b'longitudes',b'rlon'] TIME_NAMES = [b'time', b'times', b'date', b'dates', b'julian'] From 9bb2170084ec61cee178c3742cb5a1be709966fe Mon Sep 17 00:00:00 2001 From: Lewis John McGibbney Date: Sat, 29 Jul 2017 13:36:05 -0700 Subject: [PATCH 09/11] CLIMATE-920 Make examples Python 3 compatible --- examples/esgf_integration_example.py | 17 +- examples/podaac_integration_example.py | 7 +- examples/simple_model_to_model_bias.py | 14 +- examples/taylor_diagram_example.py | 13 +- examples/time_series_with_regions.py | 21 +- mccsearch/code/mccSearch.py | 6292 ++++++++++++------------ ocw/data_source/esgf.py | 11 +- ocw/esgf/download.py | 28 +- 8 files changed, 3227 insertions(+), 3176 deletions(-) diff --git a/examples/esgf_integration_example.py b/examples/esgf_integration_example.py index 7a026326..8247435f 100644 --- a/examples/esgf_integration_example.py +++ b/examples/esgf_integration_example.py @@ -18,14 +18,19 @@ import ocw.data_source.esgf as esgf from getpass import getpass import ssl +import sys if hasattr(ssl, '_create_unverified_context'): ssl._create_default_https_context = ssl._create_unverified_context -dataset_id = 'obs4MIPs.CNES.AVISO.zos.mon.v20110829|esgf-data.jpl.nasa.gov' +dataset_id = 'obs4mips.CNES.AVISO.zos.mon.v20110829|esgf-data.jpl.nasa.gov' variable = 'zosStderr' -username = raw_input('Enter your ESGF OpenID:\n') +if sys.version_info[0] >= 3: + username = input('Enter your ESGF OpenID:\n') +else: + username = raw_input('Enter your ESGF OpenID:\n') + password = getpass(prompt='Enter your ESGF Password:\n') # Multiple datasets are returned in a list if the ESGF dataset is @@ -39,7 +44,7 @@ # we only need to look at the 0-th value in the returned list. ds = datasets[0] -print '\n--------\n' -print 'Variable: ', ds.variable -print 'Shape: ', ds.values.shape -print 'A Value: ', ds.values[100][100][100] +print('\n--------\n') +print('Variable: ', ds.variable) +print('Shape: ', ds.values.shape) +print('A Value: ', ds.values[100][100][100]) diff --git a/examples/podaac_integration_example.py b/examples/podaac_integration_example.py index 7b8bb10e..61663d71 100644 --- a/examples/podaac_integration_example.py +++ b/examples/podaac_integration_example.py @@ -24,8 +24,9 @@ variable = 'uwnd' name = 'PO.DAAC_test_dataset' OUTPUT_PLOT = "ccmp_temporal_std" -""" Step 1: Load Local NetCDF Files into OCW Dataset Objects """ -print("Extracting Level4 granule %s and converting it into a OCW dataset object." % datasetId) +""" Step 1: Download remote PO.DAAC Dataset and read it into an OCW Dataset Object""" +print("Available Level4 PO.DAAC Granules: %s" % podaac.list_available_extract_granule_dataset_ids()) +print("Extracting variable '%s' from Level4 granule '%s' and converting it into a OCW dataset object." % (variable, datasetId)) ccmp_dataset = podaac.extract_l4_granule( variable=variable, dataset_id=datasetId, name=name) print("CCMP_Dataset.values shape: (times, lats, lons) - %s \n" % @@ -67,7 +68,7 @@ fname = OUTPUT_PLOT gridshape = (4, 5) # 20 Years worth of plots. 20 rows in 1 column -plot_title = "CCMP Temporal Standard Deviation" +plot_title = "Cross-Calibrated Multi-Platform Temporal Standard Deviation" sub_titles = range(2002, 2010, 1) plotter.draw_contour_map(results, lats, lons, fname, diff --git a/examples/simple_model_to_model_bias.py b/examples/simple_model_to_model_bias.py index ffa5cda9..8e834b67 100644 --- a/examples/simple_model_to_model_bias.py +++ b/examples/simple_model_to_model_bias.py @@ -17,7 +17,15 @@ import datetime from os import path -import urllib +import sys + +if sys.version_info[0] >= 3: + from urllib.request import urlretrieve +else: + # Not Python 3 - today, it is most likely to be Python 2 + # But note that this might need an update when Python 4 + # might be around one day + from urllib import urlretrieve import numpy as np @@ -39,9 +47,9 @@ FILE_2_PATH = path.join('/tmp', FILE_2) if not path.exists(FILE_1_PATH): - urllib.urlretrieve(FILE_LEADER + FILE_1, FILE_1_PATH) + urlretrieve(FILE_LEADER + FILE_1, FILE_1_PATH) if not path.exists(FILE_2_PATH): - urllib.urlretrieve(FILE_LEADER + FILE_2, FILE_2_PATH) + urlretrieve(FILE_LEADER + FILE_2, FILE_2_PATH) """ Step 1: Load Local NetCDF Files into OCW Dataset Objects """ print("Loading %s into an OCW Dataset Object" % (FILE_1_PATH,)) diff --git a/examples/taylor_diagram_example.py b/examples/taylor_diagram_example.py index 86236c88..8d5bbf0d 100644 --- a/examples/taylor_diagram_example.py +++ b/examples/taylor_diagram_example.py @@ -18,7 +18,14 @@ import datetime import sys from os import path -import urllib + +if sys.version_info[0] >= 3: + from urllib.request import urlretrieve +else: + # Not Python 3 - today, it is most likely to be Python 2 + # But note that this might need an update when Python 4 + # might be around one day + from urllib import urlretrieve import numpy @@ -36,10 +43,10 @@ # Download some example NetCDF files for the evaluation ########################################################################## if not path.exists(FILE_1): - urllib.urlretrieve(FILE_LEADER + FILE_1, FILE_1) + urlretrieve(FILE_LEADER + FILE_1, FILE_1) if not path.exists(FILE_2): - urllib.urlretrieve(FILE_LEADER + FILE_2, FILE_2) + urlretrieve(FILE_LEADER + FILE_2, FILE_2) # Load the example datasets into OCW Dataset objects. We want to load # the 'tasmax' variable values. We'll also name the datasets for use diff --git a/examples/time_series_with_regions.py b/examples/time_series_with_regions.py index 05c47211..3bb133c1 100644 --- a/examples/time_series_with_regions.py +++ b/examples/time_series_with_regions.py @@ -12,7 +12,15 @@ import numpy as np import numpy.ma as ma from os import path -import urllib +import sys + +if sys.version_info[0] >= 3: + from urllib.request import urlretrieve +else: + # Not Python 3 - today, it is most likely to be Python 2 + # But note that this might need an update when Python 4 + # might be around one day + from urllib import urlretrieve import ssl if hasattr(ssl, '_create_unverified_context'): ssl._create_default_https_context = ssl._create_unverified_context @@ -29,7 +37,7 @@ LAT_MAX = 42.24 LON_MIN = -24.0 LON_MAX = 60.0 -START = datetime.datetime(2000, 01, 1) +START = datetime.datetime(2000, 1, 1) END = datetime.datetime(2007, 12, 31) EVAL_BOUNDS = Bounds(lat_min=LAT_MIN, lat_max=LAT_MAX, @@ -48,13 +56,16 @@ # Download necessary NetCDF file if not present if not path.exists(FILE_1): - urllib.urlretrieve(FILE_LEADER + FILE_1, FILE_1) + print("Downloading %s" % (FILE_LEADER + FILE_1)) + urlretrieve(FILE_LEADER + FILE_1, FILE_1) if not path.exists(FILE_2): - urllib.urlretrieve(FILE_LEADER + FILE_2, FILE_2) + print("Downloading %s" % (FILE_LEADER + FILE_2)) + urlretrieve(FILE_LEADER + FILE_2, FILE_2) if not path.exists(FILE_3): - urllib.urlretrieve(FILE_LEADER + FILE_3, FILE_3) + print("Downloading %s" % (FILE_LEADER + FILE_3)) + urlretrieve(FILE_LEADER + FILE_3, FILE_3) """ Step 1: Load Local NetCDF File into OCW Dataset Objects and store in list""" target_datasets.append(local.load_file(FILE_1, varName, name="KNMI")) diff --git a/mccsearch/code/mccSearch.py b/mccsearch/code/mccSearch.py index 15ce0c6a..6f1fa26f 100644 --- a/mccsearch/code/mccSearch.py +++ b/mccsearch/code/mccSearch.py @@ -56,7 +56,7 @@ LAT_DISTANCE = 111.0 #the avg distance in km for 1deg lat for the region being considered LON_DISTANCE = 111.0 #the avg distance in km for 1deg lon for the region being considered STRUCTURING_ELEMENT = [[0,1,0],[1,1,1],[0,1,0]] #the matrix for determining the pattern for the contiguous boxes and must - #have same rank of the matrix it is being compared against + #have same rank of the matrix it is being compared against #criteria for determining cloud elements and edges T_BB_MAX = 243 #warmest temp to allow (-30C to -55C according to Morel and Sensi 2002) T_BB_MIN = 218 #cooler temp for the center of the system @@ -84,2079 +84,2079 @@ #************************ Begin Functions ************************* #****************************************************************** def readMergData(dirname, filelist = None): - ''' - Purpose:: - Read MERG data into RCMES format - - Input:: - dirname: a string representing the directory to the MERG files in NETCDF format - filelist (optional): a list of strings representing the filenames betweent the start and end dates provided - - Output:: - A 3D masked array (t,lat,lon) with only the variables which meet the minimum temperature - criteria for each frame - - Assumptions:: - The MERG data has been converted to NETCDF using LATS4D - The data has the same lat/lon format - - TODO:: figure out how to use netCDF4 to do the clipping tmp = netCDF4.Dataset(filelist[0]) - - ''' - - global LAT - global LON - - # these strings are specific to the MERG data - mergVarName = 'ch4' - mergTimeVarName = 'time' - mergLatVarName = 'latitude' - mergLonVarName = 'longitude' - - filelistInstructions = dirname + '/*' - if filelist == None: - filelist = glob.glob(filelistInstructions) - - - #sat_img is the array that will contain all the masked frames - mergImgs = [] - #timelist of python time strings - timelist = [] - time2store = None - tempMaskedValueNp =[] - - - filelist.sort() - nfiles = len(filelist) - - # Crash nicely if there are no netcdf files - if nfiles == 0: - print 'Error: no files in this directory! Exiting elegantly' - sys.exit() - else: - # Open the first file in the list to read in lats, lons and generate the grid for comparison - tmp = Dataset(filelist[0], 'r+',format='NETCDF4') - - alllatsraw = tmp.variables[mergLatVarName][:] - alllonsraw = tmp.variables[mergLonVarName][:] - alllonsraw[alllonsraw > 180] = alllonsraw[alllonsraw > 180] - 360. # convert to -180,180 if necessary - - #get the lat/lon info data (different resolution) - latminNETCDF = find_nearest(alllatsraw, float(LATMIN)) - latmaxNETCDF = find_nearest(alllatsraw, float(LATMAX)) - lonminNETCDF = find_nearest(alllonsraw, float(LONMIN)) - lonmaxNETCDF = find_nearest(alllonsraw, float(LONMAX)) - latminIndex = (np.where(alllatsraw == latminNETCDF))[0][0] - latmaxIndex = (np.where(alllatsraw == latmaxNETCDF))[0][0] - lonminIndex = (np.where(alllonsraw == lonminNETCDF))[0][0] - lonmaxIndex = (np.where(alllonsraw == lonmaxNETCDF))[0][0] - - #subsetting the data - latsraw = alllatsraw[latminIndex: latmaxIndex] - lonsraw = alllonsraw[lonminIndex:lonmaxIndex] - - LON, LAT = np.meshgrid(lonsraw, latsraw) - #clean up - latsraw =[] - lonsraw = [] - nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) - tmp.close - - for files in filelist: - try: - thisFile = Dataset(files, format='NETCDF4') - #clip the dataset according to user lat,lon coordinates - tempRaw = thisFile.variables[mergVarName][:,latminIndex:latmaxIndex,lonminIndex:lonmaxIndex].astype('int16') - tempMask = ma.masked_array(tempRaw, mask=(tempRaw > T_BB_MAX), fill_value=0) - - #get the actual values that the mask returned - tempMaskedValue = ma.zeros((tempRaw.shape)).astype('int16') - for index, value in maenumerate(tempMask): - time_index, lat_index, lon_index = index - tempMaskedValue[time_index,lat_index, lon_index]=value - - xtimes = thisFile.variables[mergTimeVarName] - #convert this time to a python datastring - time2store, _ = getModelTimes(xtimes, mergTimeVarName) - #extend instead of append because getModelTimes returns a list already and we don't - #want a list of list - timelist.extend(time2store) - mergImgs.extend(tempMaskedValue) - thisFile.close - thisFile = None - - except: - print "bad file! ", files - - mergImgs = ma.array(mergImgs) - - return mergImgs, timelist + ''' + Purpose:: + Read MERG data into RCMES format + + Input:: + dirname: a string representing the directory to the MERG files in NETCDF format + filelist (optional): a list of strings representing the filenames betweent the start and end dates provided + + Output:: + A 3D masked array (t,lat,lon) with only the variables which meet the minimum temperature + criteria for each frame + + Assumptions:: + The MERG data has been converted to NETCDF using LATS4D + The data has the same lat/lon format + + TODO:: figure out how to use netCDF4 to do the clipping tmp = netCDF4.Dataset(filelist[0]) + + ''' + + global LAT + global LON + + # these strings are specific to the MERG data + mergVarName = 'ch4' + mergTimeVarName = 'time' + mergLatVarName = 'latitude' + mergLonVarName = 'longitude' + + filelistInstructions = dirname + '/*' + if filelist == None: + filelist = glob.glob(filelistInstructions) + + + #sat_img is the array that will contain all the masked frames + mergImgs = [] + #timelist of python time strings + timelist = [] + time2store = None + tempMaskedValueNp =[] + + + filelist.sort() + nfiles = len(filelist) + + # Crash nicely if there are no netcdf files + if nfiles == 0: + print 'Error: no files in this directory! Exiting elegantly' + sys.exit() + else: + # Open the first file in the list to read in lats, lons and generate the grid for comparison + tmp = Dataset(filelist[0], 'r+',format='NETCDF4') + + alllatsraw = tmp.variables[mergLatVarName][:] + alllonsraw = tmp.variables[mergLonVarName][:] + alllonsraw[alllonsraw > 180] = alllonsraw[alllonsraw > 180] - 360. # convert to -180,180 if necessary + + #get the lat/lon info data (different resolution) + latminNETCDF = find_nearest(alllatsraw, float(LATMIN)) + latmaxNETCDF = find_nearest(alllatsraw, float(LATMAX)) + lonminNETCDF = find_nearest(alllonsraw, float(LONMIN)) + lonmaxNETCDF = find_nearest(alllonsraw, float(LONMAX)) + latminIndex = (np.where(alllatsraw == latminNETCDF))[0][0] + latmaxIndex = (np.where(alllatsraw == latmaxNETCDF))[0][0] + lonminIndex = (np.where(alllonsraw == lonminNETCDF))[0][0] + lonmaxIndex = (np.where(alllonsraw == lonmaxNETCDF))[0][0] + + #subsetting the data + latsraw = alllatsraw[latminIndex: latmaxIndex] + lonsraw = alllonsraw[lonminIndex:lonmaxIndex] + + LON, LAT = np.meshgrid(lonsraw, latsraw) + #clean up + latsraw =[] + lonsraw = [] + nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) + tmp.close + + for files in filelist: + try: + thisFile = Dataset(files, format='NETCDF4') + #clip the dataset according to user lat,lon coordinates + tempRaw = thisFile.variables[mergVarName][:,latminIndex:latmaxIndex,lonminIndex:lonmaxIndex].astype('int16') + tempMask = ma.masked_array(tempRaw, mask=(tempRaw > T_BB_MAX), fill_value=0) + + #get the actual values that the mask returned + tempMaskedValue = ma.zeros((tempRaw.shape)).astype('int16') + for index, value in maenumerate(tempMask): + time_index, lat_index, lon_index = index + tempMaskedValue[time_index,lat_index, lon_index]=value + + xtimes = thisFile.variables[mergTimeVarName] + #convert this time to a python datastring + time2store, _ = getModelTimes(xtimes, mergTimeVarName) + #extend instead of append because getModelTimes returns a list already and we don't + #want a list of list + timelist.extend(time2store) + mergImgs.extend(tempMaskedValue) + thisFile.close + thisFile = None + + except: + print "bad file! ", files + + mergImgs = ma.array(mergImgs) + + return mergImgs, timelist #****************************************************************** def findCloudElements(mergImgs,timelist,TRMMdirName=None): - ''' - Purpose:: - Determines the contiguous boxes for a given time of the satellite images i.e. each frame - using scipy ndimage package - - Input:: - mergImgs: masked numpy array in (time,lat,lon),T_bb representing the satellite data. This is masked based on the - maximum acceptable temperature, T_BB_MAX - timelist: a list of python datatimes - TRMMdirName (optional): string representing the path where to find the TRMM datafiles - - Output:: - CLOUD_ELEMENT_GRAPH: a Networkx directed graph where each node contains the information in cloudElementDict - The nodes are determined according to the area of contiguous squares. The nodes are linked through weighted edges. - - cloudElementDict = {'uniqueID': unique tag for this CE, - 'cloudElementTime': time of the CE, - 'cloudElementLatLon': (lat,lon,value) of MERG data of CE, - 'cloudElementCenter':list of floating-point [lat,lon] representing the CE's center - 'cloudElementArea':floating-point representing the area of the CE, - 'cloudElementEccentricity': floating-point representing the shape of the CE, - 'cloudElementTmax':integer representing the maximum Tb in CE, - 'cloudElementTmin': integer representing the minimum Tb in CE, - 'cloudElementPrecipTotal':floating-point representing the sum of all rainfall in CE if TRMMdirName entered, - 'cloudElementLatLonTRMM':(lat,lon,value) of TRMM data in CE if TRMMdirName entered, - 'TRMMArea': floating-point representing the CE if TRMMdirName entered, - 'CETRMMmax':floating-point representing the max rate in the CE if TRMMdirName entered, - 'CETRMMmin':floating-point representing the min rate in the CE if TRMMdirName entered} - Assumptions:: - Assumes we are dealing with MERG data which is 4kmx4km resolved, thus the smallest value - required according to Vila et al. (2008) is 2400km^2 - therefore, 2400/16 = 150 contiguous squares - ''' - - frame = ma.empty((1,mergImgs.shape[1],mergImgs.shape[2])) - CEcounter = 0 - frameCEcounter = 0 - frameNum = 0 - cloudElementEpsilon = 0.0 - cloudElementDict = {} - cloudElementCenter = [] #list with two elements [lat,lon] for the center of a CE - prevFrameCEs = [] #list for CEs in previous frame - currFrameCEs = [] #list for CEs in current frame - cloudElementLat = [] #list for a particular CE's lat values - cloudElementLon = [] #list for a particular CE's lon values - cloudElementLatLons = [] #list for a particular CE's (lat,lon) values - - prevLatValue = 0.0 - prevLonValue = 0.0 - TIR_min = 0.0 - TIR_max = 0.0 - temporalRes = 3 # TRMM data is 3 hourly - precipTotal = 0.0 - CETRMMList =[] - precip =[] - TRMMCloudElementLatLons =[] - - minCELatLimit = 0.0 - minCELonLimit = 0.0 - maxCELatLimit = 0.0 - maxCELonLimit = 0.0 - - nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) - - #openfile for storing ALL cloudElement information - cloudElementsFile = open((MAINDIRECTORY+'/textFiles/cloudElements.txt'),'wb') - #openfile for storing cloudElement information meeting user criteria i.e. MCCs in this case - cloudElementsUserFile = open((MAINDIRECTORY+'/textFiles/cloudElementsUserFile.txt'),'w') - - #NB in the TRMM files the info is hours since the time thus 00Z file has in 01, 02 and 03 times - for t in xrange(mergImgs.shape[0]): - #------------------------------------------------- - # #textfile name for saving the data for arcgis - # thisFileName = MAINDIRECTORY+'/' + (str(timelist[t])).replace(" ", "_") + '.txt' - # cloudElementsTextFile = open(thisFileName,'w') - #------------------------------------------------- - - #determine contiguous locations with temeperature below the warmest temp i.e. cloudElements in each frame - frame, CEcounter = ndimage.measurements.label(mergImgs[t,:,:], structure=STRUCTURING_ELEMENT) - frameCEcounter=0 - frameNum += 1 - - #for each of the areas identified, check to determine if it a valid CE via an area and T requirement - for count in xrange(CEcounter): - #[0] is time dimension. Determine the actual values from the data - #loc is a masked array - try: - loc = ndimage.find_objects(frame==(count+1))[0] - except Exception, e: - print "Error is ", e - continue - - - cloudElement = mergImgs[t,:,:][loc] - labels, lcounter = ndimage.label(cloudElement) - - #determine the true lats and lons for this particular CE - cloudElementLat = LAT[loc[0],0] - cloudElementLon = LON[0,loc[1]] - - #determine number of boxes in this cloudelement - numOfBoxes = np.count_nonzero(cloudElement) - cloudElementArea = numOfBoxes*XRES*YRES - - #If the area is greater than the area required, or if the area is smaller than the suggested area, check if it meets a convective fraction requirement - #consider as CE - - if cloudElementArea >= AREA_MIN or (cloudElementArea < AREA_MIN and ((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels)))) < CONVECTIVE_FRACTION ): - - #get some time information and labeling info - frameTime = str(timelist[t]) - frameCEcounter +=1 - CEuniqueID = 'F'+str(frameNum)+'CE'+str(frameCEcounter) - - #------------------------------------------------- - #textfile name for accesing CE data using MATLAB code - # thisFileName = MAINDIRECTORY+'/' + (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.txt' - # cloudElementsTextFile = open(thisFileName,'w') - #------------------------------------------------- - - # ------ NETCDF File stuff for brightness temp stuff ------------------------------------ - thisFileName = MAINDIRECTORY +'/MERGnetcdfCEs/cloudElements'+ (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.nc' - currNetCDFCEData = Dataset(thisFileName, 'w', format='NETCDF4') - currNetCDFCEData.description = 'Cloud Element '+CEuniqueID + ' temperature data' - currNetCDFCEData.calendar = 'standard' - currNetCDFCEData.conventions = 'COARDS' - # dimensions - currNetCDFCEData.createDimension('time', None) - currNetCDFCEData.createDimension('lat', len(LAT[:,0])) - currNetCDFCEData.createDimension('lon', len(LON[0,:])) - # variables - tempDims = ('time','lat', 'lon',) - times = currNetCDFCEData.createVariable('time', 'f8', ('time',)) - times.units = 'hours since '+ str(timelist[t])[:-6] - latitudes = currNetCDFCEData.createVariable('latitude', 'f8', ('lat',)) - longitudes = currNetCDFCEData.createVariable('longitude', 'f8', ('lon',)) - brightnesstemp = currNetCDFCEData.createVariable('brightnesstemp', 'i16',tempDims ) - brightnesstemp.units = 'Kelvin' - # NETCDF data - dates=[timelist[t]+timedelta(hours=0)] - times[:] = date2num(dates,units=times.units) - longitudes[:] = LON[0,:] - longitudes.units = "degrees_east" - longitudes.long_name = "Longitude" - - latitudes[:] = LAT[:,0] - latitudes.units = "degrees_north" - latitudes.long_name ="Latitude" - - #generate array of zeros for brightness temperature - brightnesstemp1 = ma.zeros((1,len(latitudes), len(longitudes))).astype('int16') - #-----------End most of NETCDF file stuff ------------------------------------ - - #if other dataset (TRMM) assumed to be a precipitation dataset was entered - if TRMMdirName: - #------------------TRMM stuff ------------------------------------------------- - fileDate = ((str(timelist[t])).replace(" ", "")[:-8]).replace("-","") - fileHr1 = (str(timelist[t])).replace(" ", "")[-8:-6] - - if int(fileHr1) % temporalRes == 0: - fileHr = fileHr1 - else: - fileHr = (int(fileHr1)/temporalRes) * temporalRes - if fileHr < 10: - fileHr = '0'+str(fileHr) - else: - str(fileHr) - - #open TRMM file for the resolution info and to create the appropriate sized grid - TRMMfileName = TRMMdirName+'/3B42.'+ fileDate + "."+str(fileHr)+".7A.nc" - - TRMMData = Dataset(TRMMfileName,'r', format='NETCDF4') - precipRate = TRMMData.variables['pcp'][:,:,:] - latsrawTRMMData = TRMMData.variables['latitude'][:] - lonsrawTRMMData = TRMMData.variables['longitude'][:] - lonsrawTRMMData[lonsrawTRMMData > 180] = lonsrawTRMMData[lonsrawTRMMData>180] - 360. - LONTRMM, LATTRMM = np.meshgrid(lonsrawTRMMData, latsrawTRMMData) - - nygrdTRMM = len(LATTRMM[:,0]); nxgrdTRMM = len(LONTRMM[0,:]) - precipRateMasked = ma.masked_array(precipRate, mask=(precipRate < 0.0)) - #---------regrid the TRMM data to the MERG dataset ---------------------------------- - #regrid using the do_regrid stuff from the Apache OCW - regriddedTRMM = ma.zeros((0, nygrd, nxgrd)) - #regriddedTRMM = process.do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) - regriddedTRMM = do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) - #---------------------------------------------------------------------------------- - - # #get the lat/lon info from cloudElement - #get the lat/lon info from the file - latCEStart = LAT[0][0] - latCEEnd = LAT[-1][0] - lonCEStart = LON[0][0] - lonCEEnd = LON[0][-1] - - #get the lat/lon info for TRMM data (different resolution) - latStartT = find_nearest(latsrawTRMMData, latCEStart) - latEndT = find_nearest(latsrawTRMMData, latCEEnd) - lonStartT = find_nearest(lonsrawTRMMData, lonCEStart) - lonEndT = find_nearest(lonsrawTRMMData, lonCEEnd) - latStartIndex = np.where(latsrawTRMMData == latStartT) - latEndIndex = np.where(latsrawTRMMData == latEndT) - lonStartIndex = np.where(lonsrawTRMMData == lonStartT) - lonEndIndex = np.where(lonsrawTRMMData == lonEndT) - - #get the relevant TRMM info - CEprecipRate = precipRate[:,(latStartIndex[0][0]-1):latEndIndex[0][0],(lonStartIndex[0][0]-1):lonEndIndex[0][0]] - TRMMData.close() - - # ------ NETCDF File info for writing TRMM CE rainfall ------------------------------------ - thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/TRMM' + (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.nc' - currNetCDFTRMMData = Dataset(thisFileName, 'w', format='NETCDF4') - currNetCDFTRMMData.description = 'Cloud Element '+CEuniqueID + ' precipitation data' - currNetCDFTRMMData.calendar = 'standard' - currNetCDFTRMMData.conventions = 'COARDS' - # dimensions - currNetCDFTRMMData.createDimension('time', None) - currNetCDFTRMMData.createDimension('lat', len(LAT[:,0])) - currNetCDFTRMMData.createDimension('lon', len(LON[0,:])) - - # variables - TRMMprecip = ('time','lat', 'lon',) - times = currNetCDFTRMMData.createVariable('time', 'f8', ('time',)) - times.units = 'hours since '+ str(timelist[t])[:-6] - latitude = currNetCDFTRMMData.createVariable('latitude', 'f8', ('lat',)) - longitude = currNetCDFTRMMData.createVariable('longitude', 'f8', ('lon',)) - rainFallacc = currNetCDFTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip ) - rainFallacc.units = 'mm' - - longitude[:] = LON[0,:] - longitude.units = "degrees_east" - longitude.long_name = "Longitude" - - latitude[:] = LAT[:,0] - latitude.units = "degrees_north" - latitude.long_name ="Latitude" - - finalCETRMMvalues = ma.zeros((brightnesstemp.shape)) - #-----------End most of NETCDF file stuff ------------------------------------ - - #populate cloudElementLatLons by unpacking the original values from loc to get the actual value for lat and lon - #TODO: KDW - too dirty... play with itertools.izip or zip and the enumerate with this - # as cloudElement is masked - for index,value in np.ndenumerate(cloudElement): - if value != 0 : - lat_index,lon_index = index - lat_lon_tuple = (cloudElementLat[lat_index], cloudElementLon[lon_index],value) - - #generate the comma separated file for GIS - cloudElementLatLons.append(lat_lon_tuple) - - #temp data for CE NETCDF file - brightnesstemp1[0,int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] = value - - if TRMMdirName: - finalCETRMMvalues[0,int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] = regriddedTRMM[int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] - CETRMMList.append((cloudElementLat[lat_index], cloudElementLon[lon_index], finalCETRMMvalues[0,cloudElementLat[lat_index], cloudElementLon[lon_index]])) - - - brightnesstemp[:] = brightnesstemp1[:] - currNetCDFCEData.close() - - if TRMMdirName: - - #calculate the total precip associated with the feature - for index, value in np.ndenumerate(finalCETRMMvalues): - precipTotal += value - precip.append(value) - - rainFallacc[:] = finalCETRMMvalues[:] - currNetCDFTRMMData.close() - TRMMnumOfBoxes = np.count_nonzero(finalCETRMMvalues) - TRMMArea = TRMMnumOfBoxes*XRES*YRES - try: - maxCEprecipRate = np.max(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) - minCEprecipRate = np.min(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) - except: - pass - - #sort cloudElementLatLons by lats - cloudElementLatLons.sort(key=lambda tup: tup[0]) - - #determine if the cloud element the shape - cloudElementEpsilon = eccentricity (cloudElement) - cloudElementsUserFile.write("\n\nTime is: %s" %(str(timelist[t]))) - cloudElementsUserFile.write("\nCEuniqueID is: %s" %CEuniqueID) - latCenter, lonCenter = ndimage.measurements.center_of_mass(cloudElement, labels=labels) - - #latCenter and lonCenter are given according to the particular array defining this CE - #so you need to convert this value to the overall domain truth - latCenter = cloudElementLat[round(latCenter)] - lonCenter = cloudElementLon[round(lonCenter)] - cloudElementsUserFile.write("\nCenter (lat,lon) is: %.2f\t%.2f" %(latCenter, lonCenter)) - cloudElementCenter.append(latCenter) - cloudElementCenter.append(lonCenter) - cloudElementsUserFile.write("\nNumber of boxes are: %d" %numOfBoxes) - cloudElementsUserFile.write("\nArea is: %.4f km^2" %(cloudElementArea)) - cloudElementsUserFile.write("\nAverage brightness temperature is: %.4f K" %ndimage.mean(cloudElement, labels=labels)) - cloudElementsUserFile.write("\nMin brightness temperature is: %.4f K" %ndimage.minimum(cloudElement, labels=labels)) - cloudElementsUserFile.write("\nMax brightness temperature is: %.4f K" %ndimage.maximum(cloudElement, labels=labels)) - cloudElementsUserFile.write("\nBrightness temperature variance is: %.4f K" %ndimage.variance(cloudElement, labels=labels)) - cloudElementsUserFile.write("\nConvective fraction is: %.4f " %(((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels))))*100.0)) - cloudElementsUserFile.write("\nEccentricity is: %.4f " %(cloudElementEpsilon)) - #populate the dictionary - if TRMMdirName: - cloudElementDict = {'uniqueID': CEuniqueID, 'cloudElementTime': timelist[t],'cloudElementLatLon': cloudElementLatLons, 'cloudElementCenter':cloudElementCenter, 'cloudElementArea':cloudElementArea, 'cloudElementEccentricity':cloudElementEpsilon, 'cloudElementTmax':TIR_max, 'cloudElementTmin': TIR_min, 'cloudElementPrecipTotal':precipTotal,'cloudElementLatLonTRMM':CETRMMList, 'TRMMArea': TRMMArea,'CETRMMmax':maxCEprecipRate, 'CETRMMmin':minCEprecipRate} - else: - cloudElementDict = {'uniqueID': CEuniqueID, 'cloudElementTime': timelist[t],'cloudElementLatLon': cloudElementLatLons, 'cloudElementCenter':cloudElementCenter, 'cloudElementArea':cloudElementArea, 'cloudElementEccentricity':cloudElementEpsilon, 'cloudElementTmax':TIR_max, 'cloudElementTmin': TIR_min,} - - #current frame list of CEs - currFrameCEs.append(cloudElementDict) - - #draw the graph node - CLOUD_ELEMENT_GRAPH.add_node(CEuniqueID, cloudElementDict) - - if frameNum != 1: - for cloudElementDict in prevFrameCEs: - thisCElen = len(cloudElementLatLons) - percentageOverlap, areaOverlap = cloudElementOverlap(cloudElementLatLons, cloudElementDict['cloudElementLatLon']) - - #change weights to integers because the built in shortest path chokes on floating pts according to Networkx doc - #according to Goyens et al, two CEs are considered related if there is atleast 95% overlap between them for consecutive imgs a max of 2 hrs apart - if percentageOverlap >= 0.95: - CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[0]) - - elif percentageOverlap >= 0.90 and percentageOverlap < 0.95 : - CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[1]) - - elif areaOverlap >= MIN_OVERLAP: - CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[2]) - - else: - #TODO: remove this else as we only wish for the CE details - #ensure only the non-zero elements are considered - #store intel in allCE file - labels, _ = ndimage.label(cloudElement) - cloudElementsFile.write("\n-----------------------------------------------") - cloudElementsFile.write("\n\nTime is: %s" %(str(timelist[t]))) - # cloudElementLat = LAT[loc[0],0] - # cloudElementLon = LON[0,loc[1]] - - #populate cloudElementLatLons by unpacking the original values from loc - #TODO: KDW - too dirty... play with itertools.izip or zip and the enumerate with this - # as cloudElement is masked - for index,value in np.ndenumerate(cloudElement): - if value != 0 : - lat_index,lon_index = index - lat_lon_tuple = (cloudElementLat[lat_index], cloudElementLon[lon_index]) - cloudElementLatLons.append(lat_lon_tuple) - - cloudElementsFile.write("\nLocation of rejected CE (lat,lon) points are: %s" %cloudElementLatLons) - #latCenter and lonCenter are given according to the particular array defining this CE - #so you need to convert this value to the overall domain truth - latCenter, lonCenter = ndimage.measurements.center_of_mass(cloudElement, labels=labels) - latCenter = cloudElementLat[round(latCenter)] - lonCenter = cloudElementLon[round(lonCenter)] - cloudElementsFile.write("\nCenter (lat,lon) is: %.2f\t%.2f" %(latCenter, lonCenter)) - cloudElementsFile.write("\nNumber of boxes are: %d" %numOfBoxes) - cloudElementsFile.write("\nArea is: %.4f km^2" %(cloudElementArea)) - cloudElementsFile.write("\nAverage brightness temperature is: %.4f K" %ndimage.mean(cloudElement, labels=labels)) - cloudElementsFile.write("\nMin brightness temperature is: %.4f K" %ndimage.minimum(cloudElement, labels=labels)) - cloudElementsFile.write("\nMax brightness temperature is: %.4f K" %ndimage.maximum(cloudElement, labels=labels)) - cloudElementsFile.write("\nBrightness temperature variance is: %.4f K" %ndimage.variance(cloudElement, labels=labels)) - cloudElementsFile.write("\nConvective fraction is: %.4f " %(((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels))))*100.0)) - cloudElementsFile.write("\nEccentricity is: %.4f " %(cloudElementEpsilon)) - cloudElementsFile.write("\n-----------------------------------------------") - - #reset list for the next CE - nodeExist = False - cloudElementCenter=[] - cloudElement = [] - cloudElementLat=[] - cloudElementLon =[] - cloudElementLatLons =[] - brightnesstemp1 =[] - brightnesstemp =[] - finalCETRMMvalues =[] - CEprecipRate =[] - CETRMMList =[] - precipTotal = 0.0 - precip=[] - TRMMCloudElementLatLons=[] - - #reset for the next time - prevFrameCEs =[] - prevFrameCEs = currFrameCEs - currFrameCEs =[] - - cloudElementsFile.close - cloudElementsUserFile.close - #if using ARCGIS data store code, uncomment this file close line - #cloudElementsTextFile.close - - #clean up graph - remove parent and childless nodes - outAndInDeg = CLOUD_ELEMENT_GRAPH.degree_iter() - toRemove = [node[0] for node in outAndInDeg if node[1]<1] - CLOUD_ELEMENT_GRAPH.remove_nodes_from(toRemove) - - print "number of nodes are: ", CLOUD_ELEMENT_GRAPH.number_of_nodes() - print "number of edges are: ", CLOUD_ELEMENT_GRAPH.number_of_edges() - print ("*"*80) - - #hierachial graph output - graphTitle = "Cloud Elements observed over somewhere from 0000Z to 0000Z" - #drawGraph(CLOUD_ELEMENT_GRAPH, graphTitle, edgeWeight) - - return CLOUD_ELEMENT_GRAPH + ''' + Purpose:: + Determines the contiguous boxes for a given time of the satellite images i.e. each frame + using scipy ndimage package + + Input:: + mergImgs: masked numpy array in (time,lat,lon),T_bb representing the satellite data. This is masked based on the + maximum acceptable temperature, T_BB_MAX + timelist: a list of python datatimes + TRMMdirName (optional): string representing the path where to find the TRMM datafiles + + Output:: + CLOUD_ELEMENT_GRAPH: a Networkx directed graph where each node contains the information in cloudElementDict + The nodes are determined according to the area of contiguous squares. The nodes are linked through weighted edges. + + cloudElementDict = {'uniqueID': unique tag for this CE, + 'cloudElementTime': time of the CE, + 'cloudElementLatLon': (lat,lon,value) of MERG data of CE, + 'cloudElementCenter':list of floating-point [lat,lon] representing the CE's center + 'cloudElementArea':floating-point representing the area of the CE, + 'cloudElementEccentricity': floating-point representing the shape of the CE, + 'cloudElementTmax':integer representing the maximum Tb in CE, + 'cloudElementTmin': integer representing the minimum Tb in CE, + 'cloudElementPrecipTotal':floating-point representing the sum of all rainfall in CE if TRMMdirName entered, + 'cloudElementLatLonTRMM':(lat,lon,value) of TRMM data in CE if TRMMdirName entered, + 'TRMMArea': floating-point representing the CE if TRMMdirName entered, + 'CETRMMmax':floating-point representing the max rate in the CE if TRMMdirName entered, + 'CETRMMmin':floating-point representing the min rate in the CE if TRMMdirName entered} + Assumptions:: + Assumes we are dealing with MERG data which is 4kmx4km resolved, thus the smallest value + required according to Vila et al. (2008) is 2400km^2 + therefore, 2400/16 = 150 contiguous squares + ''' + + frame = ma.empty((1,mergImgs.shape[1],mergImgs.shape[2])) + CEcounter = 0 + frameCEcounter = 0 + frameNum = 0 + cloudElementEpsilon = 0.0 + cloudElementDict = {} + cloudElementCenter = [] #list with two elements [lat,lon] for the center of a CE + prevFrameCEs = [] #list for CEs in previous frame + currFrameCEs = [] #list for CEs in current frame + cloudElementLat = [] #list for a particular CE's lat values + cloudElementLon = [] #list for a particular CE's lon values + cloudElementLatLons = [] #list for a particular CE's (lat,lon) values + + prevLatValue = 0.0 + prevLonValue = 0.0 + TIR_min = 0.0 + TIR_max = 0.0 + temporalRes = 3 # TRMM data is 3 hourly + precipTotal = 0.0 + CETRMMList =[] + precip =[] + TRMMCloudElementLatLons =[] + + minCELatLimit = 0.0 + minCELonLimit = 0.0 + maxCELatLimit = 0.0 + maxCELonLimit = 0.0 + + nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) + + #openfile for storing ALL cloudElement information + cloudElementsFile = open((MAINDIRECTORY+'/textFiles/cloudElements.txt'),'wb') + #openfile for storing cloudElement information meeting user criteria i.e. MCCs in this case + cloudElementsUserFile = open((MAINDIRECTORY+'/textFiles/cloudElementsUserFile.txt'),'w') + + #NB in the TRMM files the info is hours since the time thus 00Z file has in 01, 02 and 03 times + for t in xrange(mergImgs.shape[0]): + #------------------------------------------------- + # #textfile name for saving the data for arcgis + # thisFileName = MAINDIRECTORY+'/' + (str(timelist[t])).replace(" ", "_") + '.txt' + # cloudElementsTextFile = open(thisFileName,'w') + #------------------------------------------------- + + #determine contiguous locations with temeperature below the warmest temp i.e. cloudElements in each frame + frame, CEcounter = ndimage.measurements.label(mergImgs[t,:,:], structure=STRUCTURING_ELEMENT) + frameCEcounter=0 + frameNum += 1 + + #for each of the areas identified, check to determine if it a valid CE via an area and T requirement + for count in xrange(CEcounter): + #[0] is time dimension. Determine the actual values from the data + #loc is a masked array + try: + loc = ndimage.find_objects(frame==(count+1))[0] + except Exception, e: + print "Error is ", e + continue + + + cloudElement = mergImgs[t,:,:][loc] + labels, lcounter = ndimage.label(cloudElement) + + #determine the true lats and lons for this particular CE + cloudElementLat = LAT[loc[0],0] + cloudElementLon = LON[0,loc[1]] + + #determine number of boxes in this cloudelement + numOfBoxes = np.count_nonzero(cloudElement) + cloudElementArea = numOfBoxes*XRES*YRES + + #If the area is greater than the area required, or if the area is smaller than the suggested area, check if it meets a convective fraction requirement + #consider as CE + + if cloudElementArea >= AREA_MIN or (cloudElementArea < AREA_MIN and ((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels)))) < CONVECTIVE_FRACTION ): + + #get some time information and labeling info + frameTime = str(timelist[t]) + frameCEcounter +=1 + CEuniqueID = 'F'+str(frameNum)+'CE'+str(frameCEcounter) + + #------------------------------------------------- + #textfile name for accesing CE data using MATLAB code + # thisFileName = MAINDIRECTORY+'/' + (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.txt' + # cloudElementsTextFile = open(thisFileName,'w') + #------------------------------------------------- + + # ------ NETCDF File stuff for brightness temp stuff ------------------------------------ + thisFileName = MAINDIRECTORY +'/MERGnetcdfCEs/cloudElements'+ (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.nc' + currNetCDFCEData = Dataset(thisFileName, 'w', format='NETCDF4') + currNetCDFCEData.description = 'Cloud Element '+CEuniqueID + ' temperature data' + currNetCDFCEData.calendar = 'standard' + currNetCDFCEData.conventions = 'COARDS' + # dimensions + currNetCDFCEData.createDimension('time', None) + currNetCDFCEData.createDimension('lat', len(LAT[:,0])) + currNetCDFCEData.createDimension('lon', len(LON[0,:])) + # variables + tempDims = ('time','lat', 'lon',) + times = currNetCDFCEData.createVariable('time', 'f8', ('time',)) + times.units = 'hours since '+ str(timelist[t])[:-6] + latitudes = currNetCDFCEData.createVariable('latitude', 'f8', ('lat',)) + longitudes = currNetCDFCEData.createVariable('longitude', 'f8', ('lon',)) + brightnesstemp = currNetCDFCEData.createVariable('brightnesstemp', 'i16',tempDims ) + brightnesstemp.units = 'Kelvin' + # NETCDF data + dates=[timelist[t]+timedelta(hours=0)] + times[:] = date2num(dates,units=times.units) + longitudes[:] = LON[0,:] + longitudes.units = "degrees_east" + longitudes.long_name = "Longitude" + + latitudes[:] = LAT[:,0] + latitudes.units = "degrees_north" + latitudes.long_name ="Latitude" + + #generate array of zeros for brightness temperature + brightnesstemp1 = ma.zeros((1,len(latitudes), len(longitudes))).astype('int16') + #-----------End most of NETCDF file stuff ------------------------------------ + + #if other dataset (TRMM) assumed to be a precipitation dataset was entered + if TRMMdirName: + #------------------TRMM stuff ------------------------------------------------- + fileDate = ((str(timelist[t])).replace(" ", "")[:-8]).replace("-","") + fileHr1 = (str(timelist[t])).replace(" ", "")[-8:-6] + + if int(fileHr1) % temporalRes == 0: + fileHr = fileHr1 + else: + fileHr = (int(fileHr1)/temporalRes) * temporalRes + if fileHr < 10: + fileHr = '0'+str(fileHr) + else: + str(fileHr) + + #open TRMM file for the resolution info and to create the appropriate sized grid + TRMMfileName = TRMMdirName+'/3B42.'+ fileDate + "."+str(fileHr)+".7A.nc" + + TRMMData = Dataset(TRMMfileName,'r', format='NETCDF4') + precipRate = TRMMData.variables['pcp'][:,:,:] + latsrawTRMMData = TRMMData.variables['latitude'][:] + lonsrawTRMMData = TRMMData.variables['longitude'][:] + lonsrawTRMMData[lonsrawTRMMData > 180] = lonsrawTRMMData[lonsrawTRMMData>180] - 360. + LONTRMM, LATTRMM = np.meshgrid(lonsrawTRMMData, latsrawTRMMData) + + nygrdTRMM = len(LATTRMM[:,0]); nxgrdTRMM = len(LONTRMM[0,:]) + precipRateMasked = ma.masked_array(precipRate, mask=(precipRate < 0.0)) + #---------regrid the TRMM data to the MERG dataset ---------------------------------- + #regrid using the do_regrid stuff from the Apache OCW + regriddedTRMM = ma.zeros((0, nygrd, nxgrd)) + #regriddedTRMM = process.do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) + regriddedTRMM = do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) + #---------------------------------------------------------------------------------- + + # #get the lat/lon info from cloudElement + #get the lat/lon info from the file + latCEStart = LAT[0][0] + latCEEnd = LAT[-1][0] + lonCEStart = LON[0][0] + lonCEEnd = LON[0][-1] + + #get the lat/lon info for TRMM data (different resolution) + latStartT = find_nearest(latsrawTRMMData, latCEStart) + latEndT = find_nearest(latsrawTRMMData, latCEEnd) + lonStartT = find_nearest(lonsrawTRMMData, lonCEStart) + lonEndT = find_nearest(lonsrawTRMMData, lonCEEnd) + latStartIndex = np.where(latsrawTRMMData == latStartT) + latEndIndex = np.where(latsrawTRMMData == latEndT) + lonStartIndex = np.where(lonsrawTRMMData == lonStartT) + lonEndIndex = np.where(lonsrawTRMMData == lonEndT) + + #get the relevant TRMM info + CEprecipRate = precipRate[:,(latStartIndex[0][0]-1):latEndIndex[0][0],(lonStartIndex[0][0]-1):lonEndIndex[0][0]] + TRMMData.close() + + # ------ NETCDF File info for writing TRMM CE rainfall ------------------------------------ + thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/TRMM' + (str(timelist[t])).replace(" ", "_") + CEuniqueID +'.nc' + currNetCDFTRMMData = Dataset(thisFileName, 'w', format='NETCDF4') + currNetCDFTRMMData.description = 'Cloud Element '+CEuniqueID + ' precipitation data' + currNetCDFTRMMData.calendar = 'standard' + currNetCDFTRMMData.conventions = 'COARDS' + # dimensions + currNetCDFTRMMData.createDimension('time', None) + currNetCDFTRMMData.createDimension('lat', len(LAT[:,0])) + currNetCDFTRMMData.createDimension('lon', len(LON[0,:])) + + # variables + TRMMprecip = ('time','lat', 'lon',) + times = currNetCDFTRMMData.createVariable('time', 'f8', ('time',)) + times.units = 'hours since '+ str(timelist[t])[:-6] + latitude = currNetCDFTRMMData.createVariable('latitude', 'f8', ('lat',)) + longitude = currNetCDFTRMMData.createVariable('longitude', 'f8', ('lon',)) + rainFallacc = currNetCDFTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip ) + rainFallacc.units = 'mm' + + longitude[:] = LON[0,:] + longitude.units = "degrees_east" + longitude.long_name = "Longitude" + + latitude[:] = LAT[:,0] + latitude.units = "degrees_north" + latitude.long_name ="Latitude" + + finalCETRMMvalues = ma.zeros((brightnesstemp.shape)) + #-----------End most of NETCDF file stuff ------------------------------------ + + #populate cloudElementLatLons by unpacking the original values from loc to get the actual value for lat and lon + #TODO: KDW - too dirty... play with itertools.izip or zip and the enumerate with this + # as cloudElement is masked + for index,value in np.ndenumerate(cloudElement): + if value != 0 : + lat_index,lon_index = index + lat_lon_tuple = (cloudElementLat[lat_index], cloudElementLon[lon_index],value) + + #generate the comma separated file for GIS + cloudElementLatLons.append(lat_lon_tuple) + + #temp data for CE NETCDF file + brightnesstemp1[0,int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] = value + + if TRMMdirName: + finalCETRMMvalues[0,int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] = regriddedTRMM[int(np.where(LAT[:,0]==cloudElementLat[lat_index])[0]),int(np.where(LON[0,:]==cloudElementLon[lon_index])[0])] + CETRMMList.append((cloudElementLat[lat_index], cloudElementLon[lon_index], finalCETRMMvalues[0,cloudElementLat[lat_index], cloudElementLon[lon_index]])) + + + brightnesstemp[:] = brightnesstemp1[:] + currNetCDFCEData.close() + + if TRMMdirName: + + #calculate the total precip associated with the feature + for index, value in np.ndenumerate(finalCETRMMvalues): + precipTotal += value + precip.append(value) + + rainFallacc[:] = finalCETRMMvalues[:] + currNetCDFTRMMData.close() + TRMMnumOfBoxes = np.count_nonzero(finalCETRMMvalues) + TRMMArea = TRMMnumOfBoxes*XRES*YRES + try: + maxCEprecipRate = np.max(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) + minCEprecipRate = np.min(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) + except: + pass + + #sort cloudElementLatLons by lats + cloudElementLatLons.sort(key=lambda tup: tup[0]) + + #determine if the cloud element the shape + cloudElementEpsilon = eccentricity (cloudElement) + cloudElementsUserFile.write("\n\nTime is: %s" %(str(timelist[t]))) + cloudElementsUserFile.write("\nCEuniqueID is: %s" %CEuniqueID) + latCenter, lonCenter = ndimage.measurements.center_of_mass(cloudElement, labels=labels) + + #latCenter and lonCenter are given according to the particular array defining this CE + #so you need to convert this value to the overall domain truth + latCenter = cloudElementLat[round(latCenter)] + lonCenter = cloudElementLon[round(lonCenter)] + cloudElementsUserFile.write("\nCenter (lat,lon) is: %.2f\t%.2f" %(latCenter, lonCenter)) + cloudElementCenter.append(latCenter) + cloudElementCenter.append(lonCenter) + cloudElementsUserFile.write("\nNumber of boxes are: %d" %numOfBoxes) + cloudElementsUserFile.write("\nArea is: %.4f km^2" %(cloudElementArea)) + cloudElementsUserFile.write("\nAverage brightness temperature is: %.4f K" %ndimage.mean(cloudElement, labels=labels)) + cloudElementsUserFile.write("\nMin brightness temperature is: %.4f K" %ndimage.minimum(cloudElement, labels=labels)) + cloudElementsUserFile.write("\nMax brightness temperature is: %.4f K" %ndimage.maximum(cloudElement, labels=labels)) + cloudElementsUserFile.write("\nBrightness temperature variance is: %.4f K" %ndimage.variance(cloudElement, labels=labels)) + cloudElementsUserFile.write("\nConvective fraction is: %.4f " %(((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels))))*100.0)) + cloudElementsUserFile.write("\nEccentricity is: %.4f " %(cloudElementEpsilon)) + #populate the dictionary + if TRMMdirName: + cloudElementDict = {'uniqueID': CEuniqueID, 'cloudElementTime': timelist[t],'cloudElementLatLon': cloudElementLatLons, 'cloudElementCenter':cloudElementCenter, 'cloudElementArea':cloudElementArea, 'cloudElementEccentricity':cloudElementEpsilon, 'cloudElementTmax':TIR_max, 'cloudElementTmin': TIR_min, 'cloudElementPrecipTotal':precipTotal,'cloudElementLatLonTRMM':CETRMMList, 'TRMMArea': TRMMArea,'CETRMMmax':maxCEprecipRate, 'CETRMMmin':minCEprecipRate} + else: + cloudElementDict = {'uniqueID': CEuniqueID, 'cloudElementTime': timelist[t],'cloudElementLatLon': cloudElementLatLons, 'cloudElementCenter':cloudElementCenter, 'cloudElementArea':cloudElementArea, 'cloudElementEccentricity':cloudElementEpsilon, 'cloudElementTmax':TIR_max, 'cloudElementTmin': TIR_min,} + + #current frame list of CEs + currFrameCEs.append(cloudElementDict) + + #draw the graph node + CLOUD_ELEMENT_GRAPH.add_node(CEuniqueID, cloudElementDict) + + if frameNum != 1: + for cloudElementDict in prevFrameCEs: + thisCElen = len(cloudElementLatLons) + percentageOverlap, areaOverlap = cloudElementOverlap(cloudElementLatLons, cloudElementDict['cloudElementLatLon']) + + #change weights to integers because the built in shortest path chokes on floating pts according to Networkx doc + #according to Goyens et al, two CEs are considered related if there is atleast 95% overlap between them for consecutive imgs a max of 2 hrs apart + if percentageOverlap >= 0.95: + CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[0]) + + elif percentageOverlap >= 0.90 and percentageOverlap < 0.95 : + CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[1]) + + elif areaOverlap >= MIN_OVERLAP: + CLOUD_ELEMENT_GRAPH.add_edge(cloudElementDict['uniqueID'], CEuniqueID, weight=edgeWeight[2]) + + else: + #TODO: remove this else as we only wish for the CE details + #ensure only the non-zero elements are considered + #store intel in allCE file + labels, _ = ndimage.label(cloudElement) + cloudElementsFile.write("\n-----------------------------------------------") + cloudElementsFile.write("\n\nTime is: %s" %(str(timelist[t]))) + # cloudElementLat = LAT[loc[0],0] + # cloudElementLon = LON[0,loc[1]] + + #populate cloudElementLatLons by unpacking the original values from loc + #TODO: KDW - too dirty... play with itertools.izip or zip and the enumerate with this + # as cloudElement is masked + for index,value in np.ndenumerate(cloudElement): + if value != 0 : + lat_index,lon_index = index + lat_lon_tuple = (cloudElementLat[lat_index], cloudElementLon[lon_index]) + cloudElementLatLons.append(lat_lon_tuple) + + cloudElementsFile.write("\nLocation of rejected CE (lat,lon) points are: %s" %cloudElementLatLons) + #latCenter and lonCenter are given according to the particular array defining this CE + #so you need to convert this value to the overall domain truth + latCenter, lonCenter = ndimage.measurements.center_of_mass(cloudElement, labels=labels) + latCenter = cloudElementLat[round(latCenter)] + lonCenter = cloudElementLon[round(lonCenter)] + cloudElementsFile.write("\nCenter (lat,lon) is: %.2f\t%.2f" %(latCenter, lonCenter)) + cloudElementsFile.write("\nNumber of boxes are: %d" %numOfBoxes) + cloudElementsFile.write("\nArea is: %.4f km^2" %(cloudElementArea)) + cloudElementsFile.write("\nAverage brightness temperature is: %.4f K" %ndimage.mean(cloudElement, labels=labels)) + cloudElementsFile.write("\nMin brightness temperature is: %.4f K" %ndimage.minimum(cloudElement, labels=labels)) + cloudElementsFile.write("\nMax brightness temperature is: %.4f K" %ndimage.maximum(cloudElement, labels=labels)) + cloudElementsFile.write("\nBrightness temperature variance is: %.4f K" %ndimage.variance(cloudElement, labels=labels)) + cloudElementsFile.write("\nConvective fraction is: %.4f " %(((ndimage.minimum(cloudElement, labels=labels))/float((ndimage.maximum(cloudElement, labels=labels))))*100.0)) + cloudElementsFile.write("\nEccentricity is: %.4f " %(cloudElementEpsilon)) + cloudElementsFile.write("\n-----------------------------------------------") + + #reset list for the next CE + nodeExist = False + cloudElementCenter=[] + cloudElement = [] + cloudElementLat=[] + cloudElementLon =[] + cloudElementLatLons =[] + brightnesstemp1 =[] + brightnesstemp =[] + finalCETRMMvalues =[] + CEprecipRate =[] + CETRMMList =[] + precipTotal = 0.0 + precip=[] + TRMMCloudElementLatLons=[] + + #reset for the next time + prevFrameCEs =[] + prevFrameCEs = currFrameCEs + currFrameCEs =[] + + cloudElementsFile.close + cloudElementsUserFile.close + #if using ARCGIS data store code, uncomment this file close line + #cloudElementsTextFile.close + + #clean up graph - remove parent and childless nodes + outAndInDeg = CLOUD_ELEMENT_GRAPH.degree_iter() + toRemove = [node[0] for node in outAndInDeg if node[1]<1] + CLOUD_ELEMENT_GRAPH.remove_nodes_from(toRemove) + + print "number of nodes are: ", CLOUD_ELEMENT_GRAPH.number_of_nodes() + print "number of edges are: ", CLOUD_ELEMENT_GRAPH.number_of_edges() + print ("*"*80) + + #hierachial graph output + graphTitle = "Cloud Elements observed over somewhere from 0000Z to 0000Z" + #drawGraph(CLOUD_ELEMENT_GRAPH, graphTitle, edgeWeight) + + return CLOUD_ELEMENT_GRAPH #****************************************************************** def findPrecipRate(TRMMdirName, timelist): - ''' - Purpose:: - Determines the precipitation rates for MCSs found if TRMMdirName was not entered in findCloudElements this can be used + ''' + Purpose:: + Determines the precipitation rates for MCSs found if TRMMdirName was not entered in findCloudElements this can be used - Input:: - TRMMdirName: a string representing the directory for the original TRMM netCDF files - timelist: a list of python datatimes + Input:: + TRMMdirName: a string representing the directory for the original TRMM netCDF files + timelist: a list of python datatimes - Output:: a list of dictionary of the TRMM data - NB: also creates netCDF with TRMM data for each CE (for post processing) index - in MAINDIRECTORY/TRMMnetcdfCEs + Output:: a list of dictionary of the TRMM data + NB: also creates netCDF with TRMM data for each CE (for post processing) index + in MAINDIRECTORY/TRMMnetcdfCEs - Assumptions:: Assumes that findCloudElements was run without the TRMMdirName value + Assumptions:: Assumes that findCloudElements was run without the TRMMdirName value - ''' - allCEnodesTRMMdata =[] - TRMMdataDict={} - precipTotal = 0.0 - - os.chdir((MAINDIRECTORY+'/MERGnetcdfCEs/')) - imgFilename = '' - temporalRes = 3 #3 hours for TRMM - - #sort files - files = filter(os.path.isfile, glob.glob("*.nc")) - files.sort(key=lambda x: os.path.getmtime(x)) - - for afile in files: - fullFname = os.path.splitext(afile)[0] - noFrameExtension = (fullFname.replace("_","")).split('F')[0] - CEuniqueID = 'F' +(fullFname.replace("_","")).split('F')[1] - fileDateTimeChar = (noFrameExtension.replace(":","")).split('s')[1] - fileDateTime = fileDateTimeChar.replace("-","") - fileDate = fileDateTime[:-6] - fileHr1=fileDateTime[-6:-4] - - cloudElementData = Dataset(afile,'r', format='NETCDF4') - brightnesstemp1 = cloudElementData.variables['brightnesstemp'][:,:,:] - latsrawCloudElements = cloudElementData.variables['latitude'][:] - lonsrawCloudElements = cloudElementData.variables['longitude'][:] - - brightnesstemp = np.squeeze(brightnesstemp1, axis=0) - - if int(fileHr1) % temporalRes == 0: - fileHr = fileHr1 - else: - fileHr = (int(fileHr1)/temporalRes) * temporalRes - - if fileHr < 10: - fileHr = '0'+str(fileHr) - else: - str(fileHr) - - TRMMfileName = TRMMdirName+"/3B42."+ str(fileDate) + "."+str(fileHr)+".7A.nc" - TRMMData = Dataset(TRMMfileName,'r', format='NETCDF4') - precipRate = TRMMData.variables['pcp'][:,:,:] - latsrawTRMMData = TRMMData.variables['latitude'][:] - lonsrawTRMMData = TRMMData.variables['longitude'][:] - lonsrawTRMMData[lonsrawTRMMData > 180] = lonsrawTRMMData[lonsrawTRMMData>180] - 360. - LONTRMM, LATTRMM = np.meshgrid(lonsrawTRMMData, latsrawTRMMData) - - #nygrdTRMM = len(LATTRMM[:,0]); nxgrd = len(LONTRMM[0,:]) - nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) - - precipRateMasked = ma.masked_array(precipRate, mask=(precipRate < 0.0)) - #---------regrid the TRMM data to the MERG dataset ---------------------------------- - #regrid using the do_regrid stuff from the Apache OCW - regriddedTRMM = ma.zeros((0, nygrd, nxgrd)) - regriddedTRMM = do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) - #regriddedTRMM = process.do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) - #---------------------------------------------------------------------------------- - - # #get the lat/lon info from - latCEStart = LAT[0][0] - latCEEnd = LAT[-1][0] - lonCEStart = LON[0][0] - lonCEEnd = LON[0][-1] - - #get the lat/lon info for TRMM data (different resolution) - latStartT = find_nearest(latsrawTRMMData, latCEStart) - latEndT = find_nearest(latsrawTRMMData, latCEEnd) - lonStartT = find_nearest(lonsrawTRMMData, lonCEStart) - lonEndT = find_nearest(lonsrawTRMMData, lonCEEnd) - latStartIndex = np.where(latsrawTRMMData == latStartT) - latEndIndex = np.where(latsrawTRMMData == latEndT) - lonStartIndex = np.where(lonsrawTRMMData == lonStartT) - lonEndIndex = np.where(lonsrawTRMMData == lonEndT) - - #get the relevant TRMM info - CEprecipRate = precipRate[:,(latStartIndex[0][0]-1):latEndIndex[0][0],(lonStartIndex[0][0]-1):lonEndIndex[0][0]] - TRMMData.close() - - - # ------ NETCDF File stuff ------------------------------------ - thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/'+ fileDateTime + CEuniqueID +'.nc' - currNetCDFTRMMData = Dataset(thisFileName, 'w', format='NETCDF4') - currNetCDFTRMMData.description = 'Cloud Element '+CEuniqueID + ' rainfall data' - currNetCDFTRMMData.calendar = 'standard' - currNetCDFTRMMData.conventions = 'COARDS' - # dimensions - currNetCDFTRMMData.createDimension('time', None) - currNetCDFTRMMData.createDimension('lat', len(LAT[:,0])) - currNetCDFTRMMData.createDimension('lon', len(LON[0,:])) - # variables - TRMMprecip = ('time','lat', 'lon',) - times = currNetCDFTRMMData.createVariable('time', 'f8', ('time',)) - times.units = 'hours since '+ fileDateTime[:-6] - latitude = currNetCDFTRMMData.createVariable('latitude', 'f8', ('lat',)) - longitude = currNetCDFTRMMData.createVariable('longitude', 'f8', ('lon',)) - rainFallacc = currNetCDFTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip ) - rainFallacc.units = 'mm' - - longitude[:] = LON[0,:] - longitude.units = "degrees_east" - longitude.long_name = "Longitude" - - latitude[:] = LAT[:,0] - latitude.units = "degrees_north" - latitude.long_name ="Latitude" - - finalCETRMMvalues = ma.zeros((brightnesstemp1.shape)) - #-----------End most of NETCDF file stuff ------------------------------------ - for index,value in np.ndenumerate(brightnesstemp): - lat_index, lon_index = index - currTimeValue = 0 - if value > 0: - - finalCETRMMvalues[0,lat_index,lon_index] = regriddedTRMM[int(np.where(LAT[:,0]==LAT[lat_index,0])[0]), int(np.where(LON[0,:]==LON[0,lon_index])[0])] - - - rainFallacc[:] = finalCETRMMvalues - currNetCDFTRMMData.close() - - for index, value in np.ndenumerate(finalCETRMMvalues): - precipTotal += value - - TRMMnumOfBoxes = np.count_nonzero(finalCETRMMvalues) - TRMMArea = TRMMnumOfBoxes*XRES*YRES - - try: - minCEprecipRate = np.min(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) - except: - minCEprecipRate = 0.0 - - try: - maxCEprecipRate = np.max(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) - except: - maxCEprecipRate = 0.0 - - #add info to CLOUDELEMENTSGRAPH - #TODO try block - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(CEuniqueID): - if eachdict[1]['uniqueID'] == CEuniqueID: - if not 'cloudElementPrecipTotal' in eachdict[1].keys(): - eachdict[1]['cloudElementPrecipTotal'] = precipTotal - if not 'cloudElementLatLonTRMM' in eachdict[1].keys(): - eachdict[1]['cloudElementLatLonTRMM'] = finalCETRMMvalues - if not 'TRMMArea' in eachdict[1].keys(): - eachdict[1]['TRMMArea'] = TRMMArea - if not 'CETRMMmin' in eachdict[1].keys(): - eachdict[1]['CETRMMmin'] = minCEprecipRate - if not 'CETRMMmax' in eachdict[1].keys(): - eachdict[1]['CETRMMmax'] = maxCEprecipRate - - #clean up - precipTotal = 0.0 - latsrawTRMMData =[] - lonsrawTRMMData = [] - latsrawCloudElements=[] - lonsrawCloudElements=[] - finalCETRMMvalues =[] - CEprecipRate =[] - brightnesstemp =[] - TRMMdataDict ={} - - return allCEnodesTRMMdata + ''' + allCEnodesTRMMdata =[] + TRMMdataDict={} + precipTotal = 0.0 + + os.chdir((MAINDIRECTORY+'/MERGnetcdfCEs/')) + imgFilename = '' + temporalRes = 3 #3 hours for TRMM + + #sort files + files = filter(os.path.isfile, glob.glob("*.nc")) + files.sort(key=lambda x: os.path.getmtime(x)) + + for afile in files: + fullFname = os.path.splitext(afile)[0] + noFrameExtension = (fullFname.replace("_","")).split('F')[0] + CEuniqueID = 'F' +(fullFname.replace("_","")).split('F')[1] + fileDateTimeChar = (noFrameExtension.replace(":","")).split('s')[1] + fileDateTime = fileDateTimeChar.replace("-","") + fileDate = fileDateTime[:-6] + fileHr1=fileDateTime[-6:-4] + + cloudElementData = Dataset(afile,'r', format='NETCDF4') + brightnesstemp1 = cloudElementData.variables['brightnesstemp'][:,:,:] + latsrawCloudElements = cloudElementData.variables['latitude'][:] + lonsrawCloudElements = cloudElementData.variables['longitude'][:] + + brightnesstemp = np.squeeze(brightnesstemp1, axis=0) + + if int(fileHr1) % temporalRes == 0: + fileHr = fileHr1 + else: + fileHr = (int(fileHr1)/temporalRes) * temporalRes + + if fileHr < 10: + fileHr = '0'+str(fileHr) + else: + str(fileHr) + + TRMMfileName = TRMMdirName+"/3B42."+ str(fileDate) + "."+str(fileHr)+".7A.nc" + TRMMData = Dataset(TRMMfileName,'r', format='NETCDF4') + precipRate = TRMMData.variables['pcp'][:,:,:] + latsrawTRMMData = TRMMData.variables['latitude'][:] + lonsrawTRMMData = TRMMData.variables['longitude'][:] + lonsrawTRMMData[lonsrawTRMMData > 180] = lonsrawTRMMData[lonsrawTRMMData>180] - 360. + LONTRMM, LATTRMM = np.meshgrid(lonsrawTRMMData, latsrawTRMMData) + + #nygrdTRMM = len(LATTRMM[:,0]); nxgrd = len(LONTRMM[0,:]) + nygrd = len(LAT[:, 0]); nxgrd = len(LON[0, :]) + + precipRateMasked = ma.masked_array(precipRate, mask=(precipRate < 0.0)) + #---------regrid the TRMM data to the MERG dataset ---------------------------------- + #regrid using the do_regrid stuff from the Apache OCW + regriddedTRMM = ma.zeros((0, nygrd, nxgrd)) + regriddedTRMM = do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) + #regriddedTRMM = process.do_regrid(precipRateMasked[0,:,:], LATTRMM, LONTRMM, LAT, LON, order=1, mdi= -999999999) + #---------------------------------------------------------------------------------- + + # #get the lat/lon info from + latCEStart = LAT[0][0] + latCEEnd = LAT[-1][0] + lonCEStart = LON[0][0] + lonCEEnd = LON[0][-1] + + #get the lat/lon info for TRMM data (different resolution) + latStartT = find_nearest(latsrawTRMMData, latCEStart) + latEndT = find_nearest(latsrawTRMMData, latCEEnd) + lonStartT = find_nearest(lonsrawTRMMData, lonCEStart) + lonEndT = find_nearest(lonsrawTRMMData, lonCEEnd) + latStartIndex = np.where(latsrawTRMMData == latStartT) + latEndIndex = np.where(latsrawTRMMData == latEndT) + lonStartIndex = np.where(lonsrawTRMMData == lonStartT) + lonEndIndex = np.where(lonsrawTRMMData == lonEndT) + + #get the relevant TRMM info + CEprecipRate = precipRate[:,(latStartIndex[0][0]-1):latEndIndex[0][0],(lonStartIndex[0][0]-1):lonEndIndex[0][0]] + TRMMData.close() + + + # ------ NETCDF File stuff ------------------------------------ + thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/'+ fileDateTime + CEuniqueID +'.nc' + currNetCDFTRMMData = Dataset(thisFileName, 'w', format='NETCDF4') + currNetCDFTRMMData.description = 'Cloud Element '+CEuniqueID + ' rainfall data' + currNetCDFTRMMData.calendar = 'standard' + currNetCDFTRMMData.conventions = 'COARDS' + # dimensions + currNetCDFTRMMData.createDimension('time', None) + currNetCDFTRMMData.createDimension('lat', len(LAT[:,0])) + currNetCDFTRMMData.createDimension('lon', len(LON[0,:])) + # variables + TRMMprecip = ('time','lat', 'lon',) + times = currNetCDFTRMMData.createVariable('time', 'f8', ('time',)) + times.units = 'hours since '+ fileDateTime[:-6] + latitude = currNetCDFTRMMData.createVariable('latitude', 'f8', ('lat',)) + longitude = currNetCDFTRMMData.createVariable('longitude', 'f8', ('lon',)) + rainFallacc = currNetCDFTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip ) + rainFallacc.units = 'mm' + + longitude[:] = LON[0,:] + longitude.units = "degrees_east" + longitude.long_name = "Longitude" + + latitude[:] = LAT[:,0] + latitude.units = "degrees_north" + latitude.long_name ="Latitude" + + finalCETRMMvalues = ma.zeros((brightnesstemp1.shape)) + #-----------End most of NETCDF file stuff ------------------------------------ + for index,value in np.ndenumerate(brightnesstemp): + lat_index, lon_index = index + currTimeValue = 0 + if value > 0: + + finalCETRMMvalues[0,lat_index,lon_index] = regriddedTRMM[int(np.where(LAT[:,0]==LAT[lat_index,0])[0]), int(np.where(LON[0,:]==LON[0,lon_index])[0])] + + + rainFallacc[:] = finalCETRMMvalues + currNetCDFTRMMData.close() + + for index, value in np.ndenumerate(finalCETRMMvalues): + precipTotal += value + + TRMMnumOfBoxes = np.count_nonzero(finalCETRMMvalues) + TRMMArea = TRMMnumOfBoxes*XRES*YRES + + try: + minCEprecipRate = np.min(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) + except: + minCEprecipRate = 0.0 + + try: + maxCEprecipRate = np.max(finalCETRMMvalues[np.nonzero(finalCETRMMvalues)]) + except: + maxCEprecipRate = 0.0 + + #add info to CLOUDELEMENTSGRAPH + #TODO try block + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(CEuniqueID): + if eachdict[1]['uniqueID'] == CEuniqueID: + if not 'cloudElementPrecipTotal' in eachdict[1].keys(): + eachdict[1]['cloudElementPrecipTotal'] = precipTotal + if not 'cloudElementLatLonTRMM' in eachdict[1].keys(): + eachdict[1]['cloudElementLatLonTRMM'] = finalCETRMMvalues + if not 'TRMMArea' in eachdict[1].keys(): + eachdict[1]['TRMMArea'] = TRMMArea + if not 'CETRMMmin' in eachdict[1].keys(): + eachdict[1]['CETRMMmin'] = minCEprecipRate + if not 'CETRMMmax' in eachdict[1].keys(): + eachdict[1]['CETRMMmax'] = maxCEprecipRate + + #clean up + precipTotal = 0.0 + latsrawTRMMData =[] + lonsrawTRMMData = [] + latsrawCloudElements=[] + lonsrawCloudElements=[] + finalCETRMMvalues =[] + CEprecipRate =[] + brightnesstemp =[] + TRMMdataDict ={} + + return allCEnodesTRMMdata #****************************************************************** def findCloudClusters(CEGraph): - ''' - Purpose:: - Determines the cloud clusters properties from the subgraphs in - the graph i.e. prunes the graph according to the minimum depth - - Input:: - CEGraph: a Networkx directed graph of the CEs with weighted edges - according the area overlap between nodes (CEs) of consectuive frames - - Output:: - PRUNED_GRAPH: a Networkx directed graph of with CCs/ MCSs - - ''' - - seenNode = [] - allMCSLists =[] - pathDictList =[] - pathList=[] - - cloudClustersFile = open((MAINDIRECTORY+'/textFiles/cloudClusters.txt'),'wb') - - for eachNode in CEGraph: - #check if the node has been seen before - if eachNode not in dict(enumerate(zip(*seenNode))): - #look for all trees associated with node as the root - thisPathDistanceAndLength = nx.single_source_dijkstra(CEGraph, eachNode) - #determine the actual shortestPath and minimum depth/length - maxDepthAndMinPath = findMaxDepthAndMinPath(thisPathDistanceAndLength) - if maxDepthAndMinPath: - maxPathLength = maxDepthAndMinPath[0] - shortestPath = maxDepthAndMinPath[1] - - #add nodes and paths to PRUNED_GRAPH - for i in xrange(len(shortestPath)): - if PRUNED_GRAPH.has_node(shortestPath[i]) is False: - PRUNED_GRAPH.add_node(shortestPath[i]) - - #add edge if necessary - if i < (len(shortestPath)-1) and PRUNED_GRAPH.has_edge(shortestPath[i], shortestPath[i+1]) is False: - prunedGraphEdgeweight = CEGraph.get_edge_data(shortestPath[i], shortestPath[i+1])['weight'] - PRUNED_GRAPH.add_edge(shortestPath[i], shortestPath[i+1], weight=prunedGraphEdgeweight) - - #note information in a file for consideration later i.e. checking to see if it works - cloudClustersFile.write("\nSubtree pathlength is %d and path is %s" %(maxPathLength, shortestPath)) - #update seenNode info - seenNode.append(shortestPath) - - print "pruned graph" - print "number of nodes are: ", PRUNED_GRAPH.number_of_nodes() - print "number of edges are: ", PRUNED_GRAPH.number_of_edges() - print ("*"*80) - - graphTitle = "Cloud Clusters observed over somewhere during sometime" - #drawGraph(PRUNED_GRAPH, graphTitle, edgeWeight) - cloudClustersFile.close - - return PRUNED_GRAPH + ''' + Purpose:: + Determines the cloud clusters properties from the subgraphs in + the graph i.e. prunes the graph according to the minimum depth + + Input:: + CEGraph: a Networkx directed graph of the CEs with weighted edges + according the area overlap between nodes (CEs) of consectuive frames + + Output:: + PRUNED_GRAPH: a Networkx directed graph of with CCs/ MCSs + + ''' + + seenNode = [] + allMCSLists =[] + pathDictList =[] + pathList=[] + + cloudClustersFile = open((MAINDIRECTORY+'/textFiles/cloudClusters.txt'),'wb') + + for eachNode in CEGraph: + #check if the node has been seen before + if eachNode not in dict(enumerate(zip(*seenNode))): + #look for all trees associated with node as the root + thisPathDistanceAndLength = nx.single_source_dijkstra(CEGraph, eachNode) + #determine the actual shortestPath and minimum depth/length + maxDepthAndMinPath = findMaxDepthAndMinPath(thisPathDistanceAndLength) + if maxDepthAndMinPath: + maxPathLength = maxDepthAndMinPath[0] + shortestPath = maxDepthAndMinPath[1] + + #add nodes and paths to PRUNED_GRAPH + for i in xrange(len(shortestPath)): + if PRUNED_GRAPH.has_node(shortestPath[i]) is False: + PRUNED_GRAPH.add_node(shortestPath[i]) + + #add edge if necessary + if i < (len(shortestPath)-1) and PRUNED_GRAPH.has_edge(shortestPath[i], shortestPath[i+1]) is False: + prunedGraphEdgeweight = CEGraph.get_edge_data(shortestPath[i], shortestPath[i+1])['weight'] + PRUNED_GRAPH.add_edge(shortestPath[i], shortestPath[i+1], weight=prunedGraphEdgeweight) + + #note information in a file for consideration later i.e. checking to see if it works + cloudClustersFile.write("\nSubtree pathlength is %d and path is %s" %(maxPathLength, shortestPath)) + #update seenNode info + seenNode.append(shortestPath) + + print "pruned graph" + print "number of nodes are: ", PRUNED_GRAPH.number_of_nodes() + print "number of edges are: ", PRUNED_GRAPH.number_of_edges() + print ("*"*80) + + graphTitle = "Cloud Clusters observed over somewhere during sometime" + #drawGraph(PRUNED_GRAPH, graphTitle, edgeWeight) + cloudClustersFile.close + + return PRUNED_GRAPH #****************************************************************** def findMCC (prunedGraph): - ''' - Purpose:: - Determines if subtree is a MCC according to Laurent et al 1998 criteria - - Input:: - prunedGraph: a Networkx Graph representing the CCs - - Output:: - finalMCCList: a list of list of tuples representing a MCC - - Assumptions: - frames are ordered and are equally distributed in time e.g. hrly satellite images + ''' + Purpose:: + Determines if subtree is a MCC according to Laurent et al 1998 criteria + + Input:: + prunedGraph: a Networkx Graph representing the CCs + + Output:: + finalMCCList: a list of list of tuples representing a MCC + + Assumptions: + frames are ordered and are equally distributed in time e.g. hrly satellite images - ''' - MCCList = [] - MCSList = [] - definiteMCC = [] - definiteMCS = [] - eachList =[] - eachMCCList =[] - maturing = False - decaying = False - fNode = '' - lNode = '' - removeList =[] - imgCount = 0 - imgTitle ='' - - maxShieldNode = '' - orderedPath =[] - treeTraversalList =[] - definiteMCCFlag = False - unDirGraph = nx.Graph() - aSubGraph = nx.DiGraph() - definiteMCSFlag = False - - - #connected_components is not available for DiGraph, so generate graph as undirected - unDirGraph = PRUNED_GRAPH.to_undirected() - subGraph = nx.connected_component_subgraphs(unDirGraph) - - #for each path in the subgraphs determined - for path in subGraph: - #definite is a subTree provided the duration is longer than 3 hours - - if len(path.nodes()) > MIN_MCS_DURATION: - orderedPath = path.nodes() - orderedPath.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) - #definiteMCS.append(orderedPath) - - #build back DiGraph for checking purposes/paper purposes - aSubGraph.add_nodes_from(path.nodes()) - for eachNode in path.nodes(): - if prunedGraph.predecessors(eachNode): - for node in prunedGraph.predecessors(eachNode): - aSubGraph.add_edge(node,eachNode,weight=edgeWeight[0]) - - if prunedGraph.successors(eachNode): - for node in prunedGraph.successors(eachNode): - aSubGraph.add_edge(eachNode,node,weight=edgeWeight[0]) - imgTitle = 'CC'+str(imgCount+1) - #drawGraph(aSubGraph, imgTitle, edgeWeight) #for eachNode in path: - imgCount +=1 - #----------end build back --------------------------------------------- - - mergeList, splitList = hasMergesOrSplits(path) - #add node behavior regarding neutral, merge, split or both - for node in path: - if node in mergeList and node in splitList: - addNodeBehaviorIdentifier(node,'B') - elif node in mergeList and not node in splitList: - addNodeBehaviorIdentifier(node,'M') - elif node in splitList and not node in mergeList: - addNodeBehaviorIdentifier(node,'S') - else: - addNodeBehaviorIdentifier(node,'N') - - - #Do the first part of checking for the MCC feature - #find the path - treeTraversalList = traverseTree(aSubGraph, orderedPath[0],[],[]) - #print "treeTraversalList is ", treeTraversalList - #check the nodes to determine if a MCC on just the area criteria (consecutive nodes meeting the area and temp requirements) - MCCList = checkedNodesMCC(prunedGraph, treeTraversalList) - for aDict in MCCList: - for eachNode in aDict["fullMCSMCC"]: - addNodeMCSIdentifier(eachNode[0],eachNode[1]) - - #do check for if MCCs overlap - if MCCList: - if len(MCCList) > 1: - for count in range(len(MCCList)): #for eachDict in MCCList: - #if there are more than two lists - if count >= 1: - #and the first node in this list - eachList = list(x[0] for x in MCCList[count]["possMCCList"]) - eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) - if eachList: - fNode = eachList[0] - #get the lastNode in the previous possMCC list - eachList = list(x[0] for x in MCCList[(count-1)]["possMCCList"]) - eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) - if eachList: - lNode = eachList[-1] - if lNode in CLOUD_ELEMENT_GRAPH.predecessors(fNode): - for aNode in CLOUD_ELEMENT_GRAPH.predecessors(fNode): - if aNode in eachList and aNode == lNode: - #if edge_data is equal or less than to the exisitng edge in the tree append one to the other - if CLOUD_ELEMENT_GRAPH.get_edge_data(aNode,fNode)['weight'] <= CLOUD_ELEMENT_GRAPH.get_edge_data(lNode,fNode)['weight']: - MCCList[count-1]["possMCCList"].extend(MCCList[count]["possMCCList"]) - MCCList[count-1]["fullMCSMCC"].extend(MCCList[count]["fullMCSMCC"]) - MCCList[count-1]["durationAandB"] += MCCList[count]["durationAandB"] - MCCList[count-1]["CounterCriteriaA"] += MCCList[count]["CounterCriteriaA"] - MCCList[count-1]["highestMCCnode"] = MCCList[count]["highestMCCnode"] - MCCList[count-1]["frameNum"] = MCCList[count]["frameNum"] - removeList.append(count) - #update the MCCList - if removeList: - for i in removeList: - if (len(MCCList)-1) > i: - del MCCList[i] - removeList =[] - - #check if the nodes also meet the duration criteria and the shape crieria - for eachDict in MCCList: - #order the fullMCSMCC list, then run maximum extent and eccentricity criteria - if (eachDict["durationAandB"] * TRES) >= MINIMUM_DURATION and (eachDict["durationAandB"] * TRES) <= MAXIMUM_DURATION: - eachList = list(x[0] for x in eachDict["fullMCSMCC"]) - eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) - eachMCCList = list(x[0] for x in eachDict["possMCCList"]) - eachMCCList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) - - #update the nodemcsidentifer behavior - #find the first element eachMCCList in eachList, and ensure everything ahead of it is indicated as 'I', - #find last element in eachMCCList in eachList and ensure everything after it is indicated as 'D' - #ensure that everything between is listed as 'M' - for eachNode in eachList[:(eachList.index(eachMCCList[0]))]: - addNodeMCSIdentifier(eachNode,'I') - - addNodeMCSIdentifier(eachMCCList[0],'M') - - for eachNode in eachList[(eachList.index(eachMCCList[-1])+1):]: - addNodeMCSIdentifier(eachNode, 'D') - - #update definiteMCS list - for eachNode in orderedPath[(orderedPath.index(eachMCCList[-1])+1):]: - addNodeMCSIdentifier(eachNode, 'D') - - #run maximum extent and eccentricity criteria - maxExtentNode, definiteMCCFlag = maxExtentAndEccentricity(eachList) - #print "maxExtentNode, definiteMCCFlag ", maxExtentNode, definiteMCCFlag - if definiteMCCFlag == True: - definiteMCC.append(eachList) - - - definiteMCS.append(orderedPath) - - #reset for next subGraph - aSubGraph.clear() - orderedPath=[] - MCCList =[] - MCSList =[] - definiteMCSFlag = False - - return definiteMCC, definiteMCS + ''' + MCCList = [] + MCSList = [] + definiteMCC = [] + definiteMCS = [] + eachList =[] + eachMCCList =[] + maturing = False + decaying = False + fNode = '' + lNode = '' + removeList =[] + imgCount = 0 + imgTitle ='' + + maxShieldNode = '' + orderedPath =[] + treeTraversalList =[] + definiteMCCFlag = False + unDirGraph = nx.Graph() + aSubGraph = nx.DiGraph() + definiteMCSFlag = False + + + #connected_components is not available for DiGraph, so generate graph as undirected + unDirGraph = PRUNED_GRAPH.to_undirected() + subGraph = nx.connected_component_subgraphs(unDirGraph) + + #for each path in the subgraphs determined + for path in subGraph: + #definite is a subTree provided the duration is longer than 3 hours + + if len(path.nodes()) > MIN_MCS_DURATION: + orderedPath = path.nodes() + orderedPath.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) + #definiteMCS.append(orderedPath) + + #build back DiGraph for checking purposes/paper purposes + aSubGraph.add_nodes_from(path.nodes()) + for eachNode in path.nodes(): + if prunedGraph.predecessors(eachNode): + for node in prunedGraph.predecessors(eachNode): + aSubGraph.add_edge(node,eachNode,weight=edgeWeight[0]) + + if prunedGraph.successors(eachNode): + for node in prunedGraph.successors(eachNode): + aSubGraph.add_edge(eachNode,node,weight=edgeWeight[0]) + imgTitle = 'CC'+str(imgCount+1) + #drawGraph(aSubGraph, imgTitle, edgeWeight) #for eachNode in path: + imgCount +=1 + #----------end build back --------------------------------------------- + + mergeList, splitList = hasMergesOrSplits(path) + #add node behavior regarding neutral, merge, split or both + for node in path: + if node in mergeList and node in splitList: + addNodeBehaviorIdentifier(node,'B') + elif node in mergeList and not node in splitList: + addNodeBehaviorIdentifier(node,'M') + elif node in splitList and not node in mergeList: + addNodeBehaviorIdentifier(node,'S') + else: + addNodeBehaviorIdentifier(node,'N') + + + #Do the first part of checking for the MCC feature + #find the path + treeTraversalList = traverseTree(aSubGraph, orderedPath[0],[],[]) + #print "treeTraversalList is ", treeTraversalList + #check the nodes to determine if a MCC on just the area criteria (consecutive nodes meeting the area and temp requirements) + MCCList = checkedNodesMCC(prunedGraph, treeTraversalList) + for aDict in MCCList: + for eachNode in aDict["fullMCSMCC"]: + addNodeMCSIdentifier(eachNode[0],eachNode[1]) + + #do check for if MCCs overlap + if MCCList: + if len(MCCList) > 1: + for count in range(len(MCCList)): #for eachDict in MCCList: + #if there are more than two lists + if count >= 1: + #and the first node in this list + eachList = list(x[0] for x in MCCList[count]["possMCCList"]) + eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) + if eachList: + fNode = eachList[0] + #get the lastNode in the previous possMCC list + eachList = list(x[0] for x in MCCList[(count-1)]["possMCCList"]) + eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) + if eachList: + lNode = eachList[-1] + if lNode in CLOUD_ELEMENT_GRAPH.predecessors(fNode): + for aNode in CLOUD_ELEMENT_GRAPH.predecessors(fNode): + if aNode in eachList and aNode == lNode: + #if edge_data is equal or less than to the exisitng edge in the tree append one to the other + if CLOUD_ELEMENT_GRAPH.get_edge_data(aNode,fNode)['weight'] <= CLOUD_ELEMENT_GRAPH.get_edge_data(lNode,fNode)['weight']: + MCCList[count-1]["possMCCList"].extend(MCCList[count]["possMCCList"]) + MCCList[count-1]["fullMCSMCC"].extend(MCCList[count]["fullMCSMCC"]) + MCCList[count-1]["durationAandB"] += MCCList[count]["durationAandB"] + MCCList[count-1]["CounterCriteriaA"] += MCCList[count]["CounterCriteriaA"] + MCCList[count-1]["highestMCCnode"] = MCCList[count]["highestMCCnode"] + MCCList[count-1]["frameNum"] = MCCList[count]["frameNum"] + removeList.append(count) + #update the MCCList + if removeList: + for i in removeList: + if (len(MCCList)-1) > i: + del MCCList[i] + removeList =[] + + #check if the nodes also meet the duration criteria and the shape crieria + for eachDict in MCCList: + #order the fullMCSMCC list, then run maximum extent and eccentricity criteria + if (eachDict["durationAandB"] * TRES) >= MINIMUM_DURATION and (eachDict["durationAandB"] * TRES) <= MAXIMUM_DURATION: + eachList = list(x[0] for x in eachDict["fullMCSMCC"]) + eachList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) + eachMCCList = list(x[0] for x in eachDict["possMCCList"]) + eachMCCList.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0])) + + #update the nodemcsidentifer behavior + #find the first element eachMCCList in eachList, and ensure everything ahead of it is indicated as 'I', + #find last element in eachMCCList in eachList and ensure everything after it is indicated as 'D' + #ensure that everything between is listed as 'M' + for eachNode in eachList[:(eachList.index(eachMCCList[0]))]: + addNodeMCSIdentifier(eachNode,'I') + + addNodeMCSIdentifier(eachMCCList[0],'M') + + for eachNode in eachList[(eachList.index(eachMCCList[-1])+1):]: + addNodeMCSIdentifier(eachNode, 'D') + + #update definiteMCS list + for eachNode in orderedPath[(orderedPath.index(eachMCCList[-1])+1):]: + addNodeMCSIdentifier(eachNode, 'D') + + #run maximum extent and eccentricity criteria + maxExtentNode, definiteMCCFlag = maxExtentAndEccentricity(eachList) + #print "maxExtentNode, definiteMCCFlag ", maxExtentNode, definiteMCCFlag + if definiteMCCFlag == True: + definiteMCC.append(eachList) + + + definiteMCS.append(orderedPath) + + #reset for next subGraph + aSubGraph.clear() + orderedPath=[] + MCCList =[] + MCSList =[] + definiteMCSFlag = False + + return definiteMCC, definiteMCS #****************************************************************** def traverseTree(subGraph,node, stack, checkedNodes=None): - ''' - Purpose:: - To traverse a tree using a modified depth-first iterative deepening (DFID) search algorithm - - Input:: - subGraph: a Networkx DiGraph representing a CC - lengthOfsubGraph: an integer representing the length of the subgraph - node: a string representing the node currently being checked - stack: a list of strings representing a list of nodes in a stack functionality - i.e. Last-In-First-Out (LIFO) for sorting the information from each visited node - checkedNodes: a list of strings representing the list of the nodes in the traversal - - Output:: - checkedNodes: a list of strings representing the list of the nodes in the traversal - - Assumptions: - frames are ordered and are equally distributed in time e.g. hrly satellite images + ''' + Purpose:: + To traverse a tree using a modified depth-first iterative deepening (DFID) search algorithm + + Input:: + subGraph: a Networkx DiGraph representing a CC + lengthOfsubGraph: an integer representing the length of the subgraph + node: a string representing the node currently being checked + stack: a list of strings representing a list of nodes in a stack functionality + i.e. Last-In-First-Out (LIFO) for sorting the information from each visited node + checkedNodes: a list of strings representing the list of the nodes in the traversal + + Output:: + checkedNodes: a list of strings representing the list of the nodes in the traversal + + Assumptions: + frames are ordered and are equally distributed in time e.g. hrly satellite images - ''' - if len(checkedNodes) == len(subGraph): - return checkedNodes - - if not checkedNodes: - stack =[] - checkedNodes.append(node) - - #check one level infront first...if something does exisit, stick it at the front of the stack - upOneLevel = subGraph.predecessors(node) - downOneLevel = subGraph.successors(node) - for parent in upOneLevel: - if parent not in checkedNodes and parent not in stack: - for child in downOneLevel: - if child not in checkedNodes and child not in stack: - stack.insert(0,child) - - stack.insert(0,parent) - - for child in downOneLevel: - if child not in checkedNodes and child not in stack: - if len(subGraph.predecessors(child)) > 1 or node in checkedNodes: - stack.insert(0,child) - else: - stack.append(child) - - for eachNode in stack: - if eachNode not in checkedNodes: - checkedNodes.append(eachNode) - return traverseTree(subGraph, eachNode, stack, checkedNodes) - - return checkedNodes + ''' + if len(checkedNodes) == len(subGraph): + return checkedNodes + + if not checkedNodes: + stack =[] + checkedNodes.append(node) + + #check one level infront first...if something does exisit, stick it at the front of the stack + upOneLevel = subGraph.predecessors(node) + downOneLevel = subGraph.successors(node) + for parent in upOneLevel: + if parent not in checkedNodes and parent not in stack: + for child in downOneLevel: + if child not in checkedNodes and child not in stack: + stack.insert(0,child) + + stack.insert(0,parent) + + for child in downOneLevel: + if child not in checkedNodes and child not in stack: + if len(subGraph.predecessors(child)) > 1 or node in checkedNodes: + stack.insert(0,child) + else: + stack.append(child) + + for eachNode in stack: + if eachNode not in checkedNodes: + checkedNodes.append(eachNode) + return traverseTree(subGraph, eachNode, stack, checkedNodes) + + return checkedNodes #****************************************************************** def checkedNodesMCC (prunedGraph, nodeList): - ''' - Purpose :: - Determine if this path is (or is part of) a MCC and provides - preliminary information regarding the stages of the feature - - Input:: - prunedGraph: a Networkx Graph representing all the cloud clusters - nodeList: list of strings (CE ID) from the traversal - - Output:: - potentialMCCList: list of dictionaries representing all possible MCC within the path - dictionary = {"possMCCList":[(node,'I')], "fullMCSMCC":[(node,'I')], "CounterCriteriaA": CounterCriteriaA, "durationAandB": durationAandB} - ''' - - CounterCriteriaAFlag = False - CounterCriteriaBFlag = False - INITIATIONFLAG = False - MATURITYFLAG = False - DECAYFLAG = False - thisdict = {} #will have the same items as the cloudElementDict - cloudElementAreaB = 0.0 - cloudElementAreaA = 0.0 - epsilon = 0.0 - frameNum =0 - oldNode ='' - potentialMCCList =[] - durationAandB = 0 - - #check for if the list contains only one string/node - if type(nodeList) is str: - oldNode=nodeList - nodeList =[] - nodeList.append(oldNode) - - for node in nodeList: - thisdict = thisDict(node) - CounterCriteriaAFlag = False - CounterCriteriaBFlag = False - existingFrameFlag = False - - if thisdict['cloudElementArea'] >= OUTER_CLOUD_SHIELD_AREA: - CounterCriteriaAFlag = True - INITIATIONFLAG = True - MATURITYFLAG = False - - #check if criteriaA is met - cloudElementAreaA, criteriaA = checkCriteria(thisdict['cloudElementLatLon'], OUTER_CLOUD_SHIELD_TEMPERATURE) - #TODO: calcuate the eccentricity at this point and read over????or create a new field in the dict - - if cloudElementAreaA >= OUTER_CLOUD_SHIELD_AREA: - #check if criteriaB is met - cloudElementAreaB,criteriaB = checkCriteria(thisdict['cloudElementLatLon'], INNER_CLOUD_SHIELD_TEMPERATURE) - - #if Criteria A and B have been met, then the MCC is initiated, i.e. store node as potentialMCC - if cloudElementAreaB >= INNER_CLOUD_SHIELD_AREA: - #TODO: add another field to the dictionary for the OUTER_AREA_SHIELD area - CounterCriteriaBFlag = True - #append this information on to the dictionary - addInfothisDict(node, cloudElementAreaB, criteriaB) - INITIATIONFLAG = False - MATURITYFLAG = True - stage = 'M' - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - else: - #criteria B failed - CounterCriteriaBFlag = False - if INITIATIONFLAG == True: - stage = 'I' - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - - elif (INITIATIONFLAG == False and MATURITYFLAG == True) or DECAYFLAG==True: - DECAYFLAG = True - MATURITYFLAG = False - stage = 'D' - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - else: - #criteria A failed - CounterCriteriaAFlag = False - CounterCriteriaBFlag = False - #add as a CE before or after the main feature - if INITIATIONFLAG == True or (INITIATIONFLAG == False and MATURITYFLAG == True): - stage ="I" - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - elif (INITIATIONFLAG == False and MATURITYFLAG == False) or DECAYFLAG == True: - stage = "D" - DECAYFLAG = True - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - elif (INITIATIONFLAG == False and MATURITYFLAG == False and DECAYFLAG == False): - stage ="I" - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - - - else: - #criteria A failed - CounterCriteriaAFlag = False - CounterCriteriaBFlag = False - #add as a CE before or after the main feature - if INITIATIONFLAG == True or (INITIATIONFLAG == False and MATURITYFLAG == True): - stage ="I" - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - elif (INITIATIONFLAG == False and MATURITYFLAG == False) or DECAYFLAG == True: - stage = "D" - DECAYFLAG = True - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - elif (INITIATIONFLAG == False and MATURITYFLAG == False and DECAYFLAG == False): - stage ="I" - potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) - - return potentialMCCList + ''' + Purpose :: + Determine if this path is (or is part of) a MCC and provides + preliminary information regarding the stages of the feature + + Input:: + prunedGraph: a Networkx Graph representing all the cloud clusters + nodeList: list of strings (CE ID) from the traversal + + Output:: + potentialMCCList: list of dictionaries representing all possible MCC within the path + dictionary = {"possMCCList":[(node,'I')], "fullMCSMCC":[(node,'I')], "CounterCriteriaA": CounterCriteriaA, "durationAandB": durationAandB} + ''' + + CounterCriteriaAFlag = False + CounterCriteriaBFlag = False + INITIATIONFLAG = False + MATURITYFLAG = False + DECAYFLAG = False + thisdict = {} #will have the same items as the cloudElementDict + cloudElementAreaB = 0.0 + cloudElementAreaA = 0.0 + epsilon = 0.0 + frameNum =0 + oldNode ='' + potentialMCCList =[] + durationAandB = 0 + + #check for if the list contains only one string/node + if type(nodeList) is str: + oldNode=nodeList + nodeList =[] + nodeList.append(oldNode) + + for node in nodeList: + thisdict = thisDict(node) + CounterCriteriaAFlag = False + CounterCriteriaBFlag = False + existingFrameFlag = False + + if thisdict['cloudElementArea'] >= OUTER_CLOUD_SHIELD_AREA: + CounterCriteriaAFlag = True + INITIATIONFLAG = True + MATURITYFLAG = False + + #check if criteriaA is met + cloudElementAreaA, criteriaA = checkCriteria(thisdict['cloudElementLatLon'], OUTER_CLOUD_SHIELD_TEMPERATURE) + #TODO: calcuate the eccentricity at this point and read over????or create a new field in the dict + + if cloudElementAreaA >= OUTER_CLOUD_SHIELD_AREA: + #check if criteriaB is met + cloudElementAreaB,criteriaB = checkCriteria(thisdict['cloudElementLatLon'], INNER_CLOUD_SHIELD_TEMPERATURE) + + #if Criteria A and B have been met, then the MCC is initiated, i.e. store node as potentialMCC + if cloudElementAreaB >= INNER_CLOUD_SHIELD_AREA: + #TODO: add another field to the dictionary for the OUTER_AREA_SHIELD area + CounterCriteriaBFlag = True + #append this information on to the dictionary + addInfothisDict(node, cloudElementAreaB, criteriaB) + INITIATIONFLAG = False + MATURITYFLAG = True + stage = 'M' + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + else: + #criteria B failed + CounterCriteriaBFlag = False + if INITIATIONFLAG == True: + stage = 'I' + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + + elif (INITIATIONFLAG == False and MATURITYFLAG == True) or DECAYFLAG==True: + DECAYFLAG = True + MATURITYFLAG = False + stage = 'D' + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + else: + #criteria A failed + CounterCriteriaAFlag = False + CounterCriteriaBFlag = False + #add as a CE before or after the main feature + if INITIATIONFLAG == True or (INITIATIONFLAG == False and MATURITYFLAG == True): + stage ="I" + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + elif (INITIATIONFLAG == False and MATURITYFLAG == False) or DECAYFLAG == True: + stage = "D" + DECAYFLAG = True + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + elif (INITIATIONFLAG == False and MATURITYFLAG == False and DECAYFLAG == False): + stage ="I" + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + + + else: + #criteria A failed + CounterCriteriaAFlag = False + CounterCriteriaBFlag = False + #add as a CE before or after the main feature + if INITIATIONFLAG == True or (INITIATIONFLAG == False and MATURITYFLAG == True): + stage ="I" + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + elif (INITIATIONFLAG == False and MATURITYFLAG == False) or DECAYFLAG == True: + stage = "D" + DECAYFLAG = True + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + elif (INITIATIONFLAG == False and MATURITYFLAG == False and DECAYFLAG == False): + stage ="I" + potentialMCCList = updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag) + + return potentialMCCList #****************************************************************** def updateMCCList(prunedGraph, potentialMCCList,node,stage, CounterCriteriaAFlag, CounterCriteriaBFlag): - ''' - Purpose:: - Utility function to determine if a path is (or is part of) a MCC and provides - preliminary information regarding the stages of the feature - - Input:: - prunedGraph: a Networkx Graph representing all the cloud clusters - potentialMCCList: a list of dictionaries representing the possible MCCs within a path - node: a string representing the cloud element currently being assessed - CounterCriteriaAFlag: a boolean value indicating whether the node meets the MCC criteria A according to Laurent et al - CounterCriteriaBFlag: a boolean value indicating whether the node meets the MCC criteria B according to Laurent et al - - Output:: - potentialMCCList: list of dictionaries representing all possible MCC within the path - dictionary = {"possMCCList":[(node,'I')], "fullMCSMCC":[(node,'I')], "CounterCriteriaA": CounterCriteriaA, "durationAandB": durationAandB} - - ''' - existingFrameFlag = False - existingMCSFrameFlag = False - predecessorsFlag = False - predecessorsMCSFlag = False - successorsFlag = False - successorsMCSFlag = False - frameNum = 0 - - frameNum = int((node.split('CE')[0]).split('F')[1]) - if potentialMCCList==[]: - #list empty - stage = 'I' - if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: - potentialMCCList.append({"possMCCList":[(node,stage)], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 1, "highestMCCnode":node, "frameNum":frameNum}) - elif CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - - else: - #list not empty - predecessorsFlag, index = isThereALink(prunedGraph, 1,node,potentialMCCList,1) - - if predecessorsFlag == True: - - for eachNode in potentialMCCList[index]["possMCCList"]: - if int((eachNode[0].split('CE')[0]).split('F')[1]) == frameNum : - existingFrameFlag = True - - #this MUST come after the check for the existing frame - if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: - stage = 'M' - potentialMCCList[index]["possMCCList"].append((node,stage)) - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - - - if existingFrameFlag == False: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: - stage ='M' - potentialMCCList[index]["CounterCriteriaA"]+= 1 - potentialMCCList[index]["durationAandB"]+=1 - if frameNum > potentialMCCList[index]["frameNum"]: - potentialMCCList[index]["frameNum"] = frameNum - potentialMCCList[index]["highestMCCnode"] = node - return potentialMCCList - - #if this frameNum doesn't exist and this frameNum is less than the MCC node max frame Num (including 0), then append to fullMCSMCC list - if frameNum > potentialMCCList[index]["frameNum"] or potentialMCCList[index]["frameNum"]==0: - stage = 'I' - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - return potentialMCCList - elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - return potentialMCCList - - #if predecessor and this frame number already exist in the MCC list, add the current node to the fullMCSMCC list - if existingFrameFlag == True: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - potentialMCCList[index]["CounterCriteriaA"] +=1 - return potentialMCCList - if CounterCriteriaAFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - return potentialMCCList - - if predecessorsFlag == False: - successorsFlag, index = isThereALink(prunedGraph, 2,node,potentialMCCList,2) - - if successorsFlag == True: - for eachNode in potentialMCCList[index]["possMCCList"]: - if int((eachNode[0].split('CE')[0]).split('F')[1]) == frameNum: - existingFrameFlag = True - - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: - stage = 'M' - potentialMCCList[index]["possMCCList"].append((node,stage)) - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - if frameNum > potentialMCCList[index]["frameNum"] or potentialMCCList[index]["frameNum"] == 0: - potentialMCCList[index]["frameNum"] = frameNum - potentialMCCList[index]["highestMCCnode"] = node - return potentialMCCList - - - if existingFrameFlag == False: - if stage == 'M': - stage = 'D' - if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: - potentialMCCList[index]["CounterCriteriaA"]+= 1 - potentialMCCList[index]["durationAandB"]+=1 - elif CounterCriteriaAFlag == True: - potentialMCCList[index]["CounterCriteriaA"] += 1 - elif CounterCriteriaAFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - return potentialMCCList - #if predecessor and this frame number already exist in the MCC list, add the current node to the fullMCSMCC list - else: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - potentialMCCList[index]["CounterCriteriaA"] +=1 - return potentialMCCList - if CounterCriteriaAFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - return potentialMCCList - - #if this node isn't connected to exisiting MCCs check if it is connected to exisiting MCSs ... - if predecessorsFlag == False and successorsFlag == False: - stage = 'I' - predecessorsMCSFlag, index = isThereALink(prunedGraph, 1,node,potentialMCCList,2) - if predecessorsMCSFlag == True: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: - potentialMCCList[index]["possMCCList"].append((node,'M')) - potentialMCCList[index]["fullMCSMCC"].append((node,'M')) - potentialMCCList[index]["durationAandB"] += 1 - if frameNum > potentialMCCList[index]["frameNum"]: - potentialMCCList[index]["frameNum"] = frameNum - potentialMCCList[index]["highestMCCnode"] = node - return potentialMCCList - - if potentialMCCList[index]["frameNum"] == 0 or frameNum <= potentialMCCList[index]["frameNum"]: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - potentialMCCList[index]["CounterCriteriaA"] +=1 - return potentialMCCList - elif CounterCriteriaAFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - return potentialMCCList - else: - successorsMCSFlag, index = isThereALink(prunedGraph, 2,node,potentialMCCList,2) - if successorsMCSFlag == True: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: - potentialMCCList[index]["possMCCList"].append((node,'M')) - potentialMCCList[index]["fullMCSMCC"].append((node,'M')) - potentialMCCList[index]["durationAandB"] += 1 - if frameNum > potentialMCCList[index]["frameNum"]: - potentialMCCList[index]["frameNum"] = frameNum - potentialMCCList[index]["highestMCCnode"] = node - return potentialMCCList - - - if potentialMCCList[index]["frameNum"] == 0 or frameNum <= potentialMCCList[index]["frameNum"]: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - potentialMCCList[index]["CounterCriteriaA"] +=1 - return potentialMCCList - elif CounterCriteriaAFlag == False: - potentialMCCList[index]["fullMCSMCC"].append((node,stage)) - return potentialMCCList - - #if this node isn't connected to existing MCCs or MCSs, create a new one ... - if predecessorsFlag == False and predecessorsMCSFlag == False and successorsFlag == False and successorsMCSFlag == False: - if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: - potentialMCCList.append({"possMCCList":[(node,stage)], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 1, "highestMCCnode":node, "frameNum":frameNum}) - elif CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: - potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) - - return potentialMCCList + ''' + Purpose:: + Utility function to determine if a path is (or is part of) a MCC and provides + preliminary information regarding the stages of the feature + + Input:: + prunedGraph: a Networkx Graph representing all the cloud clusters + potentialMCCList: a list of dictionaries representing the possible MCCs within a path + node: a string representing the cloud element currently being assessed + CounterCriteriaAFlag: a boolean value indicating whether the node meets the MCC criteria A according to Laurent et al + CounterCriteriaBFlag: a boolean value indicating whether the node meets the MCC criteria B according to Laurent et al + + Output:: + potentialMCCList: list of dictionaries representing all possible MCC within the path + dictionary = {"possMCCList":[(node,'I')], "fullMCSMCC":[(node,'I')], "CounterCriteriaA": CounterCriteriaA, "durationAandB": durationAandB} + + ''' + existingFrameFlag = False + existingMCSFrameFlag = False + predecessorsFlag = False + predecessorsMCSFlag = False + successorsFlag = False + successorsMCSFlag = False + frameNum = 0 + + frameNum = int((node.split('CE')[0]).split('F')[1]) + if potentialMCCList==[]: + #list empty + stage = 'I' + if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: + potentialMCCList.append({"possMCCList":[(node,stage)], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 1, "highestMCCnode":node, "frameNum":frameNum}) + elif CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + + else: + #list not empty + predecessorsFlag, index = isThereALink(prunedGraph, 1,node,potentialMCCList,1) + + if predecessorsFlag == True: + + for eachNode in potentialMCCList[index]["possMCCList"]: + if int((eachNode[0].split('CE')[0]).split('F')[1]) == frameNum : + existingFrameFlag = True + + #this MUST come after the check for the existing frame + if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: + stage = 'M' + potentialMCCList[index]["possMCCList"].append((node,stage)) + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + + + if existingFrameFlag == False: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: + stage ='M' + potentialMCCList[index]["CounterCriteriaA"]+= 1 + potentialMCCList[index]["durationAandB"]+=1 + if frameNum > potentialMCCList[index]["frameNum"]: + potentialMCCList[index]["frameNum"] = frameNum + potentialMCCList[index]["highestMCCnode"] = node + return potentialMCCList + + #if this frameNum doesn't exist and this frameNum is less than the MCC node max frame Num (including 0), then append to fullMCSMCC list + if frameNum > potentialMCCList[index]["frameNum"] or potentialMCCList[index]["frameNum"]==0: + stage = 'I' + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + return potentialMCCList + elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + return potentialMCCList + + #if predecessor and this frame number already exist in the MCC list, add the current node to the fullMCSMCC list + if existingFrameFlag == True: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + potentialMCCList[index]["CounterCriteriaA"] +=1 + return potentialMCCList + if CounterCriteriaAFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + return potentialMCCList + + if predecessorsFlag == False: + successorsFlag, index = isThereALink(prunedGraph, 2,node,potentialMCCList,2) + + if successorsFlag == True: + for eachNode in potentialMCCList[index]["possMCCList"]: + if int((eachNode[0].split('CE')[0]).split('F')[1]) == frameNum: + existingFrameFlag = True + + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: + stage = 'M' + potentialMCCList[index]["possMCCList"].append((node,stage)) + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + if frameNum > potentialMCCList[index]["frameNum"] or potentialMCCList[index]["frameNum"] == 0: + potentialMCCList[index]["frameNum"] = frameNum + potentialMCCList[index]["highestMCCnode"] = node + return potentialMCCList + + + if existingFrameFlag == False: + if stage == 'M': + stage = 'D' + if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: + potentialMCCList[index]["CounterCriteriaA"]+= 1 + potentialMCCList[index]["durationAandB"]+=1 + elif CounterCriteriaAFlag == True: + potentialMCCList[index]["CounterCriteriaA"] += 1 + elif CounterCriteriaAFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + return potentialMCCList + #if predecessor and this frame number already exist in the MCC list, add the current node to the fullMCSMCC list + else: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + potentialMCCList[index]["CounterCriteriaA"] +=1 + return potentialMCCList + if CounterCriteriaAFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + return potentialMCCList + + #if this node isn't connected to exisiting MCCs check if it is connected to exisiting MCSs ... + if predecessorsFlag == False and successorsFlag == False: + stage = 'I' + predecessorsMCSFlag, index = isThereALink(prunedGraph, 1,node,potentialMCCList,2) + if predecessorsMCSFlag == True: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: + potentialMCCList[index]["possMCCList"].append((node,'M')) + potentialMCCList[index]["fullMCSMCC"].append((node,'M')) + potentialMCCList[index]["durationAandB"] += 1 + if frameNum > potentialMCCList[index]["frameNum"]: + potentialMCCList[index]["frameNum"] = frameNum + potentialMCCList[index]["highestMCCnode"] = node + return potentialMCCList + + if potentialMCCList[index]["frameNum"] == 0 or frameNum <= potentialMCCList[index]["frameNum"]: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + potentialMCCList[index]["CounterCriteriaA"] +=1 + return potentialMCCList + elif CounterCriteriaAFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + return potentialMCCList + else: + successorsMCSFlag, index = isThereALink(prunedGraph, 2,node,potentialMCCList,2) + if successorsMCSFlag == True: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == True: + potentialMCCList[index]["possMCCList"].append((node,'M')) + potentialMCCList[index]["fullMCSMCC"].append((node,'M')) + potentialMCCList[index]["durationAandB"] += 1 + if frameNum > potentialMCCList[index]["frameNum"]: + potentialMCCList[index]["frameNum"] = frameNum + potentialMCCList[index]["highestMCCnode"] = node + return potentialMCCList + + + if potentialMCCList[index]["frameNum"] == 0 or frameNum <= potentialMCCList[index]["frameNum"]: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + potentialMCCList[index]["CounterCriteriaA"] +=1 + return potentialMCCList + elif CounterCriteriaAFlag == False: + potentialMCCList[index]["fullMCSMCC"].append((node,stage)) + return potentialMCCList + + #if this node isn't connected to existing MCCs or MCSs, create a new one ... + if predecessorsFlag == False and predecessorsMCSFlag == False and successorsFlag == False and successorsMCSFlag == False: + if CounterCriteriaAFlag == True and CounterCriteriaBFlag ==True: + potentialMCCList.append({"possMCCList":[(node,stage)], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 1, "highestMCCnode":node, "frameNum":frameNum}) + elif CounterCriteriaAFlag == True and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 1, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + elif CounterCriteriaAFlag == False and CounterCriteriaBFlag == False: + potentialMCCList.append({"possMCCList":[], "fullMCSMCC":[(node,stage)], "CounterCriteriaA": 0, "durationAandB": 0, "highestMCCnode":"", "frameNum":0}) + + return potentialMCCList #****************************************************************** def isThereALink(prunedGraph, upOrDown,node,potentialMCCList,whichList): - ''' - Purpose:: - Utility script for updateMCCList mostly because there is no Pythonic way to break out of nested loops - - Input:: - prunedGraph:a Networkx Graph representing all the cloud clusters - upOrDown: an integer representing 1- to do predecesor check and 2 - to do successor checkedNodesMCC - node: a string representing the cloud element currently being assessed - potentialMCCList: a list of dictionaries representing the possible MCCs within a path - whichList: an integer representing which list ot check in the dictionary; 1- possMCCList, 2- fullMCSMCC - - Output:: - thisFlag: a boolean representing whether the list passed has in the parent or child of the node - index: an integer representing the location in the potentialMCCList where thisFlag occurs - - ''' - thisFlag = False - index = -1 - checkList ="" - if whichList == 1: - checkList = "possMCCList" - elif whichList ==2: - checkList = "fullMCSMCC" - - #check parents - if upOrDown == 1: - for aNode in prunedGraph.predecessors(node): - #reset the index counter for this node search through potentialMCCList - index = -1 - for MCCDict in potentialMCCList: - index += 1 - if aNode in list(x[0] for x in MCCDict[checkList]): - thisFlag = True - #get out of looping so as to avoid the flag being written over when another node in the predecesor list is checked - return thisFlag, index - - #check children - if upOrDown == 2: - for aNode in prunedGraph.successors(node): - #reset the index counter for this node search through potentialMCCList - index = -1 - for MCCDict in potentialMCCList: - index += 1 - - if aNode in list(x[0] for x in MCCDict[checkList]): - thisFlag = True - return thisFlag, index - - return thisFlag, index + ''' + Purpose:: + Utility script for updateMCCList mostly because there is no Pythonic way to break out of nested loops + + Input:: + prunedGraph:a Networkx Graph representing all the cloud clusters + upOrDown: an integer representing 1- to do predecesor check and 2 - to do successor checkedNodesMCC + node: a string representing the cloud element currently being assessed + potentialMCCList: a list of dictionaries representing the possible MCCs within a path + whichList: an integer representing which list ot check in the dictionary; 1- possMCCList, 2- fullMCSMCC + + Output:: + thisFlag: a boolean representing whether the list passed has in the parent or child of the node + index: an integer representing the location in the potentialMCCList where thisFlag occurs + + ''' + thisFlag = False + index = -1 + checkList ="" + if whichList == 1: + checkList = "possMCCList" + elif whichList ==2: + checkList = "fullMCSMCC" + + #check parents + if upOrDown == 1: + for aNode in prunedGraph.predecessors(node): + #reset the index counter for this node search through potentialMCCList + index = -1 + for MCCDict in potentialMCCList: + index += 1 + if aNode in list(x[0] for x in MCCDict[checkList]): + thisFlag = True + #get out of looping so as to avoid the flag being written over when another node in the predecesor list is checked + return thisFlag, index + + #check children + if upOrDown == 2: + for aNode in prunedGraph.successors(node): + #reset the index counter for this node search through potentialMCCList + index = -1 + for MCCDict in potentialMCCList: + index += 1 + + if aNode in list(x[0] for x in MCCDict[checkList]): + thisFlag = True + return thisFlag, index + + return thisFlag, index #****************************************************************** def maxExtentAndEccentricity(eachList): - ''' - Purpose:: - Perform the final check for MCC based on maximum extent and eccentricity criteria - - Input:: - eachList: a list of strings representing the node of the possible MCCs within a path - - Output:: - maxShieldNode: a string representing the node with the maximum maxShieldNode - definiteMCCFlag: a boolean indicating that the MCC has met all requirements - - ''' - maxShieldNode ='' - maxShieldArea = 0.0 - maxShieldEccentricity = 0.0 - definiteMCCFlag = False - - if eachList: - for eachNode in eachList: - if (thisDict(eachNode)['nodeMCSIdentifier'] == 'M' or thisDict(eachNode)['nodeMCSIdentifier'] == 'D') and thisDict(eachNode)['cloudElementArea'] > maxShieldArea: - maxShieldNode = eachNode - maxShieldArea = thisDict(eachNode)['cloudElementArea'] - - maxShieldEccentricity = thisDict(maxShieldNode)['cloudElementEccentricity'] - if thisDict(maxShieldNode)['cloudElementEccentricity'] >= ECCENTRICITY_THRESHOLD_MIN and thisDict(maxShieldNode)['cloudElementEccentricity'] <= ECCENTRICITY_THRESHOLD_MAX : - #criteria met - definiteMCCFlag = True - - return maxShieldNode, definiteMCCFlag + ''' + Purpose:: + Perform the final check for MCC based on maximum extent and eccentricity criteria + + Input:: + eachList: a list of strings representing the node of the possible MCCs within a path + + Output:: + maxShieldNode: a string representing the node with the maximum maxShieldNode + definiteMCCFlag: a boolean indicating that the MCC has met all requirements + + ''' + maxShieldNode ='' + maxShieldArea = 0.0 + maxShieldEccentricity = 0.0 + definiteMCCFlag = False + + if eachList: + for eachNode in eachList: + if (thisDict(eachNode)['nodeMCSIdentifier'] == 'M' or thisDict(eachNode)['nodeMCSIdentifier'] == 'D') and thisDict(eachNode)['cloudElementArea'] > maxShieldArea: + maxShieldNode = eachNode + maxShieldArea = thisDict(eachNode)['cloudElementArea'] + + maxShieldEccentricity = thisDict(maxShieldNode)['cloudElementEccentricity'] + if thisDict(maxShieldNode)['cloudElementEccentricity'] >= ECCENTRICITY_THRESHOLD_MIN and thisDict(maxShieldNode)['cloudElementEccentricity'] <= ECCENTRICITY_THRESHOLD_MAX : + #criteria met + definiteMCCFlag = True + + return maxShieldNode, definiteMCCFlag #****************************************************************** def findMaxDepthAndMinPath (thisPathDistanceAndLength): - ''' - Purpose:: - To determine the maximum depth and min path for the headnode - - Input:: - tuple of dictionaries representing the shortest distance and paths for a node in the tree as returned by nx.single_source_dijkstra - thisPathDistanceAndLength({distance}, {path}) - {distance} = nodeAsString, valueAsInt, {path} = nodeAsString, pathAsList - - Output:: - tuple of the max pathLength and min pathDistance as a tuple (like what was input) - minDistanceAndMaxPath = ({distance},{path}) - ''' - maxPathLength = 0 - minPath = 0 - - #maxPathLength for the node in question - maxPathLength = max(len (values) for values in thisPathDistanceAndLength[1].values()) - - #if the duration is shorter then the min MCS length, then don't store! - if maxPathLength < MIN_MCS_DURATION: #MINIMUM_DURATION : - minDistanceAndMaxPath = () - - #else find the min path and max depth - else: - #max path distance for the node in question - minPath = max(values for values in thisPathDistanceAndLength[0].values()) - - #check to determine the shortest path from the longest paths returned - for pathDistance, path in itertools.izip(thisPathDistanceAndLength[0].values(), thisPathDistanceAndLength[1].values()): - pathLength = len(path) - #if pathLength is the same as the maxPathLength, then look the pathDistance to determine if the min - if pathLength == maxPathLength : - if pathDistance <= minPath: - minPath = pathLength - #store details if absolute minPath and deepest - minDistanceAndMaxPath = (pathDistance, path) - return minDistanceAndMaxPath + ''' + Purpose:: + To determine the maximum depth and min path for the headnode + + Input:: + tuple of dictionaries representing the shortest distance and paths for a node in the tree as returned by nx.single_source_dijkstra + thisPathDistanceAndLength({distance}, {path}) + {distance} = nodeAsString, valueAsInt, {path} = nodeAsString, pathAsList + + Output:: + tuple of the max pathLength and min pathDistance as a tuple (like what was input) + minDistanceAndMaxPath = ({distance},{path}) + ''' + maxPathLength = 0 + minPath = 0 + + #maxPathLength for the node in question + maxPathLength = max(len (values) for values in thisPathDistanceAndLength[1].values()) + + #if the duration is shorter then the min MCS length, then don't store! + if maxPathLength < MIN_MCS_DURATION: #MINIMUM_DURATION : + minDistanceAndMaxPath = () + + #else find the min path and max depth + else: + #max path distance for the node in question + minPath = max(values for values in thisPathDistanceAndLength[0].values()) + + #check to determine the shortest path from the longest paths returned + for pathDistance, path in itertools.izip(thisPathDistanceAndLength[0].values(), thisPathDistanceAndLength[1].values()): + pathLength = len(path) + #if pathLength is the same as the maxPathLength, then look the pathDistance to determine if the min + if pathLength == maxPathLength : + if pathDistance <= minPath: + minPath = pathLength + #store details if absolute minPath and deepest + minDistanceAndMaxPath = (pathDistance, path) + return minDistanceAndMaxPath #****************************************************************** def thisDict (thisNode): - ''' - Purpose:: - Return dictionary from graph if node exist in tree + ''' + Purpose:: + Return dictionary from graph if node exist in tree - Input:: - thisNode: a string representing the CE to get the information for + Input:: + thisNode: a string representing the CE to get the information for - Output :: - eachdict[1]: a dictionary representing the info associated with thisNode from the graph + Output :: + eachdict[1]: a dictionary representing the info associated with thisNode from the graph - ''' - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): - if eachdict[1]['uniqueID'] == thisNode: - return eachdict[1] + ''' + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): + if eachdict[1]['uniqueID'] == thisNode: + return eachdict[1] #****************************************************************** def checkCriteria (thisCloudElementLatLon, aTemperature): - ''' - Purpose:: - Determine if criteria B is met for a CEGraph - - Input:: - thisCloudElementLatLon: 2D array of (lat,lon) variable from the node dictionary being currently considered - aTemperature:a integer representing the temperature maximum for masking - - Output :: - cloudElementArea: a floating-point number representing the area in the array that meet the criteria - criteriaB - - ''' - cloudElementCriteriaBLatLon=[] - - frame, CEcounter = ndimage.measurements.label(thisCloudElementLatLon, structure=STRUCTURING_ELEMENT) - frameCEcounter=0 - #determine min and max values in lat and lon, then use this to generate teh array from LAT,LON meshgrid - - minLat = min(x[0] for x in thisCloudElementLatLon) - maxLat = max(x[0]for x in thisCloudElementLatLon) - minLon = min(x[1]for x in thisCloudElementLatLon) - maxLon = max(x[1]for x in thisCloudElementLatLon) - - minLatIndex = np.argmax(LAT[:,0] == minLat) - maxLatIndex = np.argmax(LAT[:,0]== maxLat) - minLonIndex = np.argmax(LON[0,:] == minLon) - maxLonIndex = np.argmax(LON[0,:] == maxLon) - - criteriaBframe = ma.zeros(((abs(maxLatIndex - minLatIndex)+1), (abs(maxLonIndex - minLonIndex)+1))) - - for x in thisCloudElementLatLon: - #to store the values of the subset in the new array, remove the minLatIndex and minLonindex from the - #index given in the original array to get the indices for the new array - criteriaBframe[(np.argmax(LAT[:,0] == x[0]) - minLatIndex),(np.argmax(LON[0,:] == x[1]) - minLonIndex)] = x[2] - - #keep only those values < aTemperature - tempMask = ma.masked_array(criteriaBframe, mask=(criteriaBframe >= aTemperature), fill_value = 0) - - #get the actual values that the mask returned - criteriaB = ma.zeros((criteriaBframe.shape)).astype('int16') - - for index, value in maenumerate(tempMask): - lat_index, lon_index = index - criteriaB[lat_index, lon_index]=value - - for count in xrange(CEcounter): - #[0] is time dimension. Determine the actual values from the data - #loc is a masked array - #***** returns elements down then across thus (6,4) is 6 arrays deep of size 4 - try: - - loc = ndimage.find_objects(criteriaB)[0] - except: - #this would mean that no objects were found meeting criteria B - print "no objects at this temperature!" - cloudElementArea = 0.0 - return cloudElementArea, cloudElementCriteriaBLatLon - - try: - cloudElementCriteriaB = ma.zeros((criteriaB.shape)) - cloudElementCriteriaB =criteriaB[loc] - except: - print "YIKESS" - print "CEcounter ", CEcounter, criteriaB.shape - print "criteriaB ", criteriaB - - for index,value in np.ndenumerate(cloudElementCriteriaB): - if value !=0: - t,lat,lon = index - #add back on the minLatIndex and minLonIndex to find the true lat, lon values - lat_lon_tuple = (LAT[(lat),0], LON[0,(lon)],value) - cloudElementCriteriaBLatLon.append(lat_lon_tuple) - - cloudElementArea = np.count_nonzero(cloudElementCriteriaB)*XRES*YRES - #do some cleaning up - tempMask =[] - criteriaB =[] - cloudElementCriteriaB=[] - - return cloudElementArea, cloudElementCriteriaBLatLon + ''' + Purpose:: + Determine if criteria B is met for a CEGraph + + Input:: + thisCloudElementLatLon: 2D array of (lat,lon) variable from the node dictionary being currently considered + aTemperature:a integer representing the temperature maximum for masking + + Output :: + cloudElementArea: a floating-point number representing the area in the array that meet the criteria - criteriaB + + ''' + cloudElementCriteriaBLatLon=[] + + frame, CEcounter = ndimage.measurements.label(thisCloudElementLatLon, structure=STRUCTURING_ELEMENT) + frameCEcounter=0 + #determine min and max values in lat and lon, then use this to generate teh array from LAT,LON meshgrid + + minLat = min(x[0] for x in thisCloudElementLatLon) + maxLat = max(x[0]for x in thisCloudElementLatLon) + minLon = min(x[1]for x in thisCloudElementLatLon) + maxLon = max(x[1]for x in thisCloudElementLatLon) + + minLatIndex = np.argmax(LAT[:,0] == minLat) + maxLatIndex = np.argmax(LAT[:,0]== maxLat) + minLonIndex = np.argmax(LON[0,:] == minLon) + maxLonIndex = np.argmax(LON[0,:] == maxLon) + + criteriaBframe = ma.zeros(((abs(maxLatIndex - minLatIndex)+1), (abs(maxLonIndex - minLonIndex)+1))) + + for x in thisCloudElementLatLon: + #to store the values of the subset in the new array, remove the minLatIndex and minLonindex from the + #index given in the original array to get the indices for the new array + criteriaBframe[(np.argmax(LAT[:,0] == x[0]) - minLatIndex),(np.argmax(LON[0,:] == x[1]) - minLonIndex)] = x[2] + + #keep only those values < aTemperature + tempMask = ma.masked_array(criteriaBframe, mask=(criteriaBframe >= aTemperature), fill_value = 0) + + #get the actual values that the mask returned + criteriaB = ma.zeros((criteriaBframe.shape)).astype('int16') + + for index, value in maenumerate(tempMask): + lat_index, lon_index = index + criteriaB[lat_index, lon_index]=value + + for count in xrange(CEcounter): + #[0] is time dimension. Determine the actual values from the data + #loc is a masked array + #***** returns elements down then across thus (6,4) is 6 arrays deep of size 4 + try: + + loc = ndimage.find_objects(criteriaB)[0] + except: + #this would mean that no objects were found meeting criteria B + print "no objects at this temperature!" + cloudElementArea = 0.0 + return cloudElementArea, cloudElementCriteriaBLatLon + + try: + cloudElementCriteriaB = ma.zeros((criteriaB.shape)) + cloudElementCriteriaB =criteriaB[loc] + except: + print "YIKESS" + print "CEcounter ", CEcounter, criteriaB.shape + print "criteriaB ", criteriaB + + for index,value in np.ndenumerate(cloudElementCriteriaB): + if value !=0: + t,lat,lon = index + #add back on the minLatIndex and minLonIndex to find the true lat, lon values + lat_lon_tuple = (LAT[(lat),0], LON[0,(lon)],value) + cloudElementCriteriaBLatLon.append(lat_lon_tuple) + + cloudElementArea = np.count_nonzero(cloudElementCriteriaB)*XRES*YRES + #do some cleaning up + tempMask =[] + criteriaB =[] + cloudElementCriteriaB=[] + + return cloudElementArea, cloudElementCriteriaBLatLon #****************************************************************** def hasMergesOrSplits (nodeList): - ''' - Purpose:: - Determine if nodes within a path defined from shortest_path splittingNodeDict - Input:: - nodeList: list of strings representing the nodes from a path - Output:: - splitList: a list of strings representing all the nodes in the path that split - mergeList: a list of strings representing all the nodes in the path that merged - ''' - mergeList=[] - splitList=[] - - for node,numParents in PRUNED_GRAPH.in_degree(nodeList).items(): - if numParents > 1: - mergeList.append(node) - - for node, numChildren in PRUNED_GRAPH.out_degree(nodeList).items(): - if numChildren > 1: - splitList.append(node) - #sort - splitList.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) - mergeList.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) - - return mergeList,splitList + ''' + Purpose:: + Determine if nodes within a path defined from shortest_path splittingNodeDict + Input:: + nodeList: list of strings representing the nodes from a path + Output:: + splitList: a list of strings representing all the nodes in the path that split + mergeList: a list of strings representing all the nodes in the path that merged + ''' + mergeList=[] + splitList=[] + + for node,numParents in PRUNED_GRAPH.in_degree(nodeList).items(): + if numParents > 1: + mergeList.append(node) + + for node, numChildren in PRUNED_GRAPH.out_degree(nodeList).items(): + if numChildren > 1: + splitList.append(node) + #sort + splitList.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) + mergeList.sort(key=lambda item:(len(item.split('C')[0]), item.split('C')[0])) + + return mergeList,splitList #****************************************************************** def allAncestors(path, aNode): - ''' - Purpose:: - Utility script to provide the path leading up to a nodeList - - Input:: - path: a list of strings representing the nodes in the path - aNode: a string representing a node to be checked for parents - - Output:: - path: a list of strings representing the list of the nodes connected to aNode through its parents - numOfChildren: an integer representing the number of parents of the node passed - ''' - - numOfParents = PRUNED_GRAPH.in_degree(aNode) - try: - if PRUNED_GRAPH.predecessors(aNode) and numOfParents <= 1: - path = path + PRUNED_GRAPH.predecessors(aNode) - thisNode = PRUNED_GRAPH.predecessors(aNode)[0] - return allAncestors(path,thisNode) - else: - path = path+aNode - return path, numOfParents - except: - return path, numOfParents + ''' + Purpose:: + Utility script to provide the path leading up to a nodeList + + Input:: + path: a list of strings representing the nodes in the path + aNode: a string representing a node to be checked for parents + + Output:: + path: a list of strings representing the list of the nodes connected to aNode through its parents + numOfChildren: an integer representing the number of parents of the node passed + ''' + + numOfParents = PRUNED_GRAPH.in_degree(aNode) + try: + if PRUNED_GRAPH.predecessors(aNode) and numOfParents <= 1: + path = path + PRUNED_GRAPH.predecessors(aNode) + thisNode = PRUNED_GRAPH.predecessors(aNode)[0] + return allAncestors(path,thisNode) + else: + path = path+aNode + return path, numOfParents + except: + return path, numOfParents #****************************************************************** def allDescendants(path, aNode): - ''' - Purpose:: - Utility script to provide the path leading up to a nodeList - - Input:: - path: a list of strings representing the nodes in the path - aNode: a string representing a node to be checked for children - - Output:: - path: a list of strings representing the list of the nodes connected to aNode through its children - numOfChildren: an integer representing the number of children of the node passed - ''' - - numOfChildren = PRUNED_GRAPH.out_degree(aNode) - try: - if PRUNED_GRAPH.successors(aNode) and numOfChildren <= 1: - path = path + PRUNED_GRAPH.successors(aNode) - thisNode = PRUNED_GRAPH.successors(aNode)[0] - return allDescendants(path,thisNode) - else: - path = path + aNode - #i.e. PRUNED_GRAPH.predecessors(aNode) is empty - return path, numOfChildren - except: - #i.e. PRUNED_GRAPH.predecessors(aNode) threw an exception - return path, numOfChildren + ''' + Purpose:: + Utility script to provide the path leading up to a nodeList + + Input:: + path: a list of strings representing the nodes in the path + aNode: a string representing a node to be checked for children + + Output:: + path: a list of strings representing the list of the nodes connected to aNode through its children + numOfChildren: an integer representing the number of children of the node passed + ''' + + numOfChildren = PRUNED_GRAPH.out_degree(aNode) + try: + if PRUNED_GRAPH.successors(aNode) and numOfChildren <= 1: + path = path + PRUNED_GRAPH.successors(aNode) + thisNode = PRUNED_GRAPH.successors(aNode)[0] + return allDescendants(path,thisNode) + else: + path = path + aNode + #i.e. PRUNED_GRAPH.predecessors(aNode) is empty + return path, numOfChildren + except: + #i.e. PRUNED_GRAPH.predecessors(aNode) threw an exception + return path, numOfChildren #****************************************************************** def addInfothisDict (thisNode, cloudElementArea,criteriaB): - ''' - Purpose:: - Update original dictionary node with information - - Input:: - thisNode: a string representing the unique ID of a node - cloudElementArea: a floating-point number representing the area of the cloud element - criteriaB: a masked array of floating-point numbers representing the lat,lons meeting the criteria - - Output:: None - - ''' - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): - if eachdict[1]['uniqueID'] == thisNode: - eachdict[1]['CriteriaBArea'] = cloudElementArea - eachdict[1]['CriteriaBLatLon'] = criteriaB - return + ''' + Purpose:: + Update original dictionary node with information + + Input:: + thisNode: a string representing the unique ID of a node + cloudElementArea: a floating-point number representing the area of the cloud element + criteriaB: a masked array of floating-point numbers representing the lat,lons meeting the criteria + + Output:: None + + ''' + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): + if eachdict[1]['uniqueID'] == thisNode: + eachdict[1]['CriteriaBArea'] = cloudElementArea + eachdict[1]['CriteriaBLatLon'] = criteriaB + return #****************************************************************** def addNodeBehaviorIdentifier (thisNode, nodeBehaviorIdentifier): - ''' - Purpose:: add an identifier to the node dictionary to indicate splitting, merging or neither node + ''' + Purpose:: add an identifier to the node dictionary to indicate splitting, merging or neither node - Input:: - thisNode: a string representing the unique ID of a node - nodeBehaviorIdentifier: a string representing the behavior S- split, M- merge, B- both split and merge, N- neither split or merge + Input:: + thisNode: a string representing the unique ID of a node + nodeBehaviorIdentifier: a string representing the behavior S- split, M- merge, B- both split and merge, N- neither split or merge - Output :: None + Output :: None - ''' - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): - if eachdict[1]['uniqueID'] == thisNode: - if not 'nodeBehaviorIdentifier' in eachdict[1].keys(): - eachdict[1]['nodeBehaviorIdentifier'] = nodeBehaviorIdentifier - return + ''' + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): + if eachdict[1]['uniqueID'] == thisNode: + if not 'nodeBehaviorIdentifier' in eachdict[1].keys(): + eachdict[1]['nodeBehaviorIdentifier'] = nodeBehaviorIdentifier + return #****************************************************************** def addNodeMCSIdentifier (thisNode, nodeMCSIdentifier): - ''' - Purpose:: - Add an identifier to the node dictionary to indicate splitting, merging or neither node - - Input:: - thisNode: a string representing the unique ID of a node - nodeMCSIdentifier: a string representing the stage of the MCS lifecyle 'I' for Initiation, 'M' for Maturity, 'D' for Decay - - Output :: None - - ''' - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): - if eachdict[1]['uniqueID'] == thisNode: - if not 'nodeMCSIdentifier' in eachdict[1].keys(): - eachdict[1]['nodeMCSIdentifier'] = nodeMCSIdentifier - return + ''' + Purpose:: + Add an identifier to the node dictionary to indicate splitting, merging or neither node + + Input:: + thisNode: a string representing the unique ID of a node + nodeMCSIdentifier: a string representing the stage of the MCS lifecyle 'I' for Initiation, 'M' for Maturity, 'D' for Decay + + Output :: None + + ''' + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): + if eachdict[1]['uniqueID'] == thisNode: + if not 'nodeMCSIdentifier' in eachdict[1].keys(): + eachdict[1]['nodeMCSIdentifier'] = nodeMCSIdentifier + return #****************************************************************** def updateNodeMCSIdentifier (thisNode, nodeMCSIdentifier): - ''' - Purpose:: - Update an identifier to the node dictionary to indicate splitting, merging or neither node + ''' + Purpose:: + Update an identifier to the node dictionary to indicate splitting, merging or neither node - Input:: - thisNode: thisNode: a string representing the unique ID of a node - nodeMCSIdentifier: a string representing the stage of the MCS lifecyle 'I' for Initiation, 'M' for Maturity, 'D' for Decay + Input:: + thisNode: thisNode: a string representing the unique ID of a node + nodeMCSIdentifier: a string representing the stage of the MCS lifecyle 'I' for Initiation, 'M' for Maturity, 'D' for Decay - Output :: None + Output :: None - ''' - for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): - if eachdict[1]['uniqueID'] == thisNode: - eachdict[1]['nodeMCSIdentifier'] = nodeBehaviorIdentifier + ''' + for eachdict in CLOUD_ELEMENT_GRAPH.nodes(thisNode): + if eachdict[1]['uniqueID'] == thisNode: + eachdict[1]['nodeMCSIdentifier'] = nodeBehaviorIdentifier - return + return #****************************************************************** def eccentricity (cloudElementLatLon): - ''' - Purpose:: - Determines the eccentricity (shape) of contiguous boxes - Values tending to 1 are more circular by definition, whereas - values tending to 0 are more linear - - Input:: - cloudElementLatLon: 2D array in (lat,lon) representing T_bb contiguous squares - - Output:: - epsilon: a floating-point representing the eccentricity of the matrix passed - - ''' - - epsilon = 0.0 - - #loop over all lons and determine longest (non-zero) col - #loop over all lats and determine longest (non-zero) row - for latLon in cloudElementLatLon: - #assign a matrix to determine the legit values - - nonEmptyLons = sum(sum(cloudElementLatLon)>0) - nonEmptyLats = sum(sum(cloudElementLatLon.transpose())>0) - - lonEigenvalues = 1.0 * nonEmptyLats / (nonEmptyLons+0.001) #for long oval on y axis - latEigenvalues = 1.0 * nonEmptyLons / (nonEmptyLats +0.001) #for long oval on x-axs - epsilon = min(latEigenvalues,lonEigenvalues) - - return epsilon + ''' + Purpose:: + Determines the eccentricity (shape) of contiguous boxes + Values tending to 1 are more circular by definition, whereas + values tending to 0 are more linear + + Input:: + cloudElementLatLon: 2D array in (lat,lon) representing T_bb contiguous squares + + Output:: + epsilon: a floating-point representing the eccentricity of the matrix passed + + ''' + + epsilon = 0.0 + + #loop over all lons and determine longest (non-zero) col + #loop over all lats and determine longest (non-zero) row + for latLon in cloudElementLatLon: + #assign a matrix to determine the legit values + + nonEmptyLons = sum(sum(cloudElementLatLon)>0) + nonEmptyLats = sum(sum(cloudElementLatLon.transpose())>0) + + lonEigenvalues = 1.0 * nonEmptyLats / (nonEmptyLons+0.001) #for long oval on y axis + latEigenvalues = 1.0 * nonEmptyLons / (nonEmptyLats +0.001) #for long oval on x-axs + epsilon = min(latEigenvalues,lonEigenvalues) + + return epsilon #****************************************************************** def cloudElementOverlap (currentCELatLons, previousCELatLons): - ''' - Purpose:: - Determines the percentage overlap between two list of lat-lons passed - - Input:: - currentCELatLons: a list of tuples for the current CE - previousCELatLons: a list of tuples for the other CE being considered - - Output:: - percentageOverlap: a floating-point representing the number of overlapping lat_lon tuples - areaOverlap: a floating-point number representing the area overlapping - - ''' - - latlonprev =[] - latloncurr = [] - count = 0 - percentageOverlap = 0.0 - areaOverlap = 0.0 - - #remove the temperature from the tuples for currentCELatLons and previousCELatLons then check for overlap - latlonprev = [(x[0],x[1]) for x in previousCELatLons] - latloncurr = [(x[0],x[1]) for x in currentCELatLons] - - #find overlap - count = len(list(set(latloncurr)&set(latlonprev))) - - #find area overlap - areaOverlap = count*XRES*YRES - - #find percentage - percentageOverlap = max(((count*1.0)/(len(latloncurr)*1.0)),((count*1.0)/(len(latlonprev)*1.0))) - - return percentageOverlap, areaOverlap + ''' + Purpose:: + Determines the percentage overlap between two list of lat-lons passed + + Input:: + currentCELatLons: a list of tuples for the current CE + previousCELatLons: a list of tuples for the other CE being considered + + Output:: + percentageOverlap: a floating-point representing the number of overlapping lat_lon tuples + areaOverlap: a floating-point number representing the area overlapping + + ''' + + latlonprev =[] + latloncurr = [] + count = 0 + percentageOverlap = 0.0 + areaOverlap = 0.0 + + #remove the temperature from the tuples for currentCELatLons and previousCELatLons then check for overlap + latlonprev = [(x[0],x[1]) for x in previousCELatLons] + latloncurr = [(x[0],x[1]) for x in currentCELatLons] + + #find overlap + count = len(list(set(latloncurr)&set(latlonprev))) + + #find area overlap + areaOverlap = count*XRES*YRES + + #find percentage + percentageOverlap = max(((count*1.0)/(len(latloncurr)*1.0)),((count*1.0)/(len(latlonprev)*1.0))) + + return percentageOverlap, areaOverlap #****************************************************************** def findCESpeed(node, MCSList): - ''' - Purpose:: - To determine the speed of the CEs uses vector displacement delta_lat/delta_lon (y/x) - - Input:: - node: a string representing the CE - MCSList: a list of strings representing the feature - - Output:: - CEspeed: a floating-point number representing the speed of the CE - - ''' - - delta_lon =0.0 - delta_lat =0.0 - CEspeed =[] - theSpeed = 0.0 - - - theList = CLOUD_ELEMENT_GRAPH.successors(node) - nodeLatLon=thisDict(node)['cloudElementCenter'] - - - for aNode in theList: - if aNode in MCSList: - #if aNode is part of the MCSList then determine distance - aNodeLatLon = thisDict(aNode)['cloudElementCenter'] - #calculate CE speed - #checking the lats - # nodeLatLon[0] += 90.0 - # aNodeLatLon[0] += 90.0 - # delta_lat = (nodeLatLon[0] - aNodeLatLon[0]) - delta_lat = ((thisDict(node)['cloudElementCenter'][0] +90.0) - (thisDict(aNode)['cloudElementCenter'][0]+90.0)) - # nodeLatLon[1] += 360.0 - # aNodeLatLon[1] += 360.0 - # delta_lon = (nodeLatLon[1] - aNodeLatLon[1]) - delta_lon = ((thisDict(node)['cloudElementCenter'][1]+360.0) - (thisDict(aNode)['cloudElementCenter'][1]+360.0)) - - try: - theSpeed = abs((((delta_lat/delta_lon)*LAT_DISTANCE*1000)/(TRES*3600))) #convert to s --> m/s - except: - theSpeed = 0.0 - - CEspeed.append(theSpeed) - - # print "~~~ ", thisDict(aNode)['uniqueID'] - # print "*** ", nodeLatLon, thisDict(node)['cloudElementCenter'] - # print "*** ", aNodeLatLon, thisDict(aNode)['cloudElementCenter'] - - if not CEspeed: - return 0.0 - else: - return min(CEspeed) + ''' + Purpose:: + To determine the speed of the CEs uses vector displacement delta_lat/delta_lon (y/x) + + Input:: + node: a string representing the CE + MCSList: a list of strings representing the feature + + Output:: + CEspeed: a floating-point number representing the speed of the CE + + ''' + + delta_lon =0.0 + delta_lat =0.0 + CEspeed =[] + theSpeed = 0.0 + + + theList = CLOUD_ELEMENT_GRAPH.successors(node) + nodeLatLon=thisDict(node)['cloudElementCenter'] + + + for aNode in theList: + if aNode in MCSList: + #if aNode is part of the MCSList then determine distance + aNodeLatLon = thisDict(aNode)['cloudElementCenter'] + #calculate CE speed + #checking the lats + # nodeLatLon[0] += 90.0 + # aNodeLatLon[0] += 90.0 + # delta_lat = (nodeLatLon[0] - aNodeLatLon[0]) + delta_lat = ((thisDict(node)['cloudElementCenter'][0] +90.0) - (thisDict(aNode)['cloudElementCenter'][0]+90.0)) + # nodeLatLon[1] += 360.0 + # aNodeLatLon[1] += 360.0 + # delta_lon = (nodeLatLon[1] - aNodeLatLon[1]) + delta_lon = ((thisDict(node)['cloudElementCenter'][1]+360.0) - (thisDict(aNode)['cloudElementCenter'][1]+360.0)) + + try: + theSpeed = abs((((delta_lat/delta_lon)*LAT_DISTANCE*1000)/(TRES*3600))) #convert to s --> m/s + except: + theSpeed = 0.0 + + CEspeed.append(theSpeed) + + # print "~~~ ", thisDict(aNode)['uniqueID'] + # print "*** ", nodeLatLon, thisDict(node)['cloudElementCenter'] + # print "*** ", aNodeLatLon, thisDict(aNode)['cloudElementCenter'] + + if not CEspeed: + return 0.0 + else: + return min(CEspeed) #****************************************************************** # # UTILITY SCRIPTS FOR MCCSEARCH.PY # #****************************************************************** def maenumerate(mArray): - ''' - Purpose:: - Utility script for returning the actual values from the masked array - Taken from: http://stackoverflow.com/questions/8620798/numpy-ndenumerate-for-masked-arrays - - Input:: - mArray: the masked array returned from the ma.array() command - - - Output:: - maskedValues: 3D (t,lat,lon), value of only masked values - - ''' - - mask = ~mArray.mask.ravel() - #beware yield fast, but generates a type called "generate" that does not allow for array methods - for index, maskedValue in itertools.izip(np.ndenumerate(mArray), mask): - if maskedValue: - yield index + ''' + Purpose:: + Utility script for returning the actual values from the masked array + Taken from: http://stackoverflow.com/questions/8620798/numpy-ndenumerate-for-masked-arrays + + Input:: + mArray: the masked array returned from the ma.array() command + + + Output:: + maskedValues: 3D (t,lat,lon), value of only masked values + + ''' + + mask = ~mArray.mask.ravel() + #beware yield fast, but generates a type called "generate" that does not allow for array methods + for index, maskedValue in itertools.izip(np.ndenumerate(mArray), mask): + if maskedValue: + yield index #****************************************************************** def createMainDirectory(mainDirStr): - ''' - Purpose:: - To create the main directory for storing information and - the subdirectories for storing information - Input:: - mainDir: a directory for where all information generated from - the program are to be stored - Output:: None - - ''' - global MAINDIRECTORY - - MAINDIRECTORY = mainDirStr - #if directory doesnt exist, creat it - if not os.path.exists(MAINDIRECTORY): - os.makedirs(MAINDIRECTORY) - - os.chdir((MAINDIRECTORY)) - #create the subdirectories - try: - os.makedirs('images') - os.makedirs('textFiles') - os.makedirs('MERGnetcdfCEs') - os.makedirs('TRMMnetcdfCEs') - except: - print "Directory exists already!!!" - #TODO: some nice way of prompting if it is ok to continue...or just leave - - return + ''' + Purpose:: + To create the main directory for storing information and + the subdirectories for storing information + Input:: + mainDir: a directory for where all information generated from + the program are to be stored + Output:: None + + ''' + global MAINDIRECTORY + + MAINDIRECTORY = mainDirStr + #if directory doesnt exist, creat it + if not os.path.exists(MAINDIRECTORY): + os.makedirs(MAINDIRECTORY) + + os.chdir((MAINDIRECTORY)) + #create the subdirectories + try: + os.makedirs('images') + os.makedirs('textFiles') + os.makedirs('MERGnetcdfCEs') + os.makedirs('TRMMnetcdfCEs') + except: + print "Directory exists already!!!" + #TODO: some nice way of prompting if it is ok to continue...or just leave + + return #****************************************************************** def checkForFiles(startTime, endTime, thisDir, fileType): - ''' - Purpose:: To ensure all the files between the starttime and endTime - exist in the directory supplied - - Input:: - startTime: a string yyyymmmddhh representing the starttime - endTime: a string yyyymmmddhh representing the endTime - thisDir: a string representing the directory path where to - look for the file - fileType: an integer representing the type of file in the directory - 1 - MERG original files, 2 - TRMM original files - - Output:: - status: a boolean representing whether all files exists - - ''' - filelist =[] - startFilename = '' - endFilename ='' - currFilename = '' - status = False - startyr = int(startTime[:4]) - startmm = int(startTime[4:6]) - startdd = int(startTime[6:8]) - starthr = int(startTime[-2:]) - endyr = int(endTime[:4]) - endmm = int(endTime[4:6]) - enddd = int(endTime[6:8]) - endhh = int(endTime[-2:]) - curryr = startyr - currmm = startmm - currdd = startdd - currhr = starthr - currmmStr = '' - currddStr = '' - currhrStr = '' - endmmStr = '' - endddStr ='' - endhhStr = '' - - #check that the startTime is before the endTime - if fileType == 1: - #print "fileType is 1" - startFilename = "merg_"+startTime+"_4km-pixel.nc" - endFilename = thisDir+"/merg_"+endTime+"_4km-pixel.nc" - - if fileType == 2: - #TODO:: determine closest time for TRMM files for end - #http://disc.sci.gsfc.nasa.gov/additional/faq/precipitation_faq.shtml#convert - #How do I extract time information from the TRMM 3B42 file name? section - # startFilename = "3B42."+startTime[:8]+"."+currhr+".7A.nc" - # endFilename = "3B42."+endTime[:8]+"."+endTime[-2:]+".7A.nc" - if starthr%3 == 2: - currhr += 1 - elif starthr%3 ==1: - currhr -= 1 - else: - currhr = starthr - - curryr, currmmStr, currddStr, currhrStr,_,_,_ = findTime(curryr, currmm, currdd, currhr) - - startFilename = "3B42."+str(curryr)+currmmStr+currddStr+"."+currhrStr+".7A.nc" - if endhh%3 == 2: - endhh += 1 - elif endhh%3 ==1: - endhh -= 1 - - endyr, endmmStr, endddStr, endhhStr, _, _, _ = findTime(endyr, endmm, enddd, endhh) - - endFilename = thisDir+"/3B42."+str(endyr)+endmmStr+endddStr+"."+endhhStr+".7A.nc" - - #check for files between startTime and endTime - currFilename = thisDir+"/"+startFilename - - while currFilename is not endFilename: - - if not os.path.isfile(currFilename): - print "file is missing! Filename: ", currFilename - status = False - return status, filelist - else: - #create filelist - filelist.append(currFilename) - - status = True - if currFilename == endFilename: - break - - #generate new currFilename - if fileType == 1: - currhr +=1 - elif fileType ==2: - currhr += 3 - - curryr, currmmStr, currddStr, currhrStr, currmm, currdd, currhr = findTime(curryr, currmm, currdd, currhr) - - if fileType == 1: - currFilename = thisDir+"/"+"merg_"+str(curryr)+currmmStr+currddStr+currhrStr+"_4km-pixel.nc" - if fileType == 2: - currFilename = thisDir+"/"+"3B42."+str(curryr)+currmmStr+currddStr+"."+currhrStr+".7A.nc" - - return status,filelist + ''' + Purpose:: To ensure all the files between the starttime and endTime + exist in the directory supplied + + Input:: + startTime: a string yyyymmmddhh representing the starttime + endTime: a string yyyymmmddhh representing the endTime + thisDir: a string representing the directory path where to + look for the file + fileType: an integer representing the type of file in the directory + 1 - MERG original files, 2 - TRMM original files + + Output:: + status: a boolean representing whether all files exists + + ''' + filelist =[] + startFilename = '' + endFilename ='' + currFilename = '' + status = False + startyr = int(startTime[:4]) + startmm = int(startTime[4:6]) + startdd = int(startTime[6:8]) + starthr = int(startTime[-2:]) + endyr = int(endTime[:4]) + endmm = int(endTime[4:6]) + enddd = int(endTime[6:8]) + endhh = int(endTime[-2:]) + curryr = startyr + currmm = startmm + currdd = startdd + currhr = starthr + currmmStr = '' + currddStr = '' + currhrStr = '' + endmmStr = '' + endddStr ='' + endhhStr = '' + + #check that the startTime is before the endTime + if fileType == 1: + #print "fileType is 1" + startFilename = "merg_"+startTime+"_4km-pixel.nc" + endFilename = thisDir+"/merg_"+endTime+"_4km-pixel.nc" + + if fileType == 2: + #TODO:: determine closest time for TRMM files for end + #http://disc.sci.gsfc.nasa.gov/additional/faq/precipitation_faq.shtml#convert + #How do I extract time information from the TRMM 3B42 file name? section + # startFilename = "3B42."+startTime[:8]+"."+currhr+".7A.nc" + # endFilename = "3B42."+endTime[:8]+"."+endTime[-2:]+".7A.nc" + if starthr%3 == 2: + currhr += 1 + elif starthr%3 ==1: + currhr -= 1 + else: + currhr = starthr + + curryr, currmmStr, currddStr, currhrStr,_,_,_ = findTime(curryr, currmm, currdd, currhr) + + startFilename = "3B42."+str(curryr)+currmmStr+currddStr+"."+currhrStr+".7A.nc" + if endhh%3 == 2: + endhh += 1 + elif endhh%3 ==1: + endhh -= 1 + + endyr, endmmStr, endddStr, endhhStr, _, _, _ = findTime(endyr, endmm, enddd, endhh) + + endFilename = thisDir+"/3B42."+str(endyr)+endmmStr+endddStr+"."+endhhStr+".7A.nc" + + #check for files between startTime and endTime + currFilename = thisDir+"/"+startFilename + + while currFilename is not endFilename: + + if not os.path.isfile(currFilename): + print "file is missing! Filename: ", currFilename + status = False + return status, filelist + else: + #create filelist + filelist.append(currFilename) + + status = True + if currFilename == endFilename: + break + + #generate new currFilename + if fileType == 1: + currhr +=1 + elif fileType ==2: + currhr += 3 + + curryr, currmmStr, currddStr, currhrStr, currmm, currdd, currhr = findTime(curryr, currmm, currdd, currhr) + + if fileType == 1: + currFilename = thisDir+"/"+"merg_"+str(curryr)+currmmStr+currddStr+currhrStr+"_4km-pixel.nc" + if fileType == 2: + currFilename = thisDir+"/"+"3B42."+str(curryr)+currmmStr+currddStr+"."+currhrStr+".7A.nc" + + return status,filelist #****************************************************************** def findTime(curryr, currmm, currdd, currhr): - ''' - Purpose:: To determine the new yr, mm, dd, hr - - Input:: curryr, an integer representing the year - currmm, an integer representing the month - currdd, an integer representing the day - currhr, an integer representing the hour - - Output::curryr, an integer representing the year - currmm, an integer representing the month - currdd, an integer representing the day - currhr, an integer representing the hour - ''' - if currhr > 23: - currhr = 0 - currdd += 1 - if currdd > 30 and (currmm == 4 or currmm == 6 or currmm == 9 or currmm == 11): - currmm +=1 - elif currdd > 31 and (currmm == 1 or currmm ==3 or currmm == 5 or currmm == 7 or currmm == 8 or currmm == 10): - currmm +=1 - currdd = 1 - elif currdd > 31 and currmm == 12: - currmm = 1 - currdd = 1 - curryr += 1 - elif currdd > 28 and currmm == 2 and (curryr%4)!=0: - currmm = 3 - currdd = 1 - elif (curryr%4)==0 and currmm == 2 and currdd>29: - currmm = 3 - currdd = 1 - - if currmm < 10: - currmmStr="0"+str(currmm) - else: - currmmStr = str(currmm) - - if currdd < 10: - currddStr = "0"+str(currdd) - else: - currddStr = str(currdd) - - if currhr < 10: - currhrStr = "0"+str(currhr) - else: - currhrStr = str(currhr) - - return curryr, currmmStr, currddStr, currhrStr, currmm, currdd, currhr + ''' + Purpose:: To determine the new yr, mm, dd, hr + + Input:: curryr, an integer representing the year + currmm, an integer representing the month + currdd, an integer representing the day + currhr, an integer representing the hour + + Output::curryr, an integer representing the year + currmm, an integer representing the month + currdd, an integer representing the day + currhr, an integer representing the hour + ''' + if currhr > 23: + currhr = 0 + currdd += 1 + if currdd > 30 and (currmm == 4 or currmm == 6 or currmm == 9 or currmm == 11): + currmm +=1 + elif currdd > 31 and (currmm == 1 or currmm ==3 or currmm == 5 or currmm == 7 or currmm == 8 or currmm == 10): + currmm +=1 + currdd = 1 + elif currdd > 31 and currmm == 12: + currmm = 1 + currdd = 1 + curryr += 1 + elif currdd > 28 and currmm == 2 and (curryr%4)!=0: + currmm = 3 + currdd = 1 + elif (curryr%4)==0 and currmm == 2 and currdd>29: + currmm = 3 + currdd = 1 + + if currmm < 10: + currmmStr="0"+str(currmm) + else: + currmmStr = str(currmm) + + if currdd < 10: + currddStr = "0"+str(currdd) + else: + currddStr = str(currdd) + + if currhr < 10: + currhrStr = "0"+str(currhr) + else: + currhrStr = str(currhr) + + return curryr, currmmStr, currddStr, currhrStr, currmm, currdd, currhr #****************************************************************** def find_nearest(thisArray,value): - ''' - Purpose :: to determine the value within an array closes to - another value - - Input :: - Output:: - ''' - idx = (np.abs(thisArray-value)).argmin() - return thisArray[idx] + ''' + Purpose :: to determine the value within an array closes to + another value + + Input :: + Output:: + ''' + idx = (np.abs(thisArray-value)).argmin() + return thisArray[idx] #****************************************************************** #****************************************************************** def postProcessingNetCDF(dataset, dirName = None): - ''' - Purpose:: - Utility script displaying the data in NETCDF4 files - - Input:: - dataset: integer representing original MERG (1) or post-processed MERG data (2) or post-processed TRMM(3) - string: Directory to the location of the raw (MERG) files, preferably zipped - - Output:: - Generates 2D plots in location as specfied in the code - - ''' - coreDir = os.path.dirname(os.path.abspath(__file__)) - imgFilename = '' - - if dataset == 1: - var = 'ch4' - plotTitle = 'Original MERG data ' - elif dataset == 2: - var = 'brightnesstemp' - plotTitle = 'MERG CE data' - elif dataset== 3: - var = 'precipitation_Accumulation' - plotTitle = 'TRMM CE data' - - - #sort files - os.chdir((dirName+'/')) - files = filter(os.path.isfile, glob.glob("*.nc")) - files.sort(key=lambda x: os.path.getmtime(x)) - - for eachfile in files: - fullFname = os.path.splitext(eachfile)[0] - fnameNoExtension = fullFname.split('.nc')[0] - - fname = dirName+'/'+fnameNoExtension+'.nc' - - if os.path.isfile(fname): - fileData = Dataset(fname,'r',format='NETCDF4') - file_variable = fileData.variables[var][:] - lats = fileData.variables['latitude'][:] - lons = fileData.variables['longitude'][:] - LONDATA, LATDATA = np.meshgrid(lons,lats) - nygrd = len(LATDATA[:,0]) - nxgrd = len(LONDATA[0,:]) - fileData.close() - - imgFilename = MAINDIRECTORY+'/images/'+fnameNoExtension + '.gif' - - if dataset == 3: - createPrecipPlot(np.squeeze(file_variable, axis=0), LATDATA[:,0], LONDATA[0,:], plotTitle,imgFilename) - else: - plotter.draw_contour_map(file_variable, LATDATA[:,0], LONDATA[0,:], imgFilename, ptitle=plotTitle) - - return + ''' + Purpose:: + Utility script displaying the data in NETCDF4 files + + Input:: + dataset: integer representing original MERG (1) or post-processed MERG data (2) or post-processed TRMM(3) + string: Directory to the location of the raw (MERG) files, preferably zipped + + Output:: + Generates 2D plots in location as specfied in the code + + ''' + coreDir = os.path.dirname(os.path.abspath(__file__)) + imgFilename = '' + + if dataset == 1: + var = 'ch4' + plotTitle = 'Original MERG data ' + elif dataset == 2: + var = 'brightnesstemp' + plotTitle = 'MERG CE data' + elif dataset== 3: + var = 'precipitation_Accumulation' + plotTitle = 'TRMM CE data' + + + #sort files + os.chdir((dirName+'/')) + files = filter(os.path.isfile, glob.glob("*.nc")) + files.sort(key=lambda x: os.path.getmtime(x)) + + for eachfile in files: + fullFname = os.path.splitext(eachfile)[0] + fnameNoExtension = fullFname.split('.nc')[0] + + fname = dirName+'/'+fnameNoExtension+'.nc' + + if os.path.isfile(fname): + fileData = Dataset(fname,'r',format='NETCDF4') + file_variable = fileData.variables[var][:] + lats = fileData.variables['latitude'][:] + lons = fileData.variables['longitude'][:] + LONDATA, LATDATA = np.meshgrid(lons,lats) + nygrd = len(LATDATA[:,0]) + nxgrd = len(LONDATA[0,:]) + fileData.close() + + imgFilename = MAINDIRECTORY+'/images/'+fnameNoExtension + '.gif' + + if dataset == 3: + createPrecipPlot(np.squeeze(file_variable, axis=0), LATDATA[:,0], LONDATA[0,:], plotTitle,imgFilename) + else: + plotter.draw_contour_map(file_variable, LATDATA[:,0], LONDATA[0,:], imgFilename, ptitle=plotTitle) + + return #****************************************************************** def drawGraph (thisGraph, graphTitle, edgeWeight=None): - ''' - Purpose:: - Utility function to draw graph in the hierachial format - - Input:: - thisGraph: a Networkx directed graph - graphTitle: a string representing the graph title - edgeWeight: (optional) a list of integers representing the edge weights in the graph - - Output:: None - - ''' - - imgFilename = MAINDIRECTORY+'/images/'+ graphTitle+".gif" - fig=plt.figure(facecolor='white', figsize=(16,12)) - - edge95 = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[0]] - edge90 = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[1]] - edegeOverlap = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[2]] - - nx.write_dot(thisGraph, 'test.dot') - plt.title(graphTitle) - pos = nx.graphviz_layout(thisGraph, prog='dot') - #draw graph in parts - #nodes - nx.draw_networkx_nodes(thisGraph, pos, with_labels=True, arrows=False) - #edges - nx.draw_networkx_edges(thisGraph, pos, edgelist=edge95, alpha=0.5, arrows=False) - nx.draw_networkx_edges(thisGraph, pos, edgelist=edge90, edge_color='b', style='dashed', arrows=False) - nx.draw_networkx_edges(thisGraph, pos, edgelist=edegeOverlap, edge_color='y', style='dashed', arrows=False) - #labels - nx.draw_networkx_labels(thisGraph,pos, arrows=False) - plt.axis('off') - plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) - #do some clean up...and ensuring that we are in the right dir - os.chdir((MAINDIRECTORY+'/')) - subprocess.call('rm test.dot', shell=True) + ''' + Purpose:: + Utility function to draw graph in the hierachial format + + Input:: + thisGraph: a Networkx directed graph + graphTitle: a string representing the graph title + edgeWeight: (optional) a list of integers representing the edge weights in the graph + + Output:: None + + ''' + + imgFilename = MAINDIRECTORY+'/images/'+ graphTitle+".gif" + fig=plt.figure(facecolor='white', figsize=(16,12)) + + edge95 = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[0]] + edge90 = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[1]] + edegeOverlap = [(u,v) for (u,v,d) in thisGraph.edges(data=True) if d['weight'] == edgeWeight[2]] + + nx.write_dot(thisGraph, 'test.dot') + plt.title(graphTitle) + pos = nx.graphviz_layout(thisGraph, prog='dot') + #draw graph in parts + #nodes + nx.draw_networkx_nodes(thisGraph, pos, with_labels=True, arrows=False) + #edges + nx.draw_networkx_edges(thisGraph, pos, edgelist=edge95, alpha=0.5, arrows=False) + nx.draw_networkx_edges(thisGraph, pos, edgelist=edge90, edge_color='b', style='dashed', arrows=False) + nx.draw_networkx_edges(thisGraph, pos, edgelist=edegeOverlap, edge_color='y', style='dashed', arrows=False) + #labels + nx.draw_networkx_labels(thisGraph,pos, arrows=False) + plt.axis('off') + plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) + #do some clean up...and ensuring that we are in the right dir + os.chdir((MAINDIRECTORY+'/')) + subprocess.call('rm test.dot', shell=True) #****************************************************************** def getModelTimes(xtimes, timeVarName): ''' @@ -2206,14 +2206,14 @@ def getModelTimes(xtimes, timeVarName): xtime = int(xtime) if int(xtime) == 0: - xtime = 1 + xtime = 1 if units == 'minutes': dt = timedelta(minutes=xtime) new_time = base_time + dt elif units == 'hours': - dt = timedelta(hours=int(xtime)) - new_time = base_time + dt# timedelta(hours=int(xtime)) + dt = timedelta(hours=int(xtime)) + new_time = base_time + dt# timedelta(hours=int(xtime)) elif units == 'days': dt = timedelta(days=xtime) new_time = base_time + dt @@ -2328,7 +2328,7 @@ def decodeTimeFromString(time_string): pass try: - mytime = datetime.strptime(time_string,'%Y-%m-%d %H') + mytime = datetime.strptime(time_string,'%Y-%m-%d %H') return mytime except ValueError: @@ -2354,1215 +2354,1215 @@ def do_regrid(q, lat, lon, lat2, lon2, order=1, mdi=-999999999): # #****************************************************************** def numberOfFeatures(finalMCCList): - ''' - Purpose:: - To count the number of MCCs found for the period - - Input:: - finalMCCList: a list of list of strings representing a list of list of nodes representing a MCC - - Output:: - an integer representing the number of MCCs found - - ''' - return len(finalMCCList) + ''' + Purpose:: + To count the number of MCCs found for the period + + Input:: + finalMCCList: a list of list of strings representing a list of list of nodes representing a MCC + + Output:: + an integer representing the number of MCCs found + + ''' + return len(finalMCCList) #****************************************************************** def temporalAndAreaInfoMetric(finalMCCList): - ''' - Purpose:: - To provide information regarding the temporal properties of the MCCs found - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - - Output:: - allMCCtimes: a list of dictionaries {MCCtimes, starttime, endtime, duration, area} representing a list of dictionaries - of MCC temporal details for each MCC in the period considered - - Assumptions:: - the final time hour --> the event lasted throughout that hr, therefore +1 to endtime - ''' - #TODO: in real data edit this to use datetime - #starttime =0 - #endtime =0 - #duration = 0 - MCCtimes =[] - allMCCtimes=[] - MCSArea =[] - - if finalMCCList: - for eachMCC in finalMCCList: - #get the info from the node - for eachNode in eachMCC: - MCCtimes.append(thisDict(eachNode)['cloudElementTime']) - MCSArea.append(thisDict(eachNode)['cloudElementArea']) - - #sort and remove duplicates - MCCtimes=list(set(MCCtimes)) - MCCtimes.sort() - tdelta = MCCtimes[1] - MCCtimes[0] - starttime = MCCtimes[0] - endtime = MCCtimes[-1] - duration = (endtime - starttime) + tdelta - print "starttime ", starttime, "endtime ", endtime, "tdelta ", tdelta, "duration ", duration, "MCSAreas ", MCSArea - allMCCtimes.append({'MCCtimes':MCCtimes, 'starttime':starttime, 'endtime':endtime, 'duration':duration, 'MCSArea': MCSArea}) - MCCtimes=[] - MCSArea=[] - else: - allMCCtimes =[] - tdelta = 0 - - return allMCCtimes, tdelta + ''' + Purpose:: + To provide information regarding the temporal properties of the MCCs found + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + + Output:: + allMCCtimes: a list of dictionaries {MCCtimes, starttime, endtime, duration, area} representing a list of dictionaries + of MCC temporal details for each MCC in the period considered + + Assumptions:: + the final time hour --> the event lasted throughout that hr, therefore +1 to endtime + ''' + #TODO: in real data edit this to use datetime + #starttime =0 + #endtime =0 + #duration = 0 + MCCtimes =[] + allMCCtimes=[] + MCSArea =[] + + if finalMCCList: + for eachMCC in finalMCCList: + #get the info from the node + for eachNode in eachMCC: + MCCtimes.append(thisDict(eachNode)['cloudElementTime']) + MCSArea.append(thisDict(eachNode)['cloudElementArea']) + + #sort and remove duplicates + MCCtimes=list(set(MCCtimes)) + MCCtimes.sort() + tdelta = MCCtimes[1] - MCCtimes[0] + starttime = MCCtimes[0] + endtime = MCCtimes[-1] + duration = (endtime - starttime) + tdelta + print "starttime ", starttime, "endtime ", endtime, "tdelta ", tdelta, "duration ", duration, "MCSAreas ", MCSArea + allMCCtimes.append({'MCCtimes':MCCtimes, 'starttime':starttime, 'endtime':endtime, 'duration':duration, 'MCSArea': MCSArea}) + MCCtimes=[] + MCSArea=[] + else: + allMCCtimes =[] + tdelta = 0 + + return allMCCtimes, tdelta #****************************************************************** def longestDuration(allMCCtimes): - ''' - Purpose:: - To determine the longest MCC for the period + ''' + Purpose:: + To determine the longest MCC for the period - Input:: - allMCCtimes: a list of dictionaries {MCCtimes, starttime, endtime, duration, area} representing a list of dictionaries - of MCC temporal details for each MCC in the period considered + Input:: + allMCCtimes: a list of dictionaries {MCCtimes, starttime, endtime, duration, area} representing a list of dictionaries + of MCC temporal details for each MCC in the period considered - Output:: - an integer - lenMCC: representing the duration of the longest MCC found - a list of strings - longestMCC: representing the nodes of longest MCC + Output:: + an integer - lenMCC: representing the duration of the longest MCC found + a list of strings - longestMCC: representing the nodes of longest MCC - Assumptions:: + Assumptions:: - ''' + ''' - # MCCList = [] - # lenMCC = 0 - # longestMCC =[] + # MCCList = [] + # lenMCC = 0 + # longestMCC =[] - # #remove duplicates - # MCCList = list(set(finalMCCList)) + # #remove duplicates + # MCCList = list(set(finalMCCList)) - # longestMCC = max(MCCList, key = lambda tup:len(tup)) - # lenMCC = len(longestMCC) + # longestMCC = max(MCCList, key = lambda tup:len(tup)) + # lenMCC = len(longestMCC) - # return lenMCC, longestMCC + # return lenMCC, longestMCC - return max([MCC['duration'] for MCC in allMCCtimes]) + return max([MCC['duration'] for MCC in allMCCtimes]) #****************************************************************** def shortestDuration(allMCCtimes): - ''' - Purpose:: To determine the shortest MCC for the period + ''' + Purpose:: To determine the shortest MCC for the period - Input:: list of dictionaries - allMCCtimes {MCCtimes, starttime, endtime, duration): a list of dictionaries - of MCC temporal details for each MCC in the period considered + Input:: list of dictionaries - allMCCtimes {MCCtimes, starttime, endtime, duration): a list of dictionaries + of MCC temporal details for each MCC in the period considered - Output::an integer - lenMCC: representing the duration of the shortest MCC found - a list of strings - longestMCC: representing the nodes of shortest MCC + Output::an integer - lenMCC: representing the duration of the shortest MCC found + a list of strings - longestMCC: representing the nodes of shortest MCC - Assumptions:: + Assumptions:: - ''' - # lenMCC = 0 - # shortestMCC =[] - # MCCList =[] - - # #remove duplicates - # MCCList = list(set(finalMCCList)) + ''' + # lenMCC = 0 + # shortestMCC =[] + # MCCList =[] + + # #remove duplicates + # MCCList = list(set(finalMCCList)) - # shortestMCC = min(MCCList, key = lambda tup:len(tup)) - # lenMCC = len(shortestMCC) + # shortestMCC = min(MCCList, key = lambda tup:len(tup)) + # lenMCC = len(shortestMCC) - # return lenMCC, shortestMCC - return min([MCC['duration'] for MCC in allMCCtimes]) + # return lenMCC, shortestMCC + return min([MCC['duration'] for MCC in allMCCtimes]) #****************************************************************** def averageDuration(allMCCtimes): - ''' - Purpose:: To determine the average MCC length for the period + ''' + Purpose:: To determine the average MCC length for the period - Input:: list of dictionaries - allMCCtimes {MCCtimes, starttime, endtime, duration): a list of dictionaries - of MCC temporal details for each MCC in the period considered + Input:: list of dictionaries - allMCCtimes {MCCtimes, starttime, endtime, duration): a list of dictionaries + of MCC temporal details for each MCC in the period considered - Output::a floating-point representing the average duration of a MCC in the period - - Assumptions:: + Output::a floating-point representing the average duration of a MCC in the period + + Assumptions:: - ''' + ''' - return sum([MCC['duration'] for MCC in allMCCtimes], timedelta(seconds=0))/len(allMCCtimes) + return sum([MCC['duration'] for MCC in allMCCtimes], timedelta(seconds=0))/len(allMCCtimes) #****************************************************************** def averageTime (allTimes): - ''' - Purpose:: - To determine the average time in a list of datetimes - e.g. of use is finding avg starttime, - Input:: - allTimes: a list of datetimes representing all of a given event e.g. start time - - Output:: - a floating-point number representing the average of the times given - - ''' - avgTime = 0 - - for aTime in allTimes: - avgTime += aTime.second + 60*aTime.minute + 3600*aTime.hour - - if len(allTimes) > 1: - avgTime /= len(allTimes) - - rez = str(avgTime/3600) + ' ' + str((avgTime%3600)/60) + ' ' + str(avgTime%60) - return datetime.strptime(rez, "%H %M %S") + ''' + Purpose:: + To determine the average time in a list of datetimes + e.g. of use is finding avg starttime, + Input:: + allTimes: a list of datetimes representing all of a given event e.g. start time + + Output:: + a floating-point number representing the average of the times given + + ''' + avgTime = 0 + + for aTime in allTimes: + avgTime += aTime.second + 60*aTime.minute + 3600*aTime.hour + + if len(allTimes) > 1: + avgTime /= len(allTimes) + + rez = str(avgTime/3600) + ' ' + str((avgTime%3600)/60) + ' ' + str(avgTime%60) + return datetime.strptime(rez, "%H %M %S") #****************************************************************** def averageFeatureSize(finalMCCList): - ''' - Purpose:: To determine the average MCC size for the period - - Input:: a list of list of strings - finalMCCList: a list of list of nodes representing a MCC - - Output::a floating-point representing the average area of a MCC in the period - - Assumptions:: - - ''' - thisMCC = 0.0 - thisMCCAvg = 0.0 - - #for each node in the list, get the are information from the dictionary - #in the graph and calculate the area - for eachPath in finalMCCList: - for eachNode in eachPath: - thisMCC += thisDict(eachNode)['cloudElementArea'] - - thisMCCAvg += (thisMCC/len(eachPath)) - thisMCC = 0.0 - - #calcuate final average - return thisMCCAvg/(len(finalMCCList)) + ''' + Purpose:: To determine the average MCC size for the period + + Input:: a list of list of strings - finalMCCList: a list of list of nodes representing a MCC + + Output::a floating-point representing the average area of a MCC in the period + + Assumptions:: + + ''' + thisMCC = 0.0 + thisMCCAvg = 0.0 + + #for each node in the list, get the are information from the dictionary + #in the graph and calculate the area + for eachPath in finalMCCList: + for eachNode in eachPath: + thisMCC += thisDict(eachNode)['cloudElementArea'] + + thisMCCAvg += (thisMCC/len(eachPath)) + thisMCC = 0.0 + + #calcuate final average + return thisMCCAvg/(len(finalMCCList)) #****************************************************************** def commonFeatureSize(finalMCCList): - ''' - Purpose:: - To determine the common (mode) MCC size for the period - - Input:: - finalMCCList: a list of list of strings representing the list of nodes representing a MCC - - Output:: - a floating-point representing the average area of a MCC in the period - - Assumptions:: - - ''' - thisMCC = 0.0 - thisMCCAvg = [] - - #for each node in the list, get the area information from the dictionary - #in the graph and calculate the area - for eachPath in finalMCCList: - for eachNode in eachPath: - thisMCC += eachNode['cloudElementArea'] - - thisMCCAvg.append(thisMCC/len(eachPath)) - thisMCC = 0.0 - - #calcuate - hist, bin_edges = np.histogram(thisMCCAvg) - return hist,bin_edges + ''' + Purpose:: + To determine the common (mode) MCC size for the period + + Input:: + finalMCCList: a list of list of strings representing the list of nodes representing a MCC + + Output:: + a floating-point representing the average area of a MCC in the period + + Assumptions:: + + ''' + thisMCC = 0.0 + thisMCCAvg = [] + + #for each node in the list, get the area information from the dictionary + #in the graph and calculate the area + for eachPath in finalMCCList: + for eachNode in eachPath: + thisMCC += eachNode['cloudElementArea'] + + thisMCCAvg.append(thisMCC/len(eachPath)) + thisMCC = 0.0 + + #calcuate + hist, bin_edges = np.histogram(thisMCCAvg) + return hist,bin_edges #****************************************************************** def precipTotals(finalMCCList): - ''' - Purpose:: - Precipitation totals associated with a cloud element - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - - Output:: - precipTotal: a floating-point number representing the total amount of precipitation associated - with the feature - ''' - precipTotal = 0.0 - CEprecip =0.0 - MCSPrecip=[] - allMCSPrecip =[] - count = 0 - - if finalMCCList: - #print "len finalMCCList is: ", len(finalMCCList) - for eachMCC in finalMCCList: - #get the info from the node - for node in eachMCC: - eachNode=thisDict(node) - count += 1 - if count == 1: - prevHr = int(str(eachNode['cloudElementTime']).replace(" ", "")[-8:-6]) - - currHr =int(str(eachNode['cloudElementTime']).replace(" ", "")[-8:-6]) - if prevHr == currHr: - CEprecip += eachNode['cloudElementPrecipTotal'] - else: - MCSPrecip.append((prevHr,CEprecip)) - CEprecip = eachNode['cloudElementPrecipTotal'] - #last value in for loop - if count == len(eachMCC): - MCSPrecip.append((currHr, CEprecip)) - - precipTotal += eachNode['cloudElementPrecipTotal'] - prevHr = currHr - - MCSPrecip.append(('0',precipTotal)) - - allMCSPrecip.append(MCSPrecip) - precipTotal =0.0 - CEprecip = 0.0 - MCSPrecip = [] - count = 0 - - print "allMCSPrecip ", allMCSPrecip - - return allMCSPrecip + ''' + Purpose:: + Precipitation totals associated with a cloud element + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + + Output:: + precipTotal: a floating-point number representing the total amount of precipitation associated + with the feature + ''' + precipTotal = 0.0 + CEprecip =0.0 + MCSPrecip=[] + allMCSPrecip =[] + count = 0 + + if finalMCCList: + #print "len finalMCCList is: ", len(finalMCCList) + for eachMCC in finalMCCList: + #get the info from the node + for node in eachMCC: + eachNode=thisDict(node) + count += 1 + if count == 1: + prevHr = int(str(eachNode['cloudElementTime']).replace(" ", "")[-8:-6]) + + currHr =int(str(eachNode['cloudElementTime']).replace(" ", "")[-8:-6]) + if prevHr == currHr: + CEprecip += eachNode['cloudElementPrecipTotal'] + else: + MCSPrecip.append((prevHr,CEprecip)) + CEprecip = eachNode['cloudElementPrecipTotal'] + #last value in for loop + if count == len(eachMCC): + MCSPrecip.append((currHr, CEprecip)) + + precipTotal += eachNode['cloudElementPrecipTotal'] + prevHr = currHr + + MCSPrecip.append(('0',precipTotal)) + + allMCSPrecip.append(MCSPrecip) + precipTotal =0.0 + CEprecip = 0.0 + MCSPrecip = [] + count = 0 + + print "allMCSPrecip ", allMCSPrecip + + return allMCSPrecip #****************************************************************** def precipMaxMin(finalMCCList): - ''' - TODO: this doesnt work the np.min/max function seems to be not working with the nonzero option..possibly a problem upstream with cloudElementLatLonTRMM - Purpose:: - Precipitation maximum and min rates associated with each CE in MCS - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - - Output:: - MCSPrecip: a list indicating max and min rate for each CE identified - - ''' - maxCEprecip = 0.0 - minCEprecip =0.0 - MCSPrecip=[] - allMCSPrecip =[] - - - if finalMCCList: - if type(finalMCCList[0]) is str: # len(finalMCCList) == 1: - for node in finalMCCList: - eachNode = thisDict(node) - CETRMM = eachNode['cloudElementLatLonTRMM'] - - print "all ", np.min(CETRMM[np.nonzero(CETRMM)]) - print "minCEprecip ", np.min(eachNode['cloudElementLatLonTRMM']) #[np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - - print "maxCEprecip ", np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - sys.exit() - maxCEprecip = np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - minCEprecip = np.min(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - MCSPrecip.append((eachNode['uniqueID'],minCEprecip, maxCEprecip)) - - else: - for eachMCC in finalMCCList: - #get the info from the node - for node in eachMCC: - eachNode=thisDict(node) - #find min and max precip - maxCEprecip = np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - minCEprecip = np.min(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) - MCSPrecip.append((eachNode['uniqueID'],minCEprecip, maxCEprecip)) - allMCSPrecip.append(MCSPrecip) - MCSPrecip =[] - - return MCSPrecip + ''' + TODO: this doesnt work the np.min/max function seems to be not working with the nonzero option..possibly a problem upstream with cloudElementLatLonTRMM + Purpose:: + Precipitation maximum and min rates associated with each CE in MCS + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + + Output:: + MCSPrecip: a list indicating max and min rate for each CE identified + + ''' + maxCEprecip = 0.0 + minCEprecip =0.0 + MCSPrecip=[] + allMCSPrecip =[] + + + if finalMCCList: + if type(finalMCCList[0]) is str: # len(finalMCCList) == 1: + for node in finalMCCList: + eachNode = thisDict(node) + CETRMM = eachNode['cloudElementLatLonTRMM'] + + print "all ", np.min(CETRMM[np.nonzero(CETRMM)]) + print "minCEprecip ", np.min(eachNode['cloudElementLatLonTRMM']) #[np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + + print "maxCEprecip ", np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + sys.exit() + maxCEprecip = np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + minCEprecip = np.min(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + MCSPrecip.append((eachNode['uniqueID'],minCEprecip, maxCEprecip)) + + else: + for eachMCC in finalMCCList: + #get the info from the node + for node in eachMCC: + eachNode=thisDict(node) + #find min and max precip + maxCEprecip = np.max(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + minCEprecip = np.min(eachNode['cloudElementLatLonTRMM'][np.nonzero(eachNode['cloudElementLatLonTRMM'])]) + MCSPrecip.append((eachNode['uniqueID'],minCEprecip, maxCEprecip)) + allMCSPrecip.append(MCSPrecip) + MCSPrecip =[] + + return MCSPrecip #****************************************************************** # # PLOTS # #****************************************************************** def displaySize(finalMCCList): - ''' - Purpose:: - To create a figure showing the area verse time for each MCS - - Input:: - finalMCCList: a list of list of strings representing the list of nodes representing a MCC - - Output:: - None - - ''' - timeList =[] - count=1 - imgFilename='' - minArea=10000.0 - maxArea=0.0 - eachNode={} - - #for each node in the list, get the area information from the dictionary - #in the graph and calculate the area - - if finalMCCList: - for eachMCC in finalMCCList: - #get the info from the node - for node in eachMCC: - eachNode=thisDict(node) - timeList.append(eachNode['cloudElementTime']) - - if eachNode['cloudElementArea'] < minArea: - minArea = eachNode['cloudElementArea'] - if eachNode['cloudElementArea'] > maxArea: - maxArea = eachNode['cloudElementArea'] - - - #sort and remove duplicates - timeList=list(set(timeList)) - timeList.sort() - tdelta = timeList[1] - timeList[0] - starttime = timeList[0]-tdelta - endtime = timeList[-1]+tdelta - timeList.insert(0, starttime) - timeList.append(endtime) - - #plot info - plt.close('all') - title = 'Area distribution of the MCC over somewhere' - fig=plt.figure(facecolor='white', figsize=(18,10)) #figsize=(10,8))#figsize=(16,12)) - fig,ax = plt.subplots(1, facecolor='white', figsize=(10,10)) - - #the data - for node in eachMCC: #for eachNode in eachMCC: - eachNode=thisDict(node) - if eachNode['cloudElementArea'] < 80000 : #2400.00: - ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'bo', markersize=10) - elif eachNode['cloudElementArea'] >= 80000.00 and eachNode['cloudElementArea'] < 160000.00: - ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'yo',markersize=20) - else: - ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'ro',markersize=30) - - #axes and labels - maxArea += 1000.00 - ax.set_xlim(starttime,endtime) - ax.set_ylim(minArea,maxArea) - ax.set_ylabel('Area in km^2', fontsize=12) - ax.set_title(title) - ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d%H:%M:%S') - fig.autofmt_xdate() - - plt.subplots_adjust(bottom=0.2) - - imgFilename = MAINDIRECTORY+'/images/'+ str(count)+'MCS.gif' - plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) - - #if time in not already in the time list, append it - timeList=[] - count += 1 - return + ''' + Purpose:: + To create a figure showing the area verse time for each MCS + + Input:: + finalMCCList: a list of list of strings representing the list of nodes representing a MCC + + Output:: + None + + ''' + timeList =[] + count=1 + imgFilename='' + minArea=10000.0 + maxArea=0.0 + eachNode={} + + #for each node in the list, get the area information from the dictionary + #in the graph and calculate the area + + if finalMCCList: + for eachMCC in finalMCCList: + #get the info from the node + for node in eachMCC: + eachNode=thisDict(node) + timeList.append(eachNode['cloudElementTime']) + + if eachNode['cloudElementArea'] < minArea: + minArea = eachNode['cloudElementArea'] + if eachNode['cloudElementArea'] > maxArea: + maxArea = eachNode['cloudElementArea'] + + + #sort and remove duplicates + timeList=list(set(timeList)) + timeList.sort() + tdelta = timeList[1] - timeList[0] + starttime = timeList[0]-tdelta + endtime = timeList[-1]+tdelta + timeList.insert(0, starttime) + timeList.append(endtime) + + #plot info + plt.close('all') + title = 'Area distribution of the MCC over somewhere' + fig=plt.figure(facecolor='white', figsize=(18,10)) #figsize=(10,8))#figsize=(16,12)) + fig,ax = plt.subplots(1, facecolor='white', figsize=(10,10)) + + #the data + for node in eachMCC: #for eachNode in eachMCC: + eachNode=thisDict(node) + if eachNode['cloudElementArea'] < 80000 : #2400.00: + ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'bo', markersize=10) + elif eachNode['cloudElementArea'] >= 80000.00 and eachNode['cloudElementArea'] < 160000.00: + ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'yo',markersize=20) + else: + ax.plot(eachNode['cloudElementTime'], eachNode['cloudElementArea'],'ro',markersize=30) + + #axes and labels + maxArea += 1000.00 + ax.set_xlim(starttime,endtime) + ax.set_ylim(minArea,maxArea) + ax.set_ylabel('Area in km^2', fontsize=12) + ax.set_title(title) + ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d%H:%M:%S') + fig.autofmt_xdate() + + plt.subplots_adjust(bottom=0.2) + + imgFilename = MAINDIRECTORY+'/images/'+ str(count)+'MCS.gif' + plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) + + #if time in not already in the time list, append it + timeList=[] + count += 1 + return #****************************************************************** def displayPrecip(finalMCCList): - ''' - Purpose:: - To create a figure showing the precip rate verse time for each MCS - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - - Output:: None - - ''' - timeList =[] - oriTimeList=[] - colorBarTime =[] - count=1 - imgFilename='' - TRMMprecipDis =[] - percentagePrecipitating = []#0.0 - CEArea=[] - nodes=[] - xy=[] - x=[] - y=[] - precip = [] - partialArea =[] - totalSize=0.0 - - firstTime = True - xStart =0.0 - yStart = 0.0 - - num_bins = 5 - - - #for each node in the list, get the area information from the dictionary - #in the graph and calculate the area - - if finalMCCList: - for eachMCC in finalMCCList: - #get the info from the node - for node in eachMCC: - eachNode=thisDict(node) - if firstTime == True: - xStart = eachNode['cloudElementCenter'][1]#lon - yStart = eachNode['cloudElementCenter'][0]#lat - timeList.append(eachNode['cloudElementTime']) - percentagePrecipitating.append((eachNode['TRMMArea']/eachNode['cloudElementArea'])*100.0) - CEArea.append(eachNode['cloudElementArea']) - nodes.append(eachNode['uniqueID']) - # print eachNode['uniqueID'], eachNode['cloudElementCenter'][1], eachNode['cloudElementCenter'][0] - x.append(eachNode['cloudElementCenter'][1])#-xStart) - y.append(eachNode['cloudElementCenter'][0])#-yStart) - - firstTime= False - - #convert the timeList[] to list of floats - for i in xrange(len(timeList)): #oriTimeList: - colorBarTime.append(time.mktime(timeList[i].timetuple())) - - totalSize = sum(CEArea) - partialArea = [(a/totalSize)*30000 for a in CEArea] - - # print "x ", x - # print "y ", y - - #plot info - plt.close('all') - - title = 'Precipitation distribution of the MCS ' - fig,ax = plt.subplots(1, facecolor='white', figsize=(20,7)) - - cmap = plt.jet - ax.scatter(x, y, s=partialArea, c= colorBarTime, edgecolors='none', marker='o', cmap =cmap) - colorBarTime=[] - colorBarTime =list(set(timeList)) - colorBarTime.sort() - cb = colorbar_index(ncolors=len(colorBarTime), nlabels=colorBarTime, cmap = cmap) - - #axes and labels - ax.set_xlabel('Degrees Longtude', fontsize=12) - ax.set_ylabel('Degrees Latitude', fontsize=12) - ax.set_title(title) - ax.grid(True) - plt.subplots_adjust(bottom=0.2) - - for i, txt in enumerate(nodes): - if CEArea[i] >= 2400.00: - ax.annotate('%d'%percentagePrecipitating[i]+'%', (x[i],y[i])) - precip=[] - - imgFilename = MAINDIRECTORY+'/images/MCSprecip'+ str(count)+'.gif' - plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) - - #reset for next image - timeList=[] - percentagePrecipitating =[] - CEArea =[] - x=[] - y=[] - colorBarTime=[] - nodes=[] - precip=[] - count += 1 - firstTime = True - return + ''' + Purpose:: + To create a figure showing the precip rate verse time for each MCS + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + + Output:: None + + ''' + timeList =[] + oriTimeList=[] + colorBarTime =[] + count=1 + imgFilename='' + TRMMprecipDis =[] + percentagePrecipitating = []#0.0 + CEArea=[] + nodes=[] + xy=[] + x=[] + y=[] + precip = [] + partialArea =[] + totalSize=0.0 + + firstTime = True + xStart =0.0 + yStart = 0.0 + + num_bins = 5 + + + #for each node in the list, get the area information from the dictionary + #in the graph and calculate the area + + if finalMCCList: + for eachMCC in finalMCCList: + #get the info from the node + for node in eachMCC: + eachNode=thisDict(node) + if firstTime == True: + xStart = eachNode['cloudElementCenter'][1]#lon + yStart = eachNode['cloudElementCenter'][0]#lat + timeList.append(eachNode['cloudElementTime']) + percentagePrecipitating.append((eachNode['TRMMArea']/eachNode['cloudElementArea'])*100.0) + CEArea.append(eachNode['cloudElementArea']) + nodes.append(eachNode['uniqueID']) + # print eachNode['uniqueID'], eachNode['cloudElementCenter'][1], eachNode['cloudElementCenter'][0] + x.append(eachNode['cloudElementCenter'][1])#-xStart) + y.append(eachNode['cloudElementCenter'][0])#-yStart) + + firstTime= False + + #convert the timeList[] to list of floats + for i in xrange(len(timeList)): #oriTimeList: + colorBarTime.append(time.mktime(timeList[i].timetuple())) + + totalSize = sum(CEArea) + partialArea = [(a/totalSize)*30000 for a in CEArea] + + # print "x ", x + # print "y ", y + + #plot info + plt.close('all') + + title = 'Precipitation distribution of the MCS ' + fig,ax = plt.subplots(1, facecolor='white', figsize=(20,7)) + + cmap = plt.jet + ax.scatter(x, y, s=partialArea, c= colorBarTime, edgecolors='none', marker='o', cmap =cmap) + colorBarTime=[] + colorBarTime =list(set(timeList)) + colorBarTime.sort() + cb = colorbar_index(ncolors=len(colorBarTime), nlabels=colorBarTime, cmap = cmap) + + #axes and labels + ax.set_xlabel('Degrees Longtude', fontsize=12) + ax.set_ylabel('Degrees Latitude', fontsize=12) + ax.set_title(title) + ax.grid(True) + plt.subplots_adjust(bottom=0.2) + + for i, txt in enumerate(nodes): + if CEArea[i] >= 2400.00: + ax.annotate('%d'%percentagePrecipitating[i]+'%', (x[i],y[i])) + precip=[] + + imgFilename = MAINDIRECTORY+'/images/MCSprecip'+ str(count)+'.gif' + plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) + + #reset for next image + timeList=[] + percentagePrecipitating =[] + CEArea =[] + x=[] + y=[] + colorBarTime=[] + nodes=[] + precip=[] + count += 1 + firstTime = True + return #****************************************************************** def plotPrecipHistograms(finalMCCList, num_bins=5): - ''' - Purpose:: - To create plots (histograms) of the each TRMMnetcdfCEs files - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - num_bins: an integer representing the number of bins - Output:: - plots - ''' - - precip =[] - imgFilename = " " - lastTime =" " - firstTime = True - MCScount = 0 - MSClen =0 - thisCount = 0 - totalPrecip=np.zeros((1,137,440)) - - #TODO: use try except block instead - if finalMCCList: - - for eachMCC in finalMCCList: - firstTime = True - MCScount +=1 - #totalPrecip=np.zeros((1,137,440)) - totalPrecip=np.zeros((1,413,412)) - - #get the info from the node - for node in eachMCC: - eachNode=thisDict(node) - thisTime = eachNode['cloudElementTime'] - MCSlen = len(eachMCC) - thisCount += 1 - - #this is the precipitation distribution plot from displayPrecip - - if eachNode['cloudElementArea'] >= 2400.0: - if (str(thisTime) != lastTime and lastTime != " ") or thisCount == MCSlen: - # plt.close('all') - # title = 'TRMM precipitation distribution for '+ str(thisTime) - - # fig,ax = plt.subplots(1, facecolor='white', figsize=(7,5)) - - # n,binsdg = np.histogram(precip, num_bins) - # wid = binsdg[1:] - binsdg[:-1] - # plt.bar(binsdg[:-1], n/float(len(precip)), width=wid) - - # #make percentage plot - # formatter = FuncFormatter(to_percent) - # plt.xlim(min(binsdg), max(binsdg)) - # ax.set_xticks(binsdg) - # ax.set_xlabel('Precipitation [mm]', fontsize=12) - # ax.set_ylabel('Area', fontsize=12) - # ax.set_title(title) - # # Set the formatter - # plt.gca().yaxis.set_major_formatter(formatter) - # plt.gca().xaxis.set_major_formatter(FormatStrFormatter('%0.0f')) - # imgFilename = MAINDIRECTORY+'/images/'+str(thisTime)+eachNode['uniqueID']+'TRMMMCS.gif' - - # plt.savefig(imgFilename, transparent=True) - data_names =['Precipitation [mm]','Area'] - plotter.draw_histogram(precip,data_names,imgFilename, num_bins) - precip =[] - - # ------ NETCDF File get info ------------------------------------ - thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/TRMM' + str(thisTime).replace(" ", "_") + eachNode['uniqueID'] +'.nc' - TRMMData = Dataset(thisFileName,'r', format='NETCDF4') - precipRate = TRMMData.variables['precipitation_Accumulation'][:,:,:] - CEprecipRate = precipRate[0,:,:] - TRMMData.close() - if firstTime==True: - totalPrecip=np.zeros((CEprecipRate.shape)) - - totalPrecip = np.add(totalPrecip, precipRate) - # ------ End NETCDF File ------------------------------------ - for index, value in np.ndenumerate(CEprecipRate): - if value != 0.0: - precip.append(value) - - lastTime = str(thisTime) - firstTime = False - else: - lastTime = str(thisTime) - firstTime = False - return + ''' + Purpose:: + To create plots (histograms) of the each TRMMnetcdfCEs files + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + num_bins: an integer representing the number of bins + Output:: + plots + ''' + + precip =[] + imgFilename = " " + lastTime =" " + firstTime = True + MCScount = 0 + MSClen =0 + thisCount = 0 + totalPrecip=np.zeros((1,137,440)) + + #TODO: use try except block instead + if finalMCCList: + + for eachMCC in finalMCCList: + firstTime = True + MCScount +=1 + #totalPrecip=np.zeros((1,137,440)) + totalPrecip=np.zeros((1,413,412)) + + #get the info from the node + for node in eachMCC: + eachNode=thisDict(node) + thisTime = eachNode['cloudElementTime'] + MCSlen = len(eachMCC) + thisCount += 1 + + #this is the precipitation distribution plot from displayPrecip + + if eachNode['cloudElementArea'] >= 2400.0: + if (str(thisTime) != lastTime and lastTime != " ") or thisCount == MCSlen: + # plt.close('all') + # title = 'TRMM precipitation distribution for '+ str(thisTime) + + # fig,ax = plt.subplots(1, facecolor='white', figsize=(7,5)) + + # n,binsdg = np.histogram(precip, num_bins) + # wid = binsdg[1:] - binsdg[:-1] + # plt.bar(binsdg[:-1], n/float(len(precip)), width=wid) + + # #make percentage plot + # formatter = FuncFormatter(to_percent) + # plt.xlim(min(binsdg), max(binsdg)) + # ax.set_xticks(binsdg) + # ax.set_xlabel('Precipitation [mm]', fontsize=12) + # ax.set_ylabel('Area', fontsize=12) + # ax.set_title(title) + # # Set the formatter + # plt.gca().yaxis.set_major_formatter(formatter) + # plt.gca().xaxis.set_major_formatter(FormatStrFormatter('%0.0f')) + # imgFilename = MAINDIRECTORY+'/images/'+str(thisTime)+eachNode['uniqueID']+'TRMMMCS.gif' + + # plt.savefig(imgFilename, transparent=True) + data_names =['Precipitation [mm]','Area'] + plotter.draw_histogram(precip,data_names,imgFilename, num_bins) + precip =[] + + # ------ NETCDF File get info ------------------------------------ + thisFileName = MAINDIRECTORY+'/TRMMnetcdfCEs/TRMM' + str(thisTime).replace(" ", "_") + eachNode['uniqueID'] +'.nc' + TRMMData = Dataset(thisFileName,'r', format='NETCDF4') + precipRate = TRMMData.variables['precipitation_Accumulation'][:,:,:] + CEprecipRate = precipRate[0,:,:] + TRMMData.close() + if firstTime==True: + totalPrecip=np.zeros((CEprecipRate.shape)) + + totalPrecip = np.add(totalPrecip, precipRate) + # ------ End NETCDF File ------------------------------------ + for index, value in np.ndenumerate(CEprecipRate): + if value != 0.0: + precip.append(value) + + lastTime = str(thisTime) + firstTime = False + else: + lastTime = str(thisTime) + firstTime = False + return #****************************************************************** def plotAccTRMM (finalMCCList): - ''' - Purpose:: - (1) generate a file with the accumulated precipiation for the MCS - (2) generate the appropriate image - TODO: NB: as the domain changes, will need to change XDEF and YDEF by hand to accomodate the new domain - TODO: look into getting the info from the NETCDF file - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + ''' + Purpose:: + (1) generate a file with the accumulated precipiation for the MCS + (2) generate the appropriate image + TODO: NB: as the domain changes, will need to change XDEF and YDEF by hand to accomodate the new domain + TODO: look into getting the info from the NETCDF file + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - Output:: - a netcdf file containing the accumulated precip - a 2D matlablibplot - ''' - os.chdir((MAINDIRECTORY+'/TRMMnetcdfCEs')) - fname ='' - imgFilename = '' - firstPartName = '' - firstTime = True - replaceExpXDef = '' - - #generate the file name using MCCTimes - #if the file name exists, add it to the accTRMM file - for path in finalMCCList: - for eachNode in path: - thisNode = thisDict(eachNode) - fname = 'TRMM'+ str(thisNode['cloudElementTime']).replace(" ", "_") + thisNode['uniqueID'] +'.nc' - - if os.path.isfile(fname): - #open NetCDF file add info to the accu - #print "opening TRMM file ", fname - TRMMCEData = Dataset(fname,'r',format='NETCDF4') - precipRate = TRMMCEData.variables['precipitation_Accumulation'][:] - lats = TRMMCEData.variables['latitude'][:] - lons = TRMMCEData.variables['longitude'][:] - LONTRMM, LATTRMM = np.meshgrid(lons,lats) - nygrdTRMM = len(LATTRMM[:,0]) - nxgrdTRMM = len(LONTRMM[0,:]) - precipRate = ma.masked_array(precipRate, mask=(precipRate < 0.0)) - TRMMCEData.close() - - if firstTime == True: - firstPartName = str(thisNode['cloudElementTime']).replace(" ", "_")+'-' - accuPrecipRate = ma.zeros((precipRate.shape)) - firstTime = False - - accuPrecipRate += precipRate - - imgFilename = MAINDIRECTORY+'/images/MCSaccu'+firstPartName+str(thisNode['cloudElementTime']).replace(" ", "_")+'.gif' - - #create new netCDF file - accuTRMMFile = MAINDIRECTORY+'/TRMMnetcdfCEs/accu'+firstPartName+str(thisNode['cloudElementTime']).replace(" ", "_")+'.nc' - #write the file - accuTRMMData = Dataset(accuTRMMFile, 'w', format='NETCDF4') - accuTRMMData.description = 'Accumulated precipitation data' - accuTRMMData.calendar = 'standard' - accuTRMMData.conventions = 'COARDS' - # dimensions - accuTRMMData.createDimension('time', None) - accuTRMMData.createDimension('lat', nygrdTRMM) - accuTRMMData.createDimension('lon', nxgrdTRMM) - - # variables - TRMMprecip = ('time','lat', 'lon',) - times = accuTRMMData.createVariable('time', 'f8', ('time',)) - times.units = 'hours since '+ str(thisNode['cloudElementTime']).replace(" ", "_")[:-6] - latitude = accuTRMMData.createVariable('latitude', 'f8', ('lat',)) - longitude = accuTRMMData.createVariable('longitude', 'f8', ('lon',)) - rainFallacc = accuTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip) - rainFallacc.units = 'mm' - - longitude[:] = LONTRMM[0,:] - longitude.units = "degrees_east" - longitude.long_name = "Longitude" - - latitude[:] = LATTRMM[:,0] - latitude.units = "degrees_north" - latitude.long_name ="Latitude" - - rainFallacc[:] = accuPrecipRate[:] - - accuTRMMData.close() - - #do plot - plotTitle = 'TRMM Accumulated [mm]' - createPrecipPlot(np.squeeze(accuPrecipRate, axis=0), LATTRMM[:,0], LONTRMM[0,:], plotTitle,imgFilename) - - return + Output:: + a netcdf file containing the accumulated precip + a 2D matlablibplot + ''' + os.chdir((MAINDIRECTORY+'/TRMMnetcdfCEs')) + fname ='' + imgFilename = '' + firstPartName = '' + firstTime = True + replaceExpXDef = '' + + #generate the file name using MCCTimes + #if the file name exists, add it to the accTRMM file + for path in finalMCCList: + for eachNode in path: + thisNode = thisDict(eachNode) + fname = 'TRMM'+ str(thisNode['cloudElementTime']).replace(" ", "_") + thisNode['uniqueID'] +'.nc' + + if os.path.isfile(fname): + #open NetCDF file add info to the accu + #print "opening TRMM file ", fname + TRMMCEData = Dataset(fname,'r',format='NETCDF4') + precipRate = TRMMCEData.variables['precipitation_Accumulation'][:] + lats = TRMMCEData.variables['latitude'][:] + lons = TRMMCEData.variables['longitude'][:] + LONTRMM, LATTRMM = np.meshgrid(lons,lats) + nygrdTRMM = len(LATTRMM[:,0]) + nxgrdTRMM = len(LONTRMM[0,:]) + precipRate = ma.masked_array(precipRate, mask=(precipRate < 0.0)) + TRMMCEData.close() + + if firstTime == True: + firstPartName = str(thisNode['cloudElementTime']).replace(" ", "_")+'-' + accuPrecipRate = ma.zeros((precipRate.shape)) + firstTime = False + + accuPrecipRate += precipRate + + imgFilename = MAINDIRECTORY+'/images/MCSaccu'+firstPartName+str(thisNode['cloudElementTime']).replace(" ", "_")+'.gif' + + #create new netCDF file + accuTRMMFile = MAINDIRECTORY+'/TRMMnetcdfCEs/accu'+firstPartName+str(thisNode['cloudElementTime']).replace(" ", "_")+'.nc' + #write the file + accuTRMMData = Dataset(accuTRMMFile, 'w', format='NETCDF4') + accuTRMMData.description = 'Accumulated precipitation data' + accuTRMMData.calendar = 'standard' + accuTRMMData.conventions = 'COARDS' + # dimensions + accuTRMMData.createDimension('time', None) + accuTRMMData.createDimension('lat', nygrdTRMM) + accuTRMMData.createDimension('lon', nxgrdTRMM) + + # variables + TRMMprecip = ('time','lat', 'lon',) + times = accuTRMMData.createVariable('time', 'f8', ('time',)) + times.units = 'hours since '+ str(thisNode['cloudElementTime']).replace(" ", "_")[:-6] + latitude = accuTRMMData.createVariable('latitude', 'f8', ('lat',)) + longitude = accuTRMMData.createVariable('longitude', 'f8', ('lon',)) + rainFallacc = accuTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip) + rainFallacc.units = 'mm' + + longitude[:] = LONTRMM[0,:] + longitude.units = "degrees_east" + longitude.long_name = "Longitude" + + latitude[:] = LATTRMM[:,0] + latitude.units = "degrees_north" + latitude.long_name ="Latitude" + + rainFallacc[:] = accuPrecipRate[:] + + accuTRMMData.close() + + #do plot + plotTitle = 'TRMM Accumulated [mm]' + createPrecipPlot(np.squeeze(accuPrecipRate, axis=0), LATTRMM[:,0], LONTRMM[0,:], plotTitle,imgFilename) + + return #****************************************************************** def plotAccuInTimeRange(starttime, endtime): - ''' - Purpose:: - Create accumulated precip plot within a time range given using all CEs - - Input:: - starttime: a string representing the time to start the accumulations format yyyy-mm-dd_hh:mm:ss - endtime: a string representing the time to end the accumulations format yyyy-mm-dd_hh:mm:ss - - Output:: - a netcdf file containing the accumulated precip for specified times - a 2D matlablibplot - - ''' - - os.chdir((MAINDIRECTORY+'/TRMMnetcdfCEs/')) - - imgFilename = '' - firstPartName = '' - firstTime = True - - fileList = [] - sTime = datetime.strptime(starttime.replace("_"," "),'%Y-%m-%d %H:%M:%S') - eTime = datetime.strptime(endtime.replace("_"," "),'%Y-%m-%d %H:%M:%S') - thisTime = sTime - - while thisTime <= eTime: - fileList = filter(os.path.isfile, glob.glob(('TRMM'+ str(thisTime).replace(" ", "_") + '*' +'.nc'))) - for fname in fileList: - TRMMCEData = Dataset(fname,'r',format='NETCDF4') - precipRate = TRMMCEData.variables['precipitation_Accumulation'][:] - lats = TRMMCEData.variables['latitude'][:] - lons = TRMMCEData.variables['longitude'][:] - LONTRMM, LATTRMM = np.meshgrid(lons,lats) - nygrdTRMM = len(LATTRMM[:,0]) - nxgrdTRMM = len(LONTRMM[0,:]) - precipRate = ma.masked_array(precipRate, mask=(precipRate < 0.0)) - TRMMCEData.close() - - if firstTime == True: - accuPrecipRate = ma.zeros((precipRate.shape)) - firstTime = False - - accuPrecipRate += precipRate - - #increment the time - thisTime +=timedelta(hours=TRES) - - #create new netCDF file - accuTRMMFile = MAINDIRECTORY+'/TRMMnetcdfCEs/accu'+starttime+'-'+endtime+'.nc' - print "accuTRMMFile ", accuTRMMFile - #write the file - accuTRMMData = Dataset(accuTRMMFile, 'w', format='NETCDF4') - accuTRMMData.description = 'Accumulated precipitation data' - accuTRMMData.calendar = 'standard' - accuTRMMData.conventions = 'COARDS' - # dimensions - accuTRMMData.createDimension('time', None) - accuTRMMData.createDimension('lat', nygrdTRMM) - accuTRMMData.createDimension('lon', nxgrdTRMM) - - # variables - TRMMprecip = ('time','lat', 'lon',) - times = accuTRMMData.createVariable('time', 'f8', ('time',)) - times.units = 'hours since '+ starttime[:-6] - latitude = accuTRMMData.createVariable('latitude', 'f8', ('lat',)) - longitude = accuTRMMData.createVariable('longitude', 'f8', ('lon',)) - rainFallacc = accuTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip) - rainFallacc.units = 'mm' - - longitude[:] = LONTRMM[0,:] - longitude.units = "degrees_east" - longitude.long_name = "Longitude" - - latitude[:] = LATTRMM[:,0] - latitude.units = "degrees_north" - latitude.long_name ="Latitude" - - rainFallacc[:] = accuPrecipRate[:] - - accuTRMMData.close() - - #plot the stuff - imgFilename = MAINDIRECTORY+'/images/accu'+starttime+'-'+endtime+'.gif' - plotTitle = "TRMM Accumulated Precipitation [mm] "+starttime+'-'+endtime - createPrecipPlot(np.squeeze(accuPrecipRate, axis=0), LATTRMM[:,0], LONTRMM[0,:], plotTitle,imgFilename) - - return + ''' + Purpose:: + Create accumulated precip plot within a time range given using all CEs + + Input:: + starttime: a string representing the time to start the accumulations format yyyy-mm-dd_hh:mm:ss + endtime: a string representing the time to end the accumulations format yyyy-mm-dd_hh:mm:ss + + Output:: + a netcdf file containing the accumulated precip for specified times + a 2D matlablibplot + + ''' + + os.chdir((MAINDIRECTORY+'/TRMMnetcdfCEs/')) + + imgFilename = '' + firstPartName = '' + firstTime = True + + fileList = [] + sTime = datetime.strptime(starttime.replace("_"," "),'%Y-%m-%d %H:%M:%S') + eTime = datetime.strptime(endtime.replace("_"," "),'%Y-%m-%d %H:%M:%S') + thisTime = sTime + + while thisTime <= eTime: + fileList = filter(os.path.isfile, glob.glob(('TRMM'+ str(thisTime).replace(" ", "_") + '*' +'.nc'))) + for fname in fileList: + TRMMCEData = Dataset(fname,'r',format='NETCDF4') + precipRate = TRMMCEData.variables['precipitation_Accumulation'][:] + lats = TRMMCEData.variables['latitude'][:] + lons = TRMMCEData.variables['longitude'][:] + LONTRMM, LATTRMM = np.meshgrid(lons,lats) + nygrdTRMM = len(LATTRMM[:,0]) + nxgrdTRMM = len(LONTRMM[0,:]) + precipRate = ma.masked_array(precipRate, mask=(precipRate < 0.0)) + TRMMCEData.close() + + if firstTime == True: + accuPrecipRate = ma.zeros((precipRate.shape)) + firstTime = False + + accuPrecipRate += precipRate + + #increment the time + thisTime +=timedelta(hours=TRES) + + #create new netCDF file + accuTRMMFile = MAINDIRECTORY+'/TRMMnetcdfCEs/accu'+starttime+'-'+endtime+'.nc' + print "accuTRMMFile ", accuTRMMFile + #write the file + accuTRMMData = Dataset(accuTRMMFile, 'w', format='NETCDF4') + accuTRMMData.description = 'Accumulated precipitation data' + accuTRMMData.calendar = 'standard' + accuTRMMData.conventions = 'COARDS' + # dimensions + accuTRMMData.createDimension('time', None) + accuTRMMData.createDimension('lat', nygrdTRMM) + accuTRMMData.createDimension('lon', nxgrdTRMM) + + # variables + TRMMprecip = ('time','lat', 'lon',) + times = accuTRMMData.createVariable('time', 'f8', ('time',)) + times.units = 'hours since '+ starttime[:-6] + latitude = accuTRMMData.createVariable('latitude', 'f8', ('lat',)) + longitude = accuTRMMData.createVariable('longitude', 'f8', ('lon',)) + rainFallacc = accuTRMMData.createVariable('precipitation_Accumulation', 'f8',TRMMprecip) + rainFallacc.units = 'mm' + + longitude[:] = LONTRMM[0,:] + longitude.units = "degrees_east" + longitude.long_name = "Longitude" + + latitude[:] = LATTRMM[:,0] + latitude.units = "degrees_north" + latitude.long_name ="Latitude" + + rainFallacc[:] = accuPrecipRate[:] + + accuTRMMData.close() + + #plot the stuff + imgFilename = MAINDIRECTORY+'/images/accu'+starttime+'-'+endtime+'.gif' + plotTitle = "TRMM Accumulated Precipitation [mm] "+starttime+'-'+endtime + createPrecipPlot(np.squeeze(accuPrecipRate, axis=0), LATTRMM[:,0], LONTRMM[0,:], plotTitle,imgFilename) + + return #****************************************************************** def createPrecipPlot(dataset, lats, lons, plotTitle,imgFilename): - ''' - Purpose:: - To create the actual plots for precip data only - - Input:: - dataset: a 2d numpy (lon,lat) dataset of the precip data to be plotted - domainDict: a dictionary with the domain (lons and lats) details required - plotTitle: a string representing the title for the plot - imgFilename: a string representing the string (including path) of where the plot is to be showed - - Output:: - A 2D plot with precipitation using the NWS precipitation colormap (from matlplotlib Basemap) - - ''' - - fig,ax = plt.subplots(1, facecolor='white', figsize=(8.5,11.)) #, dpi=300) - latmin = np.min(lats) - latmax = np.max(lats) - lonmin = np.min(lons) - lonmax = np.max(lons) - - m = Basemap(projection = 'merc', llcrnrlon = lonmin, urcrnrlon = lonmax, llcrnrlat = latmin, urcrnrlat = latmax, resolution = 'l', ax=ax) - m.drawcoastlines(linewidth = 1) - m.drawcountries(linewidth = 0.75) - #draw meridians - meridians = np.arange(180.,360.,5.) - m.drawmeridians(meridians, labels=[0,0,0,1], linewidth=0.75, fontsize=10) - #draw parallels - parallels = np.arange(0.,90,5.) - m.drawparallels(parallels, labels=[1,0,0,0], linewidth=0.75, fontsize=10) - - - #projecting on to the correct map grid - longitudes, latitudes = m.makegrid(lons.shape[0],lats.shape[0]) - x,y = m(longitudes, latitudes) - - # draw filled contours. - clevs = [0,1,2.5,5,7.5,10,15,20,30,40,50,70,100,150,200,250,300,400,500,600,750] - - #actually print the map - cs = m.contourf(x,y,dataset,clevs,cmap=cmbm.s3pcpn) - - #add colorbar - cbar = m.colorbar(cs, location = 'bottom', pad = "10%") - cbar.set_label('mm') - - plt.title(plotTitle) - - plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) - return + ''' + Purpose:: + To create the actual plots for precip data only + + Input:: + dataset: a 2d numpy (lon,lat) dataset of the precip data to be plotted + domainDict: a dictionary with the domain (lons and lats) details required + plotTitle: a string representing the title for the plot + imgFilename: a string representing the string (including path) of where the plot is to be showed + + Output:: + A 2D plot with precipitation using the NWS precipitation colormap (from matlplotlib Basemap) + + ''' + + fig,ax = plt.subplots(1, facecolor='white', figsize=(8.5,11.)) #, dpi=300) + latmin = np.min(lats) + latmax = np.max(lats) + lonmin = np.min(lons) + lonmax = np.max(lons) + + m = Basemap(projection = 'merc', llcrnrlon = lonmin, urcrnrlon = lonmax, llcrnrlat = latmin, urcrnrlat = latmax, resolution = 'l', ax=ax) + m.drawcoastlines(linewidth = 1) + m.drawcountries(linewidth = 0.75) + #draw meridians + meridians = np.arange(180.,360.,5.) + m.drawmeridians(meridians, labels=[0,0,0,1], linewidth=0.75, fontsize=10) + #draw parallels + parallels = np.arange(0.,90,5.) + m.drawparallels(parallels, labels=[1,0,0,0], linewidth=0.75, fontsize=10) + + + #projecting on to the correct map grid + longitudes, latitudes = m.makegrid(lons.shape[0],lats.shape[0]) + x,y = m(longitudes, latitudes) + + # draw filled contours. + clevs = [0,1,2.5,5,7.5,10,15,20,30,40,50,70,100,150,200,250,300,400,500,600,750] + + #actually print the map + cs = m.contourf(x,y,dataset,clevs,cmap=cmbm.s3pcpn) + + #add colorbar + cbar = m.colorbar(cs, location = 'bottom', pad = "10%") + cbar.set_label('mm') + + plt.title(plotTitle) + + plt.savefig(imgFilename, facecolor=fig.get_facecolor(), transparent=True) + return #****************************************************************** def createTextFile(finalMCCList, identifier): - ''' - Purpose:: - Create a text file with information about the MCS - This function is expected to be especially of use regarding long term record checks - - Input:: - finalMCCList: a list of dictionaries representing a list of nodes representing a MCC - identifier: an integer representing the type of list that has been entered...this is for creating file purposes - 1 - MCCList; 2- MCSList - - Output:: - a user readable text file with all information about each MCS - a user readable text file with the summary of the MCS - - Assumptions:: - ''' - - durations=0.0 - startTimes =[] - endTimes=[] - averagePropagationSpeed = 0.0 - speedCounter = 0 - maxArea =0.0 - amax = 0.0 - avgMaxArea =[] - maxAreaCounter =0.0 - maxAreaTime='' - eccentricity = 0.0 - firstTime = True - matureFlag = True - timeMCSMatures='' - maxCEprecipRate = 0.0 - minCEprecipRate = 0.0 - averageArea = 0.0 - averageAreaCounter = 0 - durationOfMatureMCC = 0 - avgMaxPrecipRate = 0.0 - avgMaxPrecipRateCounter = 0 - avgMinPrecipRate = 0.0 - avgMinPrecipRateCounter = 0 - CEspeed = 0.0 - MCSspeed = 0.0 - MCSspeedCounter = 0 - MCSPrecipTotal = 0.0 - avgMCSPrecipTotalCounter = 0 - bigPtotal = 0.0 - bigPtotalCounter = 0 - allPropagationSpeeds =[] - averageAreas =[] - areaAvg = 0.0 - avgPrecipTotal = 0.0 - avgPrecipTotalCounter = 0 - avgMaxMCSPrecipRate = 0.0 - avgMaxMCSPrecipRateCounter = 0 - avgMinMCSPrecipRate = 0.0 - avgMinMCSPrecipRateCounter = 0 - minMax =[] - avgPrecipArea = [] - location =[] - avgPrecipAreaPercent = 0.0 - precipArea = 0.0 - precipAreaPercent = 0.0 - precipPercent =[] - precipCounter = 0 - precipAreaAvg = 0.0 - minSpeed = 0.0 - maxSpeed =0.0 - - if identifier == 1: - MCSUserFile = open((MAINDIRECTORY+'/textFiles/MCCsUserFile.txt'),'wb') - MCSSummaryFile = open((MAINDIRECTORY+'/textFiles/MCCSummary.txt'),'wb') - MCSPostFile = open((MAINDIRECTORY+'/textFiles/MCCPostPrecessing.txt'),'wb') - - if identifier == 2: - MCSUserFile = open((MAINDIRECTORY+'/textFiles/MCSsUserFile.txt'),'wb') - MCSSummaryFile = open((MAINDIRECTORY+'/textFiles/MCSSummary.txt'),'wb') - MCSPostFile = open((MAINDIRECTORY+'/textFiles/MCSPostPrecessing.txt'),'wb') - - for eachPath in finalMCCList: - eachPath.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0], nodeID.split('CE')[1])) - MCSPostFile.write("\n %s" %eachPath) - - startTime = thisDict(eachPath[0])['cloudElementTime'] - endTime = thisDict(eachPath[-1])['cloudElementTime'] - duration = (endTime - startTime) + timedelta(hours=TRES) - - # convert datatime duration to seconds and add to the total for the average duration of all MCS in finalMCCList - durations += (duration.total_seconds()) - - #durations += duration - startTimes.append(startTime) - endTimes.append(endTime) - - #get the precip info - - for eachNode in eachPath: - - thisNode = thisDict(eachNode) - - #set first time min "fake" values - if firstTime == True: - minCEprecipRate = thisNode['CETRMMmin'] - avgMinMCSPrecipRate += thisNode['CETRMMmin'] - firstTime = False - - #calculate the speed - if thisNode['cloudElementArea'] >= OUTER_CLOUD_SHIELD_AREA: - averagePropagationSpeed += findCESpeed(eachNode, eachPath) - speedCounter +=1 - - #Amax: find max area - if thisNode['cloudElementArea'] > maxArea: - maxArea = thisNode['cloudElementArea'] - maxAreaTime = str(thisNode['cloudElementTime']) - eccentricity = thisNode['cloudElementEccentricity'] - location = thisNode['cloudElementCenter'] - - #determine the time the feature matures - if matureFlag == True: - timeMCSMatures = str(thisNode['cloudElementTime']) - matureFlag = False - - #find min and max precip rate - if thisNode['CETRMMmin'] < minCEprecipRate: - minCEprecipRate = thisNode['CETRMMmin'] - - if thisNode['CETRMMmax'] > maxCEprecipRate: - maxCEprecipRate = thisNode['CETRMMmax'] - - - #calculations for only the mature stage - if thisNode['nodeMCSIdentifier'] == 'M': - #calculate average area of the maturity feature only - averageArea += thisNode['cloudElementArea'] - averageAreaCounter += 1 - durationOfMatureMCC +=1 - avgMaxPrecipRate += thisNode['CETRMMmax'] - avgMaxPrecipRateCounter += 1 - avgMinPrecipRate += thisNode['CETRMMmin'] - avgMinPrecipRateCounter += 1 - avgMaxMCSPrecipRate += thisNode['CETRMMmax'] - avgMaxMCSPrecipRateCounter += 1 - avgMinMCSPrecipRate += thisNode['CETRMMmin'] - avgMinMCSPrecipRateCounter += 1 - - #the precip percentage (TRMM area/CE area) - if thisNode['cloudElementArea'] >= 0.0 and thisNode['TRMMArea'] >= 0.0: - precipArea += thisNode['TRMMArea'] - avgPrecipArea.append(thisNode['TRMMArea']) - avgPrecipAreaPercent += (thisNode['TRMMArea']/thisNode['cloudElementArea']) - precipPercent.append((thisNode['TRMMArea']/thisNode['cloudElementArea'])) - precipCounter += 1 - - #system speed for only mature stage - CEspeed = findCESpeed(eachNode,eachPath) - if CEspeed > 0.0 : - MCSspeed += CEspeed - MCSspeedCounter += 1 - - #find accumulated precip - if thisNode['cloudElementPrecipTotal'] > 0.0: - MCSPrecipTotal += thisNode['cloudElementPrecipTotal'] - avgMCSPrecipTotalCounter +=1 - - #A: calculate the average Area of the (mature) MCS - if averageAreaCounter > 0: # and averageAreaCounter > 0: - averageArea/= averageAreaCounter - averageAreas.append(averageArea) - - #v: MCS speed - if MCSspeedCounter > 0: # and MCSspeed > 0.0: - MCSspeed /= MCSspeedCounter - - #smallP_max: calculate the average max precip rate (mm/h) - if avgMaxMCSPrecipRateCounter > 0 : #and avgMaxPrecipRate > 0.0: - avgMaxMCSPrecipRate /= avgMaxMCSPrecipRateCounter - - #smallP_min: calculate the average min precip rate (mm/h) - if avgMinMCSPrecipRateCounter > 0 : #and avgMinPrecipRate > 0.0: - avgMinMCSPrecipRate /= avgMinMCSPrecipRateCounter - - #smallP_avg: calculate the average precipitation (mm hr-1) - if MCSPrecipTotal > 0.0: # and avgMCSPrecipTotalCounter> 0: - avgMCSPrecipTotal = MCSPrecipTotal/avgMCSPrecipTotalCounter - avgPrecipTotal += avgMCSPrecipTotal - avgPrecipTotalCounter += 1 - - #smallP_total = MCSPrecipTotal - #precip over the MCS lifetime prep for bigP_total - if MCSPrecipTotal > 0.0: - bigPtotal += MCSPrecipTotal - bigPtotalCounter += 1 - - if maxArea > 0.0: - avgMaxArea.append(maxArea) - maxAreaCounter += 1 - - #verage precipate area precentage (TRMM/CE area) - if precipCounter > 0: - avgPrecipAreaPercent /= precipCounter - precipArea /= precipCounter - - - #write stuff to file - MCSUserFile.write("\n\n\nStarttime is: %s " %(str(startTime))) - MCSUserFile.write("\nEndtime is: %s " %(str(endTime))) - MCSUserFile.write("\nLife duration is %s hrs" %(str(duration))) - MCSUserFile.write("\nTime of maturity is %s " %(timeMCSMatures)) - MCSUserFile.write("\nDuration mature stage is: %s " %durationOfMatureMCC*TRES) - MCSUserFile.write("\nAverage area is: %.4f km^2 " %(averageArea)) - MCSUserFile.write("\nMax area is: %.4f km^2 " %(maxArea)) - MCSUserFile.write("\nMax area time is: %s " %(maxAreaTime)) - MCSUserFile.write("\nEccentricity at max area is: %.4f " %(eccentricity)) - MCSUserFile.write("\nCenter (lat,lon) at max area is: %.2f\t%.2f" %(location[0], location[1])) - MCSUserFile.write("\nPropagation speed is %.4f " %(MCSspeed)) - MCSUserFile.write("\nMCS minimum preicip rate is %.4f mmh^-1" %(minCEprecipRate)) - MCSUserFile.write("\nMCS maximum preicip rate is %.4f mmh^-1" %(maxCEprecipRate)) - MCSUserFile.write("\nTotal precipitation during MCS is %.4f mm/lifetime" %(MCSPrecipTotal)) - MCSUserFile.write("\nAverage MCS precipitation is %.4f mm" %(avgMCSPrecipTotal)) - MCSUserFile.write("\nAverage MCS maximum precipitation is %.4f mmh^-1" %(avgMaxMCSPrecipRate)) - MCSUserFile.write("\nAverage MCS minimum precipitation is %.4f mmh^-1" %(avgMinMCSPrecipRate)) - MCSUserFile.write("\nAverage precipitation area is %.4f km^2 " %(precipArea)) - MCSUserFile.write("\nPrecipitation area percentage of mature system %.4f percent " %(avgPrecipAreaPercent*100)) - - - #append stuff to lists for the summary file - if MCSspeed > 0.0: - allPropagationSpeeds.append(MCSspeed) - averagePropagationSpeed += MCSspeed - speedCounter += 1 - - #reset vars for next MCS in list - aaveragePropagationSpeed = 0.0 - averageArea = 0.0 - averageAreaCounter = 0 - durationOfMatureMCC = 0 - MCSspeed = 0.0 - MCSspeedCounter = 0 - MCSPrecipTotal = 0.0 - avgMaxMCSPrecipRate =0.0 - avgMaxMCSPrecipRateCounter = 0 - avgMinMCSPrecipRate = 0.0 - avgMinMCSPrecipRateCounter = 0 - firstTime = True - matureFlag = True - avgMCSPrecipTotalCounter=0 - avgPrecipAreaPercent = 0.0 - precipArea = 0.0 - precipCounter = 0 - maxArea = 0.0 - maxAreaTime='' - eccentricity = 0.0 - timeMCSMatures='' - maxCEprecipRate = 0.0 - minCEprecipRate = 0.0 - location =[] - - #LD: average duration - if len(finalMCCList) > 1: - durations /= len(finalMCCList) - durations /= 3600.0 #convert to hours - - #A: average area - areaAvg = sum(averageAreas)/ len(finalMCCList) - #create histogram plot here - if len(averageAreas) > 1: - imgFilename = MAINDIRECTORY+'/images/averageAreas.gif' - plotter.draw_histogram(averageAreas, ["Average Area [km^2]", "Area [km^2]"],imgFilename,10) - - #Amax: average maximum area - if maxAreaCounter > 0.0: #and avgMaxArea > 0.0 : - amax = sum(avgMaxArea)/ maxAreaCounter - #create histogram plot here - if len(avgMaxArea) > 1: - imgFilename = MAINDIRECTORY+'/images/avgMaxArea.gif' - plotter.draw_histogram(avgMaxArea, ["Maximum Area [km^2]", "Area [km^2]"], imgFilename,10) - - #v_avg: calculate the average propagation speed - if speedCounter > 0 : # and averagePropagationSpeed > 0.0 - averagePropagationSpeed /= speedCounter - - #bigP_min: calculate the min rate in mature system - if avgMinPrecipRate > 0.0: # and avgMinPrecipRateCounter > 0.0: - avgMinPrecipRate /= avgMinPrecipRateCounter - - #bigP_max: calculate the max rate in mature system - if avgMinPrecipRateCounter > 0.0: #and avgMaxPrecipRate > 0.0: - avgMaxPrecipRate /= avgMaxPrecipRateCounter - - #bigP_avg: average total preicip rate mm/hr - if avgPrecipTotalCounter > 0.0: # and avgPrecipTotal > 0.0: - avgPrecipTotal /= avgPrecipTotalCounter - - #bigP_total: total precip rate mm/LD - if bigPtotalCounter > 0.0: #and bigPtotal > 0.0: - bigPtotal /= bigPtotalCounter - - #precipitation area percentage - if len(precipPercent) > 0: - precipAreaPercent = (sum(precipPercent)/len(precipPercent))*100.0 - - #average precipitation area - if len(avgPrecipArea) > 0: - precipAreaAvg = sum(avgPrecipArea)/len(avgPrecipArea) - if len(avgPrecipArea) > 1: - imgFilename = MAINDIRECTORY+'/images/avgPrecipArea.gif' - plotter.draw_histogram(avgPrecipArea, ["Average Rainfall Area [km^2]", "Area [km^2]"],imgFilename,10) - - - sTime = str(averageTime(startTimes)) - eTime = str(averageTime(endTimes)) - if len (allPropagationSpeeds) > 1: - maxSpeed = max(allPropagationSpeeds) - minSpeed = min(allPropagationSpeeds) - - #write stuff to the summary file - MCSSummaryFile.write("\nNumber of features is %d " %(len(finalMCCList))) - MCSSummaryFile.write("\nAverage duration is %.4f hrs " %(durations)) - MCSSummaryFile.write("\nAverage startTime is %s " %(sTime[-8:])) - MCSSummaryFile.write("\nAverage endTime is %s " %(eTime[-8:])) - MCSSummaryFile.write("\nAverage size is %.4f km^2 " %(areaAvg)) - MCSSummaryFile.write("\nAverage precipitation area is %.4f km^2 " %(precipAreaAvg)) - MCSSummaryFile.write("\nAverage maximum size is %.4f km^2 " %(amax)) - MCSSummaryFile.write("\nAverage propagation speed is %.4f ms^-1" %(averagePropagationSpeed)) - MCSSummaryFile.write("\nMaximum propagation speed is %.4f ms^-1 " %(maxSpeed)) - MCSSummaryFile.write("\nMinimum propagation speed is %.4f ms^-1 " %(minSpeed)) - MCSSummaryFile.write("\nAverage minimum precipitation rate is %.4f mmh^-1" %(avgMinPrecipRate)) - MCSSummaryFile.write("\nAverage maximum precipitation rate is %.4f mm h^-1" %(avgMaxPrecipRate)) - MCSSummaryFile.write("\nAverage precipitation is %.4f mm h^-1 " %(avgPrecipTotal)) - MCSSummaryFile.write("\nAverage total precipitation during MCSs is %.4f mm/LD " %(bigPtotal)) - MCSSummaryFile.write("\nAverage precipitation area percentage is %.4f percent " %(precipAreaPercent)) - - - MCSUserFile.close - MCSSummaryFile.close - MCSPostFile.close - return + ''' + Purpose:: + Create a text file with information about the MCS + This function is expected to be especially of use regarding long term record checks + + Input:: + finalMCCList: a list of dictionaries representing a list of nodes representing a MCC + identifier: an integer representing the type of list that has been entered...this is for creating file purposes + 1 - MCCList; 2- MCSList + + Output:: + a user readable text file with all information about each MCS + a user readable text file with the summary of the MCS + + Assumptions:: + ''' + + durations=0.0 + startTimes =[] + endTimes=[] + averagePropagationSpeed = 0.0 + speedCounter = 0 + maxArea =0.0 + amax = 0.0 + avgMaxArea =[] + maxAreaCounter =0.0 + maxAreaTime='' + eccentricity = 0.0 + firstTime = True + matureFlag = True + timeMCSMatures='' + maxCEprecipRate = 0.0 + minCEprecipRate = 0.0 + averageArea = 0.0 + averageAreaCounter = 0 + durationOfMatureMCC = 0 + avgMaxPrecipRate = 0.0 + avgMaxPrecipRateCounter = 0 + avgMinPrecipRate = 0.0 + avgMinPrecipRateCounter = 0 + CEspeed = 0.0 + MCSspeed = 0.0 + MCSspeedCounter = 0 + MCSPrecipTotal = 0.0 + avgMCSPrecipTotalCounter = 0 + bigPtotal = 0.0 + bigPtotalCounter = 0 + allPropagationSpeeds =[] + averageAreas =[] + areaAvg = 0.0 + avgPrecipTotal = 0.0 + avgPrecipTotalCounter = 0 + avgMaxMCSPrecipRate = 0.0 + avgMaxMCSPrecipRateCounter = 0 + avgMinMCSPrecipRate = 0.0 + avgMinMCSPrecipRateCounter = 0 + minMax =[] + avgPrecipArea = [] + location =[] + avgPrecipAreaPercent = 0.0 + precipArea = 0.0 + precipAreaPercent = 0.0 + precipPercent =[] + precipCounter = 0 + precipAreaAvg = 0.0 + minSpeed = 0.0 + maxSpeed =0.0 + + if identifier == 1: + MCSUserFile = open((MAINDIRECTORY+'/textFiles/MCCsUserFile.txt'),'wb') + MCSSummaryFile = open((MAINDIRECTORY+'/textFiles/MCCSummary.txt'),'wb') + MCSPostFile = open((MAINDIRECTORY+'/textFiles/MCCPostPrecessing.txt'),'wb') + + if identifier == 2: + MCSUserFile = open((MAINDIRECTORY+'/textFiles/MCSsUserFile.txt'),'wb') + MCSSummaryFile = open((MAINDIRECTORY+'/textFiles/MCSSummary.txt'),'wb') + MCSPostFile = open((MAINDIRECTORY+'/textFiles/MCSPostPrecessing.txt'),'wb') + + for eachPath in finalMCCList: + eachPath.sort(key=lambda nodeID:(len(nodeID.split('C')[0]), nodeID.split('C')[0], nodeID.split('CE')[1])) + MCSPostFile.write("\n %s" %eachPath) + + startTime = thisDict(eachPath[0])['cloudElementTime'] + endTime = thisDict(eachPath[-1])['cloudElementTime'] + duration = (endTime - startTime) + timedelta(hours=TRES) + + # convert datatime duration to seconds and add to the total for the average duration of all MCS in finalMCCList + durations += (duration.total_seconds()) + + #durations += duration + startTimes.append(startTime) + endTimes.append(endTime) + + #get the precip info + + for eachNode in eachPath: + + thisNode = thisDict(eachNode) + + #set first time min "fake" values + if firstTime == True: + minCEprecipRate = thisNode['CETRMMmin'] + avgMinMCSPrecipRate += thisNode['CETRMMmin'] + firstTime = False + + #calculate the speed + if thisNode['cloudElementArea'] >= OUTER_CLOUD_SHIELD_AREA: + averagePropagationSpeed += findCESpeed(eachNode, eachPath) + speedCounter +=1 + + #Amax: find max area + if thisNode['cloudElementArea'] > maxArea: + maxArea = thisNode['cloudElementArea'] + maxAreaTime = str(thisNode['cloudElementTime']) + eccentricity = thisNode['cloudElementEccentricity'] + location = thisNode['cloudElementCenter'] + + #determine the time the feature matures + if matureFlag == True: + timeMCSMatures = str(thisNode['cloudElementTime']) + matureFlag = False + + #find min and max precip rate + if thisNode['CETRMMmin'] < minCEprecipRate: + minCEprecipRate = thisNode['CETRMMmin'] + + if thisNode['CETRMMmax'] > maxCEprecipRate: + maxCEprecipRate = thisNode['CETRMMmax'] + + + #calculations for only the mature stage + if thisNode['nodeMCSIdentifier'] == 'M': + #calculate average area of the maturity feature only + averageArea += thisNode['cloudElementArea'] + averageAreaCounter += 1 + durationOfMatureMCC +=1 + avgMaxPrecipRate += thisNode['CETRMMmax'] + avgMaxPrecipRateCounter += 1 + avgMinPrecipRate += thisNode['CETRMMmin'] + avgMinPrecipRateCounter += 1 + avgMaxMCSPrecipRate += thisNode['CETRMMmax'] + avgMaxMCSPrecipRateCounter += 1 + avgMinMCSPrecipRate += thisNode['CETRMMmin'] + avgMinMCSPrecipRateCounter += 1 + + #the precip percentage (TRMM area/CE area) + if thisNode['cloudElementArea'] >= 0.0 and thisNode['TRMMArea'] >= 0.0: + precipArea += thisNode['TRMMArea'] + avgPrecipArea.append(thisNode['TRMMArea']) + avgPrecipAreaPercent += (thisNode['TRMMArea']/thisNode['cloudElementArea']) + precipPercent.append((thisNode['TRMMArea']/thisNode['cloudElementArea'])) + precipCounter += 1 + + #system speed for only mature stage + CEspeed = findCESpeed(eachNode,eachPath) + if CEspeed > 0.0 : + MCSspeed += CEspeed + MCSspeedCounter += 1 + + #find accumulated precip + if thisNode['cloudElementPrecipTotal'] > 0.0: + MCSPrecipTotal += thisNode['cloudElementPrecipTotal'] + avgMCSPrecipTotalCounter +=1 + + #A: calculate the average Area of the (mature) MCS + if averageAreaCounter > 0: # and averageAreaCounter > 0: + averageArea/= averageAreaCounter + averageAreas.append(averageArea) + + #v: MCS speed + if MCSspeedCounter > 0: # and MCSspeed > 0.0: + MCSspeed /= MCSspeedCounter + + #smallP_max: calculate the average max precip rate (mm/h) + if avgMaxMCSPrecipRateCounter > 0 : #and avgMaxPrecipRate > 0.0: + avgMaxMCSPrecipRate /= avgMaxMCSPrecipRateCounter + + #smallP_min: calculate the average min precip rate (mm/h) + if avgMinMCSPrecipRateCounter > 0 : #and avgMinPrecipRate > 0.0: + avgMinMCSPrecipRate /= avgMinMCSPrecipRateCounter + + #smallP_avg: calculate the average precipitation (mm hr-1) + if MCSPrecipTotal > 0.0: # and avgMCSPrecipTotalCounter> 0: + avgMCSPrecipTotal = MCSPrecipTotal/avgMCSPrecipTotalCounter + avgPrecipTotal += avgMCSPrecipTotal + avgPrecipTotalCounter += 1 + + #smallP_total = MCSPrecipTotal + #precip over the MCS lifetime prep for bigP_total + if MCSPrecipTotal > 0.0: + bigPtotal += MCSPrecipTotal + bigPtotalCounter += 1 + + if maxArea > 0.0: + avgMaxArea.append(maxArea) + maxAreaCounter += 1 + + #verage precipate area precentage (TRMM/CE area) + if precipCounter > 0: + avgPrecipAreaPercent /= precipCounter + precipArea /= precipCounter + + + #write stuff to file + MCSUserFile.write("\n\n\nStarttime is: %s " %(str(startTime))) + MCSUserFile.write("\nEndtime is: %s " %(str(endTime))) + MCSUserFile.write("\nLife duration is %s hrs" %(str(duration))) + MCSUserFile.write("\nTime of maturity is %s " %(timeMCSMatures)) + MCSUserFile.write("\nDuration mature stage is: %s " %durationOfMatureMCC*TRES) + MCSUserFile.write("\nAverage area is: %.4f km^2 " %(averageArea)) + MCSUserFile.write("\nMax area is: %.4f km^2 " %(maxArea)) + MCSUserFile.write("\nMax area time is: %s " %(maxAreaTime)) + MCSUserFile.write("\nEccentricity at max area is: %.4f " %(eccentricity)) + MCSUserFile.write("\nCenter (lat,lon) at max area is: %.2f\t%.2f" %(location[0], location[1])) + MCSUserFile.write("\nPropagation speed is %.4f " %(MCSspeed)) + MCSUserFile.write("\nMCS minimum preicip rate is %.4f mmh^-1" %(minCEprecipRate)) + MCSUserFile.write("\nMCS maximum preicip rate is %.4f mmh^-1" %(maxCEprecipRate)) + MCSUserFile.write("\nTotal precipitation during MCS is %.4f mm/lifetime" %(MCSPrecipTotal)) + MCSUserFile.write("\nAverage MCS precipitation is %.4f mm" %(avgMCSPrecipTotal)) + MCSUserFile.write("\nAverage MCS maximum precipitation is %.4f mmh^-1" %(avgMaxMCSPrecipRate)) + MCSUserFile.write("\nAverage MCS minimum precipitation is %.4f mmh^-1" %(avgMinMCSPrecipRate)) + MCSUserFile.write("\nAverage precipitation area is %.4f km^2 " %(precipArea)) + MCSUserFile.write("\nPrecipitation area percentage of mature system %.4f percent " %(avgPrecipAreaPercent*100)) + + + #append stuff to lists for the summary file + if MCSspeed > 0.0: + allPropagationSpeeds.append(MCSspeed) + averagePropagationSpeed += MCSspeed + speedCounter += 1 + + #reset vars for next MCS in list + aaveragePropagationSpeed = 0.0 + averageArea = 0.0 + averageAreaCounter = 0 + durationOfMatureMCC = 0 + MCSspeed = 0.0 + MCSspeedCounter = 0 + MCSPrecipTotal = 0.0 + avgMaxMCSPrecipRate =0.0 + avgMaxMCSPrecipRateCounter = 0 + avgMinMCSPrecipRate = 0.0 + avgMinMCSPrecipRateCounter = 0 + firstTime = True + matureFlag = True + avgMCSPrecipTotalCounter=0 + avgPrecipAreaPercent = 0.0 + precipArea = 0.0 + precipCounter = 0 + maxArea = 0.0 + maxAreaTime='' + eccentricity = 0.0 + timeMCSMatures='' + maxCEprecipRate = 0.0 + minCEprecipRate = 0.0 + location =[] + + #LD: average duration + if len(finalMCCList) > 1: + durations /= len(finalMCCList) + durations /= 3600.0 #convert to hours + + #A: average area + areaAvg = sum(averageAreas)/ len(finalMCCList) + #create histogram plot here + if len(averageAreas) > 1: + imgFilename = MAINDIRECTORY+'/images/averageAreas.gif' + plotter.draw_histogram(averageAreas, ["Average Area [km^2]", "Area [km^2]"],imgFilename,10) + + #Amax: average maximum area + if maxAreaCounter > 0.0: #and avgMaxArea > 0.0 : + amax = sum(avgMaxArea)/ maxAreaCounter + #create histogram plot here + if len(avgMaxArea) > 1: + imgFilename = MAINDIRECTORY+'/images/avgMaxArea.gif' + plotter.draw_histogram(avgMaxArea, ["Maximum Area [km^2]", "Area [km^2]"], imgFilename,10) + + #v_avg: calculate the average propagation speed + if speedCounter > 0 : # and averagePropagationSpeed > 0.0 + averagePropagationSpeed /= speedCounter + + #bigP_min: calculate the min rate in mature system + if avgMinPrecipRate > 0.0: # and avgMinPrecipRateCounter > 0.0: + avgMinPrecipRate /= avgMinPrecipRateCounter + + #bigP_max: calculate the max rate in mature system + if avgMinPrecipRateCounter > 0.0: #and avgMaxPrecipRate > 0.0: + avgMaxPrecipRate /= avgMaxPrecipRateCounter + + #bigP_avg: average total preicip rate mm/hr + if avgPrecipTotalCounter > 0.0: # and avgPrecipTotal > 0.0: + avgPrecipTotal /= avgPrecipTotalCounter + + #bigP_total: total precip rate mm/LD + if bigPtotalCounter > 0.0: #and bigPtotal > 0.0: + bigPtotal /= bigPtotalCounter + + #precipitation area percentage + if len(precipPercent) > 0: + precipAreaPercent = (sum(precipPercent)/len(precipPercent))*100.0 + + #average precipitation area + if len(avgPrecipArea) > 0: + precipAreaAvg = sum(avgPrecipArea)/len(avgPrecipArea) + if len(avgPrecipArea) > 1: + imgFilename = MAINDIRECTORY+'/images/avgPrecipArea.gif' + plotter.draw_histogram(avgPrecipArea, ["Average Rainfall Area [km^2]", "Area [km^2]"],imgFilename,10) + + + sTime = str(averageTime(startTimes)) + eTime = str(averageTime(endTimes)) + if len (allPropagationSpeeds) > 1: + maxSpeed = max(allPropagationSpeeds) + minSpeed = min(allPropagationSpeeds) + + #write stuff to the summary file + MCSSummaryFile.write("\nNumber of features is %d " %(len(finalMCCList))) + MCSSummaryFile.write("\nAverage duration is %.4f hrs " %(durations)) + MCSSummaryFile.write("\nAverage startTime is %s " %(sTime[-8:])) + MCSSummaryFile.write("\nAverage endTime is %s " %(eTime[-8:])) + MCSSummaryFile.write("\nAverage size is %.4f km^2 " %(areaAvg)) + MCSSummaryFile.write("\nAverage precipitation area is %.4f km^2 " %(precipAreaAvg)) + MCSSummaryFile.write("\nAverage maximum size is %.4f km^2 " %(amax)) + MCSSummaryFile.write("\nAverage propagation speed is %.4f ms^-1" %(averagePropagationSpeed)) + MCSSummaryFile.write("\nMaximum propagation speed is %.4f ms^-1 " %(maxSpeed)) + MCSSummaryFile.write("\nMinimum propagation speed is %.4f ms^-1 " %(minSpeed)) + MCSSummaryFile.write("\nAverage minimum precipitation rate is %.4f mmh^-1" %(avgMinPrecipRate)) + MCSSummaryFile.write("\nAverage maximum precipitation rate is %.4f mm h^-1" %(avgMaxPrecipRate)) + MCSSummaryFile.write("\nAverage precipitation is %.4f mm h^-1 " %(avgPrecipTotal)) + MCSSummaryFile.write("\nAverage total precipitation during MCSs is %.4f mm/LD " %(bigPtotal)) + MCSSummaryFile.write("\nAverage precipitation area percentage is %.4f percent " %(precipAreaPercent)) + + + MCSUserFile.close + MCSSummaryFile.close + MCSPostFile.close + return #****************************************************************** # PLOTTING UTIL SCRIPTS #****************************************************************** def to_percent(y,position): - ''' - Purpose:: - Utility script for generating the y-axis for plots - ''' - return (str(100*y)+'%') + ''' + Purpose:: + Utility script for generating the y-axis for plots + ''' + return (str(100*y)+'%') #****************************************************************** def colorbar_index(ncolors, nlabels, cmap): - ''' - Purpose:: - Utility script for crating a colorbar - Taken from http://stackoverflow.com/questions/18704353/correcting-matplotlib-colorbar-ticks - ''' - cmap = cmap_discretize(cmap, ncolors) - mappable = cm.ScalarMappable(cmap=cmap) - mappable.set_array([]) - mappable.set_clim(-0.5, ncolors+0.5) - colorbar = plt.colorbar(mappable)#, orientation='horizontal') - colorbar.set_ticks(np.linspace(0, ncolors, ncolors)) - colorbar.set_ticklabels(nlabels) - return + ''' + Purpose:: + Utility script for crating a colorbar + Taken from http://stackoverflow.com/questions/18704353/correcting-matplotlib-colorbar-ticks + ''' + cmap = cmap_discretize(cmap, ncolors) + mappable = cm.ScalarMappable(cmap=cmap) + mappable.set_array([]) + mappable.set_clim(-0.5, ncolors+0.5) + colorbar = plt.colorbar(mappable)#, orientation='horizontal') + colorbar.set_ticks(np.linspace(0, ncolors, ncolors)) + colorbar.set_ticklabels(nlabels) + return #****************************************************************** def cmap_discretize(cmap, N): - ''' - Taken from: http://stackoverflow.com/questions/18704353/correcting-matplotlib-colorbar-ticks - http://wiki.scipy.org/Cookbook/Matplotlib/ColormapTransformations - Return a discrete colormap from the continuous colormap cmap. - - cmap: colormap instance, eg. cm.jet. - N: number of colors. - - Example - x = resize(arange(100), (5,100)) - djet = cmap_discretize(cm.jet, 5) - imshow(x, cmap=djet) - ''' - - if type(cmap) == str: - cmap = plt.get_cmap(cmap) - colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.))) - colors_rgba = cmap(colors_i) - indices = np.linspace(0, 1., N+1) - cdict = {} - for ki,key in enumerate(('red','green','blue')): - cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) - for i in xrange(N+1) ] - # Return colormap object. - return mcolors.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024) + ''' + Taken from: http://stackoverflow.com/questions/18704353/correcting-matplotlib-colorbar-ticks + http://wiki.scipy.org/Cookbook/Matplotlib/ColormapTransformations + Return a discrete colormap from the continuous colormap cmap. + + cmap: colormap instance, eg. cm.jet. + N: number of colors. + + Example + x = resize(arange(100), (5,100)) + djet = cmap_discretize(cm.jet, 5) + imshow(x, cmap=djet) + ''' + + if type(cmap) == str: + cmap = plt.get_cmap(cmap) + colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.))) + colors_rgba = cmap(colors_i) + indices = np.linspace(0, 1., N+1) + cdict = {} + for ki,key in enumerate(('red','green','blue')): + cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) + for i in xrange(N+1) ] + # Return colormap object. + return mcolors.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024) #****************************************************************** # def preprocessingMERG(MERGdirname): # ''' @@ -3574,7 +3574,7 @@ def cmap_discretize(cmap, N): # Input:: # Directory to the location of the raw MERG files, preferably zipped - + # Output:: # none @@ -3653,7 +3653,7 @@ def cmap_discretize(cmap, N): # #generate the lats4D command for GrADS # lats4D = 'lats4d -v -q -lat '+LATMIN + ' ' +LATMAX +' -lon ' +LONMIN +' ' +LONMAX +' -time '+hr+'Z'+day+mth+yy + ' -func @+75 ' + '-i merg.ctl' + ' -o ' + fname - + # #lats4D = 'lats4d -v -q -lat -40 -15 -lon 10 40 -time '+hr+'Z'+day+mth+yy + ' -func @+75 ' + '-i merg.ctl' + ' -o ' + fname # #lats4D = 'lats4d -v -q -lat -5 40 -lon -90 60 -func @+75 ' + '-i merg.ctl' + ' -o ' + fname @@ -3671,4 +3671,4 @@ def cmap_discretize(cmap, N): # subprocess.call('mv *.gif mergImgs', shell=True) # return - + diff --git a/ocw/data_source/esgf.py b/ocw/data_source/esgf.py index 67c307f4..0dcc2e05 100644 --- a/ocw/data_source/esgf.py +++ b/ocw/data_source/esgf.py @@ -18,7 +18,14 @@ # import os -import urllib2 +import sys +if sys.version_info[0] >= 3: + from urllib.error import HTTPError +else: + # Not Python 3 - today, it is most likely to be Python 2 + # But note that this might need an update when Python 4 + # might be around one day + from urllib2 import HTTPError from ocw.esgf.constants import DEFAULT_ESGF_SEARCH from ocw.esgf.download import download @@ -137,7 +144,7 @@ def _download_files(file_urls, username, password, download_directory='/tmp'): '''''' try: logon(username, password) - except urllib2.HTTPError: + except HTTPError: raise ValueError('esgf._download_files: Invalid login credentials') for url in file_urls: diff --git a/ocw/esgf/download.py b/ocw/esgf/download.py index 23c107b0..690915c5 100644 --- a/ocw/esgf/download.py +++ b/ocw/esgf/download.py @@ -17,24 +17,36 @@ # under the License. # ''' -RCMES module to download a file from ESGF. +OCW module to download a file from ESGF. ''' -import urllib2 -import httplib +import sys +if sys.version_info[0] >= 3: + from http.client import HTTPSConnection + from urllib.request import build_opener + from urllib.request import HTTPCookieProcessor + from urllib.request import HTTPSHandler +else: + # Not Python 3 - today, it is most likely to be Python 2 + # But note that this might need an update when Python 4 + # might be around one day + from httplib import HTTPSConnection + from urllib2 import build_opener + from urllib2 import HTTPCookieProcessor + from urllib2 import HTTPSHandler from os.path import expanduser, join from ocw.esgf.constants import ESGF_CREDENTIALS -class HTTPSClientAuthHandler(urllib2.HTTPSHandler): +class HTTPSClientAuthHandler(HTTPSHandler): ''' HTTP handler that transmits an X509 certificate as part of the request ''' def __init__(self, key, cert): - urllib2.HTTPSHandler.__init__(self) + HTTPSHandler.__init__(self) self.key = key self.cert = cert @@ -42,7 +54,7 @@ def https_open(self, req): return self.do_open(self.getConnection, req) def getConnection(self, host, timeout=300): - return httplib.HTTPSConnection(host, key_file=self.key, cert_file=self.cert) + return HTTPSConnection(host, key_file=self.key, cert_file=self.cert) def download(url, toDirectory="/tmp"): @@ -55,8 +67,8 @@ def download(url, toDirectory="/tmp"): # setup HTTP handler certFile = expanduser(ESGF_CREDENTIALS) - opener = urllib2.build_opener(HTTPSClientAuthHandler(certFile, certFile)) - opener.add_handler(urllib2.HTTPCookieProcessor()) + opener = build_opener(HTTPSClientAuthHandler(certFile, certFile)) + opener.add_handler(HTTPCookieProcessor()) # download file localFilePath = join(toDirectory, url.split('/')[-1]) From 8e787b0212df7cbaa107de0493045aa787810610 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 2 Aug 2017 18:04:18 +0200 Subject: [PATCH 10/11] import os `os` used on lines 66 and 75. --- mccsearch/code/mccSearchUI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mccsearch/code/mccSearchUI.py b/mccsearch/code/mccSearchUI.py index 0794fd2c..a2271431 100644 --- a/mccsearch/code/mccSearchUI.py +++ b/mccsearch/code/mccSearchUI.py @@ -18,6 +18,7 @@ # Wizard for running the mccSearch program ''' +import os import networkx as nx #mccSearch modules import mccSearch From 56989f5788bc6ef9e8b2a68bad62d7ed98b1e629 Mon Sep 17 00:00:00 2001 From: Justin L Date: Wed, 9 Aug 2017 01:28:31 -0700 Subject: [PATCH 11/11] CLIMATE-887 - test_local.py speed and style guide improvements - Change multiple tests (including test_get_netcdf_variable_names) to run setUp code once. - Use small static local file TestGetNetcdfVariableNames.nc as test data for test_get_netcdf_variable_names. - Clean up PEP8/257 warnings. --- ocw/tests/TestGetNetcdfVariableNames.nc | Bin 0 -> 471996 bytes ocw/tests/test_local.py | 280 ++++++++++++++---------- test_smoke.py | 4 +- 3 files changed, 166 insertions(+), 118 deletions(-) create mode 100644 ocw/tests/TestGetNetcdfVariableNames.nc diff --git a/ocw/tests/TestGetNetcdfVariableNames.nc b/ocw/tests/TestGetNetcdfVariableNames.nc new file mode 100644 index 0000000000000000000000000000000000000000..13e2544d05a215ab0e2afe1903683ca01ad79af6 GIT binary patch literal 471996 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 z

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