From d72c7ec59226032f81c1264df8fe3627da6146e7 Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Mon, 22 Aug 2016 14:27:18 -0300 Subject: [PATCH 1/8] Karstens unique station timeseries fix --- pyseidon_dvt/stationClass/stationClass.py | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pyseidon_dvt/stationClass/stationClass.py b/pyseidon_dvt/stationClass/stationClass.py index f22784c..007e6c8 100644 --- a/pyseidon_dvt/stationClass/stationClass.py +++ b/pyseidon_dvt/stationClass/stationClass.py @@ -289,8 +289,47 @@ def __add__(self, StationClass, debug=False): if debug: print "Stacking " + key + "..." except AttributeError: continue + + #New code added by RK Aug 20 + #get unique, sorted time + if debug: + print 'Getting unique, sorted time...' + + #keyword list for unique + + time = getattr(newself.Variables, 'matlabTime') + uniquetime, unique_index=np.unique(time,return_index=True) + sort_index=np.argsort(uniquetime) + index=unique_index[sort_index] + time=time[index] #New time dimension - newself.Grid.ntime = newself.Grid.ntime + StationClass.Grid.ntime + newself.Grid.ntime = time.shape + if debug: print "New time length: " +str(newself.Grid.ntime[0]) + + #keyword list for vstack + kwl=['matlabTime', 'julianTime', 'secondTime'] + for key in kwl: + tmpN = getattr(newself.Variables, key) + setattr(newself.Variables, key, tmpN[index]) + + kwl=['u', 'v', 'w', 'tke', 'gls', 'ua', 'va','el'] + kwl2D=['ua', 'va','el'] + for key in kwl: + try: + if key in kwl2D: + tmpN = getattr(newself.Variables, key)\ + [:,newEle[:]] + setattr(newself.Variables, key,tmpN[index,:]) + if debug: print "Sorting " + key + "..." + else: + tmpN = getattr(newself.Variables, key)\ + [:,:,newEle[:]] + setattr(newself.Variables, key,tmpN[index,:]) + if debug: print "Sorting " + key + "..." + except AttributeError: + continue + #End new code added by RK Aug 20 + #Keep only matching names newself.Grid.name = self.Grid.name[origEle[:]] #Append to new object history From 720ca77c54657058259bbab365100608efdbfa6d Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Fri, 2 Sep 2016 13:05:18 -0300 Subject: [PATCH 2/8] Added 3D Harmonic Analysis compatibility for ADCP, FVCOM & Station Data --- pyseidon_dvt/adcpClass/functionsAdcp.py | 68 ++++++-- .../fvcomClass/functionsFvcomThreeD.py | 157 ++++++++++++++++++ .../stationClass/functionsStationThreeD.py | 148 +++++++++++++++++ 3 files changed, 360 insertions(+), 13 deletions(-) diff --git a/pyseidon_dvt/adcpClass/functionsAdcp.py b/pyseidon_dvt/adcpClass/functionsAdcp.py index caf5ca4..427d3b3 100644 --- a/pyseidon_dvt/adcpClass/functionsAdcp.py +++ b/pyseidon_dvt/adcpClass/functionsAdcp.py @@ -313,7 +313,7 @@ def speed_histogram(self, t_start=[], t_end=[], time_ind=[], def Harmonic_analysis(self, time_ind=[], t_start=[], t_end=[], elevation=True, velocity=False, - debug=False, **kwargs): + threeD=False, debug=False, **kwargs): ''' This function performs a harmonic analysis on the sea surface elevation time series or the velocity components timeseries. @@ -327,8 +327,10 @@ def Harmonic_analysis(self, or time index as an integer - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer - - elevation=True means that `solve' will be done for elevation. - - velocity=True means that `solve' will be done for velocity. + - elevation = True means that `solve' will be done for elevation. + - velocity = True means that `solve' will be done for velocity. + - threeD = True means that 'solve' will be done for each layer + of the velocity data instead of on depth-averaged data. Options: Options are the same as for 'solve', which are shown below with @@ -362,16 +364,29 @@ def Harmonic_analysis(self, if velocity: time = self._var.matlabTime[:] - u = self._var.ua[:] - v = self._var.va[:] + lat = self._var.lat + if not threeD: + u = self._var.ua[:] + v = self._var.va[:] - if not argtime==[]: - time = time[argtime[:]] - u = u[argtime[:]] - v = v[argtime[:]] + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] - lat = self._var.lat - harmo = solve(time, u, v, lat, **kwargs) + harmo = solve(time, u, v, lat, **kwargs) + else: + harmo = {} + for layerIndex in range(self._var.u.data.shape[1]): + u = self._var.u[:,layerIndex] + v = self._var.v[:,layerIndex] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwargs) if elevation: time = self._var.matlabTime[:] @@ -415,10 +430,37 @@ def Harmonic_reconstruction(self, harmo, time_ind=slice(None), debug=False, **kw debug = (debug or self._debug) time = self._var.matlabTime[time_ind] #TR_comments: Add debug flag in Utide: debug=self._debug - Reconstruct = reconstruct(time,harmo) - + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + return Reconstruct + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v + def verti_shear(self, t_start=[], t_end=[], time_ind=[], graph=True, dump=False, debug=False, **kwargs): """ diff --git a/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py b/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py index b91519e..11c1ca2 100644 --- a/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py +++ b/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py @@ -13,6 +13,8 @@ import matplotlib.pyplot as plt from pydap.exceptions import ServerError +from utide import solve, reconstruct + #TR comment: This all routine needs to be tested and debugged class FunctionsFvcomThreeD: """ @@ -1103,3 +1105,158 @@ def _vertical_slice(self, var, start_pt, end_pt, #ax.yaxis.set_major_formatter(ticks) plt.xlabel('Distance along line (m)') plt.ylabel('Depth (m)') + + def Harmonic_analysis_at_point(self, pt_lon, pt_lat, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + #TR_comments: Add debug flag in Utide: debug=self._debug + index = self.index_finder(pt_lon, pt_lat, debug=False) + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + harmo = {} + time = self._var.matlabTime[:] + for layerIndex in range(self._var.u.shape[1]): + #if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib + # ua = self._var.ua[:] + # va = self._var.va[:] + #else: + U = self._var.u[:,layerIndex] + V = self._var.v[:,layerIndex] + + u = self.interpolation_at_point(U, pt_lon, pt_lat, index=index, debug=debug) + v = self.interpolation_at_point(V, pt_lon, pt_lat, index=index, debug=debug) + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwarg) + + else: + time = self._var.matlabTime[:] + if type(self._var.el).__name__=='Variable': #Fix for netcdf4 lib + el = self._var.el[:] + else: + el = self._var.el + el = self.interpolation_at_point(el, pt_lon, pt_lat, + index=index, debug=debug) + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficients from harmo_analysis, dictionary if velocity + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + #TR_comments: Add debug flag in Utide: debug=self._debug + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + + return Reconstruct + + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v diff --git a/pyseidon_dvt/stationClass/functionsStationThreeD.py b/pyseidon_dvt/stationClass/functionsStationThreeD.py index 9e6120c..177d1ca 100644 --- a/pyseidon_dvt/stationClass/functionsStationThreeD.py +++ b/pyseidon_dvt/stationClass/functionsStationThreeD.py @@ -9,6 +9,8 @@ from pyseidon_dvt.utilities.BP_tools import * import time +from utide import solve, reconstruct + # Custom error from pyseidon_error import PyseidonError @@ -299,6 +301,152 @@ def flow_dir(self, station, t_start=[], t_end=[], time_ind=[], return np.squeeze(dirFlow) + def Harmonic_analysis_at_point(self, station, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or each layer of the velocity components timeseries. + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + + #Search for the station + index = self.search_index(station) + + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + argtime = time_to_index(t_start, t_end, + self._var.matlabTime, + debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + harmo = {} + for layerIndex in range(self._var.u.shape[1]): + time = self._var.matlabTime[:] + u = self._var.u[:,layerIndex,index] + v = self._var.v[:,layerIndex,index] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwarg) + + if elevation: + time = self._var.matlabTime[:] + el = self._var.el[:,index] + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + + return Reconstruct + + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v + + #TR_comments: templates # def whatever(self, debug=False): # if debug or self._debug: From e03e09be1751f5d71c53ca7100f5d051f6d2b9b8 Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:42:10 -0300 Subject: [PATCH 3/8] Added drifter class compatibility with Luna Oceans new drifter file format --- PySeidon_dvt.egg-info/PKG-INFO | 95 ++ PySeidon_dvt.egg-info/SOURCES.txt | 61 + PySeidon_dvt.egg-info/dependency_links.txt | 1 + PySeidon_dvt.egg-info/not-zip-safe | 1 + PySeidon_dvt.egg-info/top_level.txt | 1 + build/lib/pyseidon_dvt/__init__.py | 38 + build/lib/pyseidon_dvt/adcpClass/__init__.py | 10 + build/lib/pyseidon_dvt/adcpClass/adcpClass.py | 67 + .../pyseidon_dvt/adcpClass/functionsAdcp.py | 622 ++++++++ build/lib/pyseidon_dvt/adcpClass/plotsAdcp.py | 171 +++ .../pyseidon_dvt/adcpClass/rawADCPclass.py | 116 ++ .../pyseidon_dvt/adcpClass/variablesAdcp.py | 225 +++ .../lib/pyseidon_dvt/drifterClass/__init__.py | 10 + .../pyseidon_dvt/drifterClass/drifterClass.py | 86 ++ .../drifterClass/functionsDrifter.py | 20 + .../pyseidon_dvt/drifterClass/plotsDrifter.py | 122 ++ .../drifterClass/variablesDrifter.py | 73 + build/lib/pyseidon_dvt/fvcomClass/__init__.py | 10 + .../pyseidon_dvt/fvcomClass/functionsFvcom.py | 1160 +++++++++++++++ .../fvcomClass/functionsFvcomThreeD.py | 1262 +++++++++++++++++ .../lib/pyseidon_dvt/fvcomClass/fvcomClass.py | 322 +++++ .../lib/pyseidon_dvt/fvcomClass/plotsFvcom.py | 682 +++++++++ .../pyseidon_dvt/fvcomClass/variablesFvcom.py | 713 ++++++++++ .../lib/pyseidon_dvt/stationClass/__init__.py | 10 + .../stationClass/functionsStation.py | 483 +++++++ .../stationClass/functionsStationThreeD.py | 456 ++++++ .../pyseidon_dvt/stationClass/plotsStation.py | 218 +++ .../pyseidon_dvt/stationClass/stationClass.py | 446 ++++++ .../stationClass/variablesStation.py | 293 ++++ .../pyseidon_dvt/tidegaugeClass/__init__.py | 10 + .../tidegaugeClass/functionsTidegauge.py | 95 ++ .../tidegaugeClass/plotsTidegauge.py | 112 ++ .../tidegaugeClass/tidegaugeClass.py | 57 + .../tidegaugeClass/variablesTidegauge.py | 41 + build/lib/pyseidon_dvt/utilities/BP_tools.py | 360 +++++ build/lib/pyseidon_dvt/utilities/__init__.py | 10 + build/lib/pyseidon_dvt/utilities/createNC.py | 87 ++ .../utilities/interpolation_utils.py | 597 ++++++++ .../pyseidon_dvt/utilities/miscellaneous.py | 174 +++ .../utilities/object_from_dict.py | 6 + .../pyseidon_dvt/utilities/pyseidon2matlab.py | 74 + .../pyseidon_dvt/utilities/pyseidon2netcdf.py | 125 ++ .../pyseidon_dvt/utilities/pyseidon2pickle.py | 75 + .../pyseidon_dvt/utilities/pyseidon_error.py | 9 + build/lib/pyseidon_dvt/utilities/regioner.py | 178 +++ .../utilities/save_FlowFile_BPFormat.py | 388 +++++ .../utilities/shortest_element_path.py | 200 +++ build/lib/pyseidon_dvt/utilities/windrose.py | 548 +++++++ .../pyseidon_dvt/validationClass/__init__.py | 12 + .../validationClass/compareData.py | 305 ++++ .../validationClass/depthInterp.py | 203 +++ .../validationClass/interpolate.py | 59 + .../validationClass/plotsValidation.py | 329 +++++ .../pyseidon_dvt/validationClass/smooth.py | 84 ++ .../validationClass/tidalStats.py | 715 ++++++++++ .../pyseidon_dvt/validationClass/valReport.py | 402 ++++++ .../pyseidon_dvt/validationClass/valTable.py | 75 + .../validationClass/validationClass.py | 760 ++++++++++ .../validationClass/variablesValidation.py | 331 +++++ dist/PySeidon_dvt-2.1-py2.7.egg | Bin 0 -> 320841 bytes pyseidon_dvt/drifterClass/variablesDrifter.py | 57 +- 61 files changed, 14237 insertions(+), 15 deletions(-) create mode 100644 PySeidon_dvt.egg-info/PKG-INFO create mode 100644 PySeidon_dvt.egg-info/SOURCES.txt create mode 100644 PySeidon_dvt.egg-info/dependency_links.txt create mode 100644 PySeidon_dvt.egg-info/not-zip-safe create mode 100644 PySeidon_dvt.egg-info/top_level.txt create mode 100644 build/lib/pyseidon_dvt/__init__.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/adcpClass.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/functionsAdcp.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/plotsAdcp.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/rawADCPclass.py create mode 100644 build/lib/pyseidon_dvt/adcpClass/variablesAdcp.py create mode 100644 build/lib/pyseidon_dvt/drifterClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/drifterClass/drifterClass.py create mode 100644 build/lib/pyseidon_dvt/drifterClass/functionsDrifter.py create mode 100644 build/lib/pyseidon_dvt/drifterClass/plotsDrifter.py create mode 100644 build/lib/pyseidon_dvt/drifterClass/variablesDrifter.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/functionsFvcom.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/fvcomClass.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/plotsFvcom.py create mode 100644 build/lib/pyseidon_dvt/fvcomClass/variablesFvcom.py create mode 100644 build/lib/pyseidon_dvt/stationClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/stationClass/functionsStation.py create mode 100644 build/lib/pyseidon_dvt/stationClass/functionsStationThreeD.py create mode 100644 build/lib/pyseidon_dvt/stationClass/plotsStation.py create mode 100644 build/lib/pyseidon_dvt/stationClass/stationClass.py create mode 100644 build/lib/pyseidon_dvt/stationClass/variablesStation.py create mode 100644 build/lib/pyseidon_dvt/tidegaugeClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/tidegaugeClass/functionsTidegauge.py create mode 100644 build/lib/pyseidon_dvt/tidegaugeClass/plotsTidegauge.py create mode 100644 build/lib/pyseidon_dvt/tidegaugeClass/tidegaugeClass.py create mode 100644 build/lib/pyseidon_dvt/tidegaugeClass/variablesTidegauge.py create mode 100644 build/lib/pyseidon_dvt/utilities/BP_tools.py create mode 100644 build/lib/pyseidon_dvt/utilities/__init__.py create mode 100644 build/lib/pyseidon_dvt/utilities/createNC.py create mode 100644 build/lib/pyseidon_dvt/utilities/interpolation_utils.py create mode 100644 build/lib/pyseidon_dvt/utilities/miscellaneous.py create mode 100644 build/lib/pyseidon_dvt/utilities/object_from_dict.py create mode 100644 build/lib/pyseidon_dvt/utilities/pyseidon2matlab.py create mode 100644 build/lib/pyseidon_dvt/utilities/pyseidon2netcdf.py create mode 100644 build/lib/pyseidon_dvt/utilities/pyseidon2pickle.py create mode 100644 build/lib/pyseidon_dvt/utilities/pyseidon_error.py create mode 100644 build/lib/pyseidon_dvt/utilities/regioner.py create mode 100644 build/lib/pyseidon_dvt/utilities/save_FlowFile_BPFormat.py create mode 100644 build/lib/pyseidon_dvt/utilities/shortest_element_path.py create mode 100644 build/lib/pyseidon_dvt/utilities/windrose.py create mode 100644 build/lib/pyseidon_dvt/validationClass/__init__.py create mode 100644 build/lib/pyseidon_dvt/validationClass/compareData.py create mode 100644 build/lib/pyseidon_dvt/validationClass/depthInterp.py create mode 100644 build/lib/pyseidon_dvt/validationClass/interpolate.py create mode 100644 build/lib/pyseidon_dvt/validationClass/plotsValidation.py create mode 100644 build/lib/pyseidon_dvt/validationClass/smooth.py create mode 100644 build/lib/pyseidon_dvt/validationClass/tidalStats.py create mode 100644 build/lib/pyseidon_dvt/validationClass/valReport.py create mode 100644 build/lib/pyseidon_dvt/validationClass/valTable.py create mode 100644 build/lib/pyseidon_dvt/validationClass/validationClass.py create mode 100644 build/lib/pyseidon_dvt/validationClass/variablesValidation.py create mode 100644 dist/PySeidon_dvt-2.1-py2.7.egg diff --git a/PySeidon_dvt.egg-info/PKG-INFO b/PySeidon_dvt.egg-info/PKG-INFO new file mode 100644 index 0000000..9558b4c --- /dev/null +++ b/PySeidon_dvt.egg-info/PKG-INFO @@ -0,0 +1,95 @@ +Metadata-Version: 1.0 +Name: PySeidon-dvt +Version: 2.1 +Summary: Suite of tools for tidal-energy and FVCOM-user communities +Home-page: https://github.com/GrumpyNounours/PySeidon +Author: Thomas Roc +Author-email: thomas.roc@acadiau.ca,wesley.bowman23@gmail.com,lavieenroux20@gmail.com +License: GNU Affero GPL v3.0 +Description: PySeidon_dvt + ================ + ###Warning### + ####This is the development branch of PySeidon and it is subject to daily changes!### + + ### Project description ### + * This project aims to meet multiple objectives of the [EcoEnergyII](http://tidalenergy.acadiau.ca/EcoEII.html) consortium + through the setting of a dedicated server and the development of Python + based packages. This project can be seen as two folded. On the one + hand, it aims to enhance data accessibility for all the partners of + the [EcoEII](http://tidalenergy.acadiau.ca/EcoEII.html) consortium thanks to simple client protocols. On the other + hand, it aims to develop a standardised numerical toolbox gathering + specific analysis functions for measured and simulated data (FVCOM model) + to the [EcoEII](http://tidalenergy.acadiau.ca/EcoEII.html) partners. + * Additionally, this project was the ideal opportunity to transport various + scripts and packages accumulated over the years into Python. These scripts + and packages have been extensively used by the tidal energy community for + more than a decade. The 'Contributors' section of this document is a + mere attempt to acknowledge the work of those who participated directly or + indirectly to the development of this tool box. We are consciously + standing on the shoulders of a multitude of giants...so please forgive us + if we forgot one of them. + * The present package is still a work in progress, so the more feedback, + the better + + ### Installation ### + Hydrodynamic model: + * This package has been primarily developed and designed for post-processing FVCOM outputs. One can download FVCOM from [here](http://fvcom.smast.umassd.edu/fvcom/) + + Requirements: + * This package has been designed for Python 2.7: one can download Python from [here](http://www.python.org/download) + * It is also recommended to install Anaconda beforehand: one can download Anaconda from [here](http://continuum.io/downloads#all) + * The HDF5 library is also needed for this package to work: one can download the HDF5 library from [here](https://www.hdfgroup.org/HDF5/) + + Dependencies: + Althought they should be automatically resolved during the installation, this package relies on the following dependencies: + * setuptools: One can download setuptools from [here](https://pypi.python.org/pypi/setuptools#installation-instructions) + * UTide: One can download UTide from [here](https://github.com/wesleybowman/UTide) + * Pydap: One can download Pydap from [here](http://www.pydap.org/) + * NetworkX: One can download NetworkX from [here](http://networkx.github.io/documentation/latest/install.html) + * Pandas: One can download Pandas from [here](http://pandas.pydata.org/pandas-docs/stable/install.html) + * Seaborn: One can download Seaborn from [here](http://web.stanford.edu/~mwaskom/software/seaborn/installing.html) + * netCDF4: One can download netCDF4 from [here](https://pypi.python.org/pypi/netCDF4/0.8.2) + * gdal: One can download gdal from [here](https://pypi.python.org/pypi/GDAL/) + + Installation: + * Step 1a: Download PySeidon package, save it on your machine and Unzip + * Step 1b: or clone the repository + * Step 2: from a shell, change directory to PySeidon-master folder + * Step 3: from the shell, as superuser/admin, type `python setup.py install` + or `python setup.py install --user` + * Step 4: choose to automatically resolve (y) or not (n) the dependencies + * Finally, in order to test the installation, type `from pyseidon_dvt import *` in Ipython shell. + + Up-dating: + * The code will evolve and improve with time. To up-date, simply "git pull" or download the package + and go through the installation procedure again. + + Recommendations: + * The tutorials and package functioning have been designed for use in IPython shell: One can download IPython from [here](http://ipython.org/) + + ### Documentation ### + Package's documentation can be found [here](http://grumpynounours.github.io/PySeidon/index.html) + + ### Contribution guidelines ### + * [Tutorial 0: First steps](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/master/PySeidon_tuto_0.ipynb) + * [Tutorial 1: FVCOM class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/master/PySeidon_tuto_1.ipynb) + * [Tutorial 2: Station class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_2.ipynb) + * [Tutorial 3: ADCP class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_3.ipynb) + * [Tutorial 4: TideGauge class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_4.ipynb) + * [Tutorial 5: Drifter class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_5.ipynb) + * [Tutorial 6: Validation class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_6.ipynb) + + ### Contacts ### + * Project Leader: [Richard Karsten](richard.karsten@acadiau.ca) + * Repository Admin & Software Development Manager: [Thomas Roc](thomas.roc@acadiau.ca) + * Main Developers: [Thomas Roc](thomas.roc@acadiau.ca), [Jonathan Smith](https://github.com/LaVieEnRoux), [Wesley Bowman](https://github.com/wesleybowman), [Kody Crowell](https://github.com/TheKingInYellow) + + ### Contributors ### + Dr. Richard Karsten, [Aidan Bharath](https://github.com/Aidan-Bharath), Mitchell O'Flaherty-Sproul, Robie Hennigar, [Robert Covill](http://tekmap.ns.ca/), Dr. Joel Culina, Justine McMillan, Dr. Brian Polagye, [Dr. Kristen Thyng](https://github.com/kthyng)... + + ### Legal Information ### + * Original authorship attributed to Thomas Roc, Wesley Bowman and Jonathan Smith + * Copyright (c) 2014 [EcoEnergyII](http://tidalenergy.acadiau.ca/EcoEII.html) + * Licensed under an Affero GPL style license v3.0 (see License_PySeidon.txt) + +Platform: UNKNOWN diff --git a/PySeidon_dvt.egg-info/SOURCES.txt b/PySeidon_dvt.egg-info/SOURCES.txt new file mode 100644 index 0000000..da6ff47 --- /dev/null +++ b/PySeidon_dvt.egg-info/SOURCES.txt @@ -0,0 +1,61 @@ +README.txt +setup.py +PySeidon_dvt.egg-info/PKG-INFO +PySeidon_dvt.egg-info/SOURCES.txt +PySeidon_dvt.egg-info/dependency_links.txt +PySeidon_dvt.egg-info/not-zip-safe +PySeidon_dvt.egg-info/top_level.txt +pyseidon_dvt/__init__.py +pyseidon_dvt/adcpClass/__init__.py +pyseidon_dvt/adcpClass/adcpClass.py +pyseidon_dvt/adcpClass/functionsAdcp.py +pyseidon_dvt/adcpClass/plotsAdcp.py +pyseidon_dvt/adcpClass/rawADCPclass.py +pyseidon_dvt/adcpClass/variablesAdcp.py +pyseidon_dvt/drifterClass/__init__.py +pyseidon_dvt/drifterClass/drifterClass.py +pyseidon_dvt/drifterClass/functionsDrifter.py +pyseidon_dvt/drifterClass/plotsDrifter.py +pyseidon_dvt/drifterClass/variablesDrifter.py +pyseidon_dvt/fvcomClass/__init__.py +pyseidon_dvt/fvcomClass/functionsFvcom.py +pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py +pyseidon_dvt/fvcomClass/fvcomClass.py +pyseidon_dvt/fvcomClass/plotsFvcom.py +pyseidon_dvt/fvcomClass/variablesFvcom.py +pyseidon_dvt/stationClass/__init__.py +pyseidon_dvt/stationClass/functionsStation.py +pyseidon_dvt/stationClass/functionsStationThreeD.py +pyseidon_dvt/stationClass/plotsStation.py +pyseidon_dvt/stationClass/stationClass.py +pyseidon_dvt/stationClass/variablesStation.py +pyseidon_dvt/tidegaugeClass/__init__.py +pyseidon_dvt/tidegaugeClass/functionsTidegauge.py +pyseidon_dvt/tidegaugeClass/plotsTidegauge.py +pyseidon_dvt/tidegaugeClass/tidegaugeClass.py +pyseidon_dvt/tidegaugeClass/variablesTidegauge.py +pyseidon_dvt/utilities/BP_tools.py +pyseidon_dvt/utilities/__init__.py +pyseidon_dvt/utilities/createNC.py +pyseidon_dvt/utilities/interpolation_utils.py +pyseidon_dvt/utilities/miscellaneous.py +pyseidon_dvt/utilities/object_from_dict.py +pyseidon_dvt/utilities/pyseidon2matlab.py +pyseidon_dvt/utilities/pyseidon2netcdf.py +pyseidon_dvt/utilities/pyseidon2pickle.py +pyseidon_dvt/utilities/pyseidon_error.py +pyseidon_dvt/utilities/regioner.py +pyseidon_dvt/utilities/save_FlowFile_BPFormat.py +pyseidon_dvt/utilities/shortest_element_path.py +pyseidon_dvt/utilities/windrose.py +pyseidon_dvt/validationClass/__init__.py +pyseidon_dvt/validationClass/compareData.py +pyseidon_dvt/validationClass/depthInterp.py +pyseidon_dvt/validationClass/interpolate.py +pyseidon_dvt/validationClass/plotsValidation.py +pyseidon_dvt/validationClass/smooth.py +pyseidon_dvt/validationClass/tidalStats.py +pyseidon_dvt/validationClass/valReport.py +pyseidon_dvt/validationClass/valTable.py +pyseidon_dvt/validationClass/validationClass.py +pyseidon_dvt/validationClass/variablesValidation.py \ No newline at end of file diff --git a/PySeidon_dvt.egg-info/dependency_links.txt b/PySeidon_dvt.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/PySeidon_dvt.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/PySeidon_dvt.egg-info/not-zip-safe b/PySeidon_dvt.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/PySeidon_dvt.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/PySeidon_dvt.egg-info/top_level.txt b/PySeidon_dvt.egg-info/top_level.txt new file mode 100644 index 0000000..c91d302 --- /dev/null +++ b/PySeidon_dvt.egg-info/top_level.txt @@ -0,0 +1 @@ +pyseidon_dvt diff --git a/build/lib/pyseidon_dvt/__init__.py b/build/lib/pyseidon_dvt/__init__.py new file mode 100644 index 0000000..32305a7 --- /dev/null +++ b/build/lib/pyseidon_dvt/__init__.py @@ -0,0 +1,38 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import os +import sys + +local = os.path.dirname(__file__) +sys.path.append(os.path.join(local,'fvcomClass')) +sys.path.append(os.path.join(local,'adcpClass')) +sys.path.append(os.path.join(local,'drifterClass')) +sys.path.append(os.path.join(local,'stationClass')) +sys.path.append(os.path.join(local,'tidegaugeClass')) +sys.path.append(os.path.join(local,'validationClass')) +sys.path.append(os.path.join(local,'utilities')) + +#Local import +from utilities import * +from adcpClass import * +from drifterClass import * +from tidegaugeClass import * +from stationClass import * +from fvcomClass import * +from validationClass import * + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +#Permission info for OpenDap server +#print "OpenDap server connexion info:" + +__version__ = '2.0' +__all__ = ["FVCOM", "ADCP", "Drifter", "TideGauge",\ + "Validation", "Station", "utilities", "PyseidonError"] +__authors__ = ['Wesley Bowman, Thomas Roc, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/adcpClass/__init__.py b/build/lib/pyseidon_dvt/adcpClass/__init__.py new file mode 100644 index 0000000..f29eb7b --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Local import +from adcpClass import ADCP + +__authors__ = ['Wesley Bowman, Thomas Roc, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/adcpClass/adcpClass.py b/build/lib/pyseidon_dvt/adcpClass/adcpClass.py new file mode 100644 index 0000000..d77af6b --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/adcpClass.py @@ -0,0 +1,67 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +from __future__ import division +import numpy as np +import scipy.io as sio +import h5py + +#Local import +from variablesAdcp import _load_adcp +from functionsAdcp import * +from plotsAdcp import * + + +class ADCP: + """ + **A class/structure for ADCP data** + + Functionality structured as follows: :: + + _Data. = raw matlab file data + |_Variables. = useable adcp variables and quantities + |_History = Quality Control metadata + testAdcp._|_Utils. = set of useful functions + |_Plots. = plotting functions + |_method_1 + | ... = methods and analysis techniques intrinsic to ADCPs + |_method_n + + Inputs: + - Only takes a file name as input, ex: testAdcp=ADCP('./path_to_matlab_file/filename') + + *Notes* + Only handle fully processed ADCP matlab data previously quality-controlled as well + as formatted through "EnsembleData_FlowFile" matlab script at the mo. + + Throughout the package, the following conventions apply: + - Coordinates = decimal degrees East and North + - Directions = in degrees, between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - Depth = 0m is the free surface and depth is negative + """ + + def __init__(self, filename, debug=False): + """ Initialize ADCP class.""" + self._debug = debug + self._origin_file = filename + if debug: + print '-Debug mode on-' + #TR_comments: find a way to dissociate raw and processed data + self.History = ['Created from ' + filename] + #TR_comments: *_Raw and *_10minavg open with h5py whereas *_davgBS + try: + self.Data = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + except NotImplementedError: + self.Data = h5py.File(filename, 'r') + #TR_comments: Initialize class structure + self.Variables = _load_adcp(self, self.History, debug=self._debug) + self.Plots = PlotsAdcp(self.Variables, debug=self._debug) + self.Utils = FunctionsAdcp(self.Variables, + self.Plots, + self.History, + debug=self._debug) + + ##Re-assignement of utility functions as methods + self.dump_profile_data = self.Plots._dump_profile_data_as_csv + + return \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/adcpClass/functionsAdcp.py b/build/lib/pyseidon_dvt/adcpClass/functionsAdcp.py new file mode 100644 index 0000000..427d3b3 --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/functionsAdcp.py @@ -0,0 +1,622 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +import numexpr as ne +import datetime +from pyseidon_dvt.utilities.miscellaneous import * +from pyseidon_dvt.utilities.BP_tools import * +from utide import solve, reconstruct +import time + +# Custom error +from pyseidon_error import PyseidonError + +class FunctionsAdcp: + """ **'Utils' subset of FVCOM class gathers useful functions** """ + def __init__(self, variable, plot, History, debug=False): + self._debug = debug + self._plot = plot + #Create pointer to FVCOM class + setattr(self, '_var', variable) + setattr(self, '_History', History) + + return + + def flow_dir(self, t_start=[], t_end=[], time_ind=[], + exceedance=False, debug=False): + """ + Flow directions and associated norm + + Outputs: + - flowDir = flowDir at station, 1D array + - norm = velocity norm at station, 1D array + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - excedance = True, compute associated exceedance curve + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + """ + debug = debug or self._debug + if debug: + print 'Computing flow directions at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if not argtime==[]: + U = self._var.ua[argtime] + V = self._var.va[argtime] + else: + U = self._var.ua[:] + V = self._var.va[:] + + #Compute directions + if debug: + print 'Computing arctan2 and norm...' + dirFlow = np.rad2deg(np.arctan2(V,U)) + + #Compute velocity norm + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + if debug: + print '...Passed' + #Rose diagram + self._plot.rose_diagram(dirFlow, norm) + if exceedance: + self.exceedance(norm) + + return dirFlow, norm + + def ebb_flood_split(self, t_start=[], t_end=[], time_ind=[], debug=False): + """ + Compute time indices for ebb and flood but also the + principal flow directions and associated variances for (lon, lat) point + + Outputs: + - floodIndex = flood time index, 1D array of integers + - ebbIndex = ebb time index, 1D array of integers + - pr_axis = principal flow ax1s, float number in degrees + - pr_ax_var = associated variance, float number + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, 1D array of integers + + *Notes* + - may take time to compute if time period too long + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - use time_ind or t_start and t_end, not both + - assume that flood is aligned with principal direction + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing principal flow directions...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if not argtime==[]: + U = self._var.ua[argtime] + V = self._var.va[argtime] + else: + U = self._var.ua[:] + V = self._var.va[:] + + #WB version of BP's principal axis + #Assuming principal axis = flood heading + #determine principal axes - potentially a problem if axes are very kinked + # since this would misclassify part of ebb and flood + if debug: print 'Computin principal axis at point...' + pr_axis, pr_ax_var = principal_axis(U, V) + + #ebb/flood split + if debug: print 'Splitting ebb and flood at point...' + ###TR: version 1 + # reverse 0-360 deg convention + #ra = (-pr_axis - 90.0) * np.pi /180.0 + #if ra>np.pi: + # ra = ra - (2.0*np.pi) + #elif ra<-np.pi: + # ra = ra + (2.0*np.pi) + #dirFlow = np.arctan2(V,U) + ##Define bins of angles + #if ra == 0.0: + # binP = [0.0, np.pi/2.0] + # binP = [0.0, -np.pi/2.0] + #elif ra > 0.0: + # if ra == np.pi: + # binP = [np.pi/2.0 , np.pi] + # binM = [-np.pi, -np.pi/2.0 ] + # elif ra < (np.pi/2.0): + # binP = [0.0, ra + (np.pi/2.0)] + # binM = [-((np.pi/2.0)-ra), 0.0] + # else: + # binP = [ra - (np.pi/2.0), np.pi] + # binM = [-np.pi, -np.pi + (ra-(np.pi/2.0))] + #else: + # if ra == -np.pi: + # binP = [np.pi/2.0 , np.pi] + # binM = [-np.pi, -np.pi/2.0] + # elif ra > -(np.pi/2.0): + # binP = [0.0, ra + (np.pi/2.0)] + # binM = [ ((-np.pi/2.0)+ra), 0.0] + # else: + # binP = [np.pi - (ra+(np.pi/2.0)) , np.pi] + # binM = [-np.pi, ra + (np.pi/2.0)] + # + #test = (((dirFlow > binP[0]) * (dirFlow < binP[1])) + + # ((dirFlow > binM[0]) * (dirFlow < binM[1]))) + #floodIndex = np.where(test == True)[0] + #ebbIndex = np.where(test == False)[0] + ###TR: version 2 + flood_heading = np.array([-90, 90]) + pr_axis + dir_all = np.rad2deg(np.arctan2(V,U)) + ind = np.where(dir_all<0) + dir_all[ind] = dir_all[ind] + 360 + # sign speed - eliminating wrap-around + dir_PA = dir_all - pr_axis + dir_PA[dir_PA < -90] += 360 + dir_PA[dir_PA > 270] -= 360 + #general direction of flood passed as input argument + floodIndex = np.where((dir_PA >= -90) & (dir_PA<90)) + ebbIndex = np.arange(dir_PA.shape[0]) + ebbIndex = np.delete(ebbIndex,floodIndex[:]) + #TR: quick fix + if type(floodIndex).__name__=='tuple': + floodIndex = floodIndex[0] + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + return floodIndex, ebbIndex, pr_axis, pr_ax_var + + def exceedance(self, var, graph=True, dump=False, debug=False, **kwargs): + """ + This function calculate the excedence curve of a var(time). + + Inputs: + - var = given quantity, 1 array of n elements + + Options: + - graph: True->plots curve; False->does not + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + + Outputs: + - Exceedance = list of % of occurences, 1D array + - Ranges = list of signal amplitude bins, 1D array + + *Notes* + - This method is not suitable for SSE + """ + debug = (debug or self._debug) + if debug: + print 'Computing exceedance...' + + signal=var + + Max = max(signal) + dy = (Max/30.0) + Ranges = np.arange(0,(Max + dy), dy) + Exceedance = np.zeros(Ranges.shape[0]) + dt = self._var.matlabTime[1] - self._var.matlabTime[0] + Period = var.shape[0] * dt + time = np.arange(0.0, Period, dt) + + N = len(signal) + M = len(Ranges) + + for i in range(M): + r = Ranges[i] + for j in range(N-1): + if signal[j] > r: + Exceedance[i] = Exceedance[i] + (time[j+1] - time[j]) + + Exceedance = (Exceedance * 100) / Period + #Plot + if graph: + error=np.ones(Exceedance.shape) * np.std(var)/2.0 + self._plot.plot_xy(Exceedance, Ranges, yerror=error, + yLabel='Amplitudes', + xLabel='Exceedance probability in %', dump=dump, **kwargs) + + if debug: + print '...Passed' + + return Exceedance, Ranges + + def speed_histogram(self, t_start=[], t_end=[], time_ind=[], + debug=False, dump=False, **kwargs): + """ + This function plots the histogram of occurrences for the signed + flow speed. + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, 1D array of integers + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing speed histogram...' + + pI, nI, pa, pav = self.ebb_flood_split(t_start=t_start, t_end=t_end, + time_ind=time_ind, debug=debug) + dirFlow, norm = self.flow_dir(t_start=t_start, t_end=t_end, + time_ind=time_ind, exceedance=False, debug=debug) + norm[nI] = -1.0 * norm[nI] + + #compute bins + #minBound = norm.min() + #maxBound = norm.max() + #step = round((maxBound-minBound/51.0),1) + #bins = np.arange(minBound,maxBound,step) + + #plot histogram + self._plot.Histogram(norm, + title='Flow speed histogram', + xLabel='Signed flow speed (m/s)', + yLabel='Occurrences (%)', dump=dump, **kwargs) + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + + def Harmonic_analysis(self, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + threeD=False, debug=False, **kwargs): + ''' + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - elevation = True means that `solve' will be done for elevation. + - velocity = True means that `solve' will be done for velocity. + - threeD = True means that 'solve' will be done for each layer + of the velocity data instead of on depth-averaged data. + + Options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + ''' + debug = (debug or self._debug) + + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation---") + + if velocity: + time = self._var.matlabTime[:] + lat = self._var.lat + if not threeD: + u = self._var.ua[:] + v = self._var.va[:] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + harmo = solve(time, u, v, lat, **kwargs) + else: + harmo = {} + for layerIndex in range(self._var.u.data.shape[1]): + u = self._var.u[:,layerIndex] + v = self._var.v[:,layerIndex] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwargs) + + if elevation: + time = self._var.matlabTime[:] + el = self._var.el[:] + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._var.lat + harmo = solve(time, el, None, lat, **kwargs) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, time_ind=slice(None), debug=False, **kwargs): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + - elevation =True means that 'reconstruct' will be done for elevation. + - velocity =True means that 'reconstruct' will be done for velocity. + - time_ind = time indices to process, list of integers + + Output: + - Reconstruct = reconstructed signal, dictionary + + Utide's options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + """ + debug = (debug or self._debug) + time = self._var.matlabTime[time_ind] + #TR_comments: Add debug flag in Utide: debug=self._debug + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + + return Reconstruct + + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v + + def verti_shear(self, t_start=[], t_end=[], time_ind=[], + graph=True, dump=False, debug=False, **kwargs): + """ + Compute vertical shear + + Outputs: + - dveldz = vertical shear (1/s), 2D array (time, nlevel - 1) + + Utide's options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - graph = plots graph if True + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing vertical shear at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Compute depth + depth = self._var.depth[:] + + #Extracting velocity at point + if not argtime==[]: + U = self._var.east_vel[argtime,:] + V = self._var.north_vel[argtime,:] + depths = depth[argtime,:] + else: + U = self._var.east_vel[:,:] + V = self._var.north_vel[:,:] + depths = depth[:,:] + + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + # Compute shear + dz = depths[:,1:] - depths[:,:-1] + dvel = norm[:,1:] - norm[:,:-1] + dveldz = dvel / dz + + if debug: + print '...Passed' + + #Plot mean values + if graph: + mean_depth = np.mean((depths[:,1:] + depths[:,:-1]) / 2.0, axis=0) + mdat = np.ma.masked_array(dveldz,np.isnan(dveldz)) + mean_dveldz = np.mean(mdat,0) + error = np.std(mdat,axis=0) + self._plot.plot_xy(mean_dveldz, mean_depth, xerror=error[:], + title='Shear profile ', + xLabel='Shear (1/s) ', yLabel='Depth (m) ', + dump=dump, **kwargs) + + return dveldz + + def velo_norm(self, t_start=[], t_end=[], time_ind=[], + graph=True, dump=False, debug=False, **kwargs): + """ + Compute the velocity norm + + Outputs: + - velo_norm = velocity norm, 2D array (time, level) + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - graph = plots vertical profile averaged over time if True + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing velocity norm at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Computing velocity norm + if not argtime==[]: + U = self._var.east_vel[argtime, :] + V = self._var.north_vel[argtime, :] + velo_norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + else: + U = self._var.east_vel[:, :] + V = self._var.north_vel[:, :] + velo_norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + if debug: + print '...passed' + + #Plot mean values + if graph: + depth = self._var.depth + mdat = np.ma.masked_array(velo_norm,np.isnan(velo_norm)) + vel = np.mean(mdat,0) + error = np.std(mdat,axis=0) + self._plot.plot_xy(vel, depth.mean(axis=0), xerror=error[:], + title='Velocity norm profile ', + xLabel='Velocity (m/s) ', yLabel='Depth (m) ', + dump=dump, **kwargs) + + + return velo_norm + + def mattime2datetime(self, mattime, debug=False): + """ + Output the time (yyyy-mm-dd, hh:mm:ss) corresponding to + a given matlab time + + Inputs: + - mattime = matlab time (floats) + """ + time = mattime_to_datetime(mattime, debug=debug) + return time + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/adcpClass/plotsAdcp.py b/build/lib/pyseidon_dvt/adcpClass/plotsAdcp.py new file mode 100644 index 0000000..4d99900 --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/plotsAdcp.py @@ -0,0 +1,171 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +import matplotlib.ticker as ticker +import matplotlib.patches as mpatches +import seaborn +import pandas as pd +from windrose import WindroseAxes +from interpolation_utils import * + +class PlotsAdcp: + """ **'Plots' subset of FVCOM class gathers plotting functions**""" + def __init__(self, variable, debug=False): + self._debug = debug + setattr(self, '_var', variable) + + return + + def _def_fig(self): + """Defines figure window""" + self._fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + + def plot_xy(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', dump=False, **kwargs): + """ + Simple X vs Y plot + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + self._def_fig() + self._ax = self._fig.add_subplot(111) + self._ax.plot(x, y, label=title) + scale = 1 + self._ax.set_ylabel(yLabel) + self._ax.set_xlabel(xLabel) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + if not yerror==[]: + self._ax.fill_between(x, y-yerror, y+yerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if not xerror==[]: + self._ax.fill_betweenx(y, x-xerror, x+xerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if (not xerror==[]) or (not yerror==[]): + blue_patch = mpatches.Patch(color='#089FFF', + label='Standard deviation',alpha=0.2) + plt.legend(handles=[blue_patch],loc=1, fontsize=12) + #plt.legend([blue_patch],loc=1, fontsize=12) + + self._fig.show() + if dump: + self._dump_profile_data_as_csv(x, y,xerror=xerror, yerror=yerror, + title=title, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def Histogram(self, y, title=' ', xLabel=' ', yLabel=' ', dump=False, **kwargs): + """ + Histogram plot + + Inputs: + - bins = list of bin edges + - y = 1D array + + Options: + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + self._def_fig() + self._ax = self._fig.add_subplot(111) + density, bins = np.histogram(y, bins=50, normed=True, density=True) + unity_density = density / density.sum() + widths = bins[:-1] - bins[1:] + # To plot correct percentages in the y axis + self._ax.bar(bins[1:], unity_density, width=widths) + formatter = ticker.FuncFormatter(lambda v, pos: str(v * 100)) + self._ax.yaxis.set_major_formatter(formatter) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + + plt.ylabel(yLabel) + plt.xlabel(xLabel) + + self._fig.show() + + if dump: self._dump_profile_data_as_csv(bins[1:], unity_density, + title=title, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def rose_diagram(self, direction, norm): + + """ + Plot rose diagram + + Inputs: + - direction = 1D array + - norm = 1D array + """ + #Convertion + #TR: not quite sure here, seems to change from location to location + # express principal axis in compass + direction = np.mod(90.0 - direction, 360.0) + + #Create new figure + self._def_fig() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(self._fig, rect)#, axisbg='w') + self._fig.add_axes(ax) + #Rose + ax.bar(direction, norm , normed=True, opening=0.8, edgecolor='white') + #adjust legend + l = ax.legend(shadow=True, bbox_to_anchor=[-0.1, 0], loc='lower left') + plt.setp(l.get_texts(), fontsize=10) + plt.xlabel('Rose diagram in % of occurrences - Colormap of norms') + self._fig.show() + + def _dump_profile_data_as_csv(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', **kwargs): + """ + Dumps profile data in csv file + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = file name, string + - xLabel = name of the x-data, string + - yLabel = name of the y-data, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_profile_data' + filename=title + '.csv' + if xLabel == ' ': xLabel = 'X' + if yLabel == ' ': yLabel = 'Y' + if not xerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': xerror[:]}) + elif not yerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': yerror[:]}) + else: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:]}) + df.to_csv(filename, encoding='utf-8', **kwargs) diff --git a/build/lib/pyseidon_dvt/adcpClass/rawADCPclass.py b/build/lib/pyseidon_dvt/adcpClass/rawADCPclass.py new file mode 100644 index 0000000..c638e43 --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/rawADCPclass.py @@ -0,0 +1,116 @@ +from __future__ import division +import scipy.io as sio +import h5py +from os import path + +class Struct: + def __init__(self, **entries): + self.__dict__.update(entries) + + +class rawADCP: + def __init__(self, filename): + self.QC = ['raw data'] + self.load(filename) + self.Params_Stn4_SWNSreport(filename) + self.load_rbrdata() + + ## set options + self.options = {} + self.options['showPA'] = 1 + self.options['showRBRavg'] = 1 + + ## save a flow file in BPformat + #save_FlowFile_BPFormat(fileinfo,adcp,rbr,saveparams,options) + + return + + def load(self, filename): + + try: + self.mat = sio.loadmat(filename, + struct_as_record=False, squeeze_me=True) + + self.adcp = self.mat['adcp'] + + except NotImplementedError: + self.mat = h5py.File(filename) + self.adcp = self.mat['adcp'] + #self.adcp = Struct(**self.mat['adcp']) + + def Params_Stn4_SWNSreport(self, filename): + fname = filename.split('/') + filebase = fname[-1].split('_')[0] + self.fileinfo = {} + self.fileinfo['datadir'] = path.join(*fname[:-1]) + '/' + self.fileinfo['ADCP'] = filebase + '_raw' + self.fileinfo['outdir'] = path.join(*fname[:-1]) + '/' + self.fileinfo['flowfile'] = filebase + '_Flow' + self.fileinfo['rbr']= 'station4_grandPassageII_RBRSN_011857.mat' + self.fileinfo['paramfile']= 'Params_Stn4_SWNSreport' + + #%% ADCP parameters + self.saveparams = {} + self.saveparams['tmin'] = 209 + self.saveparams['tmax'] = 240 + self.saveparams['zmin'] = 0 + self.saveparams['zmax'] = 20 + self.saveparams['approxdepth'] = 15.5 + self.saveparams['flooddir'] = 0 + self.saveparams['declination'] = -17.25 + self.saveparams['lat'] = 44.2605 + self.saveparams['lon'] = -66.3354 + self.saveparams['dabADCP'] = 0.5 + self.saveparams['dabPS'] = -0.6 + self.saveparams['rbr_hr_offset'] = 3 + + def load_rbrdata(self): + rbrFile = self.fileinfo['datadir'] + self.fileinfo['rbr'] + + try: + rbrMat = sio.loadmat(rbrFile, + struct_as_record=False, squeeze_me=True) + + except NotImplementedError: + rbrMat = h5py.File(rbrFile) + + rbr = rbrMat['rbr'] + rbrout = {} + rbrout['mtime'] = rbr.yd + + rbrout['temp'] = rbr.temperature + rbrout['pres'] = rbr.pressure + rbrout['depth'] = rbr.depth + rbrout['mtime'] = rbr.yd + self.rbr = rbrout + +if __name__ == '__main__': + #filename = 'GP-120726-BPd_raw.mat' + filename = '140703-EcoEII_database/data/GP-120726-BPd_raw.mat' + data = rawADCP(filename) + + + + +#stn = 'GP-120726-BPd'; +#%% File information +#fileinfo.datadir = '../data/'; %path to raw data files +#fileinfo.ADCP = [stn '_raw']; %name of ADCP file +#fileinfo.outdir = '../data/'; %path to output directory +#fileinfo.flowfile = [stn,'_Flow']; %name of output file with Flow data +#fileinfo.rbr = ['station4_grandPassageII_RBRSN_011857.mat']; +#fileinfo.paramfile = mfilename; +# +#%% ADCP parameters +#saveparams.tmin = 209; %tmin (year day) +#saveparams.tmax = 240; %tmax (year day) +#saveparams.zmin = 0; %minimum z to include in saves file +#saveparams.zmax = 20; +#saveparams.approxdepth = 15.5; %Approximate depth +#saveparams.flooddir= 0; %Flood direction (relative to true north, CW is positive) +#saveparams.declination = -17.25;%Declination angle +#saveparams.lat = 44.2605; %latitude +#saveparams.lon = -66.3354; %longitude +#saveparams.dabADCP = 0.5; %depth above bottom of ADCP +#saveparams.dabPS = -0.6; %depth above bottom of pressure sensor +#saveparams.rbr_hr_offset = 3; % hour offset to convert rbr time to UTC diff --git a/build/lib/pyseidon_dvt/adcpClass/variablesAdcp.py b/build/lib/pyseidon_dvt/adcpClass/variablesAdcp.py new file mode 100644 index 0000000..f72c0f7 --- /dev/null +++ b/build/lib/pyseidon_dvt/adcpClass/variablesAdcp.py @@ -0,0 +1,225 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +from numpy.ma import MaskError +import h5py +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +# Custom error +from pyseidon_error import PyseidonError + +class _load_adcp: + """ + **'Variables' subset in ADCP class** + + It contains the following numpy arrays: :: + + _bins = depth of measurement bins, 1D array, shape=(bins) + |_depth = depth, negative from surface down, time serie, 2D array, shape=(time,bins) + |_dir_vel = velocity direction time serie, 2D array, shape=(time,bins) + |_east_vel = East velocity time serie, 2D array, shape=(time,bins) + |_lat = latitude, float, decimal degrees + |_lon = lontitude, float, decimal degrees + |_mag_signed_vel = signed velocity time serie, 2D array, shape=(time,bins) + |_matlabTime = matlab time, 1D array, shape=(time) + ADCP.Variables._|_north_vel = East velocity time serie, 2D array, shape=(time,bins) + |_percent_of_depth = percent of the water column measured by ADCP, float + |_surf = pressure at surface timeserie, 1D array, shape=(time) + |_el = elevation (m) at surface timeserie, 1D array, shape=(time) + |_ua = depth averaged velocity component timeserie, 1D array, shape=(time) + |_va = depth averaged velocity component timeserie, 1D array, shape=(time) + |_ucross = ???, 1D array, shape=(time) + |_ualong = ???, 1D array, shape=(time) + + """ + def __init__(self,cls, History, debug=False): + if debug: + print 'Loading variables...' + # Pointer to History + setattr(self, '_History', History) + + + # TR: fudge factor, squeeze out the 5 top % of the water column + self.percent_of_depth=0.95 + + # Convert some mat_struct objects into a dictionaries. + # This will enable compatible syntax to h5py files. + if not type(cls.Data) == h5py._hl.files.File: + try: + cls.Data['data'] = cls.Data['data'].__dict__ + except KeyError: + raise PyseidonError("Missing 'data' field from ADCP file.") + for key in cls.Data['data']: + if key is not '_fieldnames': + # in ther is a mat_struct in the mat_struct, like 'surf' + if hasattr(cls.Data['data'][key], '__module__') and \ + cls.Data['data'][key].__module__ == 'scipy.io.matlab.mio5_params': + cls.Data['data'][key] = cls.Data['data'][key].__dict__ + for kk in cls.Data['data'][key]: + if kk is not '_fieldnames': + cls.Data['data'][key][kk] = cls.Data['data'][key][kk].T + else: + cls.Data['data'][key] = cls.Data['data'][key].T + + # Convert other fields to dictionaries if they exist. + if 'pres' in cls.Data: + cls.Data['pres']=cls.Data['pres'].__dict__ + if 'time' in cls.Data: + cls.Data['time']=cls.Data['time'].__dict__ + + + try: + self.lat = np.ravel(cls.Data['lat'])[0] + self.lon = np.ravel(cls.Data['lon'])[0] + except KeyError: + if debug: + print 'Missing lon and/or lat data' + + try: + self.bins = cls.Data['data']['bins'][:].ravel() + except KeyError: + if debug: + print 'Missing bins' + + try: + self.north_vel = cls.Data['data']['north_vel'][:].T + self.east_vel = cls.Data['data']['east_vel'][:].T + self.v = self.north_vel + self.u = self.east_vel + try: + self.north_vel=np.ma.masked_array(self.north_vel,np.isnan(self.north_vel)) + self.east_vel=np.ma.masked_array(self.east_vel,np.isnan(self.east_vel)) + self.v=self.north_vel + self.u=self.east_vel + except MaskError: + print 'Failed to mask horizontal velocities (north_vel, east_vel)' + except KeyError: + if debug: + print 'Missing horizontal velocities (north_vel, east_vel)' + + try: + self.vert_vel = cls.Data['data']['vert_vel'][:].T + try: + self.vert_vel=np.ma.masked_array(self.vert_vel,np.isnan(self.vert_vel)) + except MaskError: + print 'Failed to mask vertical velocity (vert_vel)' + except KeyError: + if debug: + print 'Missing vertical velocity (vert_vel)' + + try: + self.dir_vel = cls.Data['data']['dir_vel'][:].T + try: + self.dir_vel=np.ma.masked_array(self.dir_vel,np.isnan(self.dir_vel)) + except MaskError: + print 'Failed to mask dir_vel' + except KeyError: + if debug: + print 'Missing dir_vel' + + try: + self.mag_signed_vel = cls.Data['data']['mag_signed_vel'][:].T + try: + self.mag_signed_vel=np.ma.masked_array(self.mag_signed_vel,np.isnan(self.mag_signed_vel)) + except MaskError: + print 'Failed to mask mag_signed_vel' + except KeyError: + if debug: + print 'Missing mag_signed_vel' + + try: + self.pressure = cls.Data['pres'] + self.surf = self.pressure['surf'][:].ravel() + self.el = self.surf + except KeyError: + if debug: + print 'Missing elevation data (pres.surf)' + + try: + self.time = cls.Data['time'] + self.matlabTime = self.time['mtime'][:].ravel() + except KeyError: + if debug: + print 'Missing time data (time.mtime)' + + try: + self.ucross = cls.Data['data']['Ucross'][:].T + self.ualong = cls.Data['data']['Ualong'][:].T + except KeyError: + if debug: + print 'Missing along/cross velocities (Ucross, Ualong)' + + try: + self.ua = cls.Data['data']['ua'][:].T + self.va = cls.Data['data']['va'][:].T + try: + self.ua=np.ma.masked_array(self.ua,np.isnan(self.ua)) + self.va=np.ma.masked_array(self.va,np.isnan(self.va)) + except MaskError: + print 'Failed to mask depth averaged velocities (ua, va)' + except KeyError: + if debug: + print 'Missing depth averaged velocities (ua, va)' + + + #-Append message to History field + try: + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + except AttributeError: + if debug: + print 'Missing time variable failed to add history note' + + #Find the depth average of a variable based on percent_of_depth + #choosen by the user but only if not loaded from file directly. + # Currently only working for east_vel (u) and north_vel (v) + if (('ua' not in dir(self)) and ('va' not in dir(self))): + try: + #TR: alternative with percent of the depth + ind = np.argwhere(self.bins < self.percent_of_depth * self.surf[:,np.newaxis]) + #ind = np.argwhere(self.bins < self.surf[:,np.newaxis]) + index = ind[np.r_[ind[1:,0] != ind[:-1,0], True]] + try: + data_ma_u = np.ma.array(self.east_vel, + mask=np.arange(self.east_vel.shape[1]) > index[:, 1, np.newaxis]) + data_ma_u=np.ma.masked_array(data_ma_u,np.isnan(data_ma_u)) + except MaskError: + data_ma_u=np.ma.masked_array(self.east_vel,np.isnan(self.east_vel)) + + try: + data_ma_v = np.ma.array(self.north_vel, + mask=np.arange(self.north_vel.shape[1]) > index[:, 1, np.newaxis]) + data_ma_v=np.ma.masked_array(data_ma_v,np.isnan(data_ma_v)) + except MaskError: + data_ma_v=np.ma.masked_array(self.north_vel,np.isnan(self.north_vel)) + + self.ua = np.array(data_ma_u.mean(axis=1)) + self.va = np.array(data_ma_v.mean(axis=1)) + except AttributeError: + if debug: + print 'Missing atleast one variable required ' + \ + 'to compute depth averaged velocities' + + + # Compute depth with fvcom convention, negative from surface down + try: + self.depth = np.ones(self.north_vel.shape) * np.nan + for t in range(self.matlabTime.shape[0]): + #i = np.where(np.isnan(self.north_vel[t,:]))[0][0] + #z = self.bins[i] + self.depth[t, :] = self.bins[:] - self.surf[t] + self.depth[np.where(self.depth>0.0)] = np.nan + except AttributeError: + if debug: + print 'Missing atleast one variable required ' + \ + 'to compute depth with fvcom convention' + + if debug: + print '...Passed' + + return diff --git a/build/lib/pyseidon_dvt/drifterClass/__init__.py b/build/lib/pyseidon_dvt/drifterClass/__init__.py new file mode 100644 index 0000000..ff1dde5 --- /dev/null +++ b/build/lib/pyseidon_dvt/drifterClass/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Local import +from drifterClass import Drifter + +__authors__ = ['Kody Crowell, Thomas Roc, Wesley Bowman, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/drifterClass/drifterClass.py b/build/lib/pyseidon_dvt/drifterClass/drifterClass.py new file mode 100644 index 0000000..c6b21f2 --- /dev/null +++ b/build/lib/pyseidon_dvt/drifterClass/drifterClass.py @@ -0,0 +1,86 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import scipy.io as sio +import h5py + +#Local import +from variablesDrifter import _load_drifter +# from functionsDrifter import FunctionsDrifter +from plotsDrifter import * + + +class Drifter: + """ + **A class/structure for Drifter data** + + Functionality structured as follows: :: + + _Data. = raw matlab file data + |_Variables. = useable drifter variables and quantities + |_History = Quality Control metadata + testAdcp._|_Utils. = set of useful functions + |_Plots. = plotting functions + |_method_1 + | ... = methods and analysis techniques intrinsic to drifters + |_method_n + + Inputs: + - Only takes a file name as input, ex: testDrifter=Drifter('./path_to_matlab_file/filename') + + Notes: + Only handle fully processed drifter matlab data previously quality-controlled at the mo. + + Throughout the package, the following conventions apply: + - Coordinates = decimal degrees East and North + - Directions = in degrees, between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - Depth = 0m is the free surface and depth is negative + """ + def __init__(self, filename, debug=False): + """ Initialize Drifter class. + Notes: only handle processed Drifter matlab data at the mo.""" + self._debug = debug + self._origin_file = filename + if debug: + print '-Debug mode on-' + #Load data from Matlab file + #TR_comments: find a way to dissociate raw and processed data + #TR_comments: *_Raw and *_10minavg open with h5py whereas *_davgBS + try: + self.Data = sio.loadmat(filename,struct_as_record=False, squeeze_me=True) + except NotImplementedError: + self.Data = h5py.File(filename) + + #Store info in "History" field + self.History = ['Created from ' + filename] + + # KC comment: for some reason, some drifter MATLAB files + # have 'Comments' as a key in the variables structure, + # while others have 'comments' as a key. + if debug: print '-adding comments to History-' + + if 'Comments' in self.Data: + for comment in self.Data['Comments'][:]: + self.History.append(str(comment)) + elif 'comments' in self.Data: + for comment in self.Data['comments'][:]: + self.History.append(str(comment)) + elif debug: + print '-no comments found-' + + + #Initialize class structure + self.Variables = _load_drifter(self, self.History, debug=self._debug) + self.Plots = PlotsDrifter(self.Variables, debug=self._debug) + #self.Utils = FunctionsAdcp(self.Variables, + # self.Plots, + # self.History, + # debug=self._debug) + + ##Re-assignement of utility functions as methods + self.dump_data_as_csv = self.Plots._dump_data_as_csv + + return \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/drifterClass/functionsDrifter.py b/build/lib/pyseidon_dvt/drifterClass/functionsDrifter.py new file mode 100644 index 0000000..c8348c5 --- /dev/null +++ b/build/lib/pyseidon_dvt/drifterClass/functionsDrifter.py @@ -0,0 +1,20 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +class FunctionsDrifter: + """**'Utils' subset of Tidegauge class gathers useful functions**""" + def __init__(self,cls): + self._var = cls.Variables + self._debug = cls._debug + + return + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/drifterClass/plotsDrifter.py b/build/lib/pyseidon_dvt/drifterClass/plotsDrifter.py new file mode 100644 index 0000000..1e8991b --- /dev/null +++ b/build/lib/pyseidon_dvt/drifterClass/plotsDrifter.py @@ -0,0 +1,122 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +import seaborn +import pandas as pd + +class PlotsDrifter: + """ + **'Plots' subset of Drifter class gathers plotting functions** + """ + def __init__(self, variable, debug): + self._debug = debug + # Pointer + setattr(self, '_var', variable) + + return + + def _def_fig(self): + """Defines figure window""" + self._fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + + def trajectories(self, title='Drifter trajectories & speed (m/s)', + cmin=[], cmax=[], debug=False, dump=False, **kwargs): + """ + 2D xy colormap plot of all the trajectories. + Colors represent the drifter velocity + + Options: + - title = plot title, string + - cmin = minimum limit colorbar + - cmax = maximum limit colorbar + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + debug = debug or self._debug + if debug: print "Plotting drifter's trajectories..." + lon = self._var.lon + lat = self._var.lat + + #Compute drifter's speeds + u=self._var.u + v=self._var.v + norm=np.sqrt(u**2.0 + v**2.0) + + #setting limits and levels of colormap + if cmin==[]: + if debug: + print "Computing cmin..." + cmin=norm[:].min() + if cmax==[]: + if debug: + print "Computing cmax..." + cmax=norm[:].max() + + #Figure window params + self._def_fig() + self._ax = self._fig.add_subplot(111,aspect=(1.0/np.cos(np.mean(lat)*np.pi/180.0))) + + #Scatter plot + #sc = plt.scatter(lon, lat, c=norm, lw=0, cmap=plt.cm.gist_earth) + #sc.set_clim([cmin,cmax]) + + #Quiver plot + sc = plt.quiver(lon, lat, u, v, norm, lw=0.0, scale=100.0, + cmap=plt.cm.gist_earth) + sc.set_clim([cmin,cmax]) + + #Label and axis parameters + self._ax.set_ylabel('Latitude') + self._ax.set_xlabel('Longitude') + plt.gca().patch.set_facecolor('0.5') + cbar=self._fig.colorbar(sc,ax=self._ax) + cbar.set_label(title, rotation=-90,labelpad=30) + scale = 1 + ticks = ticker.FuncFormatter(lambda lon, pos: '{0:g}'.format(lon/scale)) + self._ax.xaxis.set_major_formatter(ticks) + self._ax.yaxis.set_major_formatter(ticks) + self._ax.grid() + self._fig.show() + if dump: + self._dump_data_as_csv(norm, u, v, lon, lat, title='drifter_velocity', **kwargs) + if debug or self._debug: + print '...Passed' + + def _dump_data_as_csv(self, var1, var2, var3, x, y, title=' ', **kwargs): + """ + Dumps map data in csv file + + Inputs: + - var1 = 1 D numpy array oh n elements + - var2 = 1 D numpy array oh n elements + - var3 = 1 D numpy array oh n elements + - x = coordinates, 1 D numpy array (nele or nnode) + - y = coordinates, 1 D numpy array (nele or nnode) + + Options: + - title = file name, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_map_data' + filename=title + '.csv' + df = pd.DataFrame({'norm': var1, 'u':var2, 'v':var3, + 'lon':x[:], 'lat':y[:]}) + + df.to_csv(filename, encoding='utf-8', **kwargs) + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/drifterClass/variablesDrifter.py b/build/lib/pyseidon_dvt/drifterClass/variablesDrifter.py new file mode 100644 index 0000000..1b2a43b --- /dev/null +++ b/build/lib/pyseidon_dvt/drifterClass/variablesDrifter.py @@ -0,0 +1,73 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +# from numpy.ma import MaskError +# import h5py +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime +import sys + +class _load_drifter: + """ + **'Variables' subset in Tidegauge class** + It contains the following numpy arrays: :: + + _u = u velocity component (m/s), 1D array (ntime) + |_v = v velocity component (m/s), 1D array (ntime) + Drifter.Variables._|_matlabTime = matlab time, 1D array (ntime) + |_lon = longitudes (deg.), 1D array (ntime) + |_lat = latitudes (deg.), 1D array (ntime) + + """ + def __init__(self,cls, History, debug=False): + if debug: + print 'Loading variables...' + # Pointer to History + setattr(self, '_History', History) + try: + self.matlabTime = cls.Data['velocity'].vel_time[:] + #Sorting values with increasing time step + sortedInd = self.matlabTime.argsort() + self.matlabTime.sort() + self.lat = cls.Data['velocity'].vel_lat[sortedInd] + self.lon = cls.Data['velocity'].vel_lon[sortedInd] + self.u = cls.Data['velocity'].u[sortedInd] + self.v = cls.Data['velocity'].v[sortedInd] + # Luna Ocean Consulting Ltd. new drifter format + except KeyError: + self.matlabTime = cls.Data['time'] + self.original = {} + self.original['lat'] = cls.Data['lat'] + self.original['lon'] = cls.Data['lon'] + self.original['u'] = cls.Data['u'] + self.original['v'] = cls.Data['v'] + self.original['drift_start'] = cls.Data['drift_start'] + self.original['drift_stop'] = cls.Data['drift_stop'] + self.smooth = {} + self.smooth['lat'] = cls.Data['lat_smooth'] + self.smooth['lon'] = cls.Data['lon_smooth'] + self.smooth['u'] = cls.Data['u_smooth'] + self.smooth['v'] = cls.Data['v_smooth'] + self.smooth['drift_start'] = cls.Data['drift_start_smooth'] + self.smooth['drift_stop'] = cls.Data['drift_stop_smooth'] + except: + sys.exit('Drifter file format incompatible') + + #-Append message to History field + try: + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + except ValueError: + start = mattime_to_datetime(np.nanmin(self.matlabTime)) + end = mattime_to_datetime(np.nanmax(self.matlabTime)) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + + if debug: print '...Passed' + + return diff --git a/build/lib/pyseidon_dvt/fvcomClass/__init__.py b/build/lib/pyseidon_dvt/fvcomClass/__init__.py new file mode 100644 index 0000000..93446d3 --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Local import +from fvcomClass import FVCOM + +__authors__ = ['Thomas Roc, Wesley Bowman, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/fvcomClass/functionsFvcom.py b/build/lib/pyseidon_dvt/fvcomClass/functionsFvcom.py new file mode 100644 index 0000000..6458786 --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/functionsFvcom.py @@ -0,0 +1,1160 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +from scipy.interpolate import interp1d +import numexpr as ne +import datetime +from pyseidon_dvt.utilities.interpolation_utils import * +from pyseidon_dvt.utilities.miscellaneous import * +from pyseidon_dvt.utilities.BP_tools import * +from utide import solve, reconstruct +import time +import matplotlib.tri as Tri +from pydap.exceptions import ServerError +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +class FunctionsFvcom: + """ + **'Util2D' subset of FVCOM class gathers useful functions and methods for 2D and 3D runs** + """ + def __init__(self, variable, grid, plot, History, debug): + self._debug = debug + self._plot = plot + #Create pointer to FVCOM class + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + setattr(self, '_History', History) + + return + + #TR comment: I don't think I need this anymore + def _centers(self, debug=False): + """ + Create new variable 'bathy and elevation at center points' (m) + -> FVCOM.Grid.hc, elc + + *Notes* + - Can take time over the full domain + """ + debug = debug or self._debug + if debug: + print 'Computing central bathy...' + + #Interpolation at centers + size = self._grid.nele + size1 = self._grid.ntime + elc = np.zeros((size1, size)) + hc = np.zeros((size)) + #TR comment: I am dubeous about the interpolation method here + for ind, value in enumerate(self._grid.trinodes[:]): + value.sort()#due to new version of netCDF4 + elc[:, ind] = np.mean(self._var.el[:, value], axis=1) + hc[ind] = np.mean(self._grid.h[value]) + + #Custom return + setattr(self._grid, 'hc', hc) + setattr(self._var, 'elc', elc) + + # Add metadata entry + self._History.append('bathymetry at center points computed') + self._History.append('elevation at center points computed') + print '-Central bathy and elevation added to FVCOM.Grid.-' + + if debug: + print '...Passed' + + def slope(self, debug=False): + """ + This method computes a new variable: 'bathymetric slope' (degrees) + -> FVCOM.Grid.slope + """ + x = self._grid.x[:] + y = self._grid.y[:] + if not hasattr(self._grid, 'triangleXY'): + # Mesh triangle + if debug: + print "Computing triangulation..." + trinodes = self._grid.trinodes[:] + tri = Tri.Triangulation(x, y, triangles=trinodes) + self._grid.triangleXY = tri + else: + tri = self._grid.triangleXY + + if debug: print "Cubic interpolation..." + try: + tci = Tri.CubicTriInterpolator(tri, self._grid.h[:]) + except ValueError: # quick fix for library incompatibility on Acadia's server + tci = Tri.CubicTriInterpolator(tri, self._grid.h[:].copy()) + (Ex, Ey) = tci.gradient(tri.x, tri.y) + slope = np.sqrt(Ex**2 + Ey**2) + + if debug: print "Conversion to degrees..." + slope = np.rad2deg(np.arctan(slope)) + + # Custom return + setattr(self._grid, 'slope', slope) + + # Add metadata entry + self._History.append('bathymetric slope computed') + print '-Bathymetric slope added to FVCOM.Grid.-' + + def hori_velo_norm(self, debug=False): + """ + This method computes a new variable: 'horizontal velocity norm' (m/s) + -> FVCOM.Variables.hori_velo_norm + + Notes: + - Can take time over the full domain + """ + debug = debug or self._debug + if debug: + print 'Computing horizontal velocity norm...' + + try: + u = self._var.ua[:] + v = self._var.va[:] + vel = ne.evaluate('sqrt(u**2 + v**2)').squeeze() + + except (MemoryError, ServerError) as e: + if e == ServerError: + print '---Data too large for server---' + print 'Tip: Save data on your machine or use partial data' + elif e == MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + #Custom return + setattr(self._var, 'hori_velo_norm', vel) + + # Add metadata entry + self._History.append('horizontal velocity norm computed') + print '-Horizontal velocity norm added to FVCOM.Variables.-' + + if debug: + print '...Passed' + + def flow_dir(self, debug=False): + """" + This method create new variable 'depth averaged flow directions' (deg.) + -> FVCOM.Variables.depth_av_flow_dir + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, +/-180=West, -90=South + - Can take time over the full domain + """ + if debug or self._debug: + print 'Computing flow directions...' + + try: + u = self._var.ua[:] + v = self._var.va[:] + dirFlow = np.rad2deg(np.arctan2(v,u)) + + except (MemoryError, ServerError) as e: + if e == ServerError: + print '---Data too large for server---' + print 'Tip: save data on your machine or use partial data' + elif e == MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + #Custom return + setattr(self._var, 'depth_av_flow_dir', dirFlow) + + # Add metadata entry + self._History.append('depth averaged flow directions computed') + print '-Depth averaged flow directions added to FVCOM.Variables.-' + + if debug or self._debug: + print '...Passed' + + def flow_dir_at_point(self, pt_lon, pt_lat, t_start=[], t_end=[], time_ind=[], + graph=True, exceedance=False, debug=False): + """ + This function computes flow directions and associated norm + at any give location. + + Inputs: + - pt_lon = longitude in decimal degrees East to find, float number + - pt_lat = latitude in decimal degrees North to find, float number + + Outputs: + - flowDir = flowDir at (pt_lon, pt_lat), 1D array + - norm = velocity norm at (pt_lon, pt_lat), 1D array + + Keywords: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - excedance = True, compute associated exceedance curve + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, +/-180=West, -90=South + """ + debug = debug or self._debug + if debug: + print 'Computing flow directions at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib + u = self._var.ua[:] + v = self._var.va[:] + else: + u = self._var.ua + v = self._var.va + + #Extraction at point + # Finding closest point + index = self.index_finder(pt_lon, pt_lat, debug=False) + if debug: + print 'Extraction of u and v at point...' + U = self.interpolation_at_point(u, pt_lon, pt_lat, index=index, + debug=debug) + V = self.interpolation_at_point(v, pt_lon, pt_lat, index=index, + debug=debug) + + #Compute directions + if debug: + print 'Computing arctan2 and norm...' + dirFlow = np.rad2deg(np.arctan2(V,U)) + + #Compute velocity norm + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + #use only the time indices of interest + if not argtime==[]: + dirFlow = dirFlow[argtime[:]] + norm = norm[argtime[:]] + + if debug: + print '...Passed' + #Rose diagram + if graph: + self._plot.rose_diagram(dirFlow, norm) + if exceedance: + self.exceedance(norm, graph=True, debug=debug) + + return dirFlow, norm + + def bidirectionality_at_point(self, pt_lon, pt_lat, debug=False): + """" + This function computes the depth averaged bidirectionality (deg.) at any given point + + Inputs: + - pt_lon = longitude in decimal degrees East of the reference point, float number + - pt_lat = latitude in decimal degrees North of the reference point, float number + + Outputs: + - bidir = 1D array of depth averaged bidirectionality, (nele) + + *Notes* + - bidirectionality between 0 and 90 deg., i.e. 0=perfect alignment, 90 = perpendicular abb and flood + - bidirectionality is weighted by the flow speed to filter out slack water + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing bidirectonality...' + ##compute necessary fields + if not hasattr(self._var, 'hori_velo_norm'): + self.hori_velo_norm(debug=debug) + if not hasattr(self._var, 'depth_av_flow_dir'): + self.flow_dir(debug=debug) + ##Compute weights, function of flow velocity + #weights + fI,eI,pa,pav=self.ebb_flood_split_at_point(pt_lon,pt_lat, debug=debug) + weightF=self._var.hori_velo_norm[fI,:]/np.nansum(self._var.hori_velo_norm[fI,:],0) + weightE=self._var.hori_velo_norm[eI,:]/np.nansum(self._var.hori_velo_norm[eI,:],0) + #weighted directions + dirF=np.nansum(self._var.depth_av_flow_dir[fI,:]*weightF,0) + dirF[dirF < 0] += 180.0 + dirE=np.nansum(self._var.depth_av_flow_dir[eI,:]*weightE,0) + dirE[dirE < 0] += 180.0 + ##keep angle between 0-90 deg. + bidir = (dirF - dirE) + bidir[bidir < 0] += 180.0 + bidir[bidir > 90] = 180.0 - bidir[bidir > 90] + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + return bidir + + def ebb_flood_split_at_point(self, pt_lon, pt_lat, + t_start=[], t_end=[], time_ind=[], debug=False): + """ + This functions computes time indices for ebb and flood but also the + principal flow directions and associated variances + at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East to find, float number + - pt_lat = latitude in decimal degrees North to find,float number + + Outputs: + - floodIndex = flood time index, 1D array of integers + - ebbIndex = ebb time index, 1D array of integers + - pr_axis = principal flow ax1s, float number in degrees + - pr_ax_var = associated variance, float number + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, 1D array of integers + + *Notes* + - may take time to compute if time period too long + - directions between -180 and 180 deg., i.e. 0=East, 90=North, +/-180=West, -90=South + - use time_ind or t_start and t_end, not both + - assume that flood is aligned with principal direction + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing principal flow directions...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib + u = self._var.ua[:] + v = self._var.va[:] + else: + u = self._var.ua + v = self._var.va + + #Extraction at point + # Finding closest point + index = self.index_finder(pt_lon, pt_lat, debug=False) + if debug: + print 'Extraction of u and v at point...' + U = self.interpolation_at_point(u, pt_lon, pt_lat, index=index, + debug=debug) + V = self.interpolation_at_point(v, pt_lon, pt_lat, index=index, + debug=debug) + + #use only the time indices of interest + if not argtime==[]: + U = U[argtime[:]] + V = V[argtime[:]] + + # WB version of BP's principal axis + # Assuming principal axis = flood heading + # determine principal axes - potentially a problem if axes are very kinked + # since this would misclassify part of ebb and flood + if debug: print 'Computing principal axis at point...' + pr_axis, pr_ax_var = principal_axis(U, V) + + if debug: print 'Computing ebb/flood intervals...' + #Defines interval + dir_all = np.rad2deg(np.arctan2(V,U)) + ind = np.where(dir_all < 0) + dir_all[ind] = dir_all[ind] + 360 + + # sign speed - eliminating wrap-around + dir_PA = dir_all - pr_axis + dir_PA[dir_PA < -90] += 360 + dir_PA[dir_PA > 270] -= 360 + + #general direction of flood passed as input argument + floodIndex = np.where((dir_PA >= -90) & (dir_PA < 90)) + ebbIndex = np.arange(dir_PA.shape[0]) + ebbIndex = np.delete(ebbIndex, floodIndex[:]) + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + return floodIndex[0], ebbIndex, pr_axis, pr_ax_var + + def speed_histogram(self, pt_lon, pt_lat, t_start=[], t_end=[], time_ind=[], bins=50, + debug=False, dump=False, **kwargs): + """ + This function plots the histogram of occurrences for the signed + flow speed at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East to find, float number + - pt_lat = latitude in decimal degrees North to find,float number + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, 1D array of integers + - bins = number of bins, integer + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing speed histogram...' + + pI, nI, pa, pav = self.ebb_flood_split_at_point(pt_lon, pt_lat, + t_start=t_start, t_end=t_end, time_ind=time_ind, + debug=debug) + dirFlow, norm = self.flow_dir_at_point(pt_lon, pt_lat, + t_start=t_start, t_end=t_end, time_ind=time_ind, + exceedance=False, debug=debug) + norm[nI] = -1.0 * norm[nI] + + #compute bins + #minBound = norm.min() + #maxBound = norm.max() + #step = round((maxBound-minBound/51.0),1) + #bins = np.arange(minBound,maxBound,step) + + #plot histogram + self._plot.Histogram(norm, + title='Flow speed histogram', + xLabel='Signed flow speed (m/s)', + yLabel='Occurrences (%)', + bins=int(bins), + dump=dump, **kwargs) + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + def index_finder(self, pt_lon, pt_lat, debug=False): + """ + Finds closest node index of any given point + + Inputs: + - pt_lon = longitude in decimal degrees East to find, float number + - pt_lat = latitude in decimal degrees North to find, float number + + Option: + - debug = debug flag, boolean + + Output: + - index = integer if within a triangle, -1 if outside of domain + """ + # Checking if point in domain + if not hasattr(self._grid, 'triangleLL'): + # Mesh triangle + if debug: print "Computing triangulation..." + tri = Tri.Triangulation(self._grid.lon[:], self._grid.lat[:], triangles=self._grid.trinodes[:]) + self._grid.triangleLL = tri + finder = self._grid.triangleLL.get_trifinder() + else: + finder = self._grid.triangleLL.get_trifinder() + index = int(finder(pt_lon,pt_lat)) + + return index + + def interpolation_at_point(self, var, pt_lon, pt_lat, index=[], nn=True, debug=False): + """ + This function interpolates any given variables at any give location. + + Inputs: + - var = any FVCOM grid data or variable, numpy array + - pt_lon = longitude in decimal degrees East to find, float number + - pt_lat = latitude in decimal degrees North to find, float number + + Outputs: + - varInterp = var interpolated at (pt_lon, pt_lat) + + Options: + - index = element index, integer. Use only if closest element index + is already known + - nn = if True then use the nearest location in the grid if the location is outside the grid. + + *Notes* + - use index if closest element already known + """ + debug = (debug or self._debug) + if debug: + print 'Interpolaling at point...' + if debug: start = time.time() + + if index == []: + index = self.index_finder(pt_lon, pt_lat, debug=False) + + if ((index == -1) and (nn==False)): + # nan array if outside of domain + varInterp = np.ones(var.shape[:-1]) * np.nan + elif ((index == -1) and (nn==True)): + #outside of domain with nn true use the closest node or element + if var.shape[-1] == self._grid.nnode: + idx=np.argmin((self._grid.lon[:]-pt_lon)**2+(self._grid.lat[:]-pt_lat)**2) + varInterp = var[:,idx] + obsloc=[pt_lon, pt_lat] + simloc=[self._grid.lon[idx], self._grid.lat[idx]] + print 'Using nearest location {} m from observation location'.format(distance(simloc, obsloc)) + else: + idx=np.argmin((self._grid.lonc[:]-pt_lon)**2+(self._grid.latc[:]-pt_lat)**2) + varInterp = var[:,idx] + obsloc=[pt_lon, pt_lat] + simloc=[self._grid.lonc[idx], self._grid.latc[idx]] + print 'Using nearest location {} m from observation location'.format(distance(simloc, obsloc)) + else: + lon = self._grid.lon + lat = self._grid.lat + trinodes = self._grid.trinodes + if type(index)==list: + index = index[0] + #Mitchell's method to convert deg. coordinates to relative coordinates in meters + lonweight = (lon[int(trinodes[index,0])]\ + + lon[int(trinodes[index,1])]\ + + lon[int(trinodes[index,2])]) / 3.0 + latweight = (lat[int(trinodes[index,0])]\ + + lat[int(trinodes[index,1])]\ + + lat[int(trinodes[index,2])]) / 3.0 + TPI=111194.92664455874 # earth radius * pi/180.0 + pt_y = TPI * (pt_lat - latweight) + dx_sph = pt_lon - lonweight + if (dx_sph > 180.0): + dx_sph=dx_sph-360.0 + elif (dx_sph < -180.0): + dx_sph =dx_sph+360.0 + pt_x = TPI * np.cos(np.deg2rad(pt_lat + latweight)*0.5) * dx_sph + + if debug: print "coordinates in meters: ", pt_x, pt_y + + if var.shape[-1] == self._grid.nnode: + varInterp = interpN_at_pt(var, pt_x, pt_y, index, trinodes, + self._grid.aw0, self._grid.awx, + self._grid.awy, debug=debug) + else: + triele = self._grid.triele[:] + varInterp = interpE_at_pt(var, pt_x, pt_y, index, triele, + self._grid.a1u, self._grid.a2u, + debug=debug) + + if debug: + end = time.time() + print "Processing time: ", (end - start) + + return varInterp + + def degree2metric_coordinates(self, pt_lon, pt_lat): + """ + Converts degree coordinates to relative coordinates in meters + + Inputs: + - pt_lon = longitude in deg., float + - pt_lat = latitude in deg., float + Outputs: + - pt_x = longitude in m, float + - pt_y = latitude in m, float + """ + index = self.index_finder(pt_lon, pt_lat, debug=False) + if type(index)==list: + index = index[0] + + lon = self._grid.lon + lat = self._grid.lat + trinodes = self._grid.trinodes + + lonweight = (lon[int(trinodes[index,0])]\ + + lon[int(trinodes[index,1])]\ + + lon[int(trinodes[index,2])]) / 3.0 + latweight = (lat[int(trinodes[index,0])]\ + + lat[int(trinodes[index,1])]\ + + lat[int(trinodes[index,2])]) / 3.0 + TPI=111194.92664455874 # earth radius * pi/180.0 + pt_y = TPI * (pt_lat - latweight) + dx_sph = pt_lon - lonweight + if (dx_sph > 180.0): + dx_sph=dx_sph-360.0 + elif (dx_sph < -180.0): + dx_sph =dx_sph+360.0 + pt_x = TPI * np.cos(np.deg2rad(pt_lat + latweight)*0.5) * dx_sph + + return pt_x + self._grid.xc[index], pt_y + self._grid.yc[index] + + def exceedance(self, var, pt_lon=[], pt_lat=[], + graph=True, dump=False, debug=False, **kwargs): + """ + This function calculates the excedence curve of a var(time) + at any given point. + + Inputs: + - var = given quantity, 1 or 2D array of n elements, i.e (time) or (time,ele) + Options: + - pt_lon, pt_lat = coordinates, float numbers. Necessary if var = 2D (i.e. [time, nnode or nele] + - graph: True->plots curve; False->does not + - dump = boolean, dump graph data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + Outputs: + - Exceedance = list of % of occurences, 1D array + - Ranges = list of signal amplitude bins, 1D array + *Notes* + - This method is not suitable for SSE + """ + debug = (debug or self._debug) + if debug: + print 'Computing exceedance...' + + #Distinguish between 1D and 2D var + if len(var.shape)>1: + if pt_lon==[] or pt_lat==[]: + print 'Lon, lat coordinates are needed' + sys.exit() + signal = self.interpolation_at_point(var, pt_lon, pt_lat, debug=debug) + else: + signal=var + + Max = max(signal) + dy = (Max/50.0) + Ranges = np.arange(0,(Max + dy), dy) + Exceedance = np.zeros(Ranges.shape[0]) + dt = self._var.julianTime[1] - self._var.julianTime[0] + Period = signal.shape[0] * dt + time = np.arange(0.0, Period, dt) + + N = len(signal) + M = len(Ranges) + + for i in range(M): + r = Ranges[i] + for j in range(N-1): + if signal[j] > r: + Exceedance[i] = Exceedance[i] + (time[j+1] - time[j]) + + Exceedance = (Exceedance * 100) / Period + + if debug: + print '...Passed' + + #Plot + if graph: + error=np.ones(Exceedance.shape) * np.std(var)/2.0 + #if debug: print "Error: ", str(np.std(Exceedance)) + self._plot.plot_xy(Exceedance, Ranges, yerror=error, + yLabel='Amplitudes', + xLabel='Exceedance probability in %', + dump=dump, **kwargs) + + return Exceedance, Ranges + + def vorticity(self, debug=False): + """ + This method creates a new variable: 'depth averaged vorticity (1/s)' + -> FVCOM.Variables.depth_av_vorticity + + *Notes* + - Can take time over the full domain + """ + debug = (debug or self._debug) + if debug: + print 'Computing vorticity...' + start = time.time() + + t = np.arange(self._grid.ntime) + + #Surrounding elements + n1 = self._grid.triele[:,0] + n2 = self._grid.triele[:,1] + n3 = self._grid.triele[:,2] + + ##change end bound indices + test = -1 + n1[np.where(n1==test)[0]] = 0 + n2[np.where(n2==test)[0]] = 0 + n3[np.where(n3==test)[0]] = 0 + # double check due to chunking and nans + test = self._grid.triele.shape[0] + n1[np.where(n1>=test)[0]] = 0 + n2[np.where(n2>=test)[0]] = 0 + n3[np.where(n3>=test)[0]] = 0 + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + N1 = [] + N2 = [] + N3 = [] + + N1[:] = n1[:] + N2[:] = n2[:] + N3[:] = n3[:] + + if debug: + end = time.time() + print "Check element=0, computation time in (s): ", (end - start) + print "start np.multiply" + + dvdx = np.zeros((self._grid.ntime,self._grid.nele)) + dudy = np.zeros((self._grid.ntime,self._grid.nele)) + + try: + j=0 + for i in t: + dvdx[j,:] = np.multiply(self._grid.a1u[0,:], self._var.va[i,:]) \ + + np.multiply(self._grid.a1u[1,:], self._var.va[i,N1]) \ + + np.multiply(self._grid.a1u[2,:], self._var.va[i,N2]) \ + + np.multiply(self._grid.a1u[3,:], self._var.va[i,N3]) + dudy[j,:] = np.multiply(self._grid.a2u[0,:], self._var.ua[i,:]) \ + + np.multiply(self._grid.a2u[1,:], self._var.ua[i,N1]) \ + + np.multiply(self._grid.a2u[2,:], self._var.ua[i,N2]) \ + + np.multiply(self._grid.a2u[3,:], self._var.ua[i,N3]) + j+=1 + if debug: + print "loop number ", i + except IndexError: # Strange error due to netCDF4/utils.py + j=0 + N1 = np.asarray(N1).astype(int) + N2 = np.asarray(N2).astype(int) + N3 = np.asarray(N3).astype(int) + for i in t: + dvdx[j,:] = np.multiply(self._grid.a1u[0,:], self._var.va[i,:]) \ + + np.multiply(self._grid.a1u[1,:], self._var.va[i,N1]) \ + + np.multiply(self._grid.a1u[2,:], self._var.va[i,N2]) \ + + np.multiply(self._grid.a1u[3,:], self._var.va[i,N3]) + dudy[j,:] = np.multiply(self._grid.a2u[0,:], self._var.ua[i,:]) \ + + np.multiply(self._grid.a2u[1,:], self._var.ua[i,N1]) \ + + np.multiply(self._grid.a2u[2,:], self._var.ua[i,N2]) \ + + np.multiply(self._grid.a2u[3,:], self._var.ua[i,N3]) + j+=1 + if debug: + print "loop number ", i + + vort = dvdx - dudy + + # Add metadata entry + setattr(self._var, 'depth_av_vorticity', vort) + self._History.append('depth averaged vorticity computed') + print '-Depth averaged vorticity added to FVCOM.Variables.-' + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + def vorticity_over_period(self, time_ind=[], t_start=[], t_end=[], debug=False): + """ + This function computes the depth averaged vorticity for a time period. + + Outputs: + - vort = horizontal vorticity (1/s), 2D array (time, nele) + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + *Notes* + - Can take time over the full domain + """ + debug = (debug or self._debug) + if debug: + print 'Computing vorticity...' + start = time.time() + + # Find time interval to work in + t = [] + if not time_ind==[]: + t = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + t = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + t = np.arange(t_start, t_end) + else: + t = np.arange(self._grid.ntime) + self.vorticity() + + #Checking if vorticity already computed + if not hasattr(self._var, 'depth_av_vorticity'): + #Surrounding elements + n1 = self._grid.triele[:,0] + n2 = self._grid.triele[:,1] + n3 = self._grid.triele[:,2] + + ##change end bound indices + #test = self._grid.triele.shape[0] + test = -1 + n1[np.where(n1==test)[0]] = 0 + n2[np.where(n2==test)[0]] = 0 + n3[np.where(n3==test)[0]] = 0 + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + N1 = [] + N2 = [] + N3 = [] + + N1[:] = n1[:] + N2[:] = n2[:] + N3[:] = n3[:] + + + if debug: + end = time.time() + print "Check element=0, computation time in (s): ", (end - start) + print "start np.multiply" + + dvdx = np.zeros((t.shape[0],self._grid.nele)) + dudy = np.zeros((t.shape[0],self._grid.nele)) + + j=0 + for i in t: + dvdx[j,:] = np.multiply(self._grid.a1u[0,:], self._var.va[i,:]) \ + + np.multiply(self._grid.a1u[1,:], self._var.va[i,N1]) \ + + np.multiply(self._grid.a1u[2,:], self._var.va[i,N2]) \ + + np.multiply(self._grid.a1u[3,:], self._var.va[i,N3]) + dudy[j,:] = np.multiply(self._grid.a2u[0,:], self._var.ua[i,:]) \ + + np.multiply(self._grid.a2u[1,:], self._var.ua[i,N1]) \ + + np.multiply(self._grid.a2u[2,:], self._var.ua[i,N2]) \ + + np.multiply(self._grid.a2u[3,:], self._var.ua[i,N3]) + j+=1 + if debug: + print "loop number ", i + + vort = dvdx - dudy + else: + vort = self._var.depth_av_vorticity[t[:], :] + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + return vort + + def depth(self, debug=False): + """ + This method creates a new grid variable: 'depth2D' (m) + -> FVCOM.Grid.depth2D + + *Notes* + - depth convention: 0 = free surface + - Can take time over the full domain + """ + debug = debug or self._debug + if debug: + start = time.time() + print "Computing depth..." + + #Compute depth + size = self._grid.nele + size1 = self._grid.ntime + size2 = self._grid.nlevel + elc = np.zeros((size1, size)) + hc = np.zeros((size)) + + try: + for ind, value in enumerate(self._grid.trinodes[:]): + value.sort()#due to new version of netCDF4 + elc[:, ind] = np.mean(self._var.el[:, value], axis=1) + hc[ind] = np.mean(self._grid.h[value]) + + dep = elc[:,:] + hc[None,:] + except MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + # Add metadata entry + setattr(self._grid, 'depth2D', dep) + self._History.append('depth 2D computed') + print '-Depth 2D added to FVCOM.Variables.-' + + def depth_at_point(self, pt_lon, pt_lat, index=[], debug=False): + """ + This function computes the depth at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - dep = depth, 2D array (ntime, nlevel) + + Options: + - index = element index, interger + + *Notes* + - depth convention: 0 = free surface + - index is used in case one knows already at which + element depth is requested + """ + debug = debug or self._debug + if debug: + print "Computing depth..." + start = time.time() + + #Finding index + if index==[]: + index = self.index_finder(pt_lon, pt_lat, debug=False) + + if not hasattr(self._grid, 'depth2D'): + #Compute depth + if type(self._grid.h).__name__=='Variable': #Fix for netcdf4 lib + H = self._grid.h[:] + EL = self._var.el[:] + else: + H = self._grid.h + EL = self._var.el + + h = self.interpolation_at_point(H, pt_lon, pt_lat, index=index, debug=debug) + el = self.interpolation_at_point(EL, pt_lon, pt_lat, index=index, debug=debug) + + dep = el + h + else: + if type(self._grid.depth2D).__name__=='Variable': #Fix for netcdf4 lib + d2D = self._grid.depth2D[:] + else: + d2D = self._grid.depth2D + dep = self.interpolation_at_point(d2D, pt_lon, pt_lat, index=index, debug=debug) + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + return dep + + def depth_averaged_power_density(self, debug=False): + """ + This method creates a new variable: 'depth averaged power density' (W/m2) + -> FVCOM.Variables.depth_av_power_density + + *Notes* + - The power density (pd) is then calculated as follows: pd = 0.5*1025*(u**3) + - This may take some time to compute depending on the size of the data set + """ + debug = (debug or self._debug) + if debug: print "Computing depth averaged power density..." + + if not hasattr(self._var, 'hori_velo_norm'): + self.hori_velo_norm(debug=debug) + if debug: print "Computing powers of hori velo norm..." + u = self._var.hori_velo_norm[:] + pd = ne.evaluate('0.5*1025.0*(u**3)').squeeze() + #pd = 0.5*1025.0*np.power(self._var.hori_velo_norm[:],3.0) # TR: very slow + #pd = 0.5*1025.0*self._var.hori_velo_norm[:]*self._var.hori_velo_norm[:]*self._var.hori_velo_norm[:] + + # Add metadata entry + setattr(self._var, 'depth_av_power_density', pd) + self._History.append('depth averaged power density computed') + print '-Depth averaged power density to FVCOM.Variables.-' + + def depth_averaged_power_assessment(self, power_mat, rated_speed, + cut_in=1.0, cut_out=4.5, debug=False): + """ + This method creates a new variable: 'depth averaged power assessment' (W/m2) + -> FVCOM.Variables.depth_av_power_assessment + + Inputs: + - power_mat = power matrix (u,Ct(u)), 2D array (2,n), + u being power_mat[0,:] and Ct(u) being power_mat[1,:] + - rated_speed = rated speed speed in m/s, float number + + Options: + - cut_in = cut-in speed in m/s, float number + - cut_out = cut-out speed in m/s, float number + + *Notes* + - The power density (pd) is then calculated as follows: pd = Cp*(1/2)*1025*(u**3) + - This function performs tidal turbine power assessment by accounting for + cut-in and cut-out speed, power curve/function (pc): Cp = pc(u) (where u is the flow speed) + - This may take some time to compute depending on the size of the data set + """ + debug = (debug or self._debug) + if debug: print "Computing depth averaged power density..." + + if not hasattr(self._var, 'depth_av_power_density'): + if debug: print "Computing power density..." + self.depth_averaged_power_density(debug=debug) + + if debug: print "Initialising power curve..." + Cp = interp1d(power_mat[0,:],power_mat[1,:]) + + u = self._var.hori_velo_norm[:] + pd = self._var.depth_av_power_density[:] + + pa = Cp(u)*pd + + if debug: print "finding cut-in and out..." + #TR comment huge bottleneck here + #ind = np.where(pd cut_out): + # pa[i,j] = 0.0 + inM = np.ma.masked_where(ucut_out, u).mask + ioM = inM * outM * u.mask + pa=np.ma.masked_where(ioM, pa) + + if debug: print "finding rated speed..." + parated = Cp(rated_speed)*0.5*1025.0*(rated_speed**3.0) + #TR comment huge bottleneck here + #ind = np.where(pd>pdout)[0] + #if not ind.shape[0]==0: + # pd[ind] = pdout + #for i in range(pa.shape[0]): + # for j in range(pa.shape[1]): + # if u[i,j] > rated_speed: + # pa[i,j] = parated + pa[u>rated_speed] = parated + + # Add metadata entry + setattr(self._var, 'depth_av_power_assessment', pd) + self._History.append('depth averaged power assessment computed') + print '-Depth averaged power assessment to FVCOM.Variables.-' + + def Harmonic_analysis_at_point(self, pt_lon, pt_lat, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + #TR_comments: Add debug flag in Utide: debug=self._debug + index = self.index_finder(pt_lon, pt_lat, debug=False) + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + time = self._var.matlabTime[:] + if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib + ua = self._var.ua[:] + va = self._var.va[:] + else: + ua = self._var.ua + va = self._var.va + + u = self.interpolation_at_point(ua, pt_lon, pt_lat, index=index, debug=debug) + v = self.interpolation_at_point(va, pt_lon, pt_lat, index=index, debug=debug) + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, u, v, lat, **kwarg) + + else: + time = self._var.matlabTime[:] + if type(self._var.el).__name__=='Variable': #Fix for netcdf4 lib + el = self._var.el[:] + else: + el = self._var.el + el = self.interpolation_at_point(el, pt_lon, pt_lat, + index=index, debug=debug) + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + #TR_comments: Add debug flag in Utide: debug=self._debug + Reconstruct = reconstruct(time,harmo) + + return Reconstruct diff --git a/build/lib/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py b/build/lib/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py new file mode 100644 index 0000000..11c1ca2 --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py @@ -0,0 +1,1262 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numexpr as ne +import datetime +from scipy.interpolate import interp1d +from pyseidon_dvt.utilities.interpolation_utils import * +from pyseidon_dvt.utilities.miscellaneous import * +from pyseidon_dvt.utilities.BP_tools import * +from pyseidon_dvt.utilities.shortest_element_path import * +import time +import matplotlib.pyplot as plt +from pydap.exceptions import ServerError + +from utide import solve, reconstruct + +#TR comment: This all routine needs to be tested and debugged +class FunctionsFvcomThreeD: + """ + **'Utils3D' subset of FVCOM class gathers useful methods and functions for 3D runs** + """ + def __init__(self, variable, grid, plot, util, History, debug): + #Inheritance + self._debug = debug + self._plot = plot + self._util = util + self.interpolation_at_point = self._util.interpolation_at_point + self.index_finder = self._util.index_finder + self.hori_velo_norm = self._util.hori_velo_norm + + #Create pointer to FVCOM class + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + setattr(self, '_History', History) + + return + + def depth(self, debug=False): + """ + This method computes new grid variable: 'depth' (m) + -> FVCOM.Grid.depth + + *Notes* + - depth convention: 0 = free surface + - Can take time over the full domain + """ + debug = debug or self._debug + if debug: + start = time.time() + print "Computing depth..." + + try: + elc = interpN(self._var.el[:], self._grid.trinodes[:], self._grid.aw0[:], debug=debug) + hc = interpN(self._grid.h[:], self._grid.trinodes[:], self._grid.aw0[:], debug=debug) + siglay = interpN(self._grid.siglay[:], self._grid.trinodes[:], self._grid.aw0[:], debug=debug) + zeta = elc[:,:] + hc[None,:] + dep = zeta[:,None,:]*siglay[None,:,:] + + except MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + # TR: need to find vectorized alternative + #Compute depth + # size = self._grid.nele + # size1 = self._grid.ntime + # size2 = self._grid.nlevel + # + # elc = np.zeros((size1, size)) + # hc = np.zeros((size) + # dep = np.zeros((size1, size2, size)) + # for ind in range(size): + # d = self.depth_at_point(self._grid.lonc[ind],self._grid.latc[ind],debug=debug) + # dep[:, :, ind] = d[:,:] + + # TR: does not work with netCDF4 lib + # elc = np.zeros((size1, size)) + # hc = np.zeros((size)) + # siglay = np.zeros((size2, size)) + # + # try: + # for ind, value in enumerate(self._grid.trinodes[:]): + # elc[:, ind] = np.mean(self._var.el[:, value], axis=1) + # hc[ind] = np.mean(self._grid.h[value]) + # siglay[:,ind] = np.mean(self._grid.siglay[:,value],1) + # + # #zeta = self._var.el[:,:] + self._grid.h[None,:] + # zeta = elc[:,:] + hc[None,:] + # dep = zeta[:,None,:]*siglay[None,:,:] + + # Add metadata entry + setattr(self._grid, 'depth', dep) + self._History.append('depth computed') + print '-Depth added to FVCOM.Grid.-' + + def depth_at_point(self, pt_lon, pt_lat, index=[], debug=False): + """ + This function computes depth at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - dep = depth, 2D array (ntime, nlevel) + + Options: + - index = element index, interger. Use only if closest element + index is already known + + *Notes* + - depth convention: 0 = free surface + - index is used in case one knows already at which + element depth is requested + """ + debug = debug or self._debug + if debug: + print "Computing depth..." + start = time.time() + + #Finding index + if index==[]: + index = self.index_finder(pt_lon, pt_lat, debug=False) + + if not hasattr(self._grid, 'depth'): + #Compute depth + h = self.interpolation_at_point(self._grid.h, pt_lon, pt_lat, + index=index, debug=debug) + el = self.interpolation_at_point(self._var.el, pt_lon, pt_lat, + index=index, debug=debug) + siglay = self.interpolation_at_point(self._grid.siglay, pt_lon, pt_lat, + index=index, debug=debug) + zeta = el + h + dep = zeta[:,None]*siglay[None,:] + else: + dep = self.interpolation_at_point(self._grid.depth[:], + pt_lon, pt_lat, index=index, + debug=debug) + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + return dep + + def interp_at_depth(self, var, depth, ind=[], debug=False): + """ + This function interpolates any given FVCOM.Variables field + onto a specified depth plan + + Inputs: + - var = 3 dimensional (time, sigma level, element) variable, array + - depth = interpolation depth (float in meters), if negative = from + sea surface downwards, if positive = from sea bottom upwards + Options: + - ind = array of closest indexes to depth, 2D array (ntime, nele) + + Output: + - interpVar = 2 dimensional (time, element) variable, masked array + - ind = array of closest indexes to depth, 2D array (ntime, nele) + """ + debug = debug or self._debug + if debug: print 'Interpolating at '+str(depth)+' meter depth...' + + #checking if depth field already calculated + if not hasattr(self._grid, 'depth'): + self.depth() + Depth = self._grid.depth[:]#otherwise to slow with netcdf4 lib + if depth > 0.0: # Changing vertical axis convention + # for tt in range(Depth.shape[0]): + # for ii in range(Depth.shape[2]): + # mini = np.min(np.squeeze(Depth[tt, :, ii])) + # Depth[tt, :, ii] = Depth[tt, :, ii] - mini + # Alternative + mini = np.min(Depth, axis=1) + Depth = Depth - mini[:, None, :] + dep = Depth[:] - depth + #Finding closest values to specified depth + if ind==[]: + if debug: print 'Finding closest indexes to depth...' + #mask negative value + dep = np.ma.masked_where(dep<0.0, dep) + #find min argument in masked array + ind = dep.argmin(axis=1) + ind=ind.astype(float) + #set to nan to shallow elements + ind[ind==dep.shape[1]-1.0] = np.nan + + #ind = np.zeros((dep.shape[0],dep.shape[2])) + #for i in range(dep.shape[0]): + # for k in range(dep.shape[2]): + # test = dep[i,:,k] + # if not test[test>0.0].shape==test.shape: + # ind[i,k] = test[test>0.0].argmin() + # else: + # ind[i,k] = np.nan + + inddown = ind + 1 + + if debug: print 'Computing weights...' + ##weight matrix & interp + #interpVar = np.ones((var.shape[0], var.shape[2]))*np.nan + #for i in range(ind.shape[0]): + # for j in range(ind.shape[1]): + # iU = ind[i,j] + # iD = inddown[i,j] + # if not np.isnan(iU): + # iU = int(iU) + # iD = int(iD) + # length = np.abs(self._grid.depth[i,iU,j]\ + # - self._grid.depth[i,iD,j]) + # wU = np.abs(depth - self._grid.depth[i,iU,j])/length + # wD = np.abs(depth - self._grid.depth[i,iD,j])/length + # interpVar[i,j] = (wU * var[i,iU,j]) + (wD * var[i,iD,j]) + # else: + # interpVar[i,j] = np.nan + #if debug: print 'Computing nan mask...' + #interpVar = np.ma.masked_array(interpVar,np.isnan(interpVar)) + + ##Streamlining + I = []; J = []; U = []; D = [] + for i in range(ind.shape[0]): + for j in range(ind.shape[1]): + iU = ind[i,j] + iD = inddown[i,j] + I.append(i) + J.append(j) + U.append(iU) + D.append(iD) + if debug: print 'Convert lists to arrays...' + I=np.asarray(I); J=np.asarray(J); U=np.asarray(U); D=np.asarray(D) + if debug: print 'Find nan indices...' + nanI = np.ones(U.shape) + nanU = np.where(np.isnan(U)) + nanD = np.where(np.isnan(D)) + nanI[nanU] = np.nan + nanI[nanD] = np.nan + if debug: print 'convert to integer...' + U[nanU] = 0 + D[nanD] = 0 + I = I.astype(int) + J = J.astype(int) + U = U.astype(int) + D = D.astype(int) + + if type(var).__name__=='Variable': #Fix for netcdf4 lib + Var = var[:] + else: + Var = var + # dUp = Depth[I,U,J] + # varUp = Var[I,U,J] + # dDo = Depth[I,D,J] + # varDo = Var[I,D,J] + # TR: append to speed up caching + if debug: print 'Caching...' + for i in I: + dUp = Depth[i,U,J] + varUp = Var[i,U,J] + dDo = Depth[i,D,J] + varDo = Var[i,D,J] + if debug: print 'Compute weights...' + lengths = np.abs(dUp - dDo) + wU = np.abs(depth - dUp)/lengths + wD = np.abs(depth - dDo)/lengths + if debug: print 'interpolation...' + interpVar = nanI * ((wU * varUp) + (wD * varDo)) + if debug: print 'reshaping...' + interpVar = np.reshape(interpVar, (Var.shape[0], Var.shape[2])) + if debug: print 'masking...' + interpVar = np.ma.masked_array(interpVar,np.isnan(interpVar)) + if debug: print '...Passed' + + return interpVar, ind + + def verti_shear(self, debug=False): + """ + This method computes a new variable: 'vertical shear' (1/s) + -> FVCOM.Variables.verti_shear + + *Notes* + - Can take time over the full doma + """ + debug = debug or self._debug + if debug: + print 'Computing vertical shear...' + + #Compute depth if necessary + if not hasattr(self._grid, 'depth'): + depth = self.depth(debug=debug) + depth = self._grid.depth[:] + + # Checking if horizontal velocity norm already exists + if not hasattr(self._var, 'velo_norm'): + self.velo_norm() + vel = self._var.velo_norm[:] + + try: + #Sigma levels to consider + top_lvl = self._grid.nlevel - 1 + bot_lvl = 0 + sLvl = range(bot_lvl, top_lvl+1) + + # Compute shear + dz = depth[:,sLvl[1:],:] - depth[:,sLvl[:-1],:] + dvel = vel[:,sLvl[1:],:] - vel[:,sLvl[:-1],:] + dveldz = dvel / dz + except MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + #Custom return + setattr(self._var, 'verti_shear', dveldz) + + # Add metadata entry + self._History.append('vertical shear computed') + print '-Vertical shear added to FVCOM.Variables.-' + + if debug: + print '...Passed' + + def verti_shear_at_point(self, pt_lon, pt_lat, t_start=[], t_end=[], time_ind=[], + bot_lvl=[], top_lvl=[], graph=True, dump=False, debug=False): + """ + This function computes vertical shear at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - dveldz = vertical shear (1/s), 2D array (time, nlevel - 1) + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - bot_lvl = index of the bottom level to consider, integer + - top_lvl = index of the top level to consider, integer + - graph = plots graph if True + - dump = boolean, dump profile data in csv file + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing vertical shear at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + # Finding closest point + index = self.index_finder(pt_lon, pt_lat, debug=False) + + #Compute depth + depth = self.depth_at_point(pt_lon, pt_lat, index=index, debug=debug) + + #Sigma levels to consider + if top_lvl==[]: + top_lvl = self._grid.nlevel - 1 + if bot_lvl==[]: + bot_lvl = 0 + sLvl = range(bot_lvl, top_lvl+1) + + + # Checking if vertical shear already exists + if not hasattr(self._var, 'verti_shear'): + if type(self._var.u).__name__=='Variable': + u = self._var.u[:] + v = self._var.v[:] + else: + u = self._var.u + v = self._var.v + + #Extraction at point + if debug: + print 'Extraction of u and v at point...' + U = self.interpolation_at_point(u, pt_lon, pt_lat, + index=index, debug=debug) + V = self.interpolation_at_point(v, pt_lon, pt_lat, + index=index, debug=debug) + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + # Compute shear + dz = depth[:,sLvl[1:]] - depth[:,sLvl[:-1]] + dvel = norm[:,sLvl[1:]] - norm[:,sLvl[:-1]] + dveldz = dvel / dz + else: + if type(self._var.verti_shear).__name__=='Variable': + shear = self._var.verti_shear[:] + else: + shear = self._var.verti_shear + dveldz = self.interpolation_at_point(self._var.verti_shear, + pt_lon, pt_lat, + index=index, debug=debug) + + if debug: + print '...Passed' + #use time indices of interest + if not argtime==[]: + dveldz = dveldz[argtime,:] + depth = depth[argtime,:] + + #Plot mean values + if graph: + mean_depth = np.mean((depth[:,sLvl[1:]] + + depth[:,sLvl[:-1]]) / 2.0, 0) + mean_dveldz = np.mean(dveldz,0) + error = np.std(dveldz,axis=0)/2.0 + self._plot.plot_xy(mean_dveldz, mean_depth, xerror=error[:], + title='Shear profile ', + xLabel='Shear (1/s) ', yLabel='Depth (m) ', + dump=dump) + + return dveldz + + def velo_norm(self, debug=False): + """ + This method computes a new variable: 'velocity norm' (m/s) + -> FVCOM.Variables.velo_norm + + *Notes* + -Can take time over the full domain + """ + if debug or self._debug: + print 'Computing velocity norm...' + #Check if w if there + try: + try: + #Computing velocity norm + u = self._var.u[:] + v = self._var.v[:] + w = self._var.w[:] + vel = ne.evaluate('sqrt(u**2 + v**2 + w**2)').squeeze() + except (MemoryError, ServerError) as e: + print '---Data too large for machine memory or server---' + print 'Tip: Save data on your machine first' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + except AttributeError: + try: + #Computing velocity norm + u = self._var.u[:] + v = self._var.v[:] + vel = ne.evaluate('sqrt(u**2 + v**2)').squeeze() + except (MemoryError, ServerError) as e: + print '---Data too large for machine memory or server---' + print 'Tip: Save data on your machine first' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + #Custom return + setattr(self._var, 'velo_norm', vel) + + # Add metadata entry + self._History.append('Velocity norm computed') + print '-Velocity norm added to FVCOM.Variables.-' + + if debug or self._debug: + print '...Passed' + + def velo_norm_at_point(self, pt_lon, pt_lat, t_start=[], t_end=[], time_ind=[], + graph=True, dump=False, debug=False): + """ + This function computes the velocity norm at any given point. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - velo_norm = velocity norm, 2D array (time, level) + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - graph = boolean, plots or not veritcal profile + - dump = boolean, dump profile data in csv file + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing velocity norm at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + try: + if not hasattr(self._var, 'velo_norm'): + if type(self._var.u).__name__=='Variable': #Fix for netcdf4 lib + u = self._var.u[:] + v = self._var.v[:] + w = self._var.w[:] + else: + u = self._var.u + v = self._var.v + w = self._var.w + else: + vel = self._var.velo_norm + except AttributeError: + if not hasattr(self._var, 'velo_norm'): + if type(self._var.u).__name__=='Variable': #Fix for netcdf4 lib + u = self._var.u[:] + v = self._var.v[:] + else: + u = self._var.u + v = self._var.v + else: + if type(self._var.velo_norm).__name__=='Variable': #Fix for netcdf4 lib: + vel = self._var.velo_norm[:] + else: + vel = self._var.velo_norm + + # Finding closest point + index = self.index_finder(pt_lon, pt_lat, debug=False) + + #Computing horizontal velocity norm + if debug: + print 'Extraction of u, v and w at point...' + if not hasattr(self._var, 'velo_norm'): + U = self.interpolation_at_point(u, pt_lon, pt_lat, + index=index, debug=debug) + V = self.interpolation_at_point(v, pt_lon, pt_lat, + index=index, debug=debug) + if 'w' in locals(): + W = self.interpolation_at_point(w, pt_lon, pt_lat, + index=index, debug=debug) + velo_norm = ne.evaluate('sqrt(U**2 + V**2 + W**2)').squeeze() + else: + velo_norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + else: + velo_norm = self.interpolation_at_point(vel, pt_lon, pt_lat, + index=index, debug=debug) + if debug: + print '...passed' + + #use only the time indices of interest + if not argtime==[]: + velo_norm = velo_norm[argtime[:],:,:] + + #Plot mean values + if graph: + depth = self.depth_at_point(pt_lon, pt_lat, index=index) + mean_depth = np.mean(depth, 0) + mean_vel = np.mean(velo_norm,0) + error = np.std(velo_norm,axis=0)/2.0 + self._plot.plot_xy(mean_vel, mean_depth, xerror=error[:], + title='Flow speed vertical ', + xLabel='Flow speed (1/s) ', yLabel='Depth (m) ', + dump=dump) + + return velo_norm + + + def flow_dir_at_point(self, pt_lon, pt_lat, t_start=[], t_end=[], time_ind=[], + vertical=True, debug=False): + """ + This function computes flow directions and associated norm + at any given location. + + Inputs: + - pt_lon = longitude in decimal degrees East to find + - pt_lat = latitude in decimal degrees North to find + + Outputs: + - flowDir = flowDir at (pt_lon, pt_lat), 2D array (ntime, nlevel) + - norm = velocity norm at (pt_lon, pt_lat), 2D array (ntime, nlevel) + + Options: + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - time_ind = time indices to work in, list of integers + - vertical = True, compute flowDir for each vertical level + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing flow directions at point...' + + # Finding closest point + index = self.index_finder(pt_lon, pt_lat, debug=False) + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if self._var._3D and vertical: + if type(self._var.u).__name__=='Variable': #Fix for netcdf4 lib + u = self._var.u[:] + v = self._var.v[:] + else: + u = self._var.u + v = self._var.v + else: + if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib: + u = self._var.ua[:] + v = self._var.va[:] + else: + u = self._var.ua + v = self._var.va + + #Extraction at point + if debug: + print 'Extraction of u and v at point...' + U = self._util.interpolation_at_point(u, pt_lon, pt_lat, + index=index, debug=debug) + V = self._util.interpolation_at_point(v, pt_lon, pt_lat, + index=index, debug=debug) + + #Compute velocity norm + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + #Compute directions + if debug: + print 'Computing arctan2...' + dirFlow = np.rad2deg(np.arctan2(V,U)) + + if debug: print '...Passed' + #use only the time indices of interest + if not argtime==[]: + dirFlow = dirFlow[argtime[:],:] + + return dirFlow, norm + + def flow_dir(self, debug=False): + """" + This method computes a new variable: 'flow directions' (deg.) + -> FVCOM.Variables.flow_dir + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - Can take time over the full domain + """ + if debug or self._debug: + print 'Computing flow directions...' + + try: + u = self._var.u[:] + v = self._var.v[:] + dirFlow = np.rad2deg(np.arctan2(v,u)) + except (MemoryError, ServerError) as e: + print '---Data too large for machine memory or server---' + print 'Tip: Save data on your machine' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + #Custom return + setattr(self._var, 'flow_dir', dirFlow) + + # Add metadata entry + self._History.append('flow directions computed') + print '-Flow directions added to FVCOM.Variables.-' + + if debug or self._debug: + print '...Passed' + + def vorticity(self, debug=False): + """ + This method creates a new variable: 'depth averaged vorticity' (1/s) + -> FVCOM.Variables.vorticity + + *Notes* + - Can take time over the full domain + """ + debug = (debug or self._debug) + if debug: + print 'Computing vorticity...' + start = time.time() + + t = np.arange(self._grid.ntime) + + #Surrounding elements + n1 = self._grid.triele[:,0] + n2 = self._grid.triele[:,1] + n3 = self._grid.triele[:,2] + #No need anymore + ##change end bound indices + #test = self._grid.triele.shape[0] + #n1[np.where(n1==test)[0]] = 0 + #n2[np.where(n2==test)[0]] = 0 + #n3[np.where(n3==test)[0]] = 0 + + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + N1 = [] + N2 = [] + N3 = [] + + N1[:] = n1[:] + N2[:] = n2[:] + N3[:] = n3[:] + + if debug: + end = time.time() + print "Check element=0, computation time in (s): ", (end - start) + print "start np.multiply" + + x0 = self._grid.xc + y0 = self._grid.yc + + dvdx = np.zeros((self._grid.ntime,self._grid.nlevel,self._grid.nele)) + dudy = np.zeros((self._grid.ntime,self._grid.nlevel,self._grid.nele)) + nele = self._grid.nele + + j=0 + for i in t: + dvdx[j,:,:] = np.multiply(self._grid.a1u[0,:].reshape(1,nele), + self._var.v[i,:,:]) \ + + np.multiply(self._grid.a1u[1,:].reshape(nele,1), + self._var.v[i,:,N1]).T \ + + np.multiply(self._grid.a1u[2,:].reshape(nele,1), + self._var.v[i,:,N2]).T \ + + np.multiply(self._grid.a1u[3,:].reshape(nele,1), + self._var.v[i,:,N3]).T + dudy[j,:,:] = np.multiply(self._grid.a2u[0,:].reshape(1,nele), + self._var.u[i,:,:]) \ + + np.multiply(self._grid.a2u[1,:].reshape(nele,1), + self._var.u[i,:,N1]).T \ + + np.multiply(self._grid.a2u[2,:].reshape(nele,1), + self._var.u[i,:,N2]).T \ + + np.multiply(self._grid.a2u[3,:].reshape(nele,1), + self._var.u[i,:,N3]).T + j+=1 + if debug: + print "loop number ", i + + vort = dvdx - dudy + + # Add metadata entry + setattr(self._var, 'vorticity', vort) + self._History.append('vorticity computed') + print '-Vorticity added to FVCOM.Variables.-' + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + def vorticity_over_period(self, time_ind=[], t_start=[], t_end=[], debug=False): + """ + This function computes the vorticity for a time period. + + Outputs: + - vort = horizontal vorticity (1/s), 2D array (time, nele) + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + *Notes* + - Can take time over the full domain + """ + debug = (debug or self._debug) + if debug: + print 'Computing vorticity...' + start = time.time() + + # Find time interval to work in + t = [] + if not time_ind==[]: + t = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + t = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + t = np.arange(t_start, t_end) + else: + t = np.arange(self._grid.ntime) + + #Checking if vorticity already computed + if not hasattr(self._var, 'vorticity'): + #Surrounding elements + n1 = self._grid.triele[:,0] + n2 = self._grid.triele[:,1] + n3 = self._grid.triele[:,2] + #No need anymore + ##change end bound indices + #test = self._grid.triele.shape[0] + #n1[np.where(n1==test)[0]] = 0 + #n2[np.where(n2==test)[0]] = 0 + #n3[np.where(n3==test)[0]] = 0 + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + N1 = [] + N2 = [] + N3 = [] + + N1[:] = n1[:] + N2[:] = n2[:] + N3[:] = n3[:] + + if debug: + end = time.time() + print "Check element=0, computation time in (s): ", (end - start) + print "start np.multiply" + + x0 = self._grid.xc[:] + y0 = self._grid.yc[:] + + dvdx = np.zeros((t.shape[0],self._grid.nlevel,self._grid.nele)) + dudy = np.zeros((t.shape[0],self._grid.nlevel,self._grid.nele)) + + j=0 + for i in t: + dvdx[j,:,:] = np.multiply(self._grid.a1u[0,:], self._var.v[i,:,:]) \ + + np.multiply(self._grid.a1u[1,:], self._var.v[i,:,N1]) \ + + np.multiply(self._grid.a1u[2,:], self._var.v[i,:,N2]) \ + + np.multiply(self._grid.a1u[3,:], self._var.v[i,:,N3]) + dudy[j,:,:] = np.multiply(self._grid.a2u[0,:], self._var.u[i,:,:]) \ + + np.multiply(self._grid.a2u[1,:], self._var.u[i,:,N1]) \ + + np.multiply(self._grid.a2u[2,:], self._var.u[i,:,N2]) \ + + np.multiply(self._grid.a2u[3,:], self._var.u[i,:,N3]) + j+=1 + if debug: + print "loop number ", i + + vort = dvdx - dudy + else: + vort = self._var.vorticity[t[:],:,:] + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + return vort + + def power_density(self, debug=False): + """ + This method creates a new variable: 'power density' (W/m2) + -> FVCOM.Variables.power_density + + The power density (pd) is then calculated as follows: + pd = 0.5*1025*(u**3) + + *Notes* + - This may take some time to compute depending on the size + of the data set + """ + debug = (debug or self._debug) + if debug: print "Computing power density..." + + if not hasattr(self._var, 'velo_norm'): + self.velo_norm(debug=debug) + if debug: print "Computing power density variable..." + u = self._var.velo_norm[:] + pd = ne.evaluate('0.5*1025.0*(u**3)').squeeze() + #pd = 0.5*1025.0*np.power(self._var.hori_velo_norm[:],3.0) # TR: very slow + #pd = 0.5*1025.0*self._var.hori_velo_norm[:]*self._var.hori_velo_norm[:]*self._var.hori_velo_norm[:] + + # Add metadata entry + setattr(self._var, 'power_density', pd) + self._History.append('power density computed') + print '-Power density to FVCOM.Variables.-' + + def power_assessment_at_depth(self, depth, power_mat, rated_speed, + cut_in=1.0, cut_out=4.5, debug=False): + """ + This function computes power assessment (W/m2) at given depth. + + Description: + This function performs tidal turbine power assessment by accounting for + cut-in and cut-out speed, power curve/function (pc): + Cp = pc(u) + (where u is the flow speed) + + The power density (pd) is then calculated as follows: + pd = Cp*(1/2)*1025*(u**3) + + Inputs: + - depth = given depth from the surface, float + - power_mat = power matrix (u,Cp(u)), 2D array (2,n), + u being power_mat[0,:] and Ct(u) being power_mat[1,:] + - rated_speed = rated speed speed in m/s, float number + + + Output: + - pa = power assessment in (W/m2), 2D masked array (ntime, nele) + + Options: + - cut_in = cut-in speed in m/s, float number + - cut_out = cut-out speed in m/s, float number + + *Notes* + - This may take some time to compute depending on the size + of the data set + """ + debug = (debug or self._debug) + if debug: print "Computing depth averaged power density..." + + if not hasattr(self._var, 'power_density'): + self.power_density(debug=debug) + + if debug: print "Initialising power curve..." + Cp = interp1d(power_mat[0,:],power_mat[1,:]) + + u, ind = self.interp_at_depth(self._var.velo_norm[:], depth, debug=debug) + pd, ind2 = self.interp_at_depth(self._var.power_density[:], depth, + ind=ind, debug=debug) + + pa = Cp(u)*pd + + if debug: print "finding cut-in..." + #TR comment huge bottleneck here + #ind = np.where(pd cut_out): + # pa[i,j] = 0.0 + + inM = np.ma.masked_where(ucut_out, u).mask + ioM = inM * outM * u.mask + pa=np.ma.masked_where(ioM, pa) + + if debug: print "finding rated speed..." + parated = Cp(rated_speed)*0.5*1025.0*(rated_speed**3.0) + #TR comment huge bottleneck here + #ind = np.where(pd>pdout)[0] + #if not ind.shape[0]==0: + # pd[ind] = pdout + #for i in range(pa.shape[0]): + # for j in range(pa.shape[1]): + # if u[i,j] > rated_speed: + # pa[i,j] = parated + pa[u>rated_speed] = parated + + return pa + + def _vertical_slice(self, var, start_pt, end_pt, + time_ind=[], t_start=[], t_end=[], + title='Title', cmax=[], cmin=[], debug=False): + """ + Draw vertical slice in var along the shortest path between + start_point, end_pt. + + Inputs: + - var = 2D dimensional (sigma level, element) variable, array + - start_pt = starting point, [longitude, latitude] + - end_pt = ending point, [longitude, latitude] + + Options: + - time_ind = reference time indices for surface elevation, list of integer + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), + or time index as an integer + + Keywords for plot: + - title = plot title, string + - cmin = minimum limit colorbar + - cmax = maximum limit colorbar + """ + debug = debug or self._debug + if not self._var._3D: + print "Error: Only available for 3D runs." + raise + else: + lons = [start_pt[0], end_pt[0]] + lats = [start_pt[1], end_pt[1]] + #Finding the closest elements to start and end points + index = closest_points(lons, lats, + self._grid.lonc, + self._grid.latc, debug=debug) + + #Finding the shortest path between start and end points + if debug : print "Computing shortest path..." + short_path = shortest_element_path(self._grid.lonc[:], + self._grid.latc[:], + self._grid.lon[:], + self._grid.lat[:], + self._grid.trinodes[:], + self._grid.h[:], debug=debug) + el, _ = short_path.getTargets([index]) + # Plot shortest path + short_path.graphGrid(plot=True) + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Extract along line + ele=np.asarray(el[:])[0,:] + varP = var[:,ele] + # Depth along line + if debug : print "Computing depth..." + depth = np.zeros((self._grid.ntime, self._grid.nlevel, ele.shape[0])) + I=0 + for ind in ele: + value = self._grid.trinodes[ind] + h = np.mean(self._grid.h[value]) + zeta = np.mean(self._var.el[:,value],1) + h + siglay = np.mean(self._grid.siglay[:,value],1) + depth[:,:,I] = zeta[:,None]*siglay[None,:] + I+=1 + # Average depth over time + if not argtime==[]: + depth = np.mean(depth[argtime,:,:], 0) + else: + depth = np.mean(depth, 0) + + # Compute distance along line + x = self._grid.xc[ele] + y = self._grid.yc[ele] + # Pythagore + cumulative path + line = np.zeros(depth.shape) + dl = np.sqrt(np.square(x[1:]-x[:-1]) + np.square(y[1:]-y[:-1])) + for i in range(1,dl.shape[0]): + dl[i] = dl[i] + dl[i-1] + line[:,1:] = dl[:] + + #turn into gridded + #print 'Compute gridded data' + #nx, ny = 100, 100 + #xi = np.linspace(x.min(), x.max(), nx) + #yi = np.linspace(y.min(), y.max(), ny) + + #Plot features + #setting limits and levels of colormap + if cmax==[]: + cmax = varP[:].max() + if cmin==[]: + cmin = varP[:].min() + step = (cmax-cmin) / 20.0 + levels=np.arange(cmin, (cmax+step), step) + #plt.clf() + fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + ax = fig.add_subplot(111) #,aspect=(1.0/np.cos(np.mean(lat)*np.pi/180.0))) + #levels = np.linspace(0,3.3,34) + #cs = ax.contourf(line,depth,varP,levels=levels, cmap=plt.cm.jet) + cs = ax.contourf(line,depth,varP,levels=levels, vmax=cmax,vmin=cmin, + cmap=plt.get_cmap('jet')) + cbar = fig.colorbar(cs) + #cbar.set_label(title, rotation=-90,labelpad=30) + ax.contour(line,depth,varP,cs.levels) #, linewidths=0.5,colors='k') + #ax.set_title() + plt.title(title) + #scale = 1 + #ticks = ticker.FuncFormatter(lambda lon, pos: '{0:g}'.format(lon/scale)) + #ax.xaxis.set_major_formatter(ticks) + #ax.yaxis.set_major_formatter(ticks) + plt.xlabel('Distance along line (m)') + plt.ylabel('Depth (m)') + + def Harmonic_analysis_at_point(self, pt_lon, pt_lat, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Inputs: + - pt_lon = longitude in decimal degrees East, float number + - pt_lat = latitude in decimal degrees North, float number + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - time_ind = time indices to work in, list of integers + - t_start = start time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + #TR_comments: Add debug flag in Utide: debug=self._debug + index = self.index_finder(pt_lon, pt_lat, debug=False) + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + harmo = {} + time = self._var.matlabTime[:] + for layerIndex in range(self._var.u.shape[1]): + #if type(self._var.ua).__name__=='Variable': #Fix for netcdf4 lib + # ua = self._var.ua[:] + # va = self._var.va[:] + #else: + U = self._var.u[:,layerIndex] + V = self._var.v[:,layerIndex] + + u = self.interpolation_at_point(U, pt_lon, pt_lat, index=index, debug=debug) + v = self.interpolation_at_point(V, pt_lon, pt_lat, index=index, debug=debug) + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwarg) + + else: + time = self._var.matlabTime[:] + if type(self._var.el).__name__=='Variable': #Fix for netcdf4 lib + el = self._var.el[:] + else: + el = self._var.el + el = self.interpolation_at_point(el, pt_lon, pt_lat, + index=index, debug=debug) + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficients from harmo_analysis, dictionary if velocity + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + #TR_comments: Add debug flag in Utide: debug=self._debug + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + + return Reconstruct + + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v diff --git a/build/lib/pyseidon_dvt/fvcomClass/fvcomClass.py b/build/lib/pyseidon_dvt/fvcomClass/fvcomClass.py new file mode 100644 index 0000000..1ae6103 --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/fvcomClass.py @@ -0,0 +1,322 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Libs import +from __future__ import division +#TR comment: 2 alternatives +import netCDF4 as nc +from scipy.io import netcdf +from pydap.client import open_url +import cPickle as pkl +import pickle as Pkl +import copy +from os.path import isfile +import gc + +#Utility import +from pyseidon_dvt.utilities.object_from_dict import ObjectFromDict +from pyseidon_dvt.utilities.pyseidon2pickle import pyseidon_to_pickle +from pyseidon_dvt.utilities.pyseidon2matlab import pyseidon_to_matlab +from pyseidon_dvt.utilities.pyseidon2netcdf import pyseidon_to_netcdf + +# Custom error +from pyseidon_error import PyseidonError + +#Local import +from variablesFvcom import _load_var, _load_grid +from functionsFvcom import * +from functionsFvcomThreeD import * +from plotsFvcom import * + +class FVCOM: + """ + **A class/structure for FVCOM data** + Functionality structured as follows: :: + + _Data. = raw netcdf file data + |_Variables. = fvcom variables and quantities + |_Grid. = fvcom grid data + |_History = Quality Control metadata + FVCOM._|_Utils2D. = set of useful functions and methods for 2D and 3D runs + |_Utils3D. = set of useful functions and methods for 3D runs + |_Plots. = plotting functions + |_Save_as = "save as" methods + + Inputs: + - filename = path to file, string, + ex: testFvcom = FVCOM('./path_to_FVOM_output_file/filename') + Note that the file can be a pickle file (i.e. *.p) or a netcdf file (i.e. *.nc) + Additionally, either a file path or a OpenDap url could be used + + Options: + - ax = defines for a specific spatial region to work with, as such: + ax = [minimun longitude, maximun longitude, minimun latitude, maximum latitude] + or use one of the following pre-defined region: ax = 'GP', 'PP', 'DG' or 'MP' + Note that this option permits to extract partial data from the overall file + and therefore reduce memory and cpu use. + + - tx = defines for a specific temporal period to work with, as such: + tx = ['2012-11-07T12:00:00','2012.11.09 12:00:00'], string of 'yyyy-mm-dd hh:mm:ss' + Note that this option permits to extract partial data from the overall file + and therefore reduce memory and cpu use + + *Notes* + Throughout the package, the following conventions apply: + - Date = string of 'yyyy-mm-dd hh:mm:ss' + - Coordinates = decimal degrees East and North + - Directions = in degrees, between -180 and 180 deg., i.e. 0=East, 90=North, +/-180=West, -90=South + - Depth = 0m is the free surface and depth is negative + """ + + def __init__(self, filename, ax=[], tx=[], debug=False): + """ Initialize FVCOM class.""" + self._debug = debug + if debug: print '-Debug mode on-' + #Force garbage collector when fvcom object created + gc.collect() + + #Loading pickle file + if filename.endswith('.p'): + f = open(filename, "rb") + try: + data = pkl.load(f) + except MemoryError: + try: + data = Pkl.load(f) + except KeyError: + data = pkl.load(f,2) + self._origin_file = data['Origin'] + self.History = data['History'] + if debug: print "Turn keys into attributs" + self.Grid = ObjectFromDict(data['Grid']) + self.Variables = ObjectFromDict(data['Variables']) + try: + if self._origin_file.startswith('http'): + #Look for file through OpenDAP server + print "Retrieving data through OpenDap server..." + self.Data = open_url(data['Origin']) + #Create fake attribut to be consistent with the rest of the code + self.Data.variables = self.Data + else: + #WB_Alternative: self.Data = sio.netcdf.netcdf_file(filename, 'r') + #WB_comments: scipy has causes some errors, and even though can be + # faster, can be unreliable + if isfile(data['Origin']): + try: + self.Data = netcdf.netcdf_file(data['Origin'], 'r',mmap=True) + #due to mmap not coping with big array > 4Gib + except (OverflowError, TypeError, ValueError) as e: + self.Data = nc.Dataset(data['Origin'], 'r', + format='NETCDF4_CLASSIC') + else: + print "the original *.nc file has not been found" + except: #TR: need to precise the type of error here + print "the original *.nc file has not been found" + pass + #Loading netcdf file + elif filename.endswith('.nc'): + if filename.startswith('http'): + #Look for file through OpenDAP server + print "Retrieving data through OpenDap server..." + self.Data = open_url(filename) + #Create fake attribut to be consistent with the rest of the code + self.Data.variables = self.Data + else: + #Look for file locally + print "Retrieving data from " + filename + " ..." + #WB_Alternative: self.Data = sio.netcdf.netcdf_file(filename, 'r') + #WB_comments: scipy has causes some errors, and even though can be + # faster, can be unreliable + try: + self.Data = netcdf.netcdf_file(filename, 'r',mmap=True) + #due to mmap not coping with big array > 4Gib + except (OverflowError, TypeError, ValueError) as e: + self.Data = nc.Dataset(filename, 'r', format='NETCDF4_CLASSIC') + text = 'Created from ' + filename + self._origin_file = filename + #Metadata + self.History = [text] + # Calling sub-class + print "Initialisation..." + #print "This might take some time..." + try: + self.Grid = _load_grid(self.Data, + ax, + self.History, + debug=self._debug) + self.Variables = _load_var(self.Data, + self.Grid, + tx, + self.History, + debug=self._debug) + except MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + elif filename.endswith('.mat'): + raise PyseidonError("---Functionality not yet implemented---") + else: + raise PyseidonError("---Wrong file format---") + + self.Plots = PlotsFvcom(self.Variables, + self.Grid, + self._debug) + self.Util2D = FunctionsFvcom(self.Variables, + self.Grid, + self.Plots, + self.History, + self._debug) + + if self.Variables._3D: + self.Util3D = FunctionsFvcomThreeD(self.Variables, + self.Grid, + self.Plots, + self.Util2D, + self.History, + self._debug) + self.Plots.vertical_slice = self.Util3D._vertical_slice + + ##Re-assignement of utility functions as methods + #self.dump_profile_data = self.Plots._dump_profile_data_as_csv + #self.dump_map_data = self.Plots._dump_map_data_as_csv + + return + + #Special methods + def __del__(self): + """making sure that all opened files are closed when deleted or overwritten""" + #TR: not sure __del__ is the best approach for that + try: + if type(self.Data).__name__ == "netcdf_file": + try: + self.Data.close() + except AttributeError: + pass + elif type(self.Data).__name__ == "Dataset": + self.Data.close() + else: + try: + f.close() + except (NameError,AttributeError) as e: + pass + except AttributeError: + try: + f.close() + except (NameError,AttributeError) as e: + pass + + def __new__(self): + """Force garbage collector when new fvcom object created""" + gc.collect() + + def __add__(self, FvcomClass, debug=False): + """ + This special method permits to stack variables + of 2 FVCOM objects through a simple addition: :: + fvcom1 += fvcom2 + + *Notes* + - fvcom1 and fvcom2 have to cover the exact + same spatial domain + - last time step of fvcom1 must be <= to the + first time step of fvcom2 + """ + debug = debug or self._debug + #Define bounding box + if debug: + print "Computing bounding box..." + if self.Grid._ax == []: + lon = self.Grid.lon[:] + lat = self.Grid.lat[:] + self.Grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + if FvcomClass.Grid._ax == []: + lon = FvcomClass.Grid.lon[:] + lat = FvcomClass.Grid.lat[:] + FvcomClass.Grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + #series of test before stacking + if not (self.Grid._ax == FvcomClass.Grid._ax): + raise PyseidonError("---Spatial regions do not match---") + elif not ((self.Grid.nele == FvcomClass.Grid.nele) and + (self.Grid.nnode == FvcomClass.Grid.nnode) and + (self.Variables._3D == FvcomClass.Variables._3D)): + raise PyseidonError("---Data dimensions do not match---") + else: + if not (self.Variables.julianTime[-1]<= + FvcomClass.Variables.julianTime[0]): + raise PyseidonError("---Data not consecutive in time---") + #Copy self to newself + newself = copy.copy(self) + #TR comment: it still points toward self and modifies it + # so cannot do fvcom3 = fvcom1 + fvcom2 + if debug: + print 'Stacking variables...' + #keyword list for hstack + kwl=['matlabTime', 'julianTime'] + for key in kwl: + tmpN = getattr(newself.Variables, key) + tmpO = getattr(FvcomClass.Variables, key) + setattr(newself.Variables, key, + np.hstack((tmpN[:], tmpO[:]))) + + #keyword list for vstack + kwl=['u', 'v', 'w', 'ua', 'va', 'el', 'tke', 'gls'] + for key in kwl: + try: + tmpN = getattr(newself.Variables, key) + tmpO = getattr(FvcomClass.Variables, key) + setattr(newself.Variables, key, + np.vstack((tmpN[:], tmpO[:]))) + except AttributeError: + continue + #New time dimension + newself.Grid.ntime = newself.Grid.ntime + FvcomClass.Grid.ntime + #Append to new object history + text = 'Data from ' + FvcomClass.History[0].split('/')[-1] \ + + ' has been stacked' + newself.History.append(text) + + return newself + + #Methods + def save_as(self, filename, fileformat='netcdf', exceptions=[], compression=False, debug=False): + """ + This method saves the current FVCOM structure as: + - *.nc, i.e. netcdf file + - *.p, i.e. python file + - *.mat, i.e. Matlab file + + Inputs: + - filename = path + name of the file to be saved, string + + Options: + - fileformat = format of the file to be saved, i.e. 'pickle', .netcdf. or 'matlab' + - exceptions = list of variables to exclude from output file + , list of strings + - compresion = compresses data with zlib and uses at least 3 significant digits, boolean + Note: Works only with netcdf format + """ + debug = debug or self._debug + if debug: + print 'Saving file...' + #Save as different formats + if fileformat=='pickle': + pyseidon_to_pickle(self, filename, exceptions=exceptions, debug=debug) + elif fileformat=='matlab': + pyseidon_to_matlab(self, filename, exceptions=exceptions, debug=debug) + elif fileformat=='netcdf': + pyseidon_to_netcdf(self, filename, exceptions=exceptions, compression=compression, debug=debug) + else: + print "---Wrong file format---" + +#Test section when running in shell >> python fvcomClass.py +#if __name__ == '__main__': + + #filename = './test_file/dn_coarse_0001.nc' + #test = FVCOM(filename) + #test.harmonics(0, cnstit='auto', notrend=True, nodiagn=True) + #WB_COMMENTS: fixed matlabttime to matlabtime + #test.reconstr(test.Variables.matlabTime) diff --git a/build/lib/pyseidon_dvt/fvcomClass/plotsFvcom.py b/build/lib/pyseidon_dvt/fvcomClass/plotsFvcom.py new file mode 100644 index 0000000..f397105 --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/plotsFvcom.py @@ -0,0 +1,682 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import os +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +import matplotlib.ticker as ticker +import matplotlib.patches as mpatches +import seaborn +import pandas as pd +try: + from osgeo import ogr + from osgeo import osr + havegdal=True +except ImportError: + print 'Gdal is not installed, osgeo cannot be imported. Saving to shape files will not be possible.' + havegdal=False +import zipfile +# Local import +from pyseidon_dvt.utilities.windrose import WindroseAxes +from pyseidon_dvt.utilities.interpolation_utils import * +#from miscellaneous import depth_at_FVCOM_element as depth_at_ind + +# Kml header +### +kml_groundoverlay = ''' + + + + __NAME__ + __COLOR__ + __VISIBILITY__ + + overlay.png + + + __SOUTH__ + __NORTH__ + __WEST__ + __EAST__ + + + + Legend + + legend.png + + + + + + + + +''' +### + +class PlotsFvcom: + """ + **'Plots' subset of FVCOM class gathers plotting functions** + """ + def __init__(self, variable, grid, debug): + self._debug = debug + #Back pointer + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + + return + + def _def_fig(self): + """Defines figure window""" + self._fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + + + def colormap_var(self, var, title=' ', cmin=[], cmax=[], cmap=[], bb = None, + degree=True, mesh=False, isoline = 'bathy', isostep = None, + dump=False, png=False, shapefile=False, kmz=False, debug=False, **kwargs): + """ + 2D xy colormap plot of any given variable and mesh. + + Input: + - var = gridded variable, 1 D numpy array (nele or nnode) + + Options: + - title = plot title, string + - cmin = minimum limit colorbar + - cmax = maximum limit colorbar + - cmap = matplolib colormap + - units = string, var's units + - bb = bounding box, ex.: + bb = [minimun longitude, maximun longitude, minimun latitude, maximum latitude] + bb = [minimun x, maximun x, minimun y, maximum y] + - mesh = True, with mesh; False, without mesh + - degree = boolean, coordinates in degrees (True) or meters (False) + - isoline = 'bathy': bathymetric isolines, 'var': variable isolines, 'none': no isolines + - isostep = increment between isolines, float + - dump = boolean, dump profile data in csv file + - png = boolean, save map as png + - shapefile = boolean, save map as shapefile + - kmz = boolean, save map as kmz + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + debug = debug or self._debug + if debug: + print 'Plotting grid...' + # Figure if var had nele or nnode dimensions + if var.shape[0] == self._grid.nele: + dim = self._grid.nele + elif var.shape[0] == self._grid.nnode: + dim = self._grid.nnode + else: + print "Var has the wrong dimension, var.shape[0]= Grid.nele or nnode" + return + + # Bounding box nodes, elements and variable + if degree: + lon = self._grid.lon[:] + lat = self._grid.lat[:] + if debug: + print "Computing bounding box..." + if bb is None: + if self._grid._ax == []: + self._grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + bb = self._grid._ax + + if not hasattr(self._grid, 'triangleLL'): + # Mesh triangle + if debug: + print "Computing triangulation..." + trinodes = self._grid.trinodes[:] + tri = Tri.Triangulation(lon, lat, triangles=trinodes) + self._grid.triangleLL = tri + else: + tri = self._grid.triangleLL + + else: + x = self._grid.x[:] + y = self._grid.y[:] + if debug: + print "Computing bounding box..." + if bb is None: + bb = [x.min(), x.max(), y.min(), y.max()] + + if not hasattr(self._grid, 'triangleXY'): + # Mesh triangle + if debug: + print "Computing triangulation..." + trinodes = self._grid.trinodes[:] + tri = Tri.Triangulation(x, y, triangles=trinodes) + self._grid.triangleXY = tri + else: + tri = self._grid.triangleXY + + #setting limits and levels of colormap + if cmin==[]: + if debug: + print "Computing cmin..." + cmin=var[:].min() + if cmax==[]: + if debug: + print "Computing cmax..." + cmax=var[:].max() + step = (cmax-cmin) / 50.0 + + #Figure window params + self._def_fig() + if degree: + self._ax = self._fig.add_subplot(111, + aspect=(1.0/np.cos(np.mean(lat)*np.pi/180.0))) + else: + self._ax = self._fig.add_subplot(111, aspect=1.0) + + #Plotting functions + if debug: + print "Computing colormap..." + if cmap==[]: + cmap = plt.cm.gist_earth + f = self._ax.tripcolor(tri, var[:],vmax=cmax,vmin=cmin,cmap=cmap) + if mesh: + plt.triplot(tri, color='white', linewidth=0.5) + + #Label and axis parameters + if degree: + self._ax.set_ylabel('Latitude') + self._ax.set_xlabel('Longitude') + else: + self._ax.set_ylabel('Distance (m)') + self._ax.set_xlabel('Distance (m)') + self._ax.patch.set_facecolor('0.5') + self._ax.set_title(title) + units = kwargs.pop('units', '-') + cbar=self._fig.colorbar(f, ax=self._ax) + cbar.set_label(units, rotation=-90,labelpad=30) + scale = 1 + + if degree: + ticks = ticker.FuncFormatter(lambda lon, pos: '{0:g}'.format(lon/scale)) + self._ax.xaxis.set_major_formatter(ticks) + self._ax.yaxis.set_major_formatter(ticks) + + self._ax.set_xlim([bb[0],bb[1]]) + self._ax.set_ylim([bb[2],bb[3]]) + + # Isolines + if not isoline == 'none': + if isoline == 'bathy': + if not isostep == None: + levels = list(np.arange(round(self._grid.h.min()), round(self._grid.h.max()), isostep)) + cs = self._ax.tricontour(tri, self._grid.h, colors='w', linewidths=1.0, levels=levels) + else: + cs = self._ax.tricontour(tri, self._grid.h, colors='w', linewidths=1.0) + plt.clabel(cs, fontsize=11, inline=1) + plt.figtext(.12, .95, "Notes: white lines = bathymetric isolines", size='x-small') + elif isoline == 'var': + if var.shape[0] == self._grid.nele: + vari = interp_linear_to_nodes(var, self._grid.xc, self._grid.yc, self._grid.x, self._grid.y) + else: + vari = var + bounds=np.linspace(cmin,cmax,11) + if not isostep == None: + levels = list(np.arange(cmin, cmax, isostep)) + cs = self._ax.tricontour(tri, vari[:], vmin=cmin, vmax=cmax, colors='w', linewidths=1.0, levels=levels) + else: + cs = self._ax.tricontour(tri, vari[:], vmin=cmin, vmax=cmax, colors='w', linewidths=1.0, levels=bounds) + plt.clabel(cs, fontsize=11, inline=1) + plt.figtext(.12, .95, "Notes: white lines = isolines", size='x-small') + + # Show plot + self._ax.grid() + self._fig.show() + + # Saving + title = title.replace(" ", "_") + title = title.replace("(", "_") + title = title.replace(")", "_") + title = title.replace("-", "_") + title = title.replace("/", "_") + title = title.replace(".", "_") + savename=title.lower() + if png: + #if kmz: + # self._fig.savefig("overlay.png", bbox_inches='tight', transparent=True) + self._fig.savefig(savename+".png", bbox_inches='tight') + + if dump: + if degree: + self._dump_map_data_as_csv(var, self._grid.lonc, self._grid.latc, + title=savename, varLabel='map', + xLabel=' ', yLabel=' ', **kwargs) + else: + self._dump_map_data_as_csv(var, self._grid.xc, self._grid.yc, title=savename, + varLabel='map', xLabel=' ', yLabel=' ', **kwargs) + if debug or self._debug: + print '...Passed' + + if shapefile and havegdal: + if var.shape[0] == self._grid.nnode: + if debug: print "Interpolating var..." + var = interpN(var, self._grid.trinodes, self._grid.aw0, debug=debug) + if degree: + self._save_map_as_shapefile(var, self._grid.lon, self._grid.lat, + title=savename, varLabel='map', debug=debug) + else: + self._save_map_as_shapefile(var, self._grid.x, self._grid.y, + title=savename, varLabel='map', debug=debug) + elif shapefile and not havegdal: + print 'Shape file cannot be saved. Missing gdal.' + + if kmz: + name = kwargs.pop('name', title) + color = kwargs.pop('color', '9effffff') + visibility = str( kwargs.pop('visibility', 1) ) + kmzfile = kwargs.pop('kmzfile', savename+'.kmz') + pixels = kwargs.pop('pixels', 2048) # pixels of the max. dimension + units = kwargs.pop('units', '-') + + # Deformation dur to projection + # if degree: + # geo_aspect = np.cos(lat.mean()*np.pi/180.0) + # xsize = lon.ptp()*geo_aspect + # ysize = lat.ptp() + # xmax = lon.max() + # ymax = lat.max() + # xmin = lon.min() + # ymin = lat.min() + # else: + # geo_aspect = np.cos(self._grid.lat[:].mean()*np.pi/180.0) + # xsize = x.ptp()*geo_aspect + # ysize = y.ptp() + # xmax = x.max() + # ymax = y.max() + # xmin = x.min() + # ymin = y.min() + geo_aspect = np.cos(self._grid.lat[:].mean()*np.pi/180.0) + xsize = np.abs(bb[1] - bb[0]) * geo_aspect + ysize = np.abs(bb[3] - bb[2]) + aspect = ysize/xsize + if aspect > 1.0: + figsize = (30.0/aspect, 30.0) + else: + figsize = (30.0, 30.0*aspect) + + plt.ioff() + fig = plt.figure(figsize=figsize, facecolor=None, frameon=False, dpi=pixels // 5) + # fig = figure(facecolor=None, frameon=False, dpi=pixels//10) + ax = fig.add_axes([0, 0, 1, 1]) + pc = ax.tripcolor(tri, var[:], vmax=cmax, vmin=cmin, cmap=cmap) + + # Isolines + if not isoline == 'none': + if isoline == 'bathy': + if not isostep == None: + levels = list(np.arange(round(self._grid.h.min()), round(self._grid.h.max()), isostep)) + cs = ax.tricontour(tri, self._grid.h, colors='w', linewidths=1.0, levels=levels) + else: + cs = ax.tricontour(tri, self._grid.h, colors='w', linewidths=1.0) + units += " & bathymetric isolines" + elif isoline == 'var': + if not isostep == None: + levels = list(np.arange(cmin, cmax, isostep)) + cs = ax.tricontour(tri, vari[:], vmin=cmin, vmax=cmax, colors='w', linewidths=1.0, levels=levels) + else: + cs = ax.tricontour(tri, vari[:], vmin=cmin, vmax=cmax, colors='w', linewidths=1.0, levels=bounds) + plt.clabel(cs, fontsize=11, inline=1) + + ax.set_xlim([bb[0], bb[1]]) + ax.set_ylim([bb[2], bb[3]]) + ax.set_axis_off() + fig.savefig('overlay.png', dpi=200, transparent=True) + + # Write kmz + fz = zipfile.ZipFile(kmzfile, 'w') + fz.writestr(savename+'.kml', kml_groundoverlay.replace('__NAME__', name)\ + .replace('__COLOR__', color)\ + .replace('__VISIBILITY__', visibility)\ + .replace('__SOUTH__', str(bb[2]))\ + .replace('__NORTH__', str(bb[3]))\ + .replace('__EAST__', str(bb[1]))\ + .replace('__WEST__', str(bb[0]))) + fz.write('overlay.png') + os.remove('overlay.png') + + # colorbar png + fig = plt.figure(figsize=(1.0, 4.0), facecolor=None, frameon=False) + ax = fig.add_axes([0.0, 0.05, 0.2, 0.9]) + cb = fig.colorbar(pc, cax=ax) + cb.set_label(units, color='0.0') + for lab in cb.ax.get_yticklabels(): + plt.setp(lab, 'color', '0.0') + + fig.savefig('legend.png', transparent=True) + fz.write('legend.png') + os.remove('legend.png') + fz.close() + + def rose_diagram(self, direction, norm, png=False, title="rose_diagram"): + + """ + Plots rose diagram + + Inputs: + - direction = 1D array + - norm = 1D array + + Options: + - title = plot title, string + - png = boolean, saves rose diagram as png + """ + #Convertion + #TR: not quite sure here, seems to change from location to location + # express principal axis in compass + direction = np.mod(90.0 - direction, 360.0) + + #Create new figure + #fig = plt.figure(figsize=(18,10)) + #plt.rc('font',size='22') + self._def_fig() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(self._fig, rect)#, axisbg='w') + self._fig.add_axes(ax) + #Rose + ax.bar(direction, norm , normed=True, opening=0.8, edgecolor='white') + #adjust legend + l = ax.legend(shadow=True, bbox_to_anchor=[-0.1, 0], loc='lower left') + plt.setp(l.get_texts(), fontsize=10) + plt.xlabel('Rose diagram in % of occurrences - Colormap of norms') + self._fig.show() + + # Saving + title = title.replace(" ", "_") + title = title.replace("(", "_") + title = title.replace(")", "_") + title = title.replace("-", "_") + title = title.replace("/", "_") + title = title.replace(".", "_") + savename=title.lower() + if png: + self._fig.savefig(savename+".png", bbox_inches='tight') + + def plot_xy(self, x, y, xerror=[], yerror=[], + title='xy_plot', xLabel=' ', yLabel=' ', + png=False, dump=False, **kwargs): + """ + Simple X vs Y plot + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - png = boolean, saves map as png + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + #fig = plt.figure(figsize=(18,10)) + #plt.rc('font',size='22') + self._def_fig() + self._ax = self._fig.add_subplot(111) + self._ax.plot(x, y, label=title) + scale = 1 + self._ax.set_ylabel(yLabel) + self._ax.set_xlabel(xLabel) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + if not yerror==[]: + self._ax.fill_between(x, y-yerror, y+yerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if not xerror==[]: + self._ax.fill_betweenx(y, x-xerror, x+xerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if (not xerror==[]) or (not yerror==[]): + blue_patch = mpatches.Patch(color='#089FFF', + label='Standard deviation',alpha=0.2) + plt.legend(handles=[blue_patch],loc=1, fontsize=12) + #plt.legend([blue_patch],loc=1, fontsize=12) + + self._fig.show() + # Saving + title = title.replace(" ", "_") + title = title.replace("(", "_") + title = title.replace(")", "_") + title = title.replace("-", "_") + title = title.replace("/", "_") + title = title.replace(".", "_") + savename=title.lower().replace(" ","_") + if png: + self._fig.savefig(savename+".png", bbox_inches='tight') + + if dump: self._dump_profile_data_as_csv(x, y,xerror=xerror, yerror=yerror, + title=savename, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def Histogram(self, y, title='Histogram', xLabel=' ', yLabel=' ', bins=50, + png=False, dump=False, **kwargs): + """ + Histogram plot + + Inputs: + - bins = list of bin edges + - y = 1D array + + Options: + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - bins = number of bins, integer + - png = boolean, saves histogram as png + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + ## the histogram of the data + #fig = plt.figure(figsize=(18,10)) + self._def_fig() + self._ax = self._fig.add_subplot(111) + density, bins = np.histogram(y, bins=bins, normed=True, density=True) + unity_density = density / density.sum() + widths = bins[:-1] - bins[1:] + # To plot correct percentages in the y axis + self._ax.bar(bins[1:], unity_density, width=widths) + formatter = ticker.FuncFormatter(lambda v, pos: str(v * 100)) + self._ax.yaxis.set_major_formatter(formatter) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + + plt.ylabel(yLabel) + plt.xlabel(xLabel) + + self._fig.show() + + # Saving + title = title.replace(" ", "_") + title = title.replace("(", "_") + title = title.replace(")", "_") + title = title.replace("-", "_") + title = title.replace("/", "_") + title = title.replace(".", "_") + savename=title.lower() + if png: + self._fig.savefig(savename+".png", bbox_inches='tight') + if dump: self._dump_profile_data_as_csv(bins[1:], unity_density, + title=savename, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def add_points(self, x, y, label=' ', color='black'): + """ + Adds scattered points (x,y) on current figure, + where x and y are 1D arrays of the same lengths. + + Inputs: + - x = float number or list of float numbers + - y = float number or list of float numbers + + Options: + - Label = a string + - Color = a string, 'red', 'green', etc. or gray shades like '0.5' + """ + plt.scatter(x, y, s=50, color=color) + #TR : annotate does not work on my machine !? + plt.annotate(label, xy=(x, y), xycoords='data', xytext=(-20, 20), + textcoords='offset points', ha='right', + arrowprops=dict(arrowstyle="->", shrinkA=0), + fontsize=12) + + def _dump_profile_data_as_csv(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', **kwargs): + """ + Dumps profile data in csv file + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = file name, string + - xLabel = name of the x-data, string + - yLabel = name of the y-data, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_profile_data' + filename=title + '.csv' + if xLabel == ' ': xLabel = 'X' + if yLabel == ' ': yLabel = 'Y' + if not xerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': xerror[:]}) + elif not yerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': yerror[:]}) + else: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:]}) + df.to_csv(filename, encoding='utf-8', **kwargs) + + def _dump_map_data_as_csv(self, var, x, y, title=' ', + varLabel=' ', xLabel=' ', yLabel=' ', **kwargs): + """ + Dumps map data in csv file + + Inputs: + - var = gridded variable, 1 D numpy array (nele or nnode) + - x = coordinates, 1 D numpy array (nele or nnode) + - y = coordinates, 1 D numpy array (nele or nnode) + + Options: + - title = file name, string + - xLabel = name of the x-data, string + - yLabel = name of the y-data, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_map_data' + filename=title + '.csv' + if varLabel == ' ': varLabel = 'Z' + if xLabel == ' ': xLabel = 'X' + if yLabel == ' ': yLabel = 'Y' + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], varLabel: var[:]}) + df.to_csv(filename, encoding='utf-8', **kwargs) + + def _save_map_as_shapefile(self, var, x, y, title=' ', varLabel=' ', debug=False): + """ + Saves map as shapefile + + Inputs: + - var = gridded variable, 1 D numpy array (nele or nnode) + - x = coordinates, 1 D numpy array (nele or nnode) + - y = coordinates, 1 D numpy array (nele or nnode) + + Options: + - title = file name, string + - kwargs = keyword options associated with ??? + """ + + debug = debug or self._debug + if debug: + print 'Converting map to shapefile...' + if title == ' ': + title = 'save_map_data' + else: # reformat file name + title = title.replace(" ", "_") + title = title.replace("(", "_") + title = title.replace(")", "_") + title = title.replace("-", "_") + title = title.replace("/", "_") + title = title.replace(".", "_") + + filename=title + '.shp' + + # Projection + #epsg_in=4326 + epsg_in = 3857 # Google Projection + + # give alternative file name is already exists + driver = ogr.GetDriverByName('ESRI Shapefile') + if os.path.exists(filename): + filename = filename[:-4] + "_bis.shp" + + shapeData = driver.CreateDataSource(filename) + + spatialRefi = osr.SpatialReference() + spatialRefi.ImportFromEPSG(epsg_in) + + lyr = shapeData.CreateLayer("poly_layer", spatialRefi, ogr.wkbPolygon) + + #Features + if varLabel==' ': varLabel = 'var' + lyr.CreateField(ogr.FieldDefn(varLabel, ogr.OFTReal)) + + if debug: print "Writing ESRI Shapefile %s..." % filename + lon = x[:] + lat = y[:] + trinodes = self._grid.trinodes[:] + + if debug: print "Writing Node Array" + cnt = 0 + for row in trinodes: + val1 = -999 + ring = ogr.Geometry(ogr.wkbLinearRing) + for val in row: + if val1 == -999: + val1 = val + ring.AddPoint(lon[val], lat[val]) + #Add 1st point to close ring + ring.AddPoint(lon[val1], lat[val1]) + + poly = ogr.Geometry(ogr.wkbPolygon) + poly.AddGeometry(ring) + + #Now add field values from array + feat = ogr.Feature(lyr.GetLayerDefn()) + feat.SetGeometry(poly) + feat.SetField(varLabel, float(var[cnt])) + + lyr.CreateFeature(feat) + feat.Destroy() + poly.Destroy() + + val1 = -999 + cnt += 1 + + shapeData.Destroy() + if debug: print "Finished writing Shapefile Mesh. [Total Nodes: %d]" % cnt diff --git a/build/lib/pyseidon_dvt/fvcomClass/variablesFvcom.py b/build/lib/pyseidon_dvt/fvcomClass/variablesFvcom.py new file mode 100644 index 0000000..eff00ea --- /dev/null +++ b/build/lib/pyseidon_dvt/fvcomClass/variablesFvcom.py @@ -0,0 +1,713 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +from itertools import groupby +from operator import itemgetter +import datetime +import gc +# Parallel computing +#import multiprocessing as mp +#Local import +from pyseidon_dvt.utilities.regioner import * +from pyseidon_dvt.utilities.miscellaneous import time_to_index +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +class _load_grid: + """ + **'Grid' subset in FVCOM class contains grid related quantities** + + Some grid data are directly passed on from FVCOM output: :: + _lon = longitudes at nodes (deg.), 2D array (ntime, nnode) + |_lonc = longitudes at elements (deg.), 2D array (ntime, nele) + |_lat = latitudes at nodes (deg.), 2D array (ntime, nnode) + |_latc = latitudes at elements (deg.), 2D array (ntime, nele) + |_x = x coordinates at nodes (m), 2D array (ntime, nnode) + |_xc = x coordinates at elements (m), 2D array (ntime, nele) + |_y = y coordinates at nodes (m), 2D array (ntime, nnode) + |_yc = y coordinates at nodes (m), 2D array (ntime, nele) + FVCOM.Grid._|_h = bathymetry (m), 2D array (ntime, nnode) + |_nele = element dimension, integer + |_nnode = node dimension, integer + |_nlevel = vertical level dimension, integer + |_ntime = time dimension, integer + |_trinodes = surrounding node indices, 2D array (3, nele) + |_triele = surrounding element indices, 2D array (3, nele) + |_siglay = sigma layers, 2D array (nlevel, nnode) + |_siglay = sigma levels, 2D array (nlevel+1, nnode) + |_and a all bunch of grid parameters... + | i.e. a1u, a2u, aw0, awx, awy + + Some others shall be generated as methods are being called, ex: :: + ... + |_triangle = triangulation object for plotting purposes + + """ + def __init__(self, data, ax, History, debug=False): + self._debug = debug + if debug: + print 'Loading grid...' + #Pointer to History + setattr(self, '_History', History) + + #list of required grid variable + gridvar = ['lon','lat','lonc','latc','x','y','xc','yc', + 'a1u','a2u','aw0','awx','awy'] + + # Figure out which quantity to treat + self._gridvar = [] + for key in gridvar: + if key in data.variables.keys(): + self._gridvar.append(key) + else: + if key in ["a1u", "a2u", "aw0", "awx", "awy"]: + print "--- "+key+" is missing. Some interpolation functions will not work ---" + if key in ["lonc", "latc", "xc", "yc"]: + print "--- "+key+" is missing. Some element based functions will not work ---" + + for key in self._gridvar: + try: + setattr(self, key, data.variables[key].data) + except AttributeError: #exception for nc.dataset type data + setattr(self, key, data.variables[key])#[:]) + + #special treatment for triele & trinodes due to Save_as(netcdf) + datavar = data.variables.keys() + if "trinodes" in datavar: + try: + setattr(self, 'trinodes', data.variables['trinodes'].data) + except AttributeError: #exception for nc.dataset type data + setattr(self, 'trinodes', data.variables['trinodes'])#[:]) + elif "nv" in datavar: + try: + self.trinodes = np.transpose(data.variables['nv'].data) - 1 + except AttributeError: #exception for nc.dataset type data + self.trinodes = np.transpose(data.variables['nv'][:]) - 1 + else: + print "--- surrounding node indices (nv) missing. Some functions will not work ---" + if "triele" in datavar: + try: + setattr(self, 'triele', data.variables['triele'].data) + except AttributeError: #exception for nc.dataset type data + setattr(self, 'triele', data.variables['triele'])#[:]) + elif "nbe" in datavar: + try: + self.triele = np.transpose(data.variables['nbe'].data) - 1 + except AttributeError: #exception for nc.dataset type data + self.triele = np.transpose(data.variables['nbe'][:]) - 1 + else: + print "--- surrounding element indices (nbe) missing. Some functions will not work ---" + + #special treatment for depth2D & depth due to Save_as(netcdf) + if "depth2D" in datavar: + setattr(self, "depth2D", data.variables["depth2D"])#[:]) + if "depth" in datavar: + setattr(self, "depth", data.variables["depth"])#[:]) + + if ax==[]: + #Define bounding box + self._ax = [] + #Append message to History field + text = 'Full spatial domain' + self._History.append(text) + #Define the rest of the grid variables + try: self.h = data.variables['h'][:] + except KeyError: pass + try: self.siglay = data.variables['siglay'][:] + except KeyError: pass + try: self.siglev = data.variables['siglev'][:] + except KeyError: pass + try: self.nlevel = self.siglay.shape[0] + except AttributeError: pass + try: self.nele = self.lonc.shape[0] + except AttributeError: pass + try: self.nnode = self.lon.shape[0] + except: pass + else: + #Checking for pre-defined regions + if ax=='GP': ax=[-66.36, -66.31, 44.24, 44.3] + elif ax=='PP': ax=[-66.23, -66.19, 44.37, 44.41] + elif ax=='DG': ax=[-65.84, -65.73, 44.64, 44.72] + elif ax=='MP': ax=[-65.5, -63.3, 45.0, 46.0] + + print 'Re-indexing may take some time...' + Data = regioner(self, ax, debug=debug) + #list of grid variable + gridvar = ['lon','lat','lonc','latc','x','y','xc','yc', + 'a1u','a2u','aw0','awx','awy','nv','nbe'] + + # Figure out which quantity to treat + self._gridvar = [] + for key in gridvar: + if key in data.variables.keys(): + self._gridvar.append(key) + else: + if debug: print "Grid related field "+key+" is missing !" + + for key in self._gridvar: + setattr(self, key, Data[key][:]) + # Special treatment here + self.trinodes = Data['nv'][:] + self.triele = Data['nbe'][:] + self.triangle = Data['triangle'] + # Only load the element within the box + self._node_index = Data['node_index'] + self._element_index = Data['element_index'] + + # different loading technique if using OpenDap server + if type(data.variables).__name__ == 'DatasetType': + # Split into consecutive integers to optimise loading + # TR comment: data.variables['ww'].data[:,:,region_n] doesn't + # work with non consecutive indices + H=0 + for k, g in groupby(enumerate(self._node_index), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + # if debug: print 'Index bound: ' + str(ID[0]) + '-' + str(ID[-1]+1) + if H==0: + try: + self.h = data.variables['h'].data[ID[0]:(ID[-1]+1)] + self.siglay = data.variables['siglay'].data[:,ID[0]:(ID[-1]+1)] + self.siglev = data.variables['siglev'].data[:,ID[0]:(ID[-1]+1)] + except AttributeError: #exception for nc.dataset type data + self.h = data.variables['h'][ID[0]:(ID[-1]+1)] + self.siglay = data.variables['siglay'][:,ID[0]:(ID[-1]+1)] + self.siglev = data.variables['siglev'][:,ID[0]:(ID[-1]+1)] + else: + try: + self.h = np.hstack((self.h, + data.variables['h'].data[ID[0]:(ID[-1]+1)])) + self.siglay = np.hstack((self.siglay, + data.variables['siglay'].data[:,ID[0]:(ID[-1]+1)])) + self.siglev = np.hstack((self.siglev, + data.variables['siglev'].data[:,ID[0]:(ID[-1]+1)])) + except AttributeError: #exception for nc.dataset type data + self.h = np.hstack((self.h, + data.variables['h'][ID[0]:(ID[-1]+1)])) + self.siglay = np.hstack((self.siglay, + data.variables['siglay'][:,ID[0]:(ID[-1]+1)])) + self.siglev = np.hstack((self.siglev, + data.variables['siglev'][:,ID[0]:(ID[-1]+1)])) + H=1 + else: + try: + self.h = data.variables['h'].data[self._node_index] + self.siglay = data.variables['siglay'].data[:,self._node_index] + self.siglev = data.variables['siglev'].data[:,self._node_index] + except AttributeError: #exception for nc.dataset type data + self.h = data.variables['h'][self._node_index] + self.siglay = data.variables['siglay'][:,self._node_index] + self.siglev = data.variables['siglev'][:,self._node_index] + #Dimensions + self.nlevel = self.siglay.shape[0] + self.nele = Data['element_index'].shape[0] + self.nnode = Data['node_index'].shape[0] + + del Data + #Define bounding box + self._ax = ax + # Add metadata entry + text = 'Bounding box =' + str(ax) + self._History.append(text) + print '-Now working in bounding box-' + + if debug: + print '...Passed' + + return + +class _load_var: + """ + **'Variables' subset in FVCOM class contains the numpy arrays** + + Some variables are directly passed on from FVCOM output: :: + + _el = elevation (m), 2D array (ntime, nnode) + |_julianTime = julian date, 1D array (ntime) + |_matlabTime = matlab time, 1D array (ntime) + |_tauc = bottom shear stress (m2/s2), + | 2D array (ntime, nele) + |_ua = depth averaged u velocity component (m/s), + | 2D array (ntime, nele) + |_va = depth averaged v velocity component (m/s), + FVCOM.Variables._| 2D array (ntime, nele) + |_u = u velocity component (m/s), + | 3D array (ntime, nlevel, nele) + |_v = v velocity component (m/s), + | 3D array (ntime, nlevel, nele) + |_w = w velocity component (m/s), + | 3D array (ntime, nlevel, nele) + + Some others shall be generated as methods are being called, ex: :: + ... + |_hori_velo_norm = horizontal velocity norm (m/s), + | 2D array (ntime, nele) + |_velo_norm = velocity norm (m/s), + | 3D array (ntime, nlevel, nele) + |_verti_shear = vertical shear (1/s), + | 3D array (ntime, nlevel, nele) + |_vorticity... + + """ + def __init__(self, data, grid, tx, History, debug=False): + self._debug = debug + self._3D = False + self._opendap = type(data.variables).__name__=='DatasetType' + + # Pointer to History + setattr(self, '_History', History) + + # Parallel computing attributs + #self._cpus = mp.cpu_count() + + #List of keywords + kwl2D = ['ua', 'va', 'zeta','depth_av_flow_dir', 'hori_velo_norm', + 'depth_av_vorticity', 'depth_av_power_density', + 'depth_av_power_assessment', 'tauc'] + kwl3D = ['ww', 'u', 'v', 'gls', 'tke', 'flow_dir', 'velo_norm', + 'verti_shear', 'vorticity', 'power_density'] + #List of aliaSes + al2D = ['ua', 'va', 'el','depth_av_flow_dir', 'hori_velo_norm', + 'depth_av_vorticity', 'depth_av_power_density', + 'depth_av_power_assessment', 'tauc'] + al3D = ['w', 'u', 'v', 'gls', 'tke', 'flow_dir', 'velo_norm', + 'verti_shear', 'vorticity', 'power_density'] + + # Figure out which quantity to treat + self._kwl2D = [] + self._al2D = [] + for key, aliaS in zip(kwl2D, al2D): + if key in data.variables.keys(): + self._kwl2D.append(key) + self._al2D.append(aliaS) + else: + if debug: print key, " is missing !" + + + self._kwl3D = [] + self._al3D = [] + for key, aliaS in zip(kwl3D, al3D): + if key in data.variables.keys(): + self._kwl3D.append(key) + self._al3D.append(aliaS) + else: + if debug: print key, " is missing !" + + if not len(self._kwl3D)==0: self._3D = True + + #Loading time stamps + try: + julianTime = data.variables['julianTime'] + except KeyError: + # exception due to Save_as(netcdf) + julianTime=data.variables['time'] + # Work out if time is in julian time + timeFlag = 0.0 + try: + for key in julianTime.attributes: + if "julian" in julianTime.attributes[key].lower(): + timeFlag += 1.0 + except AttributeError: # pydap lib error + if "julian" in julianTime.format.lower(): + timeFlag += 1.0 + # if not julian time, convert in days in needed + if timeFlag == 0.0: + try: + for key in julianTime.attributes: + if "second" in julianTime.attributes[key].lower(): + timeFlag += 1.0 + except AttributeError: # pydap lib error + if "second" in julianTime.units.lower(): + timeFlag += 1.0 + if not timeFlag == 0.0: # TR: this conversion needs to be improved by introducing the right epoch + dayTime = julianTime[:] / (24*60*60) # convert in days + julianTime = dayTime + + if tx==[]: + # get time and adjust it to matlab datenum + self.julianTime = julianTime[:] + self.matlabTime = self.julianTime[:] + 678942.0 + #-Append message to History field + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Full temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + #Add time dimension to grid variables + grid.ntime = self.julianTime.shape[0] + if debug: print 'Full temporal domain' + else: + #Time period + region_t = self._t_region(tx, julianTime, debug=debug) + self._region_time = region_t + ts = self._region_time[0] + te = self._region_time[-1] + 1 + # get time and adjust it to matlab datenum + self.julianTime = julianTime[ts:te] + self.matlabTime = self.julianTime + 678942.0 + #-Append message to History field + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + #Add time dimension to grid variables + grid.ntime = self.julianTime.shape[0] + if debug: print "ntime: ", grid.ntime + if debug: print "region_t shape: ", region_t.shape + + # Define which loading function to use + if grid._ax==[] and tx==[]: + loadVar = self._load_full_time_full_region + for key, aliaS in zip(self._kwl2D, self._al2D): + loadVar(data, key, aliaS, debug=debug) + for key, aliaS in zip(self._kwl3D, self._al3D): + loadVar(data, key, aliaS, debug=debug) + else: + + if grid._ax!=[] and tx!=[]: + loadVar = self._load_partial_time_partial_region + elif grid._ax==[] and tx!=[]: + loadVar = self._load_partial_time_full_region + else: + loadVar = self._load_full_time_partial_region + + # Loading 2D variables + for key, aliaS in zip(self._kwl2D, self._al2D): + gc.collect() # force garbage collector in an attempt to free up some RAM + loadVar(data, grid, key, aliaS, debug=debug) + + # Loading 3D variables + for key, aliaS in zip(self._kwl3D, self._al3D): + gc.collect() # force garbage collector in an attempt to free up some RAM + loadVar(data, grid, key, aliaS, debug=debug) + + ##-------Parallelized loading block------- + # if debug: startT = time.time() + # + # divisor = len(self._kwl2D)//self._cpus + # remainder = len(self._kwl2D)%self._cpus + # + # if debug: print "Parallel loading 2D vars..." + # if debug: start2D = time.time() + # + # for i in range(divisor): + # start = self._cpus * i + # end = start + (self._cpus-1) + # processes = [mp.Process(target=loadVar, args=(data, grid, key, aliaS, debug))\ + # for key, aliaS in zip(self._kwl2D[start:end], self._al2D[start:end])] + # # Run processes + # for p in processes: + # p.start() + # # Exit the completed processes + # for p in processes: + # p.join() + # + # # Remaining vars + # if remainder != 0: + # start = int(-1 * remainder) + # processes = [mp.Process(target=loadVar, args=(data, grid, key, aliaS, debug))\ + # for key, aliaS in zip(self._kwl2D[start:], self._al2D[start:])] + # # Run processes + # for p in processes: + # p.start() + # # Exit the completed processes + # for p in processes: + # p.join() + # + # if debug: end2D = time.time() + # if debug: print "...processing time: ", (end2D - start2D) + # + # if debug: print "Parallel loading 3D vars..." + # if debug: start3D = time.time() + # + # for i in range(divisor): + # start = self._cpus * i + # end = start + (self._cpus-1) + # processes = [mp.Process(target=loadVar, args=(data, grid,key, aliaS, debug))\ + # for key, aliaS in zip(self._kwl3D[start:end], self._al3D[start:end])] + # # Run processes + # for p in processes: + # p.start() + # # Exit the completed processes + # for p in processes: + # p.join() + # + # # Remaining vars + # if remainder != 0: + # start = int(-1 * remainder) + # processes = [mp.Process(target=loadVar, args=(data, grid, key, aliaS, debug))\ + # for key, aliaS in zip(self._kwl3D[start:], self._al3D[start:])] + # # Run processes + # for p in processes: + # p.start() + # # Exit the completed processes + # for p in processes: + # p.join() + # + # if debug: end3D = time.time() + # if debug: endT = time.time() + # if debug: print "...-loading 3D- processing time: ", (end3D - start3D) + # if debug: print "...-loading 2D & 3D- processing time: ", (endT - startT) + # #-------end------- + + if debug: print '...Passed' + + # Define method loadvar for use in import + self._loadVar = loadVar + + return + + def _load_full_time_full_region(self, data, key, aliaS, debug=False): + """ + loading variables for full time and space domains + + Inputs: + - key = FVCOM variable name, str + - aliaS = PySeidon variable alias, str + + Options: + - debug = debug flag, boolean + """ + if debug: print "loading " + str(aliaS) +"..." + try: + setattr(self, aliaS, data.variables[key].data) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, data.variables[key]) + + def _load_partial_time_partial_region(self, data, grid, key, aliaS, debug=False): + """ + loading variables for partial time and space domains + + Inputs: + - key = FVCOM variable name, str + - aliaS = PySeidon variable alias, str + + Options: + - debug = debug flag, boolean + """ + if debug: print "loading " + str(aliaS) +"..." + + # define time bounds + ts = self._region_time[0] + te = self._region_time[-1] + 1 + + if key == 'zeta': + region = grid._node_index + horiDim = grid.nnode + else: + region = grid._element_index + horiDim = grid.nele + + if key == 'verti_shear': + vertiDim = grid.nlevel-1 + else: + vertiDim = grid.nlevel + + # Find out if using netCDF4 or scipy + try: + Test = data.variables[key].data + self._scipynetcdf = True + except AttributeError: # exeception due nc.Dataset + self._scipynetcdf = False + + if self._opendap: + # loop over contiguous indexes for opendap + H = 0 #local counter + for k, g in groupby(enumerate(region), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + #if debug: print 'Index bound: ' + str(ID[0]) + '-' + str(ID[-1]+1) + if key in self._kwl2D: + if self._scipynetcdf: + #TR : Don't I need to transpose here? + var = data.variables[key].data[ts:te,ID[0]:(ID[-1]+1)] + else: + var = data.variables[key][ts:te,ID[0]:(ID[-1]+1)] + if H == 0: + setattr(self, aliaS,var) + H = 1 + else: + setattr(self, aliaS, np.hstack((getattr(self, aliaS), var))) + else: + if self._scipynetcdf: + #TR : Don't I need to transpose here? + var = data.variables[key].data[ts:te,:,ID[0]:(ID[-1]+1)] + else: + var = data.variables[key][ts:te,:,ID[0]:(ID[-1]+1)] + if H == 0: + setattr(self, aliaS,var) + H = 1 + else: + setattr(self, aliaS, np.dstack((getattr(self, aliaS), var))) + # TR comment: looping on time indices is a trick from Mitchell O'Flaherty-Sproul to improve loading time + else: + I = 0 + if key in self._kwl2D: + setattr(self, aliaS, np.zeros((grid.ntime, horiDim))) + for i in self._region_time: + if self._scipynetcdf: + getattr(self, aliaS)[I,:] = np.transpose(data.variables[key].data[i, region]) + else: + getattr(self, aliaS)[I,:] = (data.variables[key][i, region]) + I += 1 + else: + setattr(self, aliaS, np.zeros((grid.ntime, vertiDim, horiDim))) + for i in self._region_time: + if self._scipynetcdf: + getattr(self, aliaS)[I,:,:] = np.transpose(data.variables[key].data[i, :, region]) + else: + getattr(self, aliaS)[I,:,:] = (data.variables[key][i, :, region]) + I += 1 + + + def _load_full_time_partial_region(self, data, grid, key, aliaS, debug=False): + """ + loading variables for full time domain and partial space domain + + Inputs: + - key = FVCOM variable name, str + - aliaS = PySeidon variable alias, str + + Options: + - debug = debug flag, boolean + """ + if debug: print "loading " + str(aliaS) +"..." + if key == 'zeta': + region = grid._node_index + horiDim = grid.nnode + else: + region = grid._element_index + horiDim = grid.nele + + if key == 'verti_shear': + vertiDim = grid.nlevel-1 + else: + vertiDim = grid.nlevel + + # Find out if using netCDF4 or scipy + try: + Test = data.variables[key].data + self._scipynetcdf = True + except AttributeError: # exeception due nc.Dataset + self._scipynetcdf = False + + if self._opendap: + # loop over contiguous indexes for opendap + H = 0 #local counter + for k, g in groupby(enumerate(region), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + #if debug: print 'Index bound: ' + str(ID[0]) + '-' + str(ID[-1]+1) + if key in self._kwl2D: + if self._scipynetcdf: + #TR : Don't I need to transpose here? + var = data.variables[key].data[:,ID[0]:(ID[-1]+1)] + else: + var = data.variables[key][:,ID[0]:(ID[-1]+1)] + if H == 0: + setattr(self, aliaS,var) + H = 1 + else: + setattr(self, aliaS, np.hstack((getattr(self, aliaS), var))) + else: + if self._scipynetcdf: + #TR : Don't I need to transpose here? + var = data.variables[key].data[:,:,ID[0]:(ID[-1]+1)] + else: + var = data.variables[key][:,:,ID[0]:(ID[-1]+1)] + if H == 0: + setattr(self, aliaS,var) + H = 1 + else: + setattr(self, aliaS, np.dstack((getattr(self, aliaS), var))) + else: + # TR comment: looping on time indices is a trick from Mitchell O'Flaherty-Sproul to improve loading time + if key in self._kwl2D: + setattr(self, aliaS, np.zeros((grid.ntime, horiDim))) + for i in range(grid.ntime): + if self._scipynetcdf: + getattr(self, aliaS)[i,:] = np.transpose(data.variables[key].data[i, region]) + else: + getattr(self, aliaS)[i,:] = (data.variables[key][i, region]) + else: + setattr(self, aliaS, np.zeros((grid.ntime, vertiDim, horiDim))) + for i in range(grid.ntime): + if self._scipynetcdf: + getattr(self, aliaS)[i,:,:] = np.transpose(data.variables[key].data[i, :, region]) + else: + getattr(self, aliaS)[i,:,:] = (data.variables[key][i, :, region]) + + def _load_partial_time_full_region(self, data, grid, key, aliaS, debug=False): + """ + loading variables for partial time domain and full space domain + + Inputs: + - key = FVCOM variable name, str + - aliaS = PySeidon variable alias, str + + Options: + - debug = debug flag, boolean + """ + if debug: print "loading " + str(aliaS) +"..." + + # define time bounds + ts = self._region_time[0] + te = self._region_time[-1] + 1 + + if key == 'zeta': + horiDim = grid.nnode + else: + horiDim = grid.nele + + if key == 'verti_shear': + vertiDim = grid.nlevel-1 + else: + vertiDim = grid.nlevel + + # Find out if using netCDF4 or scipy + try: + Test = data.variables[key].data + self._scipynetcdf = True + except AttributeError: # exeception due nc.Dataset + self._scipynetcdf = False + + if self._opendap: + if key in self._kwl2D: + if self._scipynetcdf: + var = data.variables[key].data[ts:te,:] + else: + var = data.variables[key][ts:te,:] + else: + if self._scipynetcdf: + var = data.variables[key].data[ts:te,:,:] + else: + var = data.variables[key][ts:te,:,:] + setattr(self, aliaS,var) + else: + I = 0 + # TR comment: looping on time indices is a trick from Mitchell O'Flaherty-Sproul to improve loading time + if key in self._kwl2D: + setattr(self, aliaS, np.zeros((grid.ntime, horiDim))) + for i in self._region_time: + if self._scipynetcdf: + getattr(self, aliaS)[I,:] = np.transpose(data.variables[key].data[i, :]) + else: + getattr(self, aliaS)[I,:] = (data.variables[key][i, :]) + I += 1 + else: + setattr(self, aliaS, np.zeros((grid.ntime, vertiDim, horiDim))) + for i in self._region_time: + if self._scipynetcdf: + getattr(self, aliaS)[I,:,:] = np.transpose(data.variables[key].data[i, :, :]) + else: + getattr(self, aliaS)[I,:,:] = (data.variables[key][i, :, :]) + I += 1 + + def _t_region(self, tx, julianTime, debug=False): + """Return time indices included in time period, aka tx""" + debug = debug or self._debug + if debug: print 'Computing region_t...' + start = datetime.datetime.strptime(tx[0], '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(tx[1], '%Y-%m-%d %H:%M:%S') + region_t = time_to_index(start, end, julianTime[:], debug=debug) + if debug: print '...Passed' + print '-Now working in time box-' + return region_t diff --git a/build/lib/pyseidon_dvt/stationClass/__init__.py b/build/lib/pyseidon_dvt/stationClass/__init__.py new file mode 100644 index 0000000..5afd3fc --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Local import +from stationClass import Station + +__authors__ = ['Wesley Bowman, Thomas Roc, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/stationClass/functionsStation.py b/build/lib/pyseidon_dvt/stationClass/functionsStation.py new file mode 100644 index 0000000..764824f --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/functionsStation.py @@ -0,0 +1,483 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import sys +import numexpr as ne +from pyseidon_dvt.utilities.miscellaneous import * +from pyseidon_dvt.utilities.BP_tools import * +from utide import solve, reconstruct +import time + +# Custom error +from pyseidon_error import PyseidonError + +class FunctionsStation: + """ + **'Util2D' subset of Station class gathers useful functions for 2D and 3D runs** + """ + def __init__(self, variable, grid, plot, History, debug): + self._debug = debug + self._plot = plot + #Create pointer to Station class + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + setattr(self, '_History', History) + + def search_index(self, station): + """Search for the station index""" + if type(station)==int: + index = station + elif type(station).__name__ in ['str', 'ndarray']: + station = "".join(station).strip().upper() + for i in range(self._grid.nele): + if station=="".join(self._grid.name[i]).strip().upper(): + index=i + else: + raise PyseidonError("---Wrong station input---") + if not 'index' in locals(): + raise PyseidonError("---Wrong station input---") + + return index + + def flow_dir(self, station, t_start=[], t_end=[], time_ind=[], + exceedance=False, debug=False): + """ + Flow directions and associated norm at any give location. + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - flowDir = flowDir at station, 1D array + - norm = velocity norm at station, 1D array + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - excedance = True, compute associated exceedance curve + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + """ + debug = debug or self._debug + if debug: + print 'Computing flow directions at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + argtime = time_to_index(t_start, t_end, self._var.matlabTime, debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Search for the station + index = self.search_index(station) + + #Choose the right pair of velocity components + if not argtime==[]: + U = self._var.ua[argtime,index] + V = self._var.va[argtime,index] + else: + U = self._var.ua[:,index] + V = self._var.va[:,index] + + #Compute directions + if debug: + print 'Computing arctan2 and norm...' + dirFlow = np.rad2deg(np.arctan2(V,U)) + + #Compute velocity norm + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + if debug: + print '...Passed' + #Rose diagram + self._plot.rose_diagram(dirFlow, norm) + if exceedance: + self.exceedance(norm) + + return dirFlow, norm + + def ebb_flood_split(self, station, + t_start=[], t_end=[], time_ind=[], debug=False): + """ + Compute time indices for ebb and flood but also the + principal flow directions and associated variances for (lon, lat) point + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - floodIndex = flood time index, 1D array of integers + - ebbIndex = ebb time index, 1D array of integers + - pr_axis = principal flow ax1s, float number in degrees + - pr_ax_var = associated variance, float number + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + + *Notes* + - may take time to compute if time period too long + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - use time_ind or t_start and t_end, not both + - assume that flood is aligned with principal direction + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing principal flow directions...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + argtime = time_to_index(t_start, t_end, + self._var.matlabTime, + debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Search for the station + index = self.search_index(station) + + #Choose the right pair of velocity components + if not argtime==[]: + U = self._var.ua[argtime,index] + V = self._var.va[argtime,index] + else: + U = self._var.ua[:,index] + V = self._var.va[:,index] + + #WB version of BP's principal axis + if debug: print 'Computin principal axis at point...' + pr_axis, pr_ax_var = principal_axis(U, V) + + #ebb/flood split + if debug: print 'Splitting ebb and flood at point...' + ###TR: version 1 + ## reverse 0-360 deg convention + #ra = (-pr_axis - 90.0) * np.pi /180.0 + #if ra>np.pi: + # ra = ra - (2.0*np.pi) + #elif ra<-np.pi: + # ra = ra + (2.0*np.pi) + #dirFlow = np.arctan2(V,U) + ##Define bins of angles + #if ra == 0.0: + # binP = [0.0, np.pi/2.0] + # binP = [0.0, -np.pi/2.0] + #elif ra > 0.0: + # if ra == np.pi: + # binP = [np.pi/2.0 , np.pi] + # binM = [-np.pi, -np.pi/2.0 ] + # elif ra < (np.pi/2.0): + # binP = [0.0, ra + (np.pi/2.0)] + # binM = [-((np.pi/2.0)-ra), 0.0] + # else: + # binP = [ra - (np.pi/2.0), np.pi] + # binM = [-np.pi, -np.pi + (ra-(np.pi/2.0))] + #else: + # if ra == -np.pi: + # binP = [np.pi/2.0 , np.pi] + # binM = [-np.pi, -np.pi/2.0] + # elif ra > -(np.pi/2.0): + # binP = [0.0, ra + (np.pi/2.0)] + # binM = [ ((-np.pi/2.0)+ra), 0.0] + # else: + # binP = [np.pi - (ra+(np.pi/2.0)) , np.pi] + # binM = [-np.pi, ra + (np.pi/2.0)] + # + #test = (((dirFlow > binP[0]) * (dirFlow < binP[1])) + + # ((dirFlow > binM[0]) * (dirFlow < binM[1]))) + #floodIndex = np.where(test == True)[0] + #ebbIndex = np.where(test == False)[0] + + #Defines interval + flood_heading = np.array([-90, 90]) + pr_axis + dir_all = np.rad2deg(np.arctan2(V,U)) + ind = np.where(dir_all<0) + dir_all[ind] = dir_all[ind] + 360 + + # sign speed - eliminating wrap-around + dir_PA = dir_all - pr_axis + dir_PA[dir_PA < -90] += 360 + dir_PA[dir_PA > 270] -= 360 + + #general direction of flood passed as input argument + floodIndex = np.where((dir_PA >= -90) & (dir_PA<90)) + ebbIndex = np.arange(dir_PA.shape[0]) + ebbIndex = np.delete(ebbIndex,floodIndex[:]) + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + return floodIndex, ebbIndex, pr_axis, pr_ax_var + + def exceedance(self, var, station=[], debug=False): + """ + This function calculate the excedence curve of a var(time). + + Inputs: + - var = given quantity, 1 or 2D array of n elements, i.e (time) or (time,ele) + + Options: + - station = either station index (interger) or name (string) + Necessary if var = 2D (i.e. [time, nnode or nele] + - graph: True->plots curve; False->does not + + Outputs: + - Exceedance = list of % of occurences, 1D array + - Ranges = list of signal amplitude bins, 1D array + + *Notes* + - This method is not suitable for SSE + """ + debug = (debug or self._debug) + if debug: + print 'Computing exceedance...' + + #Distinguish between 1D and 2D var + if len(var.shape)>1: + if station==[]: + print 'Lon, lat coordinates are needed' + sys.exit() + #Search for the station + index = self.search_index(station) + signal = var[:,index] + else: + signal=var + + Max = max(signal) + dy = (Max/30.0) + Ranges = np.arange(0,(Max + dy), dy) + Exceedance = np.zeros(Ranges.shape[0]) + dt = self._var.julianTime[1] - self._var.julianTime[0] + if dt==0: + dt = self._var.secondTime[1] - self._var.secondTime[0] + Period = var.shape[0] * dt + time = np.arange(0.0, Period, dt) + + N = len(signal) + M = len(Ranges) + + for i in range(M): + r = Ranges[i] + for j in range(N-1): + if signal[j] > r: + Exceedance[i] = Exceedance[i] + (time[j+1] - time[j]) + + Exceedance = (Exceedance * 100) / Period + + if debug: + print '...Passed' + + #Plot + self._plot.plot_xy(Exceedance, Ranges, yLabel='Amplitudes', + xLabel='Exceedance probability in %') + + return Exceedance, Ranges + + def depth(self, station, debug=False): + """ + Compute depth at given point + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - dep = depth, 2D array (ntime, nlevel) + + *Notes* + - depth convention: 0 = free surface + """ + debug = debug or self._debug + if debug: + print "Computing depth..." + start = time.time() + + #Search for the station + index = self.search_index(station) + + #Compute depth + h = self._grid.h[index] + el = self._var.el[:,index] + dep = el + h + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + return dep + + def speed_histogram(self, station, t_start=[], t_end=[], time_ind=[], debug=False): + """ + This function plots the histogram of occurrences for the signed + flow speed at any given point. + + Inputs: + - station = either station index (interger) or name (string) + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + start = time.time() + print 'Computing speed histogram...' + + pI, nI, pa, pav = self.ebb_flood_split(station, + t_start=t_start, t_end=t_end, time_ind=time_ind, + debug=debug) + dirFlow, norm = self.flow_dir(station, + t_start=t_start, t_end=t_end, time_ind=time_ind, + exceedance=False, debug=debug) + norm[nI] = -1.0 * norm[nI] + + #compute bins + #minBound = norm.min() + #maxBound = norm.max() + #step = round((maxBound-minBound/51.0),1) + #bins = np.arange(minBound,maxBound,step) + + #plot histogram + self._plot.Histogram(norm, + xLabel='Signed flow speed (m/s)', + yLabel='Occurrences (%)') + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + + def Harmonic_analysis_at_point(self, station, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + + #Search for the station + index = self.search_index(station) + + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + argtime = time_to_index(t_start, t_end, + self._var.matlabTime, + debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + time = self._var.matlabTime[:] + u = self._var.ua[:,index] + v = self._var.va[:,index] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, u, v, lat, **kwarg) + + if elevation: + time = self._var.matlabTime[:] + el = self._var.el[:,index] + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + Reconstruct = reconstruct(time, harmo) + + + return Reconstruct diff --git a/build/lib/pyseidon_dvt/stationClass/functionsStationThreeD.py b/build/lib/pyseidon_dvt/stationClass/functionsStationThreeD.py new file mode 100644 index 0000000..177d1ca --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/functionsStationThreeD.py @@ -0,0 +1,456 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +import numexpr as ne +from pyseidon_dvt.utilities.miscellaneous import * +from pyseidon_dvt.utilities.BP_tools import * +import time + +from utide import solve, reconstruct + +# Custom error +from pyseidon_error import PyseidonError + +class FunctionsStationThreeD: + """ + **'Utils3D' subset of Station class gathers useful functions for 3D runs** + """ + def __init__(self, variable, grid, plot, History, debug): + #Inheritance + self._debug = debug + self._plot = plot + + #Create pointer to FVCOM class + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + setattr(self, '_History', History) + + def search_index(self, station): + """Search for the station index""" + if type(station)==int: + index = station + elif type(station).__name__ in ['str', 'ndarray']: + station = "".join(station).strip().upper() + for i in range(self._grid.nele): + if station=="".join(self._grid.name[i]).strip().upper(): + index=i + else: + raise PyseidonError("---Wrong station input---") + if not 'index' in locals(): + raise PyseidonError("---Wrong station input---") + + return index + + def depth(self, station, debug=False): + """ + Compute depth at given point + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - dep = depth, 2D array (ntime, nlevel) + + Notes: + - depth convention: 0 = free surface + - index is used in case one knows already at which + element depth is requested + """ + debug = debug or self._debug + if debug: + print "Computing depth..." + start = time.time() + + #Search for the station + index = self.search_index(station) + + #Compute depth + h = self._grid.h[index] + el = self._var.el[:,index] + zeta = el + h + siglay = self._grid.siglay[:,index] + dep = zeta[:, np.newaxis]*siglay[np.newaxis, :] + + if debug: + end = time.time() + print "Computation time in (s): ", (end - start) + + return np.squeeze(dep) + + def verti_shear(self, station, t_start=[], t_end=[], time_ind=[], + bot_lvl=[], top_lvl=[], graph=True, debug=False): + """ + Compute vertical shear at any given location + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - dveldz = vertical shear (1/s), 2D array (time, nlevel - 1) + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - bot_lvl = index of the bottom level to consider, integer + - top_lvl = index of the top level to consider, integer + - graph = plots graph if True + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing vertical shear at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Search for the station + index = self.search_index(station) + + #Compute depth + dep = self.depth(station, debug=debug) + + if not argtime==[]: + depth = dep[argtime,:] + else: + depth = dep + + #Sigma levels to consider + if top_lvl==[]: + top_lvl = self._grid.nlevel - 1 + if bot_lvl==[]: + bot_lvl = 0 + sLvl = range(bot_lvl, top_lvl+1) + + #Extracting velocity at point + if not argtime==[]: + U = self._var.u[argtime,:,index] + V = self._var.v[argtime,:,index] + else: + U = self._var.u[:,:,index] + V = self._var.v[:,:,index] + + norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + # Compute shear + dz = depth[:,sLvl[1:]] - depth[:,sLvl[:-1]] + dvel = norm[:,sLvl[1:]] - norm[:,sLvl[:-1]] + dveldz = dvel / dz + + if debug: + print '...Passed' + + #Plot mean values + if graph: + mean_depth = np.mean((depth[:,sLvl[1:]] + + depth[:,sLvl[:-1]]) / 2.0, 0) + mean_dveldz = np.mean(dveldz,0) + error = np.std(dveldz,axis=0) + self._plot.plot_xy(mean_dveldz, mean_depth, xerror=error[:], + title='Shear profile ', + xLabel='Shear (1/s) ', yLabel='Depth (m) ') + + return np.squeeze(dveldz) + + def velo_norm(self, station, t_start=[], t_end=[], time_ind=[], + graph=True, debug=False): + """ + Compute the velocity norm at any given location + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - velo_norm = velocity norm, 2D array (time, level) + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - graph = plots vertical profile averaged over time if True + + *Notes* + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing velocity norm at point...' + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Search for the station + index = self.search_index(station) + + #Computing velocity norm + try: + if not argtime==[]: + U = self._var.u[argtime, :, index] + V = self._var.v[argtime, :, index] + W = self._var.w[argtime, :, index] + velo_norm = ne.evaluate('sqrt(U**2 + V**2 + W**2)').squeeze() + else: + U = self._var.u[:, :, index] + V = self._var.v[:, :, index] + W = self._var.w[:, :, index] + velo_norm = ne.evaluate('sqrt(U**2 + V**2 + W**2)').squeeze() + except AttributeError: + if not argtime==[]: + U = self._var.u[argtime, :, index] + V = self._var.v[argtime, :, index] + velo_norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + else: + U = self._var.u[:, :, index] + V = self._var.v[:, :, index] + velo_norm = ne.evaluate('sqrt(U**2 + V**2)').squeeze() + + if debug: + print '...passed' + + #Plot mean values + if graph: + depth = np.mean(self.depth(station),axis=0) + vel = np.mean(velo_norm,axis=0) + error = np.std(velo_norm,axis=0) + self._plot.plot_xy(vel, depth, xerror=error[:], + title='Velocity norm profile ', + xLabel='Velocity (m/s) ', yLabel='Depth (m) ') + + + return velo_norm + + def flow_dir(self, station, t_start=[], t_end=[], time_ind=[], + vertical=True, debug=False): + """ + Compute flow directions and associated norm at any given location. + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - flowDir = flowDir at (pt_lon, pt_lat), 2D array (ntime, nlevel) + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - vertical = True, compute flowDir for each vertical level + + *Notes* + - directions between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - use time_ind or t_start and t_end, not both + """ + debug = debug or self._debug + if debug: + print 'Computing flow directions at point...' + + #Search for the station + index = self.search_index(station) + + # Find time interval to work in + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + start = datetime.datetime.strptime(t_start, '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strptime(t_end, '%Y-%m-%d %H:%M:%S') + argtime = time_to_index(start, end, self._var.julianTime[:], debug=debug) + else: + argtime = np.arange(t_start, t_end) + + #Choose the right pair of velocity components + if not argtime==[]: + if self._var._3D and vertical: + u = self._var.u[argtime,:,index] + v = self._var.v[argtime,:,index] + else: + u = self._var.ua[argtime,index] + v = self._var.va[argtime,index] + + #Compute directions + if debug: print 'Computing arctan2 and norm...' + dirFlow = np.rad2deg(np.arctan2(v,u)) + + if debug: + print '...Passed' + + return np.squeeze(dirFlow) + + def Harmonic_analysis_at_point(self, station, + time_ind=[], t_start=[], t_end=[], + elevation=True, velocity=False, + debug=False, **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or each layer of the velocity components timeseries. + + Inputs: + - station = either station index (interger) or name (string) + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - t_start = start time, as string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - t_end = end time, as a string ('yyyy-mm-ddThh:mm:ss'), or time index as an integer + - time_ind = time indices to work in, list of integers + - elevation=True means that 'solve' will be done for elevation. + - velocity=True means that 'solve' will be done for velocity. + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + + #Search for the station + index = self.search_index(station) + + argtime = [] + if not time_ind==[]: + argtime = time_ind + elif not t_start==[]: + if type(t_start)==str: + argtime = time_to_index(t_start, t_end, + self._var.matlabTime, + debug=debug) + else: + argtime = np.arange(t_start, t_end) + + if velocity == elevation: + raise PyseidonError("---Can only process either velocities or elevation. Change options---") + + if velocity: + harmo = {} + for layerIndex in range(self._var.u.shape[1]): + time = self._var.matlabTime[:] + u = self._var.u[:,layerIndex,index] + v = self._var.v[:,layerIndex,index] + + if not argtime==[]: + time = time[argtime[:]] + u = u[argtime[:]] + v = v[argtime[:]] + + lat = self._grid.lat[index] + harmo['Layer_'+str(layerIndex)] = solve(time, u, v, lat, **kwarg) + + if elevation: + time = self._var.matlabTime[:] + el = self._var.el[:,index] + + if not argtime==[]: + time = time[argtime[:]] + el = el[argtime[:]] + + lat = self._grid.lat[index] + harmo = solve(time, el, None, lat, **kwarg) + #Write meta-data only if computed over all the elements + + return harmo + + def Harmonic_reconstruction(self, harmo, recon_time=[], time_ind=slice(None), debug=False, **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + - time_ind = time indices to process, list of integers + - recon_time = time you want the harmonic coefficients to be reconstructed at + + Output: + - Reconstruct = reconstructed signal, dictionary + + Options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + + """ + debug = (debug or self._debug) + if recon_time == []: + time = self._var.matlabTime[time_ind] + else: + time = recon_time + + if not 'Layer_0' in harmo: + Reconstruct = reconstruct(time, harmo) + else: + Reconstruct = {} + for layer in harmo: + Reconstruct[layer] = reconstruct(time, harmo[layer]) + + return Reconstruct + + def velo_stack(self, Reconstruct, debug=False): + """ + This function seperates and stacks u & v into respective matrices + from a 3D harmonically reconstructed dictionary + + Inputs: + - Reconstruct = reconstructed signal, dictionary from Harmonic_reconstruction() + + Outputs: + - u = stacked matrix of u velocity + - v = stacked matrix of v velocity + + """ + debug = (debug or self._debug) + u = Reconstruct['Layer_0']['u'] + v = Reconstruct['Layer_0']['v'] + for i in range(len(Reconstruct))[1:]: + u = np.vstack((u, Reconstruct['Layer_'+str(i)]['u'])) + v = np.vstack((v, Reconstruct['Layer_'+str(i)]['v'])) + + return u, v + + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/stationClass/plotsStation.py b/build/lib/pyseidon_dvt/stationClass/plotsStation.py new file mode 100644 index 0000000..b76b861 --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/plotsStation.py @@ -0,0 +1,218 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +import matplotlib.patches as mpatches +import seaborn +import pandas as pd +from windrose import WindroseAxes +from interpolation_utils import * +#from miscellaneous import depth_at_FVCOM_element as depth_at_ind + +class PlotsStation: + """ + Description: + 'Plots' subset of Station class gathers plotting functions + """ + def __init__(self, variable, grid, debug): + self._debug = debug + #Back pointer + setattr(self, '_var', variable) + setattr(self, '_grid', grid) + + def _def_fig(self): + """Defines figure window""" + self._fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + + def rose_diagram(self, direction, norm): + + """ + Plot rose diagram + + Inputs: + - direction = 1D array + - norm = 1D array + """ + #Convertion + #TR: not quite sure here, seems to change from location to location + # express principal axis in compass + direction = np.mod(90.0 - direction, 360.0) + + #Create new figure + self._def_fig() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(self._fig, rect)#, axisbg='w') + self._fig.add_axes(ax) + #Rose + ax.bar(direction, norm , normed=True, opening=0.8, edgecolor='white') + #adjust legend + l = ax.legend(shadow=True, bbox_to_anchor=[-0.1, 0], loc='lower left') + plt.setp(l.get_texts(), fontsize=10) + plt.xlabel('Rose diagram in % of occurrences - Colormap of norms') + self._plt.show() + + def plot_xy(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', dump=False, **kwargs): + """ + Simple X vs Y plot + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + self._def_fig() + self._ax = self._fig.add_subplot(111) + self._ax.plot(x, y, label=title) + scale = 1 + self._ax.set_ylabel(yLabel) + self._ax.set_xlabel(xLabel) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + if not yerror==[]: + self._ax.fill_between(x, y-yerror, y+yerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if not xerror==[]: + self._ax.fill_betweenx(y, x-xerror, x+xerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if (not xerror==[]) or (not yerror==[]): + blue_patch = mpatches.Patch(color='#089FFF', + label='Standard deviation',alpha=0.2) + plt.legend(handles=[blue_patch],loc=1, fontsize=12) + #plt.legend([blue_patch],loc=1, fontsize=12) + + self._fig.show() + if dump: self._dump_profile_data_as_csv(x, y,xerror=xerror, yerror=yerror, + title=title, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def Histogram(self, y, title=' ', xLabel=' ', yLabel=' '): + """ + Histogram plot + + Inputs: + - bins = list of bin edges + - y = 1D array + + Options: + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + """ + self._def_fig() + self._ax = self._fig.add_subplot(111) + density, bins = np.histogram(y, bins=50, normed=True, density=True) + unity_density = density / density.sum() + widths = bins[:-1] - bins[1:] + # To plot correct percentages in the y axis + self._ax.bar(bins[1:], unity_density, width=widths) + formatter = ticker.FuncFormatter(lambda v, pos: str(v * 100)) + self._ax.yaxis.set_major_formatter(formatter) + + plt.ylabel(yLabel) + plt.xlabel(xLabel) + + self._fig.show() + + def add_points(self, x, y, label=' ', color='black'): + """ + Add scattered points (x,y) on current figure, + where x and y are 1D arrays of the same lengths. + + Inputs: + - x = float number + - y = float numbe + + Options: + - Label = a string + - Color = a string, 'red', 'green', etc. or gray shades like '0.5' + """ + plt.scatter(x, y, s=100, color=color) + #TR : annotate does not work on my machine !? + plt.annotate(label, xy=(x, y), xycoords='data', xytext=(-20, 20), + textcoords='offset points', ha='right', + arrowprops=dict(arrowstyle="->", shrinkA=0)) + + def rose_diagram(self, direction, norm): + + """ + Plot rose diagram + + Inputs: + - direction = 1D array + - norm = 1D array + """ + #Convertion + #TR: not quite sure here, seems to change from location to location + # express principal axis in compass + direction = np.mod(90.0 - direction, 360.0) + + #Create new figure + #fig = plt.figure(figsize=(18,10)) + #plt.rc('font',size='22') + self._def_fig() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(self._fig, rect)#, axisbg='w') + self._fig.add_axes(ax) + #Rose + ax.bar(direction, norm , normed=True, opening=0.8, edgecolor='white') + #adjust legend + l = ax.legend(shadow=True, bbox_to_anchor=[-0.1, 0], loc='lower left') + plt.setp(l.get_texts(), fontsize=10) + plt.xlabel('Rose diagram in % of occurrences - Colormap of norms') + self._fig.show() + + def _dump_profile_data_as_csv(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', **kwargs): + """ + Dumps profile data in csv file + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = file name, string + - xLabel = name of the x-data, string + - yLabel = name of the y-data, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_profile_data' + filename=title + '.csv' + if xLabel == ' ': xLabel = 'X' + if yLabel == ' ': yLabel = 'Y' + if not xerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': xerror[:]}) + elif not yerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': yerror[:]}) + else: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:]}) + df.to_csv(filename, encoding='utf-8', **kwargs) + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/stationClass/stationClass.py b/build/lib/pyseidon_dvt/stationClass/stationClass.py new file mode 100644 index 0000000..007e6c8 --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/stationClass.py @@ -0,0 +1,446 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +import netCDF4 as nc +from scipy.io import netcdf +from scipy.io import savemat +from pydap.client import open_url +import cPickle as pkl +import copy + +#Utility import +from pyseidon_dvt.utilities.object_from_dict import ObjectFromDict +from pyseidon_dvt.utilities.miscellaneous import findFiles + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +#Local import +from variablesStation import _load_var, _load_grid +from functionsStation import * +from functionsStationThreeD import * +from plotsStation import * + +class Station: + """ + **A class/structure for Station data** + + Functionality structured as follows: :: + + _Data. = raw netcdf file data + |_Variables. = fvcom station variables and quantities + |_Grid. = fvcom station grid data + |_History = Quality Control metadata + Station._|_Utils2D. = set of useful functions for 2D and 3D station + |_Utils3D. = set of useful functions for 3D station + |_Plots. = plotting functions + |_Harmonic_analysis = harmonic analysis based UTide package + |_Harmonic_reconstruction = harmonic reconstruction based UTide package + + Inputs: + - filename = path to netcdf file or folder, string, + ex: testFvcom=Station('./path_to_FVOM_output_file/filename') + testFvcom=Station('./path_to_FVOM_output_file/folder/') + + Note that if the path point to a folder all the similar netCDF station files + will be stack together. + Note that the file can be a pickle file (i.e. *.p) or a netcdf file (i.e. *.nc). + + Options: + - elements = indices to extract, list of integers + + *Notes* + Throughout the package, the following conventions apply: + - Date = string of 'yyyy-mm-dd hh:mm:ss' + - Coordinates = decimal degrees East and North + - Directions = in degrees, between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - Depth = 0m is the free surface and depth is negative + + """ + def __init__(self, filename, elements=slice(None), debug=False): + #Class attributs + self._debug = debug + self._isMulti(filename) + if not self._multi: + self._load(filename, elements, debug=debug ) + self.Plots = PlotsStation(self.Variables, + self.Grid, + self._debug) + self.Util2D = FunctionsStation(self.Variables, + self.Grid, + self.Plots, + self.History, + self._debug) + if self.Variables._3D: + self.Util3D = FunctionsStationThreeD( + self.Variables, + self.Grid, + self.Plots, + self.History, + self._debug) + else: + print "---Finding matching files---" + self._matches = findFiles(filename, 'STATION') + filename = self._matches.pop(0) + self._load(filename, elements, debug=debug ) + self.Plots = PlotsStation(self.Variables, + self.Grid, + self._debug) + self.Util2D = FunctionsStation(self.Variables, + self.Grid, + self.Plots, + self.History, + self._debug) + if self.Variables._3D: + self.Util3D = FunctionsStationThreeD( + self.Variables, + self.Grid, + self.Plots, + self.History, + self._debug) + for entry in self._matches: + #Define new + text = 'Created from ' + entry + tmp = {} + tmp['Data'] = self._load_nc(entry) + tmp['History'] = [text] + tmp['Grid'] = _load_grid(tmp['Data'], elements, [], debug=self._debug) + tmp['Variables'] = _load_var(tmp['Data'], elements, tmp['Grid'], [], + debug=self._debug) + tmp = ObjectFromDict(tmp) + self = self.__add__(tmp) + + ##Re-assignement of utility functions as methods + self.dump_profile_data = self.Plots._dump_profile_data_as_csv + + return + + def _isMulti(self, filename): + """Tells if filename point to a file or a folder""" + split = filename.split('/') + if split[-1]: + self._multi = False + else: + self._multi = True + + def _load(self, filename, elements, debug=False): + """Loads data from *.nc, *.p and OpenDap url""" + #Loading pickle file + if filename.endswith('.p'): + f = open(filename, "rb") + data = pkl.load(f) + self._origin_file = data['Origin'] + self.History = data['History'] + if debug: print "Turn keys into attributs" + self.Grid = ObjectFromDict(data['Grid']) + self.Variables = ObjectFromDict(data['Variables']) + try: + if self._origin_file.startswith('http'): + #Look for file through OpenDAP server + print "Retrieving data through OpenDap server..." + self.Data = open_url(data['Origin']) + #Create fake attribut to be consistent with the rest of the code + self.Data.variables = self.Data + else: + self.Data = self._load_nc(data['Origin']) + except: #TR: need to precise the type of error here + print "the original *.nc file has not been found" + pass + #Loading netcdf file + elif filename.endswith('.nc'): + if filename.startswith('http'): + #Look for file through OpenDAP server + print "Retrieving data through OpenDap server..." + self.Data = open_url(filename) + #Create fake attribut to be consistent with the rest of the code + self.Data.variables = self.Data + else: + #Look for file locally + print "Retrieving data from " + filename + " ..." + self.Data = self._load_nc(filename) + #Metadata + text = 'Created from ' + filename + self._origin_file = filename + self.History = [text] + # Calling sub-class + print "Initialisation..." + try: + self.Grid = _load_grid(self.Data, + elements, + self.History, + debug=self._debug) + self.Variables = _load_var(self.Data, + elements, + self.Grid, + self.History, + debug=self._debug) + + except MemoryError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + elif filename.endswith('.mat'): + raise PyseidonError("---Functionality not yet implemented---") + else: + raise PyseidonError("---Wrong file format---") + + def _load_nc(self, filename): + """loads netcdf file""" + #Look for file locally + #print "Retrieving data from " + filename + " ..." + # WB_Alternative: self.Data = sio.netcdf.netcdf_file(filename, 'r') + # WB_comments: scipy has causes some errors, and even though can be + # faster, can be unreliable + try: + Data = netcdf.netcdf_file(filename, 'r', mmap=True) + except ValueError: #TR: quick fix due to mmap + Data = nc.Dataset(filename, 'r') + return Data + + #Special methods + def __add__(self, StationClass, debug=False): + """ + This special method permit to stack variables + of 2 Station objects through a simple addition: :: + + station1 += station2 + + *Notes* + - station1 and station2 have to cover the exact + same spatial domain + - last time step of station1 must be <= to the + first time step of station2 + """ + debug = debug or self._debug + if debug: print "Find matching elements..." + #Find matching elements + origNele = self.Grid.nele + origEle = [] + origName = self.Grid.name + #origX = self.Grid.x[:] + #origY = self.Grid.y[:] + newNele = StationClass.Grid.nele + newEle = [] + newName = StationClass.Grid.name + #newX = StationClass.Grid.x[:] + #newY = StationClass.Grid.y[:] + for i in range(origNele): + for j in range(newNele): + #Match based on names + #if (all(origName[i,:]==newName[j,:])): + if origName[i]==newName[j]: + origEle.append(i) + newEle.append(j) + #Match based on coordinates + #if ((origX[i]==newX[j]) and (origY[i]==newY[j])): + # origEle.append(i) + # newEle.append(j) + + print len(origEle), " points will be stacked..." + + if len(origEle)==0: + raise PyseidonError("---No matching element found---") + elif not (self.Variables._3D == StationClass.Variables._3D): + raise PyseidonError("---Data dimensions do not match---") + else: + if not (self.Variables.julianTime[-1]<= + StationClass.Variables.julianTime[0]): + raise PyseidonError("---Data not consecutive in time---") + #Copy self to newself + newself = copy.copy(self) + #TR comment: it still points toward self and modifies it + # so cannot do Station3 = Station1 + Station2 + if debug: + print 'Stacking variables...' + #keyword list for hstack + kwl=['matlabTime', 'julianTime', 'secondTime'] + for key in kwl: + tmpN = getattr(newself.Variables, key) + tmpO = getattr(StationClass.Variables, key) + setattr(newself.Variables, key, + np.hstack((tmpN[:], tmpO[:]))) + + #keyword list for vstack + kwl=['u', 'v', 'w', 'tke', 'gls', 'ua', 'va','el'] + kwl2D=['ua', 'va','el'] + for key in kwl: + try: + if key in kwl2D: + tmpN = getattr(newself.Variables, key)\ + [:,newEle[:]] + tmpO = getattr(StationClass.Variables, key)\ + [:,origEle[:]] + setattr(newself.Variables, key, + np.vstack((tmpN[:], tmpO[:]))) + if debug: print "Stacking " + key + "..." + else: + tmpN = getattr(newself.Variables, key)\ + [:,:,newEle[:]] + tmpO = getattr(StationClass.Variables, key)\ + [:,:,origEle[:]] + setattr(newself.Variables, key, + np.vstack((tmpN[:], tmpO[:]))) + if debug: print "Stacking " + key + "..." + except AttributeError: + continue + + #New code added by RK Aug 20 + #get unique, sorted time + if debug: + print 'Getting unique, sorted time...' + + #keyword list for unique + + time = getattr(newself.Variables, 'matlabTime') + uniquetime, unique_index=np.unique(time,return_index=True) + sort_index=np.argsort(uniquetime) + index=unique_index[sort_index] + time=time[index] + #New time dimension + newself.Grid.ntime = time.shape + if debug: print "New time length: " +str(newself.Grid.ntime[0]) + + #keyword list for vstack + kwl=['matlabTime', 'julianTime', 'secondTime'] + for key in kwl: + tmpN = getattr(newself.Variables, key) + setattr(newself.Variables, key, tmpN[index]) + + kwl=['u', 'v', 'w', 'tke', 'gls', 'ua', 'va','el'] + kwl2D=['ua', 'va','el'] + for key in kwl: + try: + if key in kwl2D: + tmpN = getattr(newself.Variables, key)\ + [:,newEle[:]] + setattr(newself.Variables, key,tmpN[index,:]) + if debug: print "Sorting " + key + "..." + else: + tmpN = getattr(newself.Variables, key)\ + [:,:,newEle[:]] + setattr(newself.Variables, key,tmpN[index,:]) + if debug: print "Sorting " + key + "..." + except AttributeError: + continue + #End new code added by RK Aug 20 + + #Keep only matching names + newself.Grid.name = self.Grid.name[origEle[:]] + #Append to new object history + text = 'Data from ' + StationClass.History[0].split('/')[-1] \ + + ' has been stacked' + newself.History.append(text) + + return newself + + #Methods + def Save_as(self, filename, fileformat='pickle', debug=False): + """ + Save the current Station structure as: + - *.p, i.e. python file + - *.mat, i.e. Matlab file + + Inputs: + - filename = path + name of the file to be saved, string + + Options: + - fileformat = format of the file to be saved, i.e. 'pickle' or 'matlab' + """ + debug = debug or self._debug + if debug: + print 'Saving file...' + #Define bounding box + if debug: + print "Computing bounding box..." + if self.Grid._ax == []: + lon = self.Grid.lon[:] + lat = self.Grid.lat[:] + self.Grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + #Save as different formats + if fileformat=='pickle': + filename = filename + ".p" + f = open(filename, "wb") + data = {} + data['Origin'] = self._origin_file + data['History'] = self.History + data['Grid'] = self.Grid.__dict__ + data['Variables'] = self.Variables.__dict__ + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in data['Variables']: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(data['Variables'][key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + data['Variables'][key] = data['Variables'][key][:] + #Unpickleable objects + data['Grid'].pop("triangle", None) + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in data['Grid']: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(data['Grid'][key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + data['Grid'][key] = data['Grid'][key][:] + #Save in pickle file + if debug: + print 'Dumping in pickle file...' + try: + pkl.dump(data, f, protocol=pkl.HIGHEST_PROTOCOL) + except SystemError: + print '---Data too large for machine memory---' + print 'Tip: use ax or tx during class initialisation' + print '--- to use partial data' + raise + + f.close() + elif fileformat=='matlab': + filename = filename + ".mat" + #TR comment: based on MitchellO'Flaherty-Sproul's code + dtype = float + data = {} + Grd = {} + Var = {} + data['Origin'] = self._origin_file + data['History'] = self.History + Grd = self.Grid.__dict__ + Var = self.Variables.__dict__ + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in Var: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(Var[key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + Var[key] = Var[key][:] + #keyV = key + '-var' + #data[keyV] = Var[key] + data[key] = Var[key] + #Unpickleable objects + Grd.pop("triangle", None) + for key in Grd: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(Grd[key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + Grd[key] = Grd[key][:] + #keyG = key + '-grd' + #data[keyG] = Grd[key] + data[key] = Grd[key] + + #Save in mat file file + if debug: + print 'Dumping in matlab file...' + savemat(filename, data, oned_as='column') + else: + print "---Wrong file format---" + +#if __name__ == '__main__': diff --git a/build/lib/pyseidon_dvt/stationClass/variablesStation.py b/build/lib/pyseidon_dvt/stationClass/variablesStation.py new file mode 100644 index 0000000..797cd2d --- /dev/null +++ b/build/lib/pyseidon_dvt/stationClass/variablesStation.py @@ -0,0 +1,293 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +from itertools import groupby +from operator import itemgetter +#Local import +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +class _load_grid: + """ + **'Grid' subset in Station class contains grid related quantities** + + Some grid data are directly passed on from Station output: :: + + _lon = longitudes at nodes (deg.), 2D array (ntime, nnode) + |_lat = latitudes at nodes (deg.), 2D array (ntime, nnode) + |_x = x coordinates at nodes (m), 2D array (ntime, nnode) + |_y = y coordinates at nodes (m), 2D array (ntime, nnode) + |_h = bathymetry (m), 2D array (ntime, nnode) + |_nele = element dimension, integer + Station.Grid._|_nnode = node dimension, integer + |_nlevel = vertical level dimension, integer + |_ntime = time dimension, integer + |_siglay = sigma layers, 2D array (nlevel, nnode) + |_siglay = sigma levels, 2D array (nlevel+1, nnode) + |_name = name of the stations, (nnode, 20) + + Some others shall be generated as methods are being called, ex: :: + + ... + |_triangle = triangulation object for plotting purposes + + """ + def __init__(self, data, elements, History, debug=False): + if debug: + print 'Loading grid...' + #Pointer to History + setattr(self, '_History', History) + + self.x = data.variables['x'][elements] + self.y = data.variables['y'][elements] + self.lon = data.variables['lon'][elements] + self.lat = data.variables['lat'][elements] + self.siglay = data.variables['siglay'][:,elements] + self.siglev = data.variables['siglev'][:,elements] + self.h = data.variables['h'][elements] + self.name = data.variables['name_station'][elements,:] + self.nlevel = self.siglay.shape[0] + self.nele = self.x.shape[0] + self.nnode = self.x.shape[0] + + # formatting names + if len(self.name.shape) > 1: + newNames = np.arange(self.nele).astype(str) + for i in range(self.nele): + newNames[i]="".join(self.name[i,:]).strip() + self.name = newNames + + #Computing bounding box + lon = self.lon[:] + lat = self.lat[:] + if debug: print "Computing bounding box..." + ax = [lon.min(), lon.max(), lat.min(), lat.max()] + self._ax = ax + # Add metadata entry + text = 'Bounding box =' + str(ax) + self._History.append(text) + + if debug: + print '...Passed' + +class _load_var: + """ + **'Variables' subset in Station class contains the hydrodynamic related quantities** + + Some variables are directly passed on from Station output: :: + + _el = elevation (m), 2D array (ntime, nnode) + |_julianTime = julian date, 1D array (ntime) + |_matlabTime = matlab time, 1D array (ntime) + |_ua = depth averaged u velocity component (m/s), + | 2D array (ntime, nele) + |_va = depth averaged v velocity component (m/s), + Station.Variables._| 2D array (ntime, nele) + |_u = u velocity component (m/s), + | 3D array (ntime, nlevel, nele) + |_v = v velocity component (m/s), + | 3D array (ntime, nlevel, nele) + |_w = w velocity component (m/s), + 3D array (ntime, nlevel, nele) + + Some others shall be generated as methods are being called, ex: :: + + ... + |_hori_velo_norm = horizontal velocity norm (m/s), + | 2D array (ntime, nele) + |_velo_norm = velocity norm (m/s), + | 3D array (ntime, nlevel, nele) + |_verti_shear = vertical shear (1/s), + 3D array (ntime, nlevel, nele) + + """ + def __init__(self, data, elements, grid, History, debug=False): + if debug: print 'Loading variables...' + + #Pointer to History + setattr(self, '_grid', grid) + setattr(self, '_History', History) + + #List of keywords + kwl2D = ['ua', 'va', 'zeta'] + kwl3D = ['ww', 'u', 'v', 'gls', 'tke'] + #List of aliaSes + al2D = ['ua', 'va', 'el'] + al3D = ['w', 'u', 'v', 'gls', 'tke'] + + if debug: print '...time variables...' + self.julianTime = data.variables['time_JD'][:] + self.secondTime = data.variables['time_second'][:] + self.matlabTime = self.julianTime[:] + 678942.0 + self.secondTime[:] / (24*3600) + # Append message to History field + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Full temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + # Add time dimension to grid variables + self._grid.ntime = self.matlabTime.shape[0] + + if debug: print '...hydro variables...' + + region_e = elements + region_n = elements + #Redefine variables in bounding box + #Check if OpenDap variables or not + if type(data.variables).__name__=='DatasetType': + # TR: fix for issue wih elements = slice(none) + if elements == slice(None): + region_e = np.arange(self._grid.nele) + region_n = np.arange(self._grid.nnode) + #loading hori data + keyCount = 0 + for key, aliaS in zip(kwl2D, al2D): + #Special loading for zeta + H = 0 #local counter + if key == 'zeta': + for k, g in groupby(enumerate(region_n), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + if debug: print 'Index bound: ' +\ + str(ID[0]) + '-' + str(ID[-1]+1) + if H==0: + try: + setattr(self, aliaS, data.variables[key].data[:,ID[0]:(ID[-1]+1)]) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, data.variables[key][:,ID[0]:(ID[-1]+1)]) + H=1 + else: + try: + setattr(self, aliaS, + np.hstack((getattr(self, aliaS), + data.variables[key].data[:,ID[0]:(ID[-1]+1)]))) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, + np.hstack((getattr(self, aliaS), + data.variables[key][:,ID[0]:(ID[-1]+1)]))) + else: + try: + for k, g in groupby(enumerate(region_e), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + if debug: print 'Index bound: ' + str(ID[0]) + '-' + str(ID[-1]+1) + if H==0: + setattr(self, aliaS, + data.variables[key].\ + data[:,ID[0]:(ID[-1]+1)]) + H=1 + else: + try: + setattr(self, aliaS, + np.hstack((getattr(self, aliaS), + data.variables[key].data[:,ID[0]:(ID[-1]+1)]))) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, + np.hstack((getattr(self, aliaS), + data.variables[key][:,ID[0]:(ID[-1]+1)]))) + keyCount +=1 + except KeyError: + if debug: print key, " is missing !" + continue + if keyCount==0: + print "---Horizontal variables are missing---" + self._3D = False + + #loading verti data + keyCount = 0 + for key, aliaS in zip(kwl3D, al3D): + try: + H = 0 #local counter + for k, g in groupby(enumerate(region_e), lambda (i,x):i-x): + ID = map(itemgetter(1), g) + if debug: print 'Index bound: ' + str(ID[0]) + '-' + str(ID[-1]+1) + if H==0: + try: + setattr(self, aliaS, + data.variables[key].data[:,:,ID[0]:(ID[-1]+1)]) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, + data.variables[key][:,:,ID[0]:(ID[-1]+1)]) + H=1 + else: + try: + setattr(self, aliaS, + np.dstack((getattr(self, aliaS), + data.variables[key].data[:,:,ID[0]:(ID[-1]+1)]))) + except AttributeError: #exeception due nc.Dataset + setattr(self, aliaS, + np.dstack((getattr(self, aliaS), + data.variables[key][:,:,ID[0]:(ID[-1]+1)]))) + keyCount +=1 + except KeyError: + if debug: print key, " is missing !" + continue + if keyCount==0: + print "---Vertical variables are missing---" + else: + self._3D = True + #Not OpenDap + else: + #loading hori data + keyCount = 0 + for key, aliaS in zip(kwl2D, al2D): + try: + if key=='zeta': + setattr(self, aliaS, np.zeros((self._grid.ntime, self._grid.nnode))) + for i in range(self._grid.ntime): + try: + #TR comment: looping on time indices is a trick from + # Mitchell to improve loading time + getattr(self, aliaS)[i,:] =\ + np.transpose(data.variables[key].data[i,region_n]) + except AttributeError: #exeception due nc.Dataset + getattr(self, aliaS)[i,:] = data.variables[key][i,region_n] + else: + setattr(self, aliaS, np.zeros((self._grid.ntime, self._grid.nele))) + for i in range(self._grid.ntime): + try: + #TR comment: looping on time indices is a trick from + # Mitchell to improve loading time + getattr(self, aliaS)[i,:] =\ + np.transpose(data.variables[key].data[i,region_e]) + except AttributeError: #exeception due nc.Dataset + getattr(self, aliaS)[i,:] = data.variables[key][i,region_e] + keyCount +=1 + except KeyError: + if debug: print key, " is missing !" + continue + if keyCount==0: + print "---Horizontal variables are missing---" + self._3D = False + + #loading verti data + keyCount = 0 + for key, aliaS in zip(kwl3D, al3D): + try: + testKey = data.variables[key] + del testKey + setattr(self, aliaS, + np.zeros((self._grid.ntime,self._grid.nlevel, self._grid.nele))) + for i in range(self._grid.ntime): + #TR comment: looping on time indices is a trick from + # Mitchell to improve loading time + try: + try: + getattr(self, aliaS)[i,:,:] =\ + np.transpose(data.variables[key].data[i,:,region_e]) + except ValueError: # TR: some issue with transpose...quite puzzling + getattr(self, aliaS)[i,:,:] = data.variables[key].data[i,:,region_e] + except AttributeError: #exeception due nc.Dataset + getattr(self, aliaS)[i,:,:] =\ + data.variables[key][i,:,region_e] + keyCount +=1 + except KeyError: + if debug: print key, " is missing !" + continue + if keyCount==0: + print "---Vertical variables are missing---" + else: + self._3D = True + if debug: + print '...Passed' + diff --git a/build/lib/pyseidon_dvt/tidegaugeClass/__init__.py b/build/lib/pyseidon_dvt/tidegaugeClass/__init__.py new file mode 100644 index 0000000..e7ebb2a --- /dev/null +++ b/build/lib/pyseidon_dvt/tidegaugeClass/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Local import +from tidegaugeClass import TideGauge + +__authors__ = ['Thomas Roc, Wesley Bowman, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/tidegaugeClass/functionsTidegauge.py b/build/lib/pyseidon_dvt/tidegaugeClass/functionsTidegauge.py new file mode 100644 index 0000000..dae60f8 --- /dev/null +++ b/build/lib/pyseidon_dvt/tidegaugeClass/functionsTidegauge.py @@ -0,0 +1,95 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +from utide import solve, reconstruct +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +class FunctionsTidegauge: + """ + **'Utils' subset of TideGauge class gathers useful functions** + """ + def __init__(self, variable, plot, History, debug=False): + self._plot = plot + #Create pointer to FVCOM class + setattr(self, '_var', variable) + setattr(self, '_History', History) + + def harmonics(self, time_ind=slice(None), **kwarg): + """ + This function performs a harmonic analysis on the sea surface elevation + time series or the velocity components timeseries. + + Outputs: + - harmo = harmonic coefficients, dictionary + + Options: + - time_ind = time indices to work in, list of integers + + Utide's options: + Options are the same as for 'solve', which are shown below with + their default values: + conf_int=True; cnstit='auto'; notrend=0; prefilt=[]; nodsatlint=0; + nodsatnone=0; gwchlint=0; gwchnone=0; infer=[]; inferaprx=0; + rmin=1; method='cauchy'; tunrdn=1; linci=0; white=0; nrlzn=200; + lsfrqosmp=1; nodiagn=0; diagnplots=0; diagnminsnr=2; + ordercnstit=[]; runtimedisp='yyy' + + *Notes* + For more detailed information about 'solve', please see + https://github.com/wesleybowman/UTide + """ + harmo = solve(self._var.matlabTime[time_ind], + self._var.el, None, + self._var.lat, **kwarg) + return harmo + + def reconstr(self, harmo, time_ind=slice(None), **kwarg): + """ + This function reconstructs the velocity components or the surface elevation + from harmonic coefficients. + Harmonic_reconstruction calls 'reconstruct'. This function assumes harmonics + ('solve') has already been executed. + + Inputs: + - Harmo = harmonic coefficient from harmo_analysis + + Output: + - Reconstruct = reconstructed signal, dictionary + + Keywords: + - time_ind = time indices to process, list of integers + + Utide's options: + Options are the same as for 'reconstruct', which are shown below with + their default values: + cnstit = [], minsnr = 2, minpe = 0 + + *Notes* + For more detailed information about 'reconstruct', please see + https://github.com/wesleybowman/UTide + """ + time = self._var.matlabTime[time_ind] + ts_recon = reconstruct(time, harmo, **kwarg) + return ts_recon + + def mattime2datetime(self, mattime, debug=False): + """ + Description: + Output the time (yyyy-mm-dd, hh:mm:ss) corresponding to + a given matlab time + + Inputs: + - mattime = matlab time (floats) + """ + time = mattime_to_datetime(mattime, debug=debug) + return time + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/tidegaugeClass/plotsTidegauge.py b/build/lib/pyseidon_dvt/tidegaugeClass/plotsTidegauge.py new file mode 100644 index 0000000..941ab8f --- /dev/null +++ b/build/lib/pyseidon_dvt/tidegaugeClass/plotsTidegauge.py @@ -0,0 +1,112 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +import matplotlib.ticker as ticker +import matplotlib.patches as mpatches +import seaborn +import pandas as pd + +class PlotsTidegauge: + """ + **'Plots' subset of Tidegauge class gathers plotting functions** + """ + def __init__(self, variable, debug=False): + self._debug = debug + setattr(self, '_var', variable) + + def _def_fig(self): + """Defines figure window""" + self._fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + + def plot_xy(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', dump=False, **kwargs): + """ + Simple X vs Y plot + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = plot title, string + - xLabel = title of the x-axis, string + - yLabel = title of the y-axis, string + - dump = boolean, dump profile data in csv file + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + #fig = plt.figure(figsize=(18,10)) + #plt.rc('font',size='22') + self._def_fig() + self._ax = self._fig.add_subplot(111) + self._ax.plot(x, y, label=title) + scale = 1 + self._ax.set_ylabel(yLabel) + self._ax.set_xlabel(xLabel) + self._ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator()) + self._ax.grid(b=True, which='major', color='w', linewidth=1.5) + self._ax.grid(b=True, which='minor', color='w', linewidth=0.5) + if not yerror==[]: + self._ax.fill_between(x, y-yerror, y+yerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if not xerror==[]: + self._ax.fill_betweenx(y, x-xerror, x+xerror, + alpha=0.2, edgecolor='#1B2ACC', facecolor='#089FFF', antialiased=True) + if (not xerror==[]) or (not yerror==[]): + blue_patch = mpatches.Patch(color='#089FFF', + label='Standard deviation',alpha=0.2) + plt.legend(handles=[blue_patch],loc=1, fontsize=12) + #plt.legend([blue_patch],loc=1, fontsize=12) + + self._fig.show() + if dump: self._dump_profile_data_as_csv(x, y,xerror=xerror, yerror=yerror, + title=title, xLabel=xLabel, + yLabel=yLabel, **kwargs) + + def _dump_profile_data_as_csv(self, x, y, xerror=[], yerror=[], + title=' ', xLabel=' ', yLabel=' ', **kwargs): + """ + Dumps profile data in csv file + + Inputs: + - x = 1D array + - y = 1D array + + Options: + - xerror = error on 'x', 1D array + - yerror = error on 'y', 1D array + - title = file name, string + - xLabel = name of the x-data, string + - yLabel = name of the y-data, string + - kwargs = keyword options associated with pandas.DataFrame.to_csv, such as: + sep, header, na_rep, index,...etc + Check doc. of "to_csv" for complete list of options + """ + if title == ' ': title = 'dump_profile_data' + filename=title + '.csv' + if xLabel == ' ': xLabel = 'X' + if yLabel == ' ': yLabel = 'Y' + if not xerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': xerror[:]}) + elif not yerror == []: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:], 'error': yerror[:]}) + else: + df = pd.DataFrame({xLabel:x[:], yLabel:y[:]}) + df.to_csv(filename, encoding='utf-8', **kwargs) + +#TR_comments: templates +# def whatever(self, debug=False): +# if debug or self._debug: +# print 'Start whatever...' +# +# if debug or self._debug: +# print '...Passed' diff --git a/build/lib/pyseidon_dvt/tidegaugeClass/tidegaugeClass.py b/build/lib/pyseidon_dvt/tidegaugeClass/tidegaugeClass.py new file mode 100644 index 0000000..9743ae4 --- /dev/null +++ b/build/lib/pyseidon_dvt/tidegaugeClass/tidegaugeClass.py @@ -0,0 +1,57 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import scipy.io as sio + +#Local import +from variablesTidegauge import _load_tidegauge +from functionsTidegauge import * +from plotsTidegauge import * + +class TideGauge: + """ + **A class/structure for tide gauge data** + + Functionality structured as follows: :: + + _Data. = raw matlab file data + |_Variables. = useable tide gauge variables and quantities + TideGauge._|_History = Quality Control metadata + |_Utils. = set of useful functions + |_Plots. = plotting functions + + Inputs: + - Only takes a file name as input, ex: testTG=TideGauge('./path_to_matlab_file/filename') + + *Notes* + - Only handle fully processed tide gauge matlab data at the mo. + Throughout the package, the following conventions apply: + - Coordinates = decimal degrees East and North + - Directions = in degrees, between -180 and 180 deg., i.e. 0=East, 90=North, + +/-180=West, -90=South + - Depth = 0m is the free surface and depth is negative + """ + def __init__(self, filename, debug=False): + self._debug = debug + if debug: print '-Debug mode on-' + if debug: print 'Loading...' + #Metadata + self._origin_file = filename + self.History = ['Created from ' + filename] + + self.Data = sio.loadmat(filename, + struct_as_record=False, squeeze_me=True) + self.Variables = _load_tidegauge(self.Data, self.History, debug=self._debug) + + self.Plots = PlotsTidegauge(self.Variables, debug=self._debug) + + self.Utils = FunctionsTidegauge(self.Variables, + self.Plots, + self.History, + debug=self._debug) + + ##Re-assignement of utility functions as methods + self.dump_profile_data = self.Plots._dump_profile_data_as_csv + diff --git a/build/lib/pyseidon_dvt/tidegaugeClass/variablesTidegauge.py b/build/lib/pyseidon_dvt/tidegaugeClass/variablesTidegauge.py new file mode 100644 index 0000000..2d70dec --- /dev/null +++ b/build/lib/pyseidon_dvt/tidegaugeClass/variablesTidegauge.py @@ -0,0 +1,41 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +class _load_tidegauge: + """ + **'Variables' subset in TideGauge class** + + It contains the following numpy arrays: :: + + _lat = latitude, float, decimal degrees + |_lon = lontitude, float, decimal degrees + TideGauge.Variables._|_RBR = Raw recording and sampling features + |_matlabTime = matlab time, 1D array + |_el = sea surface elevation (m) timeserie, 1D array + """ + def __init__(self, cls, History, debug=False): + if debug: print 'Loading variables...' + + # Pointer to History + setattr(self, '_History', History) + + self.RBR = cls['RBR'] + self.data = self.RBR.data + self.matlabTime = self.RBR.date_num_Z + self.lat = self.RBR.lat + self.lon = self.RBR.lon + self.el = self.data - np.mean(self.data) + + #-Append message to History field + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + + if debug: print '...Done' + diff --git a/build/lib/pyseidon_dvt/utilities/BP_tools.py b/build/lib/pyseidon_dvt/utilities/BP_tools.py new file mode 100644 index 0000000..3992544 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/BP_tools.py @@ -0,0 +1,360 @@ +from __future__ import division +import numpy as np +from math import atan2 +#from rawADCPclass import rawADCP +from datetime import datetime +from datetime import timedelta +import scipy.io as sio +import scipy.interpolate as sip +import matplotlib.pyplot as plt + +def date2py(matlab_datenum): + python_datetime = datetime.fromordinal(int(matlab_datenum)) + \ + timedelta(days=matlab_datenum%1) - timedelta(days = 366) + + return python_datetime + + +def py2date(dt): + mdn = dt + timedelta(days = 366) + frac_seconds = (dt-datetime(dt.year,dt.month,dt.day,0,0,0)).seconds / (24.0 * 60.0 * 60.0) + frac_microseconds = dt.microsecond / (24.0 * 60.0 * 60.0 * 1000000.0) + return mdn.toordinal() + frac_seconds + frac_microseconds + +def calc_ensemble(x, ens, ens_dim): + + #initialize input + ens = int(ens) + #x = x[:, None] + + if ens_dim == 1: + ens_size = np.floor(x.shape[0]/60) + else: + pass + + #x_ens = np.empty((ens_size, 1, ens)) + x_ens = np.empty((ens_size, ens)) + x_ens[:] = np.nan + + for j in xrange(ens): + if ens_dim == 1: + ind_ens = np.arange(j, x.shape[0] - (ens - j), ens) + #x_ens[..., j] = x[ind_ens] + x_ens[..., j] = x[ind_ens] + + else: + pass + + #x_ens = np.nanmean(x_ens, axis=2) + x_ens = np.nanmean(x_ens, axis=1) + return x_ens + + +def rotate_coords(x, y, theta): + ''' + Similar to "rotate_to_channelcoords.m" code, + theta is now the angle + between the old axis and the new x-axis (CCw is positive) + ''' + + xnew = x * np.cos(theta) + y * np.sin(theta) + ynew = -x * np.sin(theta) + y * np.cos(theta) + + return xnew, ynew + +def rotate_to_true(X, Y, theta=-19): + ''' + % X,Y are the X and Y coordinates (could be speeds) relative to magnetic + % north -- inputs can be vectors + % x,y are the coordinates relative to true north + % This function assumes the measured location is Nova Scotia where the + % declination angle is -19 degrees. + % + % Sept 29, 2012: Changed print statement + % + % Sept 20, 2012: Modified the function to allow for theta to be input. + % Default will remain at -19 degrees, but this may not be accurate for all + % places in Nova Scotia. + ''' + + print 'Rotating velocities to be relative to true north (declination = {0})'.format(theta) + + Theta = theta * np.pi / 180 + + x = X * np.cos(Theta) + Y * np.sin(Theta) + y = -X * np.sin(Theta) + Y * np.cos(Theta) + + return x, y + + +def get_DirFromN(u,v): + ''' + #This function computes the direction from North with the output in degrees + #and measured clockwise from north. + # + # Inputs: + # u: eastward component + # v: northward component + ''' + + #theta = np.arctan2(u,v) * 180 / np.pi + theta = atan2(u,v) * 180 / np.pi + + ind = np.where(theta<0) + theta[ind] = theta[ind] + 360 + return theta + +def sign_speed(u_all, v_all, s_all, dir_all, flood_heading): + + if type(flood_heading)==int: + flood_heading += np.array([-90, 90]) + + s_signed_all = np.empty(spd_all.shape) + s_signed_all.fill(np.nan) + + PA_all = np.zeros(s_all.shape[-1]) + for i in xrange(s_all.shape[-1]): + u = u_all[:, i] + v = v_all[:, i] + dir = dir_all[:, i] + s = s_all[:, i] + + #determine principal axes - potentially a problem if axes are very kinked + # since this would misclassify part of ebb and flood + PA, _ = principal_axis(u, v) + PA_all[i] = PA + + # sign speed - eliminating wrap-around + dir_PA = dir - PA + + dir_PA[dir_PA < -90] += 360 + dir_PA[dir_PA > 270] -= 360 + + #dir_PA[dir_PA<-90] = dir_PA(dir_PA<-90) + 360; + #dir_PA(dir_PA>270) = dir_PA(dir_PA>270) - 360; + + #general direction of flood passed as input argument + #if PA >= flood_heading[0] and PA <= flood_heading[1]: + if flood_heading[0] <= PA <= flood_heading[1]: + #ind_fld = find(dir_PA >= -90 & dir_PA<90) + ind_fld = np.where((dir_PA >= -90) & (dir_PA<90)) + s_signed = -s + s_signed[ind_fld] = s[ind_fld] + else: + ind_ebb = np.where((dir_PA >= -90) & (dir_PA<90)) + s_signed = s + s_signed[ind_ebb] = -s[ind_ebb] + + s_signed_all[:, i] = s_signed + + return s_signed_all, PA_all + +def principal_axis(u, v): + + #create velocity matrix + U = np.vstack((u,v)).T + #eliminate NaN values + U = U[~np.isnan(U[:, 0]), :] + #convert matrix to deviate form + rep = np.tile(np.mean(U, axis=0), [len(U), 1]) + U -= rep + #compute covariance matrix + R = np.dot(U.T, U) / (len(U) - 1) + + #calculate eigenvalues and eigenvectors for covariance matrix + R[np.where(np.isnan(R))] = 0.0 + R[np.where(np.isinf(R))] = 0.0 + R[np.where(np.isneginf(R))] = 0.0 + lamb, V = np.linalg.eig(R) + #sort eignvalues in descending order so that major axis is given by first eigenvector + # sort in descending order with indices + ilamb = sorted(range(len(lamb)), key=lambda k: lamb[k], reverse=True) + lamb = sorted(lamb, reverse=True) + # reconstruct the eigenvalue matrix + lamb = np.diag(lamb) + #reorder the eigenvectors + V = V[:, ilamb] + + #rotation angle of major axis in radians relative to cartesian coordiantes + #ra = np.arctan2(V[0,1], V[1,1]) + ra = atan2(V[0,1], V[1,1]) + #express principal axis in compass coordinates + #PA = -ra * 180 / np.pi + 90 + #TR: still not sure here + PA = -ra * 180 / np.pi + #variance captured by principal + varxp_PA = np.diag(lamb[0]) / np.trace(lamb) + + return PA, varxp_PA + + +class Struct: + def __init__(self, **entries): + self.__dict__.update(entries) + +def save_FlowFile_BPFormat(fileinfo, adcp, rbr, params, options): + print adcp.mtime[0] + day1 = date2py(adcp.mtime[0][0]) + print day1 + #date_time = [date2py(tval[0]) for tval in adcp.mtime[:]] + datenum = datetime(day1.year,1,1) + timedelta(365) + datenum = datenum.toordinal() + + yd = adcp.mtime[:].ravel() - datenum + tind = np.where((yd > params.tmin) & (yd < params.tmax))[0] + + time = {} + time['mtime'] = adcp.mtime[:].ravel()[tind] + dt = np.nanmean(np.diff(time['mtime'])) + + if not rbr: + print 'Depths measured by ADCP not yet coded.' + else: + print 'Ensemble averaging rbr data' + + nens = round(dt/(rbr.mtime[1] - rbr.mtime[0])) + + + +# mini = timedelta(days=params.tmin) +# maxi = timedelta(days=params.tmax) +# +# nmin = datetime(day1.year,1,1) + mini +# nmax = datetime(day1.year,1,1) + maxi +# print yd + + + +if __name__ == '__main__': + filename = '140703-EcoEII_database/data/GP-120726-BPd_raw.mat' + data = rawADCP(filename) + rawdata = rawADCP(filename) + #adcp = Struct(**data.adcp) + rawADCP = data.adcp + adcp = data.adcp + params = Struct(**data.saveparams) + params = data.saveparams + rbr = Struct(**data.rbr) + +# save_FlowFile_BPFormat(data.fileinfo, data.adcp, data.rbr, +# data.saveparams, data.options) + + debug = False + #day1 = date2py(adcp.mtime[0][0]) + day1 = date2py(adcp['mtime'][0][0]) + #date_time = [date2py(tval[0]) for tval in adcp.mtime[:]] + datenum = datetime(day1.year,1,1) + timedelta(365) + datenum = datenum.toordinal() + #yd = adcp.mtime[:].ravel() - datenum + yd = adcp['mtime'][:].ravel() - datenum + #tind = np.where((yd > params.tmin) & (yd < params.tmax))[0] + tind = np.where((yd > params['tmin']) & (yd < params['tmax']))[0] + + time = {} + time['mtime'] = adcp['mtime'][:].ravel()[tind] + dt = np.nanmean(np.diff(time['mtime'])) + pres = {} + + if not rbr: + print 'Depths measured by ADCP not yet coded.' + else: + print 'Ensemble averaging rbr data' + + nens = round(dt/(rbr.mtime[1] - rbr.mtime[0])) + temp = np.arange(rbr.mtime[nens/2-1], rbr.mtime[-1-nens/2], dt) + temp2 = np.r_[rbr.mtime[nens/2-1]: rbr.mtime[-1-nens/2]: dt] + + # Load in matlab values + filename = './140703-EcoEII_database/scripts_examples/mtime.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matTimes = mat['mtimeens'] + filename = './140703-EcoEII_database/scripts_examples/dt.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matdt = mat['dt'] + + + mtimeens = np.arange(rbr.mtime[nens/2-1], rbr.mtime[-1-nens/2], dt) + #mtimeens = mtimeens + params.rbr_hr_offset / 24 + mtimeens = mtimeens + params['rbr_hr_offset'] / 24 + depthens = calc_ensemble(rbr.depth, nens, 1) + + filename = './140703-EcoEII_database/scripts_examples/depthens.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matdepthens = mat['depthens'] + + filename = './140703-EcoEII_database/scripts_examples/time.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matmtime = mat['mtime'] + + + debug = True + if debug: + print matTimes.shape + print temp - matTimes + print temp2 - matTimes + print dt - matdt + print depthens - matdepthens + print 'time' + print time['mtime'] - matmtime + + temp = sip.interp1d(mtimeens, depthens, kind='linear') + + #pres['surf']= temp(time['mtime']) + params.dabPS + pres['surf']= temp(time['mtime']) + params['dabPS'] + + #if data.options['showRBRavg']: + if debug: + #plt.plot(rbr.mtime+params.rbr_hr_offset/24, rbr.depth+params.dabPS, + # label='RBR') + #plt.plot(time['mtime'], pres['surf'], 'r', label='AVG') + plt.plot(rbr.mtime+params['rbr_hr_offset']/24, rbr.depth+params['dabPS'], + label='RBR') + plt.plot(time['mtime'], pres['surf'], 'r', label='AVG') + plt.xlabel('Time') + plt.ylabel('Elevation') + plt.legend(bbox_to_anchor=(0, 0, 1, 1), bbox_transform=plt.gcf().transFigure) + + plt.show() + + ## zlevels + data = {} + z = adcp['config']['ranges'][:] + params['dabADCP'] + z = z.ravel() + zind = np.where((z > params['zmin']) & (z < params['zmax']))[0] + data['bins'] = z[zind] + + ## Currents + #data['vert_vel'] = adcp['vert_vel'][:][zind, tind].T + #data['error_vel'] = adcp['error_vel'][:][zind, tind].T + data['vert_vel'] = adcp['vert_vel'][:][tind][:, zind] + data['error_vel'] = adcp['error_vel'][:][tind][:, zind] + + # If compass wasn't calibrated + #if isfield(params,'hdgmod'): + if 'hdgmod' in params: + adcp['east_vel'][:], adcp['north_vel'][:] = rotate_coords(adcp['east_vel'][:], + adcp['north_vel'][:], + params['hdgmod']) + #Comments{end+1} = 'East and north velocity rotated by params.hdgmod'; + + # Rotate east_vel and north_vel to be relative to true north + data['east_vel'], data['north_vel'] = \ + rotate_to_true(adcp['east_vel'][:][tind][:, zind], + adcp['north_vel'][:][tind][:, zind], + params['declination']) + + # Direction + data['dir_vel'] = get_DirFromN(data['east_vel'],data['north_vel']) + + # Signed Speed + spd_all = np.sqrt(data['east_vel']**2+data['north_vel']**2) + + # Determine flood and ebb based on principal direction (Polagye Routine) + print 'Getting signed speed (Principal Direction Method) -- used all speeds' + s_signed_all, PA_all = sign_speed(data['east_vel'], data['north_vel'], + spd_all, data['dir_vel'], params['flooddir']) + + data['mag_signed_vel'] = s_signed_all + +## save_FlowFile_BPFormat(data.fileinfo, adcp, data.rbr, +## params, data.options) diff --git a/build/lib/pyseidon_dvt/utilities/__init__.py b/build/lib/pyseidon_dvt/utilities/__init__.py new file mode 100644 index 0000000..2223df3 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + + +__authors__ = ['Wesley Bowman, Thomas Roc, Jonathan Smith'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/utilities/createNC.py b/build/lib/pyseidon_dvt/utilities/createNC.py new file mode 100644 index 0000000..34e6f65 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/createNC.py @@ -0,0 +1,87 @@ +import netCDF4 as nc + + +def createNC(data): + + ncFile = nc.Dataset('test.nc', 'w', format='NETCDF4') + + #ncgrp = ncFile.createGroup('regioned') + #ncgrp.createDimension('dim', None) + #ncgrp = ncFile.createGroup('regioned') + #ncFile.createDimension('dimTest', None) + + ncFile.createDimension('dim', None) + + time = ncFile.createVariable('time', 'f8', ('dim',)) + time[:] = data['time'] + + x = ncFile.createVariable('x', 'f8', ('dim',)) + x[:] = data['x'] + + y = ncFile.createVariable('y', 'f8', ('dim',)) + y[:] = data['y'] + + xc = ncFile.createVariable('xc', 'f8', ('dim',)) + xc[:] = data['xc'] + + yc = ncFile.createVariable('yc', 'f8', ('dim',)) + yc[:] = data['yc'] + + h = ncFile.createVariable('h', 'f8', ('dim',)) + h[:] = data['h'] + + lon = ncFile.createVariable('lon', 'f8', ('dim',)) + lon[:] = data['lon'] + + lat = ncFile.createVariable('lat', 'f8', ('dim',)) + lat[:] = data['lat'] + + lonc = ncFile.createVariable('lonc', 'f8', ('dim',)) + lonc[:] = data['lonc'] + + latc = ncFile.createVariable('latc', 'f8', ('dim',)) + latc[:] = data['latc'] + + elev = ncFile.createVariable('elev', 'f8', ('dim', 'dim')) + elev[:] = data['elev'] + + ua = ncFile.createVariable('ua', 'f8', ('dim', 'dim')) + ua[:] = data['ua'] + + va = ncFile.createVariable('va', 'f8', ('dim', 'dim')) + va[:] = data['va'] + + node_index = ncFile.createVariable('node_index', 'f8', ('dim',)) + node_index[:] = data['node_index'] + + element_index = ncFile.createVariable('element_index', 'f8', ('dim',)) + element_index[:] = data['element_index'] + + nbe = ncFile.createVariable('nbe', 'f8', ('dim', 'dim')) + nbe[:] = data['nbe'] + + nv = ncFile.createVariable('nv', 'f8', ('dim', 'dim')) + nv[:] = data['nv'] + + a1u = ncFile.createVariable('a1u', 'f8', ('dim', 'dim')) + a1u[:] = data['a1u'] + + a2u = ncFile.createVariable('a2u', 'f8', ('dim', 'dim')) + a2u[:] = data['a2u'] + + aw0 = ncFile.createVariable('aw0', 'f8', ('dim', 'dim')) + aw0[:] = data['aw0'] + + awx = ncFile.createVariable('awx', 'f8', ('dim', 'dim')) + awx[:] = data['awx'] + + awy = ncFile.createVariable('awy', 'f8', ('dim', 'dim')) + awy[:] = data['awy'] + + siglay = ncFile.createVariable('siglay', 'f8', ('dim', 'dim')) + siglay[:] = data['siglay'] + + siglev = ncFile.createVariable('siglev', 'f8', ('dim', 'dim')) + siglev[:] = data['siglev'] + + ncFile.close() diff --git a/build/lib/pyseidon_dvt/utilities/interpolation_utils.py b/build/lib/pyseidon_dvt/utilities/interpolation_utils.py new file mode 100644 index 0000000..f2918d0 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/interpolation_utils.py @@ -0,0 +1,597 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +import sys +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +import matplotlib.ticker as ticker +from matplotlib.path import Path +from scipy.spatial import KDTree +import scipy.interpolate as interpolate + +def closest_point(pt_lon, pt_lat, lon, lat, lonc, latc, tri, + debug=False): + ''' + Finds the closest exact lon, lat centre indexes of an FVCOM class + to given lon, lat coordinates. + + Inputs: + - pt_lon = list of longitudes in degrees to find + - pt_lat = list of latitudes in degrees to find + - lon = list of longitudes in degrees to search in + - lat = list of latitudes in degrees to search in + Outputs: + - closest_point_indexes = numpy array of grid indexes + ''' + if debug: + print 'Computing closest_point_indexes...' + points = np.array([[pt_lon], [pt_lat]]).T + point_list = np.array([lonc[:], latc[:]]).T + + #closest_dist = (np.square((point_list[:, 0] - points[:, 0, np.newaxis])) + + # np.square((point_list[:, 1] - points[:, 1, np.newaxis]))) + + #closest_point_indexes = np.argmin(closest_dist, axis=1) + + #Wesley's optimized version of this bottleneck + point_list0 = point_list[:, 0] + points0 = points[:, 0, np.newaxis] + point_list1 = point_list[:, 1] + points1 = points[:, 1, np.newaxis] + + closest_dist = ((point_list0 - points0) * + (point_list0 - points0) + + (point_list1 - points1) * + (point_list1 - points1) + ) + + #Check if it is the right index + if tri[:].max() == (lonc[:].shape[0]-1): + lo = lonc[:] + la = latc[:] + else: + lo = lon[:] + la = lat[:] + + index = np.argmin(closest_dist, axis=1)[0] + triIndex = tri[index] + triIndex.sort()#due to new version of netCDF4 + trig = Tri.Triangulation(lo[triIndex], la[triIndex], np.array([[0,1,2]])) + trif = trig.get_trifinder() + test = -1 * trif.__call__(pt_lon, pt_lat) + if test: index = np.nan #freak point + + #Thomas' optimized version of this bottleneck + #closest_point_indexes = np.zeros(points.shape[0]) + #for i in range(points.shape[0]): + # dist=((point_list-points[i,:])**2).sum(axis=1) + # ndx = d.argsort() + # closest_point_indexes[i] = ndx[0] + #if debug: print 'Closest dist: ', closest_dist + if debug: + print 'closest_point_indexes', index + print '...Passed' + + return index + +def closest_points( pt_lon, pt_lat, lon, lat, debug=False): + ''' + Finds the closest exact lon, lat centre indexes of an FVCOM class + to given lon, lat coordinates. + + Inputs: + - pt_lon = list of longitudes in degrees to find + - pt_lat = list of latitudes in degrees to find + - lon = list of longitudes in degrees to search in + - lat = list of latitudes in degrees to search in + Outputs: + - closest_point_indexes = numpy array of grid indexes + ''' + if debug: + print 'Computing closest_point_indexes...' + + lonc=lon[:] + latc=lat[:] + if not type(pt_lon)==list: + points = np.array([[pt_lon], [pt_lat]]).T + else: + points = np.array([pt_lon, pt_lat]).T + point_list = np.array([lonc, latc]).T + + #closest_dist = (np.square((point_list[:, 0] - points[:, 0, np.newaxis])) + + # np.square((point_list[:, 1] - points[:, 1, np.newaxis]))) + + #closest_point_indexes = np.argmin(closest_dist, axis=1) + + #Wesley's optimized version of this bottleneck + point_list0 = point_list[:, 0] + points0 = points[:, 0, np.newaxis] + point_list1 = point_list[:, 1] + points1 = points[:, 1, np.newaxis] + + closest_dist = ((point_list0 - points0) * + (point_list0 - points0) + + (point_list1 - points1) * + (point_list1 - points1) + ) + closest_point_indexes = np.argmin(closest_dist, axis=1) + + #Thomas' optimized version of this bottleneck + #closest_point_indexes = np.zeros(points.shape[0]) + #for i in range(points.shape[0]): + # dist=((point_list-points[i,:])**2).sum(axis=1) + # ndx = d.argsort() + # closest_point_indexes[i] = ndx[0] + if debug: + print 'Closest dist: ', closest_dist + + + if debug: + print 'closest_point_indexes', closest_point_indexes + print '...Passed' + + return closest_point_indexes + +def interpN_at_pt(var, pt_x, pt_y, index, trinodes, + aw0, awx, awy, debug=False): + """ + Interpol node variable any given variables at any give location. + Inputs: + - var = variable, numpy array, dim=(node) or (time, node) or (time, level, node) + - pt_x = x coordinate in m to find + - pt_y = y coordinate in m to find + - xc = list of x coordinates of var, numpy array, dim= ele + - yc = list of y coordinates of var, numpy array, dim= ele + - trinodes = FVCOM trinodes, numpy array, dim=(3,nele) + - index = index of the nearest element + - aw0, awx, awy = grid parameters + Outputs: + - varInterp = var interpolate at (pt_lon, pt_lat) + """ + if debug: + print 'Interpolating at node...' + + n1 = int(trinodes[index,0]) + n2 = int(trinodes[index,1]) + n3 = int(trinodes[index,2]) + # x0 = pt_x - xc[index] + # y0 = pt_y - yc[index] + #due to Mitchell's alternative, conversion made in functionsFvcom.py + x0 = pt_x + y0 = pt_y + + if len(var.shape)==1: + var0 = (aw0[0,index] * var[n1]) \ + + (aw0[1,index] * var[n2]) \ + + (aw0[2,index] * var[n3]) + varX = (awx[0,index] * var[n1]) \ + + (awx[1,index] * var[n2]) \ + + (awx[2,index] * var[n3]) + varY = (awy[0,index] * var[n1]) \ + + (awy[1,index] * var[n2]) \ + + (awy[2,index] * var[n3]) + elif len(var.shape)==2: + var0 = (aw0[0,index] * var[:,n1]) \ + + (aw0[1,index] * var[:,n2]) \ + + (aw0[2,index] * var[:,n3]) + varX = (awx[0,index] * var[:,n1]) \ + + (awx[1,index] * var[:,n2]) \ + + (awx[2,index] * var[:,n3]) + varY = (awy[0,index] * var[:,n1]) \ + + (awy[1,index] * var[:,n2]) \ + + (awy[2,index] * var[:,n3]) + else: + var0 = (aw0[0,index] * var[:,:,n1]) \ + + (aw0[1,index] * var[:,:,n2]) \ + + (aw0[2,index] * var[:,:,n3]) + varX = (awx[0,index] * var[:,:,n1]) \ + + (awx[1,index] * var[:,:,n2]) \ + + (awx[2,index] * var[:,:,n3]) + varY = (awy[0,index] * var[:,:,n1]) \ + + (awy[1,index] * var[:,:,n2]) \ + + (awy[2,index] * var[:,:,n3]) + varPt = var0 + (varX * x0) + (varY * y0) + + if debug: + if len(var.shape)==1: + zi = varPt + print 'Varpt: ', zi + print '...Passed' + + #TR comment: squeeze seems to resolve my problem with pydap + return varPt.squeeze() + +def interpN(var,trinodes,aw0,debug=False): + """ + Interpol node variable at elements. + Inputs: + - var = variable, numpy array, dim=(node) or (time, node) or (time, level, node) + - trinodes = FVCOM trinodes, numpy array, dim=(3,nele) + - aw0, awx, awy = grid parameters + Outputs: + - varInterp = var interpolated + """ + if debug: + print 'Interpolating at nodes...' + + n1 = [int(number) for number in trinodes[:,0]] + n2 = [int(number) for number in trinodes[:,1]] + n3 = [int(number) for number in trinodes[:,2]] + + if len(var.shape)==1: + var0 = (aw0[0,:] * var[n1]) \ + + (aw0[1,:] * var[n2]) \ + + (aw0[2,:] * var[n3]) + elif len(var.shape)==2: + var0 = (aw0[0,:] * var[:,n1]) \ + + (aw0[1,:] * var[:,n2]) \ + + (aw0[2,:] * var[:,n3]) + else: + var0 = (aw0[0,:] * var[:,:,n1]) \ + + (aw0[1,:] * var[:,:,n2]) \ + + (aw0[2,:] * var[:,:,n3]) + varPt = var0 + + if debug: print '...Passed' + + #TR comment: squeeze seems to resolve my problem with pydap + return varPt.squeeze() + +def interpE_at_pt(var, pt_x, pt_y, index, triele, + a1u, a2u, debug=False): + """ + Interpol element variable any given variables at any give location. + Inputs: + - var = variable, numpy array, dim=(nele) or (time, nele) or (time, level, nele) + - pt_x = x coordinate in m to find + - pt_y = y coordinate in m to find + - xc = list of x coordinates of var, numpy array, dim= nele + - yc = list of y coordinates of var, numpy array, dim= nele + - triele = FVCOM triele, numpy array, dim=(3,nele) + - index = index of the nearest element + - a1u, a2u = grid parameters + Outputs: + - varInterp = var interpolate at (pt_lon, pt_lat) + """ + if debug: + print 'Interpolating at element...' + + n1 = int(triele[index,0]) + n2 = int(triele[index,1]) + n3 = int(triele[index,2]) + + #Test for ghost points + test = [-1, var.shape[-1]] + + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + n1 = int(n1) + n2 = int(n2) + n3 = int(n3) + + # x0 = pt_x - xc[index] + # y0 = pt_y - yc[index] + #due to Mitchell's alternative, conversion made in functionsFvcom.py + x0 = pt_x + y0 = pt_y + + if len(var.shape)==1: + # Treatment of ghost points + if n1 in test: + V1 = 0.0 + else: + V1 = var[n1] + if n2 in test: + V2 = 0.0 + else: + V2 = var[n2] + if n3 in test: + V3 = 0.0 + else: + V3 = var[n3] + + dvardx = (a1u[0,index] * var[index]) \ + + (a1u[1,index] * V1) \ + + (a1u[2,index] * V2) \ + + (a1u[3,index] * V3) + dvardy = (a2u[0,index] * var[index]) \ + + (a2u[1,index] * V1) \ + + (a2u[2,index] * V2) \ + + (a2u[3,index] * V3) + varPt = var[index] + (dvardx * x0) + (dvardy * y0) + elif len(var.shape)==2: + # Treatment of ghost points + if n1 in test: + V1 = np.zeros(var.shape[0]) + else: + V1 = var[:,n1] + if n2 in test: + V2 = np.zeros(var.shape[0]) + else: + V2 = var[:,n2] + if n3 in test: + V3 = np.zeros(var.shape[0]) + else: + V3 = var[:,n3] + + dvardx = (a1u[0,index] * var[:,index]) \ + + (a1u[1,index] * V1) \ + + (a1u[2,index] * V2) \ + + (a1u[3,index] * V3) + dvardy = (a2u[0,index] * var[:,index]) \ + + (a2u[1,index] * V1) \ + + (a2u[2,index] * V2) \ + + (a2u[3,index] * V3) + varPt = var[:,index] + (dvardx * x0) + (dvardy * y0) + else: + # Treatment of ghost points + if n1 in test: + V1 = np.zeros((var.shape[0], var.shape[1])) + else: + V1 = var[:,:,n1] + if n2 in test: + V2 = np.zeros((var.shape[0], var.shape[1])) + else: + V2 = var[:,:,n2] + if n3 in test: + V3 = np.zeros((var.shape[0], var.shape[1])) + else: + V3 = var[:,:,n3] + + dvardx = (a1u[0,index] * var[:,:,index]) \ + + (a1u[1,index] * V1) \ + + (a1u[2,index] * V2) \ + + (a1u[3,index] * V3) + dvardy = (a2u[0,index] * var[:,:,index]) \ + + (a2u[1,index] * V1) \ + + (a2u[2,index] * V2) \ + + (a2u[3,index] * V3) + varPt = var[:,:,index] + (dvardx * x0) + (dvardy * y0) + + if debug: + if len(var.shape)==1: + zi = varPt + print 'Varpt: ', zi + print '...Passed' + #TR comment: squeeze seems to resolve my problem with pydap + return varPt.squeeze() + +def interpE(var, xc, yc, triele, + a1u, a2u, debug=False): + """ + Interpol element variable at node locations. + Inputs: + - var = variable, numpy array, dim=(nele) or (time, nele) or (time, level, nele) + - xc = list of x coordinates of var, numpy array, dim= nele + - yc = list of y coordinates of var, numpy array, dim= nele + - triele = FVCOM triele, numpy array, dim=(3,nele) + - a1u, a2u = grid parameters + Outputs: + - varInterp = var interpolate at (pt_lon, pt_lat) + """ + if debug: + print 'Interpolating at element...' + + n1 = [int(number) for number in triele[:,0]] + n2 = [int(number) for number in triele[:,1]] + n3 = [int(number) for number in triele[:,2]] + + #TR quick fix: due to error with pydap.proxy.ArrayProxy + # not able to cop with numpy.int + + x0 = xc[:] + y0 = yc[:] + + if len(var.shape)==1: + # Treatment of ghost points + Var = np.hstack((var,0)) + V1 = Var[n1] + V2 = Var[n2] + V3 = Var[n3] + + dvardx = (a1u[0,:] * var[:]) \ + + (a1u[1,:] * V1) \ + + (a1u[2,:] * V2) \ + + (a1u[3,:] * V3) + dvardy = (a2u[0,:] * var[:]) \ + + (a2u[1,:] * V1) \ + + (a2u[2,:] * V2) \ + + (a2u[3,:] * V3) + varPt = var[:] + (dvardx * x0) + (dvardy * y0) + elif len(var.shape)==2: + # Treatment of ghost points + Var = np.zeros((var.shape[0], var.shape[1]+1)) + Var[:,:-1] = var[:] + V1 = Var[:,n1] + V2 = Var[:,n2] + V3 = Var[:,n3] + + dvardx = (a1u[0,:] * var[:,:]) \ + + (a1u[1,:] * V1) \ + + (a1u[2,:] * V2) \ + + (a1u[3,:] * V3) + dvardy = (a2u[0,:] * var[:,:]) \ + + (a2u[1,:] * V1) \ + + (a2u[2,:] * V2) \ + + (a2u[3,:] * V3) + varPt = var[:,:] + (dvardx * x0) + (dvardy * y0) + else: + # Treatment of ghost points + Var = np.zeros((var.shape[0], var.shape[1], var.shape[2]+1)) + Var[:,:,:-1] = var[:] + V1 = Var[:,:,n1] + V2 = Var[:,:,n2] + V3 = Var[:,:,n3] + + dvardx = (a1u[0,:] * var[:,:,:]) \ + + (a1u[1,:] * V1) \ + + (a1u[2,:] * V2) \ + + (a1u[3,:] * V3) + dvardy = (a2u[0,:] * var[:,:,:]) \ + + (a2u[1,:] * V1) \ + + (a2u[2,:] * V2) \ + + (a2u[3,:] * V3) + varPt = var[:,:,:] + (dvardx * x0) + (dvardy * y0) + + if debug: + if len(var.shape)==1: + zi = varPt + print 'Varpt: ', zi + print '...Passed' + #TR comment: squeeze seems to resolve my problem with pydap + return varPt.squeeze() + +def interp_at_point(var, pt_lon, pt_lat, lon, lat, + index, trinodes, tri=[], debug=False): + """ + Interpol any given variables at any give location. + + Inputs: + - var = variable, numpy array, dim=(time, nele or node) + - pt_lon = longitude in degrees to find + - pt_lat = latitude in degrees to find + - lon = list of longitudes of var, numpy array, dim=(nele or node) + - lat = list of latitudes of var, numpy array, dim=(nele or node) + - trinodes = FVCOM trinodes, numpy array, dim=(3,nele) + Keywords: + - tri = triangulation object + Outputs: + - varInterp = var interpolate at (pt_lon, pt_lat) + """ + if debug: + print 'Interpolating at point...' + #Finding the right indexes + + #Triangulation + #if debug: + # print triIndex, lon[triIndex], lat[triIndex] + triIndex = trinodes[index] + triIndex.sort()#due to new version of netCDF4 + if tri==[]: + tri = Tri.Triangulation(lon[triIndex], lat[triIndex], np.array([[0,1,2]])) + + trif = tri.get_trifinder() + trif.__call__(pt_lon, pt_lat) + if debug: + if len(var.shape)==1: + averEl = var[triIndex] + print 'Var', averEl + inter = Tri.LinearTriInterpolator(tri, averEl) + zi = inter(pt_lon, pt_lat) + print 'zi', zi + + #Choose the right interpolation depending on the variable + if len(var.shape)==1: + triVar = np.zeros(triIndex.shape) + triVar[:] = var[triIndex] + inter = Tri.LinearTriInterpolator(tri, triVar[:]) + varInterp = inter(pt_lon, pt_lat) + elif len(var.shape)==2: + triVar = np.zeros((var.shape[0], triIndex.shape[0])) + triVar[:] = var[:, triIndex] + varInterp = np.ones(triVar.shape[0]) + for i in range(triVar.shape[0]): + inter = Tri.LinearTriInterpolator(tri, triVar[i,:]) + varInterp[i] = inter(pt_lon, pt_lat) + else: + triVar = np.zeros((var.shape[0], var.shape[1], triIndex.shape[0])) + triVar[:] = var[:, :, triIndex] + varInterp = np.ones(triVar.shape[:-1]) + for i in range(triVar.shape[0]): + for j in range(triVar.shape[1]): + inter = Tri.LinearTriInterpolator(tri, triVar[i,j,:]) + varInterp[i,j] = inter(pt_lon, pt_lat) + + if debug: + print '...Passed' + + #TR comment: squeeze seems to resolve my problem with pydap + return varInterp.squeeze() + +def interpE_at_point_bis(var, pt_lon, pt_lat, lonc, latc, debug=False): + """ + Interpol at awkward locations. + + Inputs: + - var = variable, numpy array, dim=(time, nele) + - pt_lon = longitude to find + - pt_lat = latitude to find + - lonc = list of longitudes of var, numpy array, dim=(nele) + - lonc = list of latitudes of var, numpy array, dim=(nele) + + Outputs: + - varInterp = var interpolate at (pt_lon, pt_lat) + """ + if debug: + print 'Interpolating at awkward point...' + #Finding the right indexes + points = np.array([[pt_lon], [pt_lat]]).T + point_list = np.array([lonc[:], latc[:]]).T + point_list0 = point_list[:, 0] + points0 = points[:, 0, np.newaxis] + point_list1 = point_list[:, 1] + points1 = points[:, 1, np.newaxis] + + closest_dist = ((point_list0 - points0) * + (point_list0 - points0) + + (point_list1 - points1) * + (point_list1 - points1) + ) + #Finding closest elements + triIndex = [0,0,0] + triIndex[0] = np.argmin(closest_dist, axis=1)[0] + closest_dist[:,triIndex[0]]=np.inf + triIndex[1] = np.argmin(closest_dist, axis=1)[0] + closest_dist[:,triIndex[1]]=np.inf + #test if inside triangle + test=1 + while test: + trig = Tri.Triangulation(lonc[triIndex], latc[triIndex], np.array([[0,1,2]])) + triIndex[2] = np.argmin(closest_dist, axis=1)[0] + trif = trig.get_trifinder() + test = -1 * trif.__call__(pt_lon, pt_lat) + if test: closest_dist[:,triIndex[2]]=np.inf + #new scheme + #linear equation based on plane equation + x1 = lonc[triIndex[0]]; x2 = lonc[triIndex[1]]; x3 = lonc[triIndex[2]] + y1 = latc[triIndex[0]]; y2 = latc[triIndex[1]]; y3 = latc[triIndex[2]] + a = np.array([[x2-x1,x3-x1],[y2-y1,y3-y1]]) + b = np.array([pt_lon-x1,pt_lat-y1]) + A, B = np.linalg.solve(a, b) + #applying weights + if len(var.shape)==1: + z1 = var[triIndex[0]] + z2 = var[triIndex[1]] + z3 = var[triIndex[2]] + elif len(var.shape)==2: + z1 = var[:,triIndex[0]] + z2 = var[:,triIndex[1]] + z3 = var[:,triIndex[2]] + else: + z1 = var[:,:,triIndex[0]] + z2 = var[:,:,triIndex[1]] + z3 = var[:,:,triIndex[2]] + varInterp = z1 + A * (z2 - z1) + B * (z3 - z1) + #end new scheme + if debug: + print '...Passed' + + #TR comment: squeeze seems to resolve my problem with pydap + return varInterp.squeeze() + +def interp_linear_to_nodes(var, xc, yc, x, y): + """Linear interpolation from elements to nodes""" + L = xc.shape[0] + M = x.shape[0] + orig = np.zeros((L, 2)) + ask = np.zeros((M, 2)) + orig[:, 0] = xc + orig[:, 1] = yc + ask[:, 0] = x + ask[:, 1] = y + interpol = interpolate.LinearNDInterpolator(orig, var) + varinterp = interpol(ask) + varinterp[np.where(varinterp==np.nan)]=0.0 + + return varinterp \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/utilities/miscellaneous.py b/build/lib/pyseidon_dvt/utilities/miscellaneous.py new file mode 100644 index 0000000..f54b30b --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/miscellaneous.py @@ -0,0 +1,174 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +from datetime import datetime +from datetime import timedelta +import fnmatch +import os +import time +from scipy.io import netcdf +from pydap.client import open_url + +# Custom error +from pyseidon_error import PyseidonError + +def date2py(matlab_datenum): + python_datetime = datetime.fromordinal(int(matlab_datenum)) + \ + timedelta(days=matlab_datenum%1) - timedelta(days = 366) + + return python_datetime + +def op_angles_from_vectors(u, v, debug=False): + """ + This function takes in vectors in the form (u,v) and compares them in + order to find the angles of the vectors without any wrap-around issues. + This is accomplished by finding the smallest difference between angles + compared at different wrap-around values. + This appears to work correctly. + + Inputs: + -u = velocity component along x (West-East) direction, 1D array + -v = velocity component along y (South-North) direction, 1D array + + Outputs: + -angle = corresponidng angle in degrees, 1D array + + Notes: + -Angles are reported in compass coordinates, i.e. 0 and 360 deg., + 0/360=East, 90=North, 180=West, 270=South + """ + if debug: + print 'Computing angles from velocity component...' + start = time.time() + + phi = np.mod((-1.0*np.arctan2(v,u)) * (180.0/np.pi) + 90.0, 360.0) + if len(phi.shape)==1:#Assuming the only dimension is time + #Compute difference between angles + diff1 = np.abs(phi[:-1]-phi[1:]) #initial difference between angles + diff2 = np.abs(phi[:-1]-phi[1:]-360.0) #diff when moved down a ring + diff3 = np.abs(phi[:-1]-phi[1:]+360.0) #diff when moved up a ring + + index1 = np.where((diff2 < diff1) & (diff2 < diff3))[0] + index2 = np.where((diff3 < diff1) & (diff3 < diff2))[0] + + phi[index1] = np.mod(phi[index1] - 360.0, 360.0) + phi[index2] = np.mod(phi[index2] + 360.0, 360.0) + elif len(phi.shape)==2:#Assuming the only dimension is time and sigma level + #Compute difference between angles + diff1 = np.abs(phi[:-1,:]-phi[1:,:]) #initial difference between angles + diff2 = np.abs(phi[:-1,:]-phi[1:,:]-360.0) #diff when moved down a ring + diff3 = np.abs(phi[:-1,:]-phi[1:,:]+360.0) #diff when moved up a ring + + index1 = np.where((diff2 < diff1) & (diff2 < diff3))[0] + index2 = np.where((diff3 < diff1) & (diff3 < diff2))[0] + + phi[index1] = phi[index1] - 360.0 + phi[index2] = phi[index2] + 360.0 + else: #Assuming the only dimension is time ,sigma level and element + #Compute difference between angles + diff1 = np.abs(phi[:-1,:,:]-phi[1:,:,:]) #initial difference between angles + diff2 = np.abs(phi[:-1,:,:]-phi[1:,:,:]-360.0) #diff when moved down a ring + diff3 = np.abs(phi[:-1,:,:]-phi[1:,:,:]+360.0) #diff when moved up a ring + + index1 = np.where((diff2 < diff1) & (diff2 < diff3))[0] + index2 = np.where((diff3 < diff1) & (diff3 < diff2))[0] + + phi[index1] = phi[index1] - 360.0 + phi[index2] = phi[index2] + 360.0 + + if debug: + end = time.time() + print "...processing time: ", (end - start) + + return phi + +def time_to_index(t_start, t_end, time, debug=False): + """ + Convert datetime64[us] string in FVCOM index + + Inputs: + - t_start = start time in datetime + - t_end = end time in datetime + - time = array of julian days + + Outputs: + - argtime = arry of indices + """ + start = date_to_julian_day(t_start) + end = date_to_julian_day(t_end) + + t_slice = [start, end] + + argtime = np.argwhere((time>=t_slice[0])&(time<=t_slice[-1])).ravel() + if debug: + print 'Argtime: ', argtime + if argtime == []: + raise PyseidonError("Wrong time input") + return argtime + +def mattime_to_datetime(mattime, debug=False): + """Convert matlab time to datetime64[us] """ + date = datetime.fromordinal(int(mattime)) + \ + timedelta(days=mattime%1)-timedelta(days=366) + time = np.array(date,dtype='datetime64[us]') + + return time + +def datetime_to_mattime(dt, debug=False): + """Convert datetime64[us] to matlab time""" + mdn = dt + timedelta(days = 366) + s = (dt.hour * (60.0*60.0)) + (dt.minute * 60.0) + dt.second + day = 24.0*60.0*60.0 + frac = s/day + + return mdn.toordinal() + frac + +def findFiles(filename, name): + """ + Wesley comment[elements] the name needs to be a linux expression to find files + you want. For multiple station files, this would work + name = '*station*.nc' + + For just dngrid_0001 and no restart files: + name = 'dngrid_0*.nc' + will work + """ + + name = '*' + name + '*.nc' + matches = [] + for root, dirnames, filenames in os.walk(filename): + for filename in fnmatch.filter(filenames, name): + matches.append(os.path.join(root, filename)) + filenames.remove(filename) + for filename in fnmatch.filter(filenames, name.lower()): + matches.append(os.path.join(root, filename)) + + return sorted(matches) + +def date_to_julian_day(my_date): + """Returns the Julian day number of a date.""" + # a = (14 - my_date.month)//12 + # y = my_date.year + 4800 - a + # m = my_date.month + 12*a - 3 + # s = (my_date.hour * (60.0*60.0)) + (my_date.minute * 60.0) + my_date.second + # day = 24.0*60.0*60.0 + # jtime = my_date.day + ((153*m + 2)//5) + 365*y + y//4 - y//100 + y//400 - 32045 + s/day + jtime = datetime_to_mattime(my_date) - 678942.0 + return jtime + +def distance(locs,loce): + """Returns the distance in meters between two locations in long/lat.""" + TPI=111194.92664455874 + y0c = TPI * (loce[1] - locs[1]) + dx_sph = loce[0] - locs[0] + if (dx_sph > 180.0): + dx_sph=dx_sph-360.0 + elif (dx_sph < -180.0): + dx_sph =dx_sph+360.0 + x0c = TPI * np.cos(np.deg2rad(loce[1] + locs[1])*0.5) * dx_sph + + dist=np.linalg.norm([x0c, y0c]) + + return dist diff --git a/build/lib/pyseidon_dvt/utilities/object_from_dict.py b/build/lib/pyseidon_dvt/utilities/object_from_dict.py new file mode 100644 index 0000000..333c06b --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/object_from_dict.py @@ -0,0 +1,6 @@ +class ObjectFromDict(object): + """ + Turns any given class object into a dictionnary + """ + def __init__(self, d): + self.__dict__ = d diff --git a/build/lib/pyseidon_dvt/utilities/pyseidon2matlab.py b/build/lib/pyseidon_dvt/utilities/pyseidon2matlab.py new file mode 100644 index 0000000..7275894 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/pyseidon2matlab.py @@ -0,0 +1,74 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Libs import +from __future__ import division +import numpy as np +import sys +from scipy.io import savemat + +def pyseidon_to_matlab(fvcom, filename, exceptions=[], debug=False): + """ + Saves fvcom object in a pickle file + + inputs: + - fvcom = fvcom pyseidon object + - filename = file name, string + options: + - exceptions = list of variables to exclude from output file + , list of strings + """ + #Define bounding box + if debug: + print "Computing bounding box..." + if fvcom.Grid._ax == []: + lon = fvcom.Grid.lon[:] + lat = fvcom.Grid.lat[:] + fvcom.Grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + + filename = filename + ".mat" + #TR comment: based on MitchellO'Flaherty-Sproul's code + dtype = float + data = {} + Grd = {} + Var = {} + data['Origin'] = fvcom._origin_file + data['History'] = fvcom.History + Grd = fvcom.Grid.__dict__ + Var = fvcom.Variables.__dict__ + # delete exceptions + for key in exceptions: Var.pop(key, None) + for key in exceptions: Grd.pop(key, None) + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in Var: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(Var[key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + Var[key] = Var[key][:] + #keyV = key + '-var' + try: + data[key] = np.float64(Var[key].copy()) + except AttributeError: + data[key] = Var[key] + #Unpickleable objects + Grd.pop("triangle", None) + for key in Grd: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(Grd[key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + Grd[key] = Grd[key][:] + #keyG = key + '-grd' + + try: + data[key] = np.float64(Grd[key].copy()) + except AttributeError: + data[key] = Grd[key] + + #Save in mat file file + if debug: + print 'Dumping in matlab file...' + savemat(filename, data, oned_as='column') \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/utilities/pyseidon2netcdf.py b/build/lib/pyseidon_dvt/utilities/pyseidon2netcdf.py new file mode 100644 index 0000000..b3c90a6 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/pyseidon2netcdf.py @@ -0,0 +1,125 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Libs import +from __future__ import division +import numpy as np +import sys +#TR comment: 2 alternatives +import netCDF4 as nc +#from scipy.io import netcdf + +def pyseidon_to_netcdf(fvcom, filename, exceptions=[], compression=False, debug=False): + """ + Saves fvcom object in a pickle file + + inputs: + - fvcom = fvcom pyseidon object + - filename = file name, string + options: + - exceptions = list of variables to exclude from output file + , list of strings + - compresion = compresses data with zlib and uses at least 3 significant digits, boolean + Note: Works only with netcdf format + """ + #Define bounding box + if debug: print "Computing bounding box..." + if compression: + zlib = True + least_significant_digit = 3 + else: + zlib = False + least_significant_digit = None + + # Check if netcdffilename has an extension + if not filename[-3:] == '.nc': + filename = filename + '.nc' + f = nc.Dataset(filename, 'w', format='NETCDF4_CLASSIC') + #history attribut + f.history = fvcom.History[:] + + #create dimensions + if not fvcom.Variables._3D: + ##2D dimensions + dims = {'three':3, 'four':4, + 'nele': fvcom.Grid.nele, 'node': fvcom.Grid.nnode, + 'siglay': 2, 'siglev': 3, + 'time': fvcom.Variables.julianTime.shape[0]} + else: + ##3D dimensions + dims = {'three':3, 'four':4, + 'nele': fvcom.Grid.nele, 'node': fvcom.Grid.nnode, + 'siglay': fvcom.Grid.nlevel, 'siglev': fvcom.Grid.nlevel+1, + 'vertshear':fvcom.Grid.nlevel-1, + 'time': fvcom.Variables.julianTime.shape[0]} + for key in dims.keys(): + f.createDimension(key, dims[key]) + + # list of var + varname = fvcom.Variables.__dict__.keys() + gridname = fvcom.Grid.__dict__.keys() + # getting rid of the "_var" kind + iterlist = varname[:] + for key in iterlist: + if key[0] == "_": varname.remove(key) + iterlist = gridname[:] # getting rid of the "_var" kind + for key in iterlist: + if key[0] == "_": gridname.remove(key) + mstrList = varname + gridname + + #load in netcdf file + if debug: print "Loading in nc file..." + for var in mstrList: + if not var in exceptions: # check if in list of exceptions + if var in varname: + data = fvcom.Variables + else: + data = fvcom.Grid + zlib = False + least_significant_digit = None + try: + if hasattr(data, var): + dim = [] + s = getattr(data, var).shape + for d in s: + flag = 1 + count = 0 + while flag: + if count == len(dims.keys()): # when two dimensions are the same by coincidence + #dim.append(dim[-1]) # TODO search in the entire list == d rather than last index + for k in dim: + if dims[k] == d: + newkey = k + dim.append(newkey) + flag = 0 + else: + key = dims.keys()[count] + if dims[key] == d: + if len(dim) == len(s): # in case of similar dims + count += 1 + pass + else: + if key not in dim: # make sure dimension doesn't get recorded twice + dim.append(key) + flag = 0 + count += 1 + else: + count += 1 + else: + count += 1 + dim = tuple(dim) + # exceptions which need name replacement + if var == 'w': + keyAlias = 'ww' + elif var == 'el': + keyAlias = 'zeta' + else: + keyAlias = var + tmp_var = f.createVariable(keyAlias, 'float', dim, + zlib=zlib, least_significant_digit=least_significant_digit) + tmp_var[:] = getattr(data, var)[:] + except (AttributeError, IndexError) as e: + pass + if debug: print "..."+var+" loaded..." + # clean exit + f.close() diff --git a/build/lib/pyseidon_dvt/utilities/pyseidon2pickle.py b/build/lib/pyseidon_dvt/utilities/pyseidon2pickle.py new file mode 100644 index 0000000..cdc865d --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/pyseidon2pickle.py @@ -0,0 +1,75 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +#Libs import +from __future__ import division +import cPickle as pkl + +#Local import +from functionsFvcomThreeD import * + +# Custom error +from pyseidon_error import PyseidonError + +def pyseidon_to_pickle(fvcom, filename, exceptions=[], debug=False): + """ + Saves fvcom object in a pickle file + + inputs: + - fvcom = fvcom pyseidon object + - filename = file name, string + options: + - exceptions = list of variables to exclude from output file + , list of strings + """ + #Define bounding box + if debug: + print "Computing bounding box..." + if fvcom.Grid._ax == []: + lon = fvcom.Grid.lon[:] + lat = fvcom.Grid.lat[:] + fvcom.Grid._ax = [lon.min(), lon.max(), + lat.min(), lat.max()] + filename = filename + ".p" + f = open(filename, "wb") + data = {} + data['Origin'] = fvcom._origin_file + data['History'] = fvcom.History + data['Grid'] = fvcom.Grid.__dict__ + data['Variables'] = fvcom.Variables.__dict__ + # delete exceptions + for key in exceptions: data['Variables'].pop(key, None) + for key in exceptions: data['Grid'].pop(key, None) + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in data['Variables']: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(data['Variables'][key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + data['Variables'][key] = data['Variables'][key][:] + #Unpickleable objects + data['Grid'].pop("triangle", None) + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in data['Grid']: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(data['Grid'][key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + data['Grid'][key] = data['Grid'][key][:] + #Save in pickle file + if debug: + print 'Dumping in pickle file...' + try: + pkl.dump(data, f, protocol=pkl.HIGHEST_PROTOCOL) + except SystemError: + try: + print "---Very large data, this may take a while---" + pkl.dump(data, f) + except SystemError: + raise PyseidonError("---Data too large for machine memory---\n"\ + "Tip: use ax or tx during class initialisation\n"\ + " to use partial data") + + f.close() diff --git a/build/lib/pyseidon_dvt/utilities/pyseidon_error.py b/build/lib/pyseidon_dvt/utilities/pyseidon_error.py new file mode 100644 index 0000000..b5d299c --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/pyseidon_error.py @@ -0,0 +1,9 @@ +# encoding: utf-8 + +class PyseidonError(Exception): + """ + Custom error for PySeidon library + """ + def __init__(self, arg): + # Call the base class constructor with the parameters it needs + super(PyseidonError, self).__init__(arg) diff --git a/build/lib/pyseidon_dvt/utilities/regioner.py b/build/lib/pyseidon_dvt/utilities/regioner.py new file mode 100644 index 0000000..f3b5d2f --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/regioner.py @@ -0,0 +1,178 @@ +from __future__ import division +import numpy as np +from bisect import bisect_left, bisect_right +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +#quick fix +#import netCDF4 as nc +import scipy.io.netcdf as nc + +def node_region(ax, lon, lat): + + region_n = np.argwhere((lon >= ax[0]) & + (lon <= ax[1]) & + (lat >= ax[2]) & + (lat <= ax[3])) + + region_n = region_n.ravel() + + return region_n + +def element_region(ax, lonc, latc): + + region_e = np.argwhere((lonc >= ax[0]) & + (lonc <= ax[1]) & + (latc >= ax[2]) & + (latc <= ax[3])) + + region_e = region_e.ravel() + + return region_e + + +def regioner(gridVar, ax, debug=False): + """ + Takes as input a region (given by a four elemenTakes as input a region + (given by a four element NumPy array), + and some standard data output by ncdatasort and loadnc2d_python + and returns only the data that lies within the region specified + in the region arrayt NumPy array), + and some standard data output by ncdatasort and loadnc2d_python + and returns only the data that lies within the region specified + in the region array + + Inputs: + - region = four element array containing the four corners of the + region box. Entires should be in the following form: + [long1, long2, lat1, lat2] with the following property: + abs(long1) < abs(long2), etc. + - data = standard python data dictionary for these files + - name = what should the region be called in the output file + - savedir = where should the resultant data be saved? Default is + none, i.e. the data will not be saved, only returned. + + **dim = {'2D', '3D'}** the dimension of the data to use regioner + on. Default is 2D. + """ + if debug: + print 'Reindexing...' + lon = gridVar.lon[:] + lat = gridVar.lat[:] + nbe = gridVar.triele[:] + nv = gridVar.trinodes[:] + a1u = gridVar.a1u[:] + a2u = gridVar.a2u[:] + aw0 = gridVar.aw0[:] + awx = gridVar.awx[:] + awy = gridVar.awy[:] + x = gridVar.x[:] + xc = gridVar.xc[:] + y = gridVar.y[:] + yc = gridVar.yc[:] + lonc = gridVar.lonc[:] + latc = gridVar.latc[:] + + l = nv.shape[0] + + idx = node_region(ax, lon, lat) + + #first, reindex elements in the region + element_index_tmp = np.zeros(l, int) + nv_rs = nv.reshape(l*3, order='F') + #find indices that sort nv_rs + nv_sortedind = nv_rs.argsort() + #sort the array + nv_sortd = nv_rs[nv_sortedind] + #pick out the values in the region + if debug: + print 'Extracting values from box...' + #TR comment: very slow...gonna need optimisation down the line + for i in xrange(len(idx)): + i1 = bisect_left(nv_sortd, idx[i]) + i2 = bisect_right(nv_sortd, idx[i]) + inds = nv_sortedind[i1:i2] + element_index_tmp[inds % l] = 1 + element_index = np.where(element_index_tmp == 1)[0] + element_index = element_index.astype(int) + + #TR needs to be inside the loop? + node_index = np.unique(nv[element_index,:]).astype(int) + #create new linkage arrays + nv_tmp = nv[element_index,:] + L = len(nv_tmp[:,0]) + nv_tmp2 = np.empty((1, L*3)) + + #make a new array of the node labellings for the tri's in the region + if debug: + print 'Re-labelling nodes...' + nv2 = nv_tmp.reshape(L * 3, order='F') + nv2_sortedind = nv2.argsort() + nv2_sortd = nv2[nv2_sortedind] + + for i in xrange(len(node_index)): + i1 = bisect_left(nv2_sortd, node_index[i]) + i2 = bisect_right(nv2_sortd, node_index[i]) + inds = nv2_sortedind[i1:i2] + nv_tmp2[0, inds] = i + + nv_new = np.reshape(nv_tmp2, (L, 3), 'F') + + #now do the same for nbe...sort of + nbe_index = np.unique(nbe[element_index, :]) + #ghost points + ghost=np.asarray(list(set(nbe_index) - set(element_index))) + + nbe_tmp = nbe[element_index,:] + lnbe = len(nbe_tmp[:,0]) + #nbe_tmp2 = np.empty((1, lnbe*3)) + #TR: np.empty sometimes generates freak values + nbe_tmp2 = np.ones((1, lnbe*3)) * l # ghost point default value + + if debug: + print 'Re-labelling elements...' + nbe2 = nbe_tmp.reshape(lnbe*3, order='F') + nbe_sortedind = nbe2.argsort() + nbe_sortd = nbe2[nbe_sortedind] + + #TR: iterator + I = 0 + for i in xrange(len(nbe_index)): + #TR: check if ghost point + if not nbe_index[i] in ghost: + i1 = bisect_left(nbe_sortd, nbe_index[i]) + i2 = bisect_right(nbe_sortd, nbe_index[i]) + inds = nbe_sortedind[i1:i2] + nbe_tmp2[0, inds] = I + I += 1 + + nbe_new = np.reshape(nbe_tmp2, (lnbe,3), 'F') + + #create new variables for the region + + data = {} + data['node_index'] = node_index + data['element_index'] = element_index + data['nbe'] = nbe_new.astype(int) + data['nv'] = nv_new.astype(int) + + data['a1u'] = a1u[:, element_index] + data['a2u'] = a2u[:, element_index] + data['aw0'] = aw0[:, element_index] + data['awx'] = awx[:, element_index] + data['awy'] = awy[:, element_index] + + data['x'] = x[node_index] + data['y'] = y[node_index] + data['xc'] = xc[element_index] + data['yc'] = yc[element_index] + + data['lon'] = lon[node_index] + data['lat'] = lat[node_index] + data['lonc'] = lonc[element_index] + data['latc'] = latc[element_index] + + data['triangle'] = Tri.Triangulation(data['lon'], data['lat'], \ + data['nv']) + + return data + diff --git a/build/lib/pyseidon_dvt/utilities/save_FlowFile_BPFormat.py b/build/lib/pyseidon_dvt/utilities/save_FlowFile_BPFormat.py new file mode 100644 index 0000000..8ccd3c0 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/save_FlowFile_BPFormat.py @@ -0,0 +1,388 @@ +from __future__ import division +import numpy as np +#from rawADCPclass import rawADCP +from datetime import datetime +from datetime import timedelta +import scipy.io as sio +import scipy.interpolate as sip +import matplotlib.pyplot as plt +import seaborn + +def date2py(matlab_datenum): + """ + Converts matlab's datenum time to datetime time + """ + python_datetime = datetime.fromordinal(int(matlab_datenum)) + \ + timedelta(days=matlab_datenum%1) - timedelta(days = 366) + + return python_datetime + + +def py2date(dt): + """ + Converts datetime time to matlab's datenum time + """ + mdn = dt + timedelta(days = 366) + frac_seconds = (dt-datetime(dt.year,dt.month,dt.day,0,0,0)).seconds / (24.0 * 60.0 * 60.0) + frac_microseconds = dt.microsecond / (24.0 * 60.0 * 60.0 * 1000000.0) + return mdn.toordinal() + frac_seconds + frac_microseconds + +def calc_ensemble(x, ens, ens_dim, debug=False, debug_plot=False): + if debug: "calc_ensemble..." + #initialize input + ens = int(ens) + #x = x[:, None] + + if ens_dim == 1: + ens_size = np.floor(x.shape[0]/60) + else: + pass + + #x_ens = np.empty((ens_size, 1, ens)) + x_ens = np.empty((ens_size, ens)) + x_ens[:] = np.nan + + for j in xrange(ens): + if ens_dim == 1: + ind_ens = np.arange(j, x.shape[0] - (ens - j), ens) + #x_ens[..., j] = x[ind_ens] + x_ens[..., j] = x[ind_ens] + + else: + pass + + #x_ens = np.nanmean(x_ens, axis=2) + x_ens = np.nanmean(x_ens, axis=1) + + if debug: "...calc_ensemble done." + + return x_ens + + +def rotate_coords(x, y, theta, debug=False, debug_plot=False ): + """ + Similar to "rotate_to_channelcoords.m" code, + theta is now the angle + between the old axis and the new x-axis (CCw is positive) + """ + if debug: "rotate_coords..." + xnew = x * np.cos(theta) + y * np.sin(theta) + ynew = -x * np.sin(theta) + y * np.cos(theta) + + if debug: "...rotate_coords done." + + return xnew, ynew + +def rotate_to_true(X, Y, theta=-19.0): + """ + X,Y are the X and Y coordinates (could be speeds) relative to magnetic + north -- inputs can be vectors + x,y are the coordinates relative to true north + This function assumes the measured location is Nova Scotia where the + declination angle is -19 degrees. + + + Inputs: + - X = longitudes in deg., array or list + - Y = latitudes in deg., array or list + + Outputs: + - X = true-north longitudes in deg., array or list + - Y = true-north latitudes in deg., array or list + + Options: + - theta = declination angle in deg., float + """ + + print 'Rotating velocities to be relative to true north (declination = {0})'.format(theta) + + Theta = theta * np.pi / 180 + + x = X * np.cos(Theta) + Y * np.sin(Theta) + y = -X * np.sin(Theta) + Y * np.cos(Theta) + + return x, y + + +def get_DirFromN(u,v): + """ + This function computes the direction from North with the output in degrees + and measured clockwise from north. + + Inputs: + - u = eastward component + - v = northward component + """ + + theta = np.arctan2(u,v) * 180 / np.pi + + ind = np.where(theta<0) + theta[ind] = theta[ind] + 360 + return theta + +def sign_speed(u_all, v_all, s_all, dir_all, flood_heading): + """ + Computes the signed speed + Inputs: + - u_all = u velocity component time series + - v_all = v velocity component time series + - s_all = ??? + - dir_all = direction time series + - flood_heading = direction of flood + Outputs: + - s_signed_all = signed speed + - PA_all = principal axis + """ + + if type(flood_heading)==int: + flood_heading += np.array([-90, 90]) + + s_signed_all = np.empty(s_all.shape) + s_signed_all.fill(np.nan) + + PA_all = np.zeros(s_all.shape[-1]) + for i in xrange(s_all.shape[-1]): + u = u_all[:, i] + v = v_all[:, i] + dir = dir_all[:, i] + s = s_all[:, i] + + #determine principal axes - potentially a problem if axes are very kinked + # since this would misclassify part of ebb and flood + PA, _ = principal_axis(u, v) + PA_all[i] = PA + + # sign speed - eliminating wrap-around + dir_PA = dir - PA + + dir_PA[dir_PA < -90] += 360 + dir_PA[dir_PA > 270] -= 360 + + #general direction of flood passed as input argument + if flood_heading[0] <= PA <= flood_heading[1]: + ind_fld = np.where((dir_PA >= -90) & (dir_PA<90)) + s_signed = -s + s_signed[ind_fld] = s[ind_fld] + else: + ind_ebb = np.where((dir_PA >= -90) & (dir_PA<90)) + s_signed = s + s_signed[ind_ebb] = -s[ind_ebb] + + s_signed_all[:, i] = s_signed + + return s_signed_all, PA_all + +def principal_axis(u, v): + """ + Computes the principal axis angle and its variance. + Inputs: + - u = u velocity component time series + - v = v velocity component time series + Outputs: + - PA = principal axis angle in deg., float + - varxp_PA = principal axis variance + """ + + #create velocity matrix + U = np.vstack((u,v)).T + #eliminate NaN values + U = U[~np.isnan(U[:, 0]), :] + #convert matrix to deviate form + rep = np.tile(np.mean(U, axis=0), [len(U), 1]) + U -= rep + #compute covariance matrix + R = np.dot(U.T, U) / (len(U) - 1) + + #calculate eigenvalues and eigenvectors for covariance matrix + lamb, V = np.linalg.eig(R) + #sort eignvalues in descending order so that major axis is given by first eigenvector + # sort in descending order with indices + ilamb = sorted(range(len(lamb)), key=lambda k: lamb[k], reverse=True) + lamb = sorted(lamb, reverse=True) + # reconstruct the eigenvalue matrix + lamb = np.diag(lamb) + #reorder the eigenvectors + V = V[:, ilamb] + + #rotation angle of major axis in radians relative to cartesian coordiantes + ra = np.arctan2(V[0,1], V[1,1]) + #express principal axis in compass coordinates + # WES_COMMENT: may need to change this, cause in original is -ra + PA = ra * 180 / np.pi + 90 + #variance captured by principal + varxp_PA = np.diag(lamb[0]) / np.trace(lamb) + + return PA, varxp_PA + + +class Struct: + def __init__(self, **entries): + self.__dict__.update(entries) + + +def save_FlowFile_BPFormat(fileinfo, adcp, rbr, params, options, debug=False): + """ + Processes and formats raw ADCP data into the BP's format defined by Dalhousie university + """ + + comments = ['data is in Polagye Tools format', + 'data.east_vel and data.north_vel are relative to true north', + 'The parameters were set by ' + fileinfo['paramfile']] + + day1 = date2py(adcp['mtime'][0][0]) + print day1 + #date_time = [date2py(tval[0]) for tval in adcp.mtime[:]] + datenum = datetime(day1.year,1,1) + timedelta(365) + datenum = datenum.toordinal() + + yd = adcp['mtime'][:].ravel() - datenum + tind = np.where((yd > params['tmin']) & (yd < params['tmax']))[0] + + pres = {} + time = {} + time['mtime'] = adcp['mtime'][:].ravel()[tind] + dt = np.nanmean(np.diff(time['mtime'])) + + if not rbr: + print 'Depths measured by ADCP not yet coded.' + comments.append('Depths as measured by ADCP') + else: + print 'Ensemble averaging rbr data' + comments.append('Depths as measured by RBR sensor') + + nens = round(dt/(rbr.mtime[1] - rbr.mtime[0])) + temp = np.arange(rbr.mtime[nens/2-1], rbr.mtime[-1-nens/2], dt) + #temp2 = np.r_[rbr.mtime[nens/2-1]: rbr.mtime[-1-nens/2]: dt] + + mtimeens = np.arange(rbr.mtime[nens/2-1], rbr.mtime[-1-nens/2], dt) + mtimeens = mtimeens + params['rbr_hr_offset'] / 24 + depthens = calc_ensemble(rbr.depth, nens, 1) + + temp = sip.interp1d(mtimeens, depthens, kind='linear') + + pres['surf']= temp(time['mtime']) + params['dabPS'] + + if debug: + # Load in matlab values for testing + filename = './140703-EcoEII_database/scripts_examples/mtime.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matTimes = mat['mtimeens'] + filename = './140703-EcoEII_database/scripts_examples/dt.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matdt = mat['dt'] + + + filename = './140703-EcoEII_database/scripts_examples/depthens.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matdepthens = mat['depthens'] + + filename = './140703-EcoEII_database/scripts_examples/time.mat' + mat = sio.loadmat(filename, struct_as_record=False, squeeze_me=True) + matmtime = mat['mtime'] + + print matTimes.shape + print temp - matTimes + print temp2 - matTimes + print dt - matdt + print depthens - matdepthens + print 'time' + print time['mtime'] - matmtime + + ## zlevels + data = {} + z = adcp['config']['ranges'][:] + params['dabADCP'] + z = z.ravel() + zind = np.where((z > params['zmin']) & (z < params['zmax']))[0] + data['bins'] = z[zind] + + ## Currents + data['vert_vel'] = adcp['vert_vel'][:][tind][:, zind] + data['error_vel'] = adcp['error_vel'][:][tind][:, zind] + + # If compass wasn't calibrated + if 'hdgmod' in params: + adcp['east_vel'][:], adcp['north_vel'][:] = rotate_coords(adcp['east_vel'][:], + adcp['north_vel'][:], + params['hdgmod']) + + comments.append('East and north velocity rotated by params.hdgmod') + + # Rotate east_vel and north_vel to be relative to true north + data['east_vel'], data['north_vel'] = \ + rotate_to_true(adcp['east_vel'][:][tind][:, zind], + adcp['north_vel'][:][tind][:, zind], + params['declination']) + + # Direction + data['dir_vel'] = get_DirFromN(data['east_vel'],data['north_vel']) + + # Signed Speed + spd_all = np.sqrt(data['east_vel']**2+data['north_vel']**2) + + # Determine flood and ebb based on principal direction (Polagye Routine) + print 'Getting signed speed (Principal Direction Method) -- used all speeds' + s_signed_all, PA_all = sign_speed(data['east_vel'], data['north_vel'], + spd_all, data['dir_vel'], params['flooddir']) + + data['mag_signed_vel'] = s_signed_all + + if options['showRBRavg'] or debug: + print 'Plotting RBR vs average' + plt.plot(rbr.mtime + params['rbr_hr_offset'] / 24, rbr.depth+params['dabPS'], + label='RBR') + plt.plot(time['mtime'], pres['surf'], 'r', label='AVG') + plt.xlabel('Time') + plt.ylabel('Elevation') + plt.legend(bbox_to_anchor=(0, 0, 1, 1), bbox_transform=plt.gcf().transFigure) + + plt.show() + + if options['showPA'] or debug: + print 'Plotting PA vs mean' + plt.plot(PA_all, data['bins'], label='PA') + plt.plot(np.array([PA_all[0], PA_all[-1]]), + np.array([np.mean(pres['surf']), np.mean(pres['surf'])]), + label='mean') + + plt.xlabel('Principal Axis Direction\n(clockwise from north)') + plt.ylabel('z (m)') + plt.legend(bbox_to_anchor=(0, 0, 1, 1), bbox_transform=plt.gcf().transFigure) + plt.show() + + ## save + lon = params['lon'] + lat = params['lat'] + + outfile = fileinfo['outdir'] + fileinfo['flowfile'] + print 'Saving data to {0}'.format(outfile) + + saveDict = {'data':data, 'pres':pres, 'time':time, 'lon':lon, 'lat':lat, + 'params':params, 'comments':comments} + #save(outfile,'data','pres','time','lon','lat','params','Comments') + + ## Save metadata + #metadata.progname=[mfilename('fullpath')]; + #metadata.date = datestr(now); + #metadata.paramfile = fileinfo.paramfile; + #save(outfile,'metadata','-append') + return saveDict + + + +if __name__ == '__main__': + filename = '140703-EcoEII_database/data/GP-120726-BPd_raw.mat' + data = rawADCP(filename) + rawdata = rawADCP(filename) + #adcp = Struct(**data.adcp) + #rawADCP = data.adcp + adcp = data.adcp + #params = Struct(**data.saveparams) + params = data.saveparams + rbr = Struct(**data.rbr) + +# save_FlowFile_BPFormat(data.fileinfo, data.adcp, data.rbr, +# data.saveparams, data.options) + + saveDict = \ + save_FlowFile_BPFormat(data.fileinfo, adcp, rbr, + params, data.options) diff --git a/build/lib/pyseidon_dvt/utilities/shortest_element_path.py b/build/lib/pyseidon_dvt/utilities/shortest_element_path.py new file mode 100644 index 0000000..098cb30 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/shortest_element_path.py @@ -0,0 +1,200 @@ +#from __future__ import division +#import netCDF4 as nc +import numpy as np +import scipy.spatial +import networkx as nx +import matplotlib.pyplot as plt +import matplotlib.tri as Tri +import matplotlib.ticker as ticker +import seaborn + +class shortest_element_path: + """ + Class that mostly computes the shortest path from A to B + by hopping from an element to the next + """ + def __init__(self, lonc, latc, lon, lat, trinodes, h, debug=False): + + #self.data = nc.Dataset(filename,'r') + + #latc = self.data.variables['latc'][:] + #lonc = self.data.variables['lonc'][:] + self.lonc = lonc[:] + self.latc = latc[:] + self.lat = lat[:] + self.lon = lon[:] + self.trinodes = trinodes[:] + self.h = h[:] + + #z = np.vstack((latc,lonc)).T + z = np.vstack((lonc,latc)).T + #z = np.vstack((xc, yc)).T + + self.points = map(tuple,z) + + if debug : print 'File Loaded' + + # make a Delaunay triangulation of the point data + self.delTri = scipy.spatial.Delaunay(self.points) + if debug : print 'Delaunay Triangulation Done' + + # create a set for edges that are indexes of the points + self.edges = [] + self.weight = [] + # for each Delaunay triangle + for n in xrange(self.delTri.nsimplex): + # for each edge of the triangle + # sort the vertices + # (sorting avoids duplicated edges being added to the set) + # and add to the edges set + + self.edge = sorted([self.delTri.vertices[n,0], self.delTri.vertices[n,1]]) + a = self.points[self.edge[0]] + b = self.points[self.edge[1]] + self.weight = (np.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2)) + self.edges.append((self.edge[0], self.edge[1],{'weight':self.weight})) + + self.edge = sorted([self.delTri.vertices[n,0], self.delTri.vertices[n,2]]) + a = self.points[self.edge[0]] + b = self.points[self.edge[1]] + self.weight = (np.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2)) + self.edges.append((self.edge[0], self.edge[1],{'weight':self.weight})) + + + self.edge = sorted([self.delTri.vertices[n,1], self.delTri.vertices[n,2]]) + a = self.points[self.edge[0]] + b = self.points[self.edge[1]] + self.weight = (np.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2)) + self.edges.append((self.edge[0], self.edge[1],{'weight':self.weight})) + + if debug : print 'Edges and Weighting Done' + + # make a graph based on the Delaunay triangulation edges + self.graph = nx.Graph(self.edges) + #print(graph.edges()) + + if debug : print 'Graph Constructed' + + self.pointIDXY = dict(zip(range(len(self.points)), self.points)) + + def getTargets(self, source_target, coords=False): + + self.elements = [] + self.coordinates = [] + self.maxcoordinates = [] + self.mincoordinates = [] + for i in source_target: + source = i[0] + target = i[1] + +# print '\n' +# print 'Source' +# print source + s = source +# +# print 'Target' +# print target + t = target + + if coords: + for key, value in self.pointIDXY.items(): + if value==source: + print 'Source' + print key + s = key + + if value==target: + print 'Target' + print key + t = key + + #print s,t + shortest = nx.shortest_path(self.graph,source=s,target=t,weight='weight') +# dist = nx.shortest_path_length(self.graph,source=s,target=t,weight='weight') + +# print 'Shortest Path (by elements)' +# print shortest + + self.elements.append(shortest) + + coords = [self.pointIDXY[i] for i in shortest] + self.coordinates.append(coords) + self.maxcoordinates.append(np.max(np.array(coords),axis=0)) + self.mincoordinates.append(np.min(np.array(coords),axis=0)) + +# print 'Shortest Distance (by coordinates)' +# print dist + + return self.elements, self.coordinates + + def graphGrid(self,narrowGrid=False, plot=False): + #nx.draw(self.graph, self.pointIDXY) + #plt.show() + + #lat = self.data.variables['lat'][:] + #lon = self.data.variables['lon'][:] + #nv = self.data.variables['nv'][:].T -1 + #h = self.data.variables['h'][:] + #lat = self.self.lat + #lon = self.lon + #trinodes = self.trinodes[:] + #h = self.h + + tri = Tri.Triangulation(self.lon, self.lat, triangles=self.trinodes) + # xy or latlon based on how you are #Grand Passage + + #levels=np.arange(-38,6,1) # depth contours to plot + + fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + ax = fig.add_subplot(111,aspect=(1.0/np.cos(np.mean(self.lat)*np.pi/180.0))) + #plt.tricontourf(tri,-self.h,shading='faceted',cmap=plt.cm.gist_earth) + plt.triplot(tri, color='white', linewidth=0.5) + plt.ylabel('Latitude') + plt.xlabel('Longitude') + plt.gca().patch.set_facecolor('0.5') + #cbar=plt.colorbar() + #cbar.set_label('Water Depth (m)', rotation=-90,labelpad=30) + + scale = 1 + ticks = ticker.FuncFormatter(lambda lon, pos: '{0:g}'.format(lon/scale)) + ax.xaxis.set_major_formatter(ticks) + ax.yaxis.set_major_formatter(ticks) + plt.grid() + + maxlat, maxlon = np.max(self.maxcoordinates,axis=0) + minlat, minlon = np.min(self.mincoordinates,axis=0) + if narrowGrid: + ax.set_xlim(minlon,maxlon) + ax.set_ylim(minlat,maxlat) + + + zz = len(self.elements) + for i,v in enumerate(self.elements): + source = self.pointIDXY[v[0]] + target = self.pointIDXY[v[-1]] + lab = '({:.6},{:.6})-({:.6},{:.6})'.format(source[0], source[1], + target[0], target[1]) + + plt.scatter(self.lonc[v], self.latc[v], + s=80, label=lab, c=plt.cm.Set1(i/zz)) + + #plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=2, ncol=3,fontsize='14', borderaxespad=0.) + plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=2, ncol=3) + #plt.legend() + if plot: + plt.ylabel('Latitude') + plt.xlabel('Longitude') + plt.show() + + +if __name__ == '__main__': + + filename = '/home/wesley/ncfiles/smallcape_force_0001.nc' + + test = shortest_element_path(filename) + + test.getTargets([[41420,39763],[48484,53441],[27241,24226],[21706,17458],[14587,5416]]) + test.graphGrid(narrowGrid=True) + + element_path, coordinates_path = test.getTargets([[41420,39763]]) diff --git a/build/lib/pyseidon_dvt/utilities/windrose.py b/build/lib/pyseidon_dvt/utilities/windrose.py new file mode 100644 index 0000000..13af860 --- /dev/null +++ b/build/lib/pyseidon_dvt/utilities/windrose.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__version__ = '1.4' +__author__ = 'Lionel Roubeyrie' +__mail__ = 'lionel.roubeyrie@gmail.com' +__license__ = 'CeCILL-B' + +import matplotlib +import matplotlib.cm as cm +import numpy as np +from matplotlib.patches import Rectangle, Polygon +from matplotlib.ticker import ScalarFormatter, AutoLocator +from matplotlib.text import Text, FontProperties +from matplotlib.projections.polar import PolarAxes +from numpy.lib.twodim_base import histogram2d +import matplotlib.pyplot as plt +from pylab import poly_between + +RESOLUTION = 100 +ZBASE = -1000 #The starting zorder for all drawing, negative to have the grid on + +class WindroseAxes(PolarAxes): + """ + + Create a windrose axes + + """ + + def __init__(self, *args, **kwargs): + """ + See Axes base class for args and kwargs documentation + """ + + #Uncomment to have the possibility to change the resolution directly + #when the instance is created + #self.RESOLUTION = kwargs.pop('resolution', 100) + PolarAxes.__init__(self, *args, **kwargs) + self.set_aspect('equal', adjustable='box', anchor='C') + self.radii_angle = 67.5 + self.cla() + + + def cla(self): + """ + Clear the current axes + """ + PolarAxes.cla(self) + + self.theta_angles = np.arange(0, 360, 45) + self.theta_labels = ['E', 'N-E', 'N', 'N-W', 'W', 'S-W', 'S', 'S-E'] + self.set_thetagrids(angles=self.theta_angles, labels=self.theta_labels) + + self._info = {'dir' : list(), + 'bins' : list(), + 'table' : list()} + + self.patches_list = list() + + + def _colors(self, cmap, n): + ''' + Returns a list of n colors based on the colormap cmap + + ''' + return [cmap(i) for i in np.linspace(0.0, 1.0, n)] + + + def set_radii_angle(self, **kwargs): + """ + Set the radii labels angle + """ + + null = kwargs.pop('labels', None) + angle = kwargs.pop('angle', None) + if angle is None: + angle = self.radii_angle + self.radii_angle = angle + radii = np.linspace(0.1, self.get_rmax(), 6) + radii_labels = [ "%.1f" %r for r in radii ] + radii_labels[0] = "" #Removing label 0 + null = self.set_rgrids(radii=radii, labels=radii_labels, + angle=self.radii_angle, **kwargs) + + + def _update(self): + self.set_rmax(rmax=np.max(np.sum(self._info['table'], axis=0))) + self.set_radii_angle(angle=self.radii_angle) + + + def legend(self, loc='lower left', **kwargs): + """ + Sets the legend location and her properties. + The location codes are + + 'best' : 0, + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10, + + If none of these are suitable, loc can be a 2-tuple giving x,y + in axes coords, ie, + + loc = (0, 1) is left top + loc = (0.5, 0.5) is center, center + + and so on. The following kwargs are supported: + + isaxes=True # whether this is an axes legend + prop = FontProperties(size='smaller') # the font property + pad = 0.2 # the fractional whitespace inside the legend border + shadow # if True, draw a shadow behind legend + labelsep = 0.005 # the vertical space between the legend entries + handlelen = 0.05 # the length of the legend lines + handletextsep = 0.02 # the space between the legend line and legend text + axespad = 0.02 # the border between the axes and legend edge + """ + + def get_handles(): + handles = list() + for p in self.patches_list: + if isinstance(p, matplotlib.patches.Polygon) or \ + isinstance(p, matplotlib.patches.Rectangle): + color = p.get_facecolor() + elif isinstance(p, matplotlib.lines.Line2D): + color = p.get_color() + else: + raise AttributeError("Can't handle patches") + handles.append(Rectangle((0, 0), 0.2, 0.2, + facecolor=color, edgecolor='black')) + return handles + + def get_labels(): + labels = np.copy(self._info['bins']) + labels = ["[%.1f : %0.1f[" %(labels[i], labels[i+1]) \ + for i in range(len(labels)-1)] + return labels + + null = kwargs.pop('labels', None) + null = kwargs.pop('handles', None) + handles = get_handles() + labels = get_labels() + self.legend_ = matplotlib.legend.Legend(self, handles, labels, + loc, **kwargs) + return self.legend_ + + + def _init_plot(self, dir, var, **kwargs): + """ + Internal method used by all plotting commands + """ + #self.cla() + null = kwargs.pop('zorder', None) + + #Init of the bins array if not set + bins = kwargs.pop('bins', None) + if bins is None: + bins = np.linspace(np.min(var), np.max(var), 6) + if isinstance(bins, int): + bins = np.linspace(np.min(var), np.max(var), bins) + bins = np.asarray(bins) + nbins = len(bins) + + #Number of sectors + nsector = kwargs.pop('nsector', None) + if nsector is None: + nsector = 16 + + #Sets the colors table based on the colormap or the "colors" argument + colors = kwargs.pop('colors', None) + cmap = kwargs.pop('cmap', None) + if colors is not None: + if isinstance(colors, str): + colors = [colors]*nbins + if isinstance(colors, (tuple, list)): + if len(colors) != nbins: + raise ValueError("colors and bins must have same length") + else: + if cmap is None: + cmap = cm.jet + colors = self._colors(cmap, nbins) + + #Building the angles list + angles = np.arange(0, -2*np.pi, -2*np.pi/nsector) + np.pi/2 + + normed = kwargs.pop('normed', False) + blowto = kwargs.pop('blowto', False) + + #Set the global information dictionnary + self._info['dir'], self._info['bins'], self._info['table'] = histogram(dir, var, bins, nsector, normed, blowto) + + return bins, nbins, nsector, colors, angles, kwargs + + + def contour(self, dir, var, **kwargs): + """ + Plot a windrose in linear mode. For each var bins, a line will be + draw on the axes, a segment between each sector (center to center). + Each line can be formated (color, width, ...) like with standard plot + pylab command. + + Mandatory: + * dir : 1D array - directions the wind blows from, North centred + * var : 1D array - values of the variable to compute. Typically the wind + speeds + Optional: + * nsector: integer - number of sectors used to compute the windrose + table. If not set, nsectors=16, then each sector will be 360/16=22.5°, + and the resulting computed table will be aligned with the cardinals + points. + * bins : 1D array or integer- number of bins, or a sequence of + bins variable. If not set, bins=6, then + bins=linspace(min(var), max(var), 6) + * blowto : bool. If True, the windrose will be pi rotated, + to show where the wind blow to (usefull for pollutant rose). + * colors : string or tuple - one string color ('k' or 'black'), in this + case all bins will be plotted in this color; a tuple of matplotlib + color args (string, float, rgb, etc), different levels will be plotted + in different colors in the order specified. + * cmap : a cm Colormap instance from matplotlib.cm. + - if cmap == None and colors == None, a default Colormap is used. + + others kwargs : see help(pylab.plot) + + """ + + bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var, + **kwargs) + + #closing lines + angles = np.hstack((angles, angles[-1]-2*np.pi/nsector)) + vals = np.hstack((self._info['table'], + np.reshape(self._info['table'][:,0], + (self._info['table'].shape[0], 1)))) + + offset = 0 + for i in range(nbins): + val = vals[i,:] + offset + offset += vals[i, :] + zorder = ZBASE + nbins - i + patch = self.plot(angles, val, color=colors[i], zorder=zorder, + **kwargs) + self.patches_list.extend(patch) + self._update() + + + def contourf(self, dir, var, **kwargs): + """ + Plot a windrose in filled mode. For each var bins, a line will be + draw on the axes, a segment between each sector (center to center). + Each line can be formated (color, width, ...) like with standard plot + pylab command. + + Mandatory: + * dir : 1D array - directions the wind blows from, North centred + * var : 1D array - values of the variable to compute. Typically the wind + speeds + Optional: + * nsector: integer - number of sectors used to compute the windrose + table. If not set, nsectors=16, then each sector will be 360/16=22.5°, + and the resulting computed table will be aligned with the cardinals + points. + * bins : 1D array or integer- number of bins, or a sequence of + bins variable. If not set, bins=6, then + bins=linspace(min(var), max(var), 6) + * blowto : bool. If True, the windrose will be pi rotated, + to show where the wind blow to (usefull for pollutant rose). + * colors : string or tuple - one string color ('k' or 'black'), in this + case all bins will be plotted in this color; a tuple of matplotlib + color args (string, float, rgb, etc), different levels will be plotted + in different colors in the order specified. + * cmap : a cm Colormap instance from matplotlib.cm. + - if cmap == None and colors == None, a default Colormap is used. + + others kwargs : see help(pylab.plot) + + """ + + bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var, + **kwargs) + null = kwargs.pop('facecolor', None) + null = kwargs.pop('edgecolor', None) + + #closing lines + angles = np.hstack((angles, angles[-1]-2*np.pi/nsector)) + vals = np.hstack((self._info['table'], + np.reshape(self._info['table'][:,0], + (self._info['table'].shape[0], 1)))) + offset = 0 + for i in range(nbins): + val = vals[i,:] + offset + offset += vals[i, :] + zorder = ZBASE + nbins - i + xs, ys = poly_between(angles, 0, val) + patch = self.fill(xs, ys, facecolor=colors[i], + edgecolor=colors[i], zorder=zorder, **kwargs) + self.patches_list.extend(patch) + + + def bar(self, dir, var, **kwargs): + """ + Plot a windrose in bar mode. For each var bins and for each sector, + a colored bar will be draw on the axes. + + Mandatory: + * dir : 1D array - directions the wind blows from, North centred + * var : 1D array - values of the variable to compute. Typically the wind + speeds + Optional: + * nsector: integer - number of sectors used to compute the windrose + table. If not set, nsectors=16, then each sector will be 360/16=22.5°, + and the resulting computed table will be aligned with the cardinals + points. + * bins : 1D array or integer- number of bins, or a sequence of + bins variable. If not set, bins=6 between min(var) and max(var). + * blowto : bool. If True, the windrose will be pi rotated, + to show where the wind blow to (usefull for pollutant rose). + * colors : string or tuple - one string color ('k' or 'black'), in this + case all bins will be plotted in this color; a tuple of matplotlib + color args (string, float, rgb, etc), different levels will be plotted + in different colors in the order specified. + * cmap : a cm Colormap instance from matplotlib.cm. + - if cmap == None and colors == None, a default Colormap is used. + edgecolor : string - The string color each edge bar will be plotted. + Default : no edgecolor + * opening : float - between 0.0 and 1.0, to control the space between + each sector (1.0 for no space) + + """ + + bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var, + **kwargs) + null = kwargs.pop('facecolor', None) + edgecolor = kwargs.pop('edgecolor', None) + if edgecolor is not None: + if not isinstance(edgecolor, str): + raise ValueError('edgecolor must be a string color') + opening = kwargs.pop('opening', None) + if opening is None: + opening = 0.8 + dtheta = 2*np.pi/nsector + opening = dtheta*opening + + for j in range(nsector): + offset = 0 + for i in range(nbins): + if i > 0: + offset += self._info['table'][i-1, j] + val = self._info['table'][i, j] + zorder = ZBASE + nbins - i + patch = Rectangle((angles[j]-opening/2, offset), opening, val, + facecolor=colors[i], edgecolor=edgecolor, zorder=zorder, + **kwargs) + self.add_patch(patch) + if j == 0: + self.patches_list.append(patch) + self._update() + + + def box(self, dir, var, **kwargs): + """ + Plot a windrose in proportional bar mode. For each var bins and for each + sector, a colored bar will be draw on the axes. + + Mandatory: + * dir : 1D array - directions the wind blows from, North centred + * var : 1D array - values of the variable to compute. Typically the wind + speeds + Optional: + * nsector: integer - number of sectors used to compute the windrose + table. If not set, nsectors=16, then each sector will be 360/16=22.5°, + and the resulting computed table will be aligned with the cardinals + points. + * bins : 1D array or integer- number of bins, or a sequence of + bins variable. If not set, bins=6 between min(var) and max(var). + * blowto : bool. If True, the windrose will be pi rotated, + to show where the wind blow to (usefull for pollutant rose). + * colors : string or tuple - one string color ('k' or 'black'), in this + case all bins will be plotted in this color; a tuple of matplotlib + color args (string, float, rgb, etc), different levels will be plotted + in different colors in the order specified. + * cmap : a cm Colormap instance from matplotlib.cm. + - if cmap == None and colors == None, a default Colormap is used. + edgecolor : string - The string color each edge bar will be plotted. + Default : no edgecolor + + """ + + bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var, + **kwargs) + null = kwargs.pop('facecolor', None) + edgecolor = kwargs.pop('edgecolor', None) + if edgecolor is not None: + if not isinstance(edgecolor, str): + raise ValueError('edgecolor must be a string color') + opening = np.linspace(0.0, np.pi/16, nbins) + + for j in range(nsector): + offset = 0 + for i in range(nbins): + if i > 0: + offset += self._info['table'][i-1, j] + val = self._info['table'][i, j] + zorder = ZBASE + nbins - i + patch = Rectangle((angles[j]-opening[i]/2, offset), opening[i], + val, facecolor=colors[i], edgecolor=edgecolor, + zorder=zorder, **kwargs) + self.add_patch(patch) + if j == 0: + self.patches_list.append(patch) + self._update() + +def histogram(dir, var, bins, nsector, normed=False, blowto=False): + """ + Returns an array where, for each sector of wind + (centred on the north), we have the number of time the wind comes with a + particular var (speed, polluant concentration, ...). + * dir : 1D array - directions the wind blows from, North centred + * var : 1D array - values of the variable to compute. Typically the wind + speeds + * bins : list - list of var category against we're going to compute the table + * nsector : integer - number of sectors + * normed : boolean - The resulting table is normed in percent or not. + * blowto : boolean - Normaly a windrose is computed with directions + as wind blows from. If true, the table will be reversed (usefull for + pollutantrose) + + """ + + if len(var) != len(dir): + raise ValueError, "var and dir must have same length" + + angle = 360./nsector + + dir_bins = np.arange(-angle/2 ,360.+angle, angle, dtype=np.float) + dir_edges = dir_bins.tolist() + dir_edges.pop(-1) + dir_edges[0] = dir_edges.pop(-1) + dir_bins[0] = 0. + + var_bins = bins.tolist() + var_bins.append(np.inf) + + if blowto: + dir = dir + 180. + dir[dir>=360.] = dir[dir>=360.] - 360 + + table = histogram2d(x=var, y=dir, bins=[var_bins, dir_bins], + normed=False)[0] + # add the last value to the first to have the table of North winds + table[:,0] = table[:,0] + table[:,-1] + # and remove the last col + table = table[:, :-1] + if normed: + table = table*100/table.sum() + + return dir_edges, var_bins, table + + +def wrcontour(dir, var, **kwargs): + fig = plt.figure() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(fig, rect) + fig.add_axes(ax) + ax.contour(dir, var, **kwargs) + l = ax.legend(axespad=-0.10) + plt.setp(l.get_texts(), fontsize=8) + plt.draw() + plt.show() + return ax + +def wrcontourf(dir, var, **kwargs): + fig = plt.figure() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(fig, rect) + fig.add_axes(ax) + ax.contourf(dir, var, **kwargs) + l = ax.legend(axespad=-0.10) + plt.setp(l.get_texts(), fontsize=8) + plt.draw() + plt.show() + return ax + +def wrbox(dir, var, **kwargs): + fig = plt.figure() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(fig, rect) + fig.add_axes(ax) + ax.box(dir, var, **kwargs) + l = ax.legend(axespad=-0.10) + plt.setp(l.get_texts(), fontsize=8) + plt.draw() + plt.show() + return ax + +def wrbar(dir, var, **kwargs): + fig = plt.figure() + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(fig, rect) + fig.add_axes(ax) + ax.bar(dir, var, **kwargs) + l = ax.legend(axespad=-0.10) + plt.setp(l.get_texts(), fontsize=8) + plt.draw() + plt.show() + return ax + +def clean(dir, var): + """ + Remove masked values in the two arrays, where if a direction data is masked, + the var data will also be removed in the cleaning process (and vice-versa) + """ + dirmask = dir.mask==False + varmask = var.mask==False + ind = dirmask*varmask + return dir[ind], var[ind] + +if __name__=='__main__': + from pylab import figure, show, setp, random, grid, draw + vv=random(500)*6 + dv=random(500)*360 + fig = figure(figsize=(8, 8), dpi=80, facecolor='w', edgecolor='w') + rect = [0.1, 0.1, 0.8, 0.8] + ax = WindroseAxes(fig, rect, axisbg='w') + fig.add_axes(ax) + +# ax.contourf(dv, vv, bins=np.arange(0,8,1), cmap=cm.hot) +# ax.contour(dv, vv, bins=np.arange(0,8,1), colors='k') +# ax.bar(dv, vv, normed=True, opening=0.8, edgecolor='white') + ax.box(dv, vv, normed=True) + l = ax.legend(axespad=-0.10) + setp(l.get_texts(), fontsize=8) + draw() + #print ax._info + show() + + + + + + diff --git a/build/lib/pyseidon_dvt/validationClass/__init__.py b/build/lib/pyseidon_dvt/validationClass/__init__.py new file mode 100644 index 0000000..7f20917 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +#Local import +from validationClass import Validation + +__authors__ = ['Jonathan Smith, Thomas Roc, Wesley Bowman'] +__licence__ = 'GNU Affero GPL v3.0' +__copyright__ = 'Copyright (c) 2014 EcoEnergyII' + diff --git a/build/lib/pyseidon_dvt/validationClass/compareData.py b/build/lib/pyseidon_dvt/validationClass/compareData.py new file mode 100644 index 0000000..61a817d --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/compareData.py @@ -0,0 +1,305 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +from __future__ import division + +import numpy as np +from os import mkdir +from os.path import exists +from tidalStats import TidalStats +from smooth import smooth +from depthInterp import depthFromSurf, depthFromBott +from datetime import datetime, timedelta + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +# Local import +from plotsValidation import * + +def dn2dt(datenum): + ''' + Convert matlab datenum to python datetime. + ''' + return datetime.fromordinal(int(datenum)) + timedelta(days=datenum%1) - \ + timedelta(days=366) + +def compareOBS(data, save_path, threeDim=False, depth=5, slack_velo=0.8, plot=False, save_csv=False, + phase_shift=False, debug=False, debug_plot=False): + """ + Does a comprehensive validation process between modeled and observed. + Outputs a list of important statistics for each variable, calculated + using the TidalStats class + + Inputs: + - data = dictionary containing all necessary observed and model data + - threeDim = boolean flag, 3D or not + Outputs: + - elev_suite = dictionary of useful statistics for sea elevation + - speed_suite = dictionary of useful statistics for flow speed + - dir_suite = dictionary of useful statistics for flow direction + - u_suite = dictionary of useful statistics for u velocity component + - v_suite = dictionary of useful statistics for v velocity component + - vel_suite = dictionary of useful statistics for signed flow velocity + - csp_suite = dictionary of useful statistics for cubic flow speed + Options: + - depth = interpolation depth (float in meters), if negative = from + water column top downwards, if positive = from sea bottom upwards + - slack_velo = slack water's velocity (m/s), float, everything below will be dumped out + - plot = boolean flag for plotting results + - save_csv = boolean flag for saving statistical benchmarks in csv file + """ + if debug: print "CompareOBS..." + + hasEL=False + hasUV=False + if 'el' in data['_commonlist_data']: + hasEL=True + + ulist=[var for var in ['ua', 'u'] if var in data['_commonlist_data']] + vlist=[var for var in ['va', 'v'] if var in data['_commonlist_data']] + if len(ulist)>0 and len(vlist)>0: + hasUV=True + + # take data from input dictionary + mod_time = data['mod_time'] + if not data['type'] == 'Drifter': + obs_time = data['obs_time'] + else: + obs_time = data['mod_time'] + + if hasEL: + mod_el = data['mod_timeseries']['el'] + obs_el = data['obs_timeseries']['el'] + + # Check if 3D simulation and for velocity data + if hasUV: + if threeDim: + obs_u_all = data['obs_timeseries']['u'] + obs_v_all = data['obs_timeseries']['v'] + mod_u_all = data['mod_timeseries']['u'] + mod_v_all = data['mod_timeseries']['v'] + bins = data['obs_timeseries']['bins'] + siglay = data['mod_timeseries']['siglay'] + # use depth interpolation to get a single timeseries + #mod_depth = mod_el + np.mean(obs_el[~np.isnan(obs_el)]) + mod_depth = mod_el + data['mod_timeseries']['h'] + if depth < 0.0: + (mod_u, obs_u) = depthFromSurf(mod_u_all, mod_depth, siglay, + obs_u_all, obs_el, bins, depth=depth, + debug=debug, debug_plot=debug_plot) + (mod_v, obs_v) = depthFromSurf(mod_v_all, mod_depth, siglay, + obs_v_all, obs_el, bins, depth=depth, + debug=debug, debug_plot=debug_plot) + else: + (mod_u, obs_u) = depthFromBott(mod_u_all, mod_depth, siglay, + obs_u_all, obs_el, bins, depth=depth, + debug=debug, debug_plot=debug_plot) + (mod_v, obs_v) = depthFromBott(mod_v_all, mod_depth, siglay, + obs_v_all, obs_el, bins, depth=depth, + debug=debug, debug_plot=debug_plot) + else: + if not data['type'] == 'Drifter': + obs_u = data['obs_timeseries']['ua'] + obs_v = data['obs_timeseries']['va'] + mod_u = data['mod_timeseries']['ua'] + mod_v = data['mod_timeseries']['va'] + else: + obs_u = data['obs_timeseries']['u'] + obs_v = data['obs_timeseries']['v'] + mod_u = data['mod_timeseries']['u'] + mod_v = data['mod_timeseries']['v'] + + + if debug: print "...convert times to datetime..." + mod_dt, obs_dt = [], [] + for i in mod_time: + mod_dt.append(dn2dt(i)) + for j in obs_time: + obs_dt.append(dn2dt(j)) + + if debug: print "...put data into a useful format..." + if hasUV: + mod_spd = np.sqrt(mod_u**2.0 + mod_v**2.0) + obs_spd = np.sqrt(obs_u**2.0 + obs_v**2.0) + mod_dir = np.arctan2(mod_v, mod_u) * 180.0 / np.pi + obs_dir = np.arctan2(obs_v, obs_u) * 180.0 / np.pi + if 'el' in data['_commonlist_data']: + obs_el = obs_el - np.mean(obs_el[~np.isnan(obs_el)]) + # Chose the component with the biggest variance as sign reference + if np.var(mod_v) > np.var(mod_u): + mod_signed = np.sign(mod_v) + obs_signed = np.sign(obs_v) + else: + mod_signed = np.sign(mod_u) + obs_signed = np.sign(obs_u) + + if debug: print "...check if the modeled data lines up with the observed data..." + if (mod_time[-1] < obs_time[0] or obs_time[-1] < mod_time[0]): + raise PyseidonError("---time periods do not match up---") + + else: + if debug: print "...interpolate the data onto a common time step for each data type..." + if not data['type'] == 'Drifter': + # elevation + if hasEL: + (mod_el_int, obs_el_int, step_el_int, start_el_int) = smooth(mod_el, mod_dt, obs_el, obs_dt, + debug=debug, debug_plot=debug_plot) + if hasUV: + # speed + (mod_sp_int, obs_sp_int, step_sp_int, start_sp_int) = smooth(mod_spd, mod_dt, obs_spd, obs_dt, + debug=debug, debug_plot=debug_plot) + # direction + (mod_dr_int, obs_dr_int, step_dr_int, start_dr_int) = smooth(mod_dir, mod_dt, obs_dir, obs_dt, + debug=debug, debug_plot=debug_plot) + # u velocity + (mod_u_int, obs_u_int, step_u_int, start_u_int) = smooth(mod_u, mod_dt, obs_u, obs_dt, + debug=debug, debug_plot=debug_plot) + # v velocity + (mod_v_int, obs_v_int, step_v_int, start_v_int) = smooth(mod_v, mod_dt, obs_v, obs_dt, + debug=debug, debug_plot=debug_plot) + # velocity i.e. signed speed + (mod_ve_int, obs_ve_int, step_ve_int, start_ve_int) = smooth(mod_spd * mod_signed, mod_dt, + obs_spd * obs_signed, obs_dt, + debug=debug, debug_plot=debug_plot) + # cubic signed speed + #mod_cspd = mod_spd**3.0 + #obs_cspd = obs_spd**3.0 + mod_cspd = mod_signed * mod_spd**3.0 + obs_cspd = obs_signed * obs_spd**3.0 + (mod_cspd_int, obs_cspd_int, step_cspd_int, start_cspd_int) = smooth(mod_cspd, mod_dt, obs_cspd, obs_dt, + debug=debug, debug_plot=debug_plot) + else: + # Time steps + step = mod_time[1] - mod_time[0] + start = mod_time[0] + + # Already interpolated, so no need to use smooth... + # speed + (mod_sp_int, obs_sp_int, step_sp_int, start_sp_int) = (mod_spd, obs_spd, step, start) + # direction + (mod_dr_int, obs_dr_int, step_dr_int, start_dr_int) = (mod_dir, obs_dir, step, start) + # u velocity + (mod_u_int, obs_u_int, step_u_int, start_u_int) = (mod_u, obs_u, step, start) + # v velocity + (mod_v_int, obs_v_int, step_v_int, start_v_int) = (mod_v, obs_v, step, start) + # velocity i.e. signed speed + (mod_ve_int, obs_ve_int, step_ve_int, start_ve_int) = (mod_spd, obs_spd, step, start) + # cubic signed speed + #mod_cspd = mod_spd**3.0 + #obs_cspd = obs_spd**3.0 + mod_cspd = mod_signed * mod_spd**3.0 + obs_cspd = obs_signed * obs_spd**3.0 + (mod_cspd_int, obs_cspd_int, step_cspd_int, start_cspd_int) = (mod_cspd, obs_cspd, step, start) + + if debug: print "...remove directions where velocities are small..." + if hasUV: + MIN_VEL = slack_velo + indexMin = np.where(obs_sp_int < MIN_VEL) + obs_dr_int[indexMin] = np.nan + obs_u_int[indexMin] = np.nan + obs_v_int[indexMin] = np.nan + obs_ve_int[indexMin] = np.nan + obs_cspd_int[indexMin] = np.nan + + indexMin = np.where(mod_sp_int < MIN_VEL) + mod_dr_int[indexMin] = np.nan + mod_u_int[indexMin] = np.nan + mod_v_int[indexMin] = np.nan + mod_ve_int[indexMin] = np.nan + mod_cspd_int[indexMin] = np.nan + + if debug: print "...get stats for each tidal variable..." + gear = data['type'] # Type of measurement gear (drifter, adcp,...) + + suites={} + + if hasEL: + suites['el'] = tidalSuite(gear, mod_el_int, obs_el_int, step_el_int, start_el_int, + [], [], [], [], [], [], + kind='elevation', plot=plot, + save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + if hasUV: + suites['speed'] = tidalSuite(gear, mod_sp_int, obs_sp_int, step_sp_int, start_sp_int, + [], [], [], [], [], [], + kind='speed', plot=plot, + save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + suites['dir'] = tidalSuite(gear, mod_dr_int, obs_dr_int, step_dr_int, start_dr_int, + mod_u, obs_u, mod_v, obs_v, + mod_dt, obs_dt, + kind='direction', plot=plot, + save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + suites['u'] = tidalSuite(gear, mod_u_int, obs_u_int, step_u_int, start_u_int, + [], [], [], [], [], [], + kind='u velocity', plot=plot, save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + suites['v'] = tidalSuite(gear, mod_v_int, obs_v_int, step_v_int, start_v_int, + [], [], [], [], [], [], + kind='v velocity', plot=plot, save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + + # TR: requires special treatments from here on + suites['vel'] = tidalSuite(gear, mod_ve_int, obs_ve_int, step_ve_int, start_ve_int, + mod_u, obs_u, mod_v, obs_v, + mod_dt, obs_dt, + kind='velocity', plot=plot, save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + suites['cubic_speed'] = tidalSuite(gear, mod_cspd_int, obs_cspd_int, step_cspd_int, start_cspd_int, + mod_u, obs_u, mod_v, obs_v, + mod_dt, obs_dt, + kind='cubic speed', plot=plot, save_csv=save_csv, save_path=save_path, phase_shift=phase_shift, + debug=debug, debug_plot=debug_plot) + + # output statistics in useful format + + if debug: print "...CompareOBS done." + + + return suites + + +def tidalSuite(gear, model, observed, step, start, + model_u, observed_u, model_v, observed_v, + model_time, observed_time, + kind='', plot=False, save_csv=False, save_path='./', phase_shift=False, + debug=False, debug_plot=False): + """ + Create stats classes for a given tidal variable. + + Accepts interpolated model and observed data, the timestep, and start + time. kind is a string representing the kind of data. If plot is set + to true, a time plot and regression plot will be produced. + + Returns a dictionary containing all the stats. + """ + if debug: print "tidalSuite..." + stats = TidalStats(gear, model, observed, step, start, + model_u = model_u, observed_u = observed_u, model_v = model_v, observed_v = observed_v, + model_time = model_time, observed_time = observed_time, phase_shift=phase_shift, + kind=kind, debug=debug, debug_plot=debug_plot) + stats_suite = stats.getStats(phase_shift=phase_shift) + stats_suite['r_squared'] = stats.linReg()['r_2'] + # calling special methods + if kind == 'direction': + rmse, nrmse = stats.statsForDirection(debug=debug) + stats_suite['RMSE'] = rmse + stats_suite['NRMSE'] = nrmse + try: #Fix for Drifter's data + stats_suite['phase'] = stats.getPhase(phase_shift=phase_shift, debug=debug) + except: + stats_suite['phase'] = 0.0 + + if plot or debug_plot: + plotData(stats) + plotRegression(stats, stats.linReg()) + + if save_csv: + stats.save_data(path=save_path) + plotData(stats, savepath=save_path, fname=kind+"_"+gear+"_time_series.png") + plotRegression(stats, stats.linReg(), savepath=save_path, fname=kind+"_"+gear+"_linear_regression.png") + + if debug: print "...tidalSuite done." + + return stats_suite diff --git a/build/lib/pyseidon_dvt/validationClass/depthInterp.py b/build/lib/pyseidon_dvt/validationClass/depthInterp.py new file mode 100644 index 0000000..ad30fe0 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/depthInterp.py @@ -0,0 +1,203 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +import numpy as np +from scipy.interpolate import interp1d + +''' +ASSUMPTIONS: +first dimension of matrices identify the timestep, second the depth +column vectors are organized from bottom to top +i.e. data[3] is the column at the third timestep + data[3][10] is the tenth layer from the bottom at the third timestep +ADCP bins are the depths of each ADCP layer +ADCP bin width is constant +The top ADCP value of any column is no greater than 95% of the total depth +''' + +ADCP_TOP_SURF = 0.95 + +def depthToSigma(obs_data, obs_depth, siglay, bins, debug=False, debug_plot=False): + ''' + Performs linear interpolation on 3D ADCP data to change it into a sigma + layer format, similar to an FVCOM run. + + Outputs a 2D numpy array representing the ADCP data in sigma layer + format. + ''' + if debug: print "depthToSigma..." + sig_obs = np.zeros(obs_data.shape[0], siglay.size) + + if debug: print "...map old depths to between 0 and 1, then interpol..." + # loop through columns/steps + for i, column in enumerate(obs_data): + # map old depths to between 0 and 1, make interpolation function + col_nonan = column[np.where(~np.isnan(column))[0]] + old_depths = bins[np.where(~np.isnan(column))[0]] + mapped_depths = old_depths / obs_depth[i] + f_obs = interp1d(col_nonan, mapped_depths) + # perform interpolation + sig_obs[i] = f_obs(siglay) + + if debug: print "...depthToSigma done." + + return sig_obs + +def sigmaToDepth(mod_data, mod_depth, siglay, bins, debug=False, debug_plot=False): + ''' + Performs linear interpolation on 3D FVCOM output to change it into the + same format as ADCP output (i.e. constant depths, NaNs above surface) + + Outputs a 2D numpy array representing the FVCOM matrix in ADCP format. + ''' + if debug: print "sigmaToDepth..." + bin_mod = np.zeros(mod_data.shape[0], bins.size) + bin_width = bins[1] - bins[0] + + if debug: print "...loop through columns/steps and interpol..." + # loop through columns/steps + for i, column in enumerate(mod_data): + # create interpolation function + f_mod = interp1d(column, siglay) + depth = mod_depth[i] + # loop through bins + for j in np.arange(bins.size): + # check if location is above ADCP_TOP_SURF + loc = float(bins_width * j + bins[0]) / float(depth) + if (loc <= ADCP_TOP_SURF): + bin_mod[i][j] = f_mod(loc) + else: + bin_mod[i][j] = np.nan + + if debug: print "...sigmaToDepth done." + + return bin_mod + +def depthFromSurf(mod_data, mod_depth, siglay, obs_data, obs_depth, bins, depth=5, + debug=False, debug_plot=False): + ''' + Performs linear interpolation on 3D ocean data to obtain data at a + specific distance from the surface. + + Inputs: + - mod_data = 2D numpy array of FVCOM model data + - mod_depth = 1D numpy array of model depths at each timestep + - siglay = array containing values between 0 and 1 representing the + respective percentage of depths for each sigma layer + - obs_data = 2D numpy array of observed ADCP data + - obs_depth = 1D numpy array of observed depths at each timestep + - depth = number of metres from surface of output timeseries. + Defaults to 5m + + Outputs: + - (new_mod, new_obs) = timeseries representing model and observed data + at 'depth' metres from the surface. + ''' + if debug: print "depthFromSurf..." + new_mod = np.zeros(mod_data.shape[0]) + new_obs = np.zeros(obs_data.shape[0]) + depth = np.abs(depth) + + if debug: print "...loop through simulation columns and interpol at specified depth..." + # loop over mod_data columns + for i, step in enumerate(mod_data): + # create interpolation function + #TR: quick fix + try: + f_mod = interp1d(np.abs(siglay), step) #, bounds_error=False) + # find location of specified depth and perform interpolation + location = mod_depth[i] - depth + sig_loc = float(location) / float(mod_depth[i]) + new_mod[i] = f_mod(sig_loc) + except ValueError: + f_mod = interp1d(np.abs(siglay)[::-1], step[::-1], bounds_error=False) + # find location of specified depth and perform interpolation + location = mod_depth[i] - depth + sig_loc = float(location) / float(mod_depth[i]) + new_mod[i] = f_mod(sig_loc) + + if debug: print "...loop through measurement columns and interpol at specified depth..." + # loop over obs_data columns + for ii, column in enumerate(obs_data): + # create interpolation function + col_nonan = column[np.where(~np.isnan(column))[0]] + bin_nonan = bins[np.where(~np.isnan(column))[0]] + + if not col_nonan.shape[0]==0: + # find location of specified depth and perform interpolation + try: + f_obs = interp1d(bin_nonan, col_nonan) + location = obs_depth[ii] - depth + new_obs[ii] = f_obs(location) + except ValueError: + f_obs = interp1d(bin_nonan[::-1], col_nonan[::-1], bounds_error=False) + location = obs_depth[ii] - depth + new_obs[ii] = f_obs(location) + else: + new_obs[ii] = np.nan + + if debug: print "...depthFromSurf done." + + return (new_mod, new_obs) + +def depthFromBott(mod_data, mod_depth, siglay, obs_data, obs_depth, bins, depth=5, + debug=False, debug_plot=False): + ''' + Performs linear interpolation on 3D ocean data to obtain data at a + specific distance from the surface. + + Inputs: + - mod_data = 2D numpy array of FVCOM model data + - mod_depth = 1D numpy array of model depths at each timestep + - siglay = array containing values between 0 and 1 representing the + respective percentage of depths for each sigma layer + - obs_data = 2D numpy array of observed ADCP data + - obs_depth = 1D numpy array of observed depths at each timestep + - depth = number of metres from surface of output timeseries. + Defaults to 5m + + Outputs: + - (new_mod, new_obs) = timeseries representing model and observed data + at 'depth' metres from the surface. + ''' + if debug: print "depthFromBott..." + new_mod = np.zeros(mod_data.shape[0]) + new_obs = np.zeros(obs_data.shape[0]) + depth = np.abs(depth) + + if debug: print "...loop through simulation columns and interpol at specified depth..." + # loop over mod_data columns + for i, step in enumerate(mod_data): + # create interpolation function + #TR: quick fix + try: + f_mod = interp1d(np.abs(siglay), step) #, bounds_error=False) + # find location of specified depth and perform interpolation + sig_loc = float(depth) / float(mod_depth[i]) + new_mod[i] = f_mod(sig_loc) + except ValueError: + f_mod = interp1d(np.abs(siglay)[::-1], step[::-1], bounds_error=False) + # find location of specified depth and perform interpolation + sig_loc = float(depth) / float(mod_depth[i]) + new_mod[i] = f_mod(sig_loc) + + if debug: print "...loop through measurement columns and interpol at specified depth..." + # loop over obs_data columns + for ii, column in enumerate(obs_data): + # create interpolation function + col_nonan = column[np.where(~np.isnan(column))[0]] + bin_nonan = bins[np.where(~np.isnan(column))[0]] + + if not col_nonan.shape[0]==0: + # find location of specified depth and perform interpolation + try: + f_obs = interp1d(bin_nonan, col_nonan) #, bounds_error=False) + new_obs[ii] = f_obs(depth) + except ValueError: + f_obs = interp1d(bin_nonan[::-1], col_nonan[::-1], bounds_error=False) + new_obs[ii] = f_obs(depth) + else: + new_obs[ii] = np.nan + + if debug: print "...depthFromBott done." + + return (new_mod, new_obs) \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/validationClass/interpolate.py b/build/lib/pyseidon_dvt/validationClass/interpolate.py new file mode 100644 index 0000000..4802b19 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/interpolate.py @@ -0,0 +1,59 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +from scipy.interpolate import interp1d +import numpy as np +from datetime import timedelta +import time + + +def interpol(data_1, data_2, time_step=timedelta(minutes=5), + debug=False, debug_plot=False): + ''' + Interpolates between two datasets so their points line up in the time + domain. + + Accepts two sets of data, each of which are dictionaries containing two + values: + time- array containing the datetimes corresponding to the points + pts- 1D numpy array containing the data + + Third optional argument sets the time between data points in the output + data. Is a timedelta object, defaults to 10 minutes. + ''' + if debug: print "interpol..." + if debug: print "...line up points in the time, step=5min..." + + dt_1 = data_1['time'] + dt_2 = data_2['time'] + + # create POSIX timestamp array corresponding to each dataset + times_1, times_2 = np.zeros(len(dt_1)), np.zeros(len(dt_2)) + for i in np.arange(times_1.size): + times_1[i] = time.mktime(dt_1[i].timetuple()) + for v in np.arange(times_2.size): + times_2[v] = time.mktime(dt_2[v].timetuple()) + + # generate interpolation functions using linear interpolation + f1 = interp1d(times_1, data_1['pts']) + f2 = interp1d(times_2, data_2['pts']) + + # choose interval on which to interpolate + start = max(times_1[0], times_2[0]) + end = min(times_1[-1], times_2[-1]) + length = end - start + + # determine number of steps in the interpolation interval + step_sec = time_step.total_seconds() + steps = int(length / step_sec) + + # create POSIX timestamp array for new data and perform interpolation + output_times = start + np.arange(steps) * step_sec + + series_1 = f1(output_times) + series_2 = f2(output_times) + + dt_start = max(dt_1[0], dt_2[0]) + + if debug: print "...interpol done." + + return (series_1, series_2, time_step, dt_start) diff --git a/build/lib/pyseidon_dvt/validationClass/plotsValidation.py b/build/lib/pyseidon_dvt/validationClass/plotsValidation.py new file mode 100644 index 0000000..ffa0bed --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/plotsValidation.py @@ -0,0 +1,329 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import os.path as os +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.projections import PolarAxes +import mpl_toolkits.axisartist.floating_axes as fa +import mpl_toolkits.axisartist.grid_finder as gf + +def plotRegression(tidalStatClass, lr, savepath='', fname='', debug=False): + """ + Plots a visualization of the output from linear regression, + including confidence intervals for predictands and slope. + + If a savepath and filename is defined, exports the plot as an image file + to that location. Filenames should include the image file name and extension. + + Returns: + fig, ax: maplotlib objects + """ + if debug : print "Plotting linear regression" + + #define figure frame + fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + ax = fig.add_subplot(111) + + ax.scatter(tidalStatClass.model, tidalStatClass.observed, c='b', marker='+', alpha=0.5) + + ## plot regression line + mod_max = np.amax(tidalStatClass.model) + mod_min = np.amin(tidalStatClass.model) + upper_intercept = lr['intercept'] + lr['pred_CI_width'] + lower_intercept = lr['intercept'] - lr['pred_CI_width'] + ax.plot([mod_min, mod_max], [mod_min * lr['slope'] + lr['intercept'], + mod_max * lr['slope'] + lr['intercept']], + color='k', linestyle='-', linewidth=2, label='Linear fit') + + ## plot CI's for slope + ax.plot([mod_min, mod_max], [mod_min * lr['slope_CI'][0] + lr['intercept_CI'][0], + mod_max * lr['slope_CI'][0] + lr['intercept_CI'][0]], + color='r', linestyle='--', linewidth=2) + ax.plot([mod_min, mod_max], [mod_min * lr['slope_CI'][1] + lr['intercept_CI'][1], + mod_max * lr['slope_CI'][1] + lr['intercept_CI'][1]], + color='r', linestyle='--', linewidth=2, label='Slope CI') + + ## plot CI's for predictands + ax.plot([mod_min, mod_max], [mod_min * lr['slope'] + upper_intercept, + mod_max * lr['slope'] + upper_intercept], + color='g', linestyle='--', linewidth=2) + ax.plot([mod_min, mod_max], [mod_min * lr['slope'] + lower_intercept, + mod_max * lr['slope'] + lower_intercept], + color='g', linestyle='--', linewidth=2, label='Predictand CI') + + ax.set_xlabel('Modeled Data') + ax.set_ylabel('Observed Data') + fig.suptitle('Modeled vs. Observed {}: Linear Fit'.format(tidalStatClass.kind)) + plt.legend(loc='lower right', shadow=True) + + r_string = 'R Squared: {}'.format(np.around(lr['r_2'], decimals=3)) + plt.title(r_string) + + # Pretty plot + # df = DataFrame(data={'model': tidalStatClass.model.ravel(), + # 'observed':tidalStatClass.observed.ravel()}) + # seaborn.set(style="darkgrid") + # color = seaborn.color_palette()[2] + # g = seaborn.jointplot("model", "observed", data=df, kind="reg", + # xlim=(df.model.min(), df.model.max()), + # ylim=(df.observed.min(), df.observed.max()), + # color=color, size=7) + # plt.suptitle('Modeled vs. Observed {}: Linear Fit'.format(tidalStatClass.kind)) + # KC: Changed save parameter to be a savepath - making a huge assumption here + # that people are able to enter in the savepath correctly / exists etc. + if savepath.strip() and fname.strip(): + if os.exists(savepath): + fig.savefig(savepath+fname) + fig.clear() + plt.close(fig) + else: + fig.show() + plt.show() + + return fig, ax + +def plotData(tidalStatClass, graph='time', savepath='', fname='', debug=False): + """ + Provides a visualization of the data. + + Takes an option which determines the kind of graph to be made. + time: plots the model data against the observed data over time + scatter : plots the model data vs. observed data + + If a savepath and filename is defined, exports the plot as an image file + to that location. Filenames should include the image file name and extension. + """ + if debug: print "Plotting time-series..." + #define figure frame + fig = plt.figure(figsize=(18,10)) + plt.rc('font',size='22') + ax = fig.add_subplot(111) + + if (graph == 'time'): + ax.plot(tidalStatClass.times, tidalStatClass.model, label='Model Predictions') + ax.plot(tidalStatClass.times, tidalStatClass.observed, color='r', + label='Observed Data') + ax.set_xlabel('Time') + if tidalStatClass.kind == 'elevation': + ax.set_ylabel('Elevation (m)') + if tidalStatClass.kind == 'speed': + ax.set_ylabel('Flow speed (m/s)') + if tidalStatClass.kind == 'direction': + ax.set_ylabel('Flow direction (deg.)') + if tidalStatClass.kind == 'u velocity': + ax.set_ylabel('U velocity (m/s)') + if tidalStatClass.kind == 'v velocity': + ax.set_ylabel('V velocity (m/s)') + if tidalStatClass.kind == 'velocity': + ax.set_ylabel('Signed flow speed (m/s)') + if tidalStatClass.kind == 'cubic speed': + ax.set_ylabel('Cubic speed (m3/s3)') + + fig.suptitle('Predicted and Observed {}'.format(tidalStatClass.kind)) + ax.legend(shadow=True) + + if (graph == 'scatter'): + ax.scatter(tidalStatClass.model, tidalStatClass.observed, c='b', alpha=0.5) + ax.set_xlabel('Predicted Height') + ax.set_ylabel('Observed Height') + fig.suptitle('Predicted vs. Observed {}'.format(tidalStatClass.kind)) + + if savepath.strip() and fname.strip(): + if os.exists(savepath): + fig.savefig(savepath+fname) + fig.clear() + plt.close(fig) + else: + fig.show() + plt.show() + +def taylorDiagram(benchmarks, savepath='', fname='', labels=True, debug=False): + """ + References: + Taylor, K.E.: Summarizing multiple aspects of model performance in a single diagram. + J. Geophys. Res., 106, 7183-7192, 2001 + (also see PCMDI Report 55, http://www-pcmdi.llnl.gov/publications/ab55.html) + + IPCC, 2001: Climate Change 2001: The Scientific Basis, + Contribution of Working Group I to the Third Assessment Report of the Intergovernmental Panel on Climate Change + Houghton, J.T., Y. Ding, D.J. Griggs, M. Noguer, P.J. van der Linden, X. Dai, K. Maskell, and C.A. Johnson (eds.) + Cambridge University Press, Cambridge, United Kingdom and New York, NY, USA, 881 pp. + (see http://www.grida.no/climate/ipcc_tar/wg1/317.htm#fig84) + + Code inspired by Yannick Copin's code (see https://github.com/ycopin) + """ + if debug: print "Plotting time-series..." + + # Setting up graph + tr = PolarAxes.PolarTransform() + # Correlation labels + rlocs = np.concatenate((np.arange(10)/10.,[0.95,0.99])) + tlocs = np.arccos(rlocs) # Conversion to polar angles + gl1 = gf.FixedLocator(tlocs) # Positions + tf1 = gf.DictFormatter(dict(zip(tlocs, map(str,rlocs)))) + # Standard deviation axis extent + smin = 0 + smax = 1.5 + ghelper = fa.GridHelperCurveLinear(tr, extremes=(0,np.pi/2, smin, smax), grid_locator1=gl1, tick_formatter1=tf1) + fig = plt.figure(figsize=(18,10)) + rect=111 + ax = fa.FloatingSubplot(fig, rect, grid_helper=ghelper) + fig.add_subplot(ax) + # Adjust axes + ax.axis["top"].set_axis_direction("bottom") # "Angle axis" + ax.axis["top"].toggle(ticklabels=True, label=True) + ax.axis["top"].major_ticklabels.set_axis_direction("top") + ax.axis["top"].label.set_axis_direction("top") + ax.axis["top"].label.set_text("Correlation") + ax.axis["left"].set_axis_direction("bottom") # "X axis" + ax.axis["left"].label.set_text("Standard deviation") + ax.axis["right"].set_axis_direction("top") # "Y axis" + ax.axis["right"].toggle(ticklabels=True) + ax.axis["right"].major_ticklabels.set_axis_direction("left") + ax.axis["bottom"].set_visible(False) # Useless + # Contours along standard deviations + ax.grid(False) + _ax = ax # Graphical axes + ax = ax.get_aux_axes(tr) # Polar coordinates + # Reference lines + x95 = [0.05, 13.9] # For Prcp, this is for 95th level (r = 0.195) + y95 = [0.0, 71.0] + x99 = [0.05, 19.0] # For Prcp, this is for 99th level (r = 0.254) + y99 = [0.0, 70.0] + ax.plot(x95,y95,color='k') + ax.plot(x99,y99,color='k') + # Add reference point and stddev contour + l, = ax.plot(0.0, 1.0, 'k*', ls='', ms=10, label='Reference') + t = np.linspace(0, np.pi/2) + r = np.zeros_like(t) + 1.0 + ax.plot(t,r, 'k--', label='_') + samplePoints = [l] + + # Plot points + sampleLenght = benchmarks['Type'].shape[0] + colors = plt.matplotlib.cm.jet(np.linspace(0,1,sampleLenght)) + for i in range(sampleLenght): + if labels: + l, = ax.plot(benchmarks['NRMSE'][i]/100.0, benchmarks['r2'][i], + marker='$%d$' % (i+1), ms=10, ls='', mfc=colors[i], mec=colors[i], + label= benchmarks['Type'][i] + " " + benchmarks['gear'][i]) + else: + l, = ax.plot(benchmarks['NRMSE'][i]/100.0, benchmarks['r2'][i], + marker='$%d$' % (i+1), ms=10, ls='', mfc=colors[i], mec=colors[i]) + samplePoints.append(l) + t = np.linspace(0, np.pi/2) + r = np.zeros_like(t) + 1.0 + ax.plot(t,r, 'k--', label='_') + + # Add NRMS contours, and label them + rs, ts = np.meshgrid(np.linspace(smin, smax), np.linspace(0,np.pi/2)) + # Compute centered RMS difference + rms = np.sqrt(1.0 + rs**2 - 2*rs*np.cos(ts)) + contours = ax.contour(ts, rs, rms, 5, colors='0.5') + ax.clabel(contours, inline=1, fontsize=10, fmt='%.1f') + # Add a figure legend + if labels: + lgd = fig.legend(samplePoints,[p.get_label() for p in samplePoints], + numpoints=1, prop=dict(size='small'), loc='upper right') + fig.tight_layout() + + if savepath.strip() and fname.strip(): + if os.exists(savepath): + if labels: + fig.savefig(savepath+fname, bbox_extra_artists=(lgd,), bbox_inches='tight') + else: + fig.savefig(savepath+fname, bbox_inches='tight') + fig.clear() + plt.close(fig) + else: + fig.show() + plt.show() + + return fig, ax + +def benchmarksMap(benchmarks, adcps, fvcom, savepath='', fname='', debug=False): + """ + Plots bathymetric map & model validation benchmarks + + Inputs: + - benchmarks = benchmark attribute from Validation class + - adcps = list or tuple of ADCP objects + - fvcom = FVCOM object + Options: + - savepath = folder path for saving plot, string + - fname = filename for saving plot, string + """ + #if debug: print "Computing flow speed" + #fvcom.Util2D.hori_velo_norm() + #speed = np.mean(fvcom.Variables.hori_velo_norm[:],0) + #if debug: print '...passed' + + # collecting names and locations of adcps + adcpLoc={} + try: + for adcp in adcps: + try: + key = adcp.History[0].split(' ')[-1].split('/')[-1].split('.')[0] + val = benchmarks.loc[key] + indCS = np.where(val['Type'].values == 'cubic_speed')[0][0] + adcpLoc[key] = {'location': [adcp.Variables.lon, adcp.Variables.lat], + 'r2': val['r2'][indCS], + 'NRMSE': val['NRMSE'][indCS], + 'bias': val['bias'][indCS]} + except KeyError: # in case something different than adcp in the list + pass + except TypeError: + key = adcps.History[0].split(' ')[-1].split('/')[-1].split('.')[0] + adcpLoc[key] = [adcps.Variables.lon, adcps.Variables.lat] + val = benchmarks.loc[key] + indCS = np.where(val['Type'].values == 'cubic_speed')[0][0] + adcpLoc[key] = {'location': [adcps.Variables.lon, adcps.Variables.lat], + 'r2': val['r2'][indCS], + 'NRMSE': val['NRMSE'][indCS], + 'bias': val['bias'][indCS]} + + # Plot size and color function of R2 and RMSE + # #background + #cmap=plt.cm.jet + #fvcom.Plots.colormap_var(speed, title='Averaged flow speed', mesh=False, cmap=cmap) + fvcom.Plots.colormap_var(fvcom.Grid.h, title='Bathymetric Map & Model Validation Benchmarks', mesh=False) + + for key in adcpLoc.keys(): + print '...plotting ' + key + '...' + r2 = adcpLoc[key]['r2'] # r2 for cubic velocity + nrmse = adcpLoc[key]['NRMSE'] # nrmse for cubic speed + mk = adcpLoc[key]['bias'] # over or under estimated + if np.sign(mk) == -1.0 : + mk = '_' + else: + mk = '+' + fvcom.Plots._ax.scatter(adcpLoc[key]['location'][0],adcpLoc[key]['location'][1], + marker=mk, lw=2, s=100, color='red') + fvcom.Plots._ax.annotate('r2: '+str(round(r2,2))+' |', + xy=(adcpLoc[key]['location'][0],adcpLoc[key]['location'][1]), + xycoords='data', xytext=(-55, -15), + textcoords='offset points', ha='left', + color='white', fontsize=12) + fvcom.Plots._ax.annotate('nrmse: '+str(round(nrmse,2)), + xy=(adcpLoc[key]['location'][0],adcpLoc[key]['location'][1]), + xycoords='data', xytext=(5, -15), + textcoords='offset points', ha='left', + color='white', fontsize=12) + plt.figtext(.02, .02, + "Notes:\n" + + "'+' represents over-estimation whereas '-' represents under-estimation.\n" + + "r2 and nrmse are respectively based on cubic signed speed and cubic speed. ", + size='x-small') + + if savepath.strip() and fname.strip(): + if os.exists(savepath): + fvcom.Plots._fig.savefig(savepath+fname, bbox_inches='tight') + fvcom.Plots._fig.clear() + plt.close(fvcom.Plots._fig) + else: + fvcom.Plots._fig.show() + plt.show() \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/validationClass/smooth.py b/build/lib/pyseidon_dvt/validationClass/smooth.py new file mode 100644 index 0000000..3de2a50 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/smooth.py @@ -0,0 +1,84 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +from datetime import timedelta +import numpy as np +import time + +def smooth(data_1, dt_1, data_2, dt_2, delta_t=10, debug=False, debug_plot=False): + ''' + Smooths a dataset by taking the average of all datapoints within + a certain timestep to reduce noise. Lines up two datasets in the + time domain, as well. + Accepts four variables representing the data. data_1 and data_2 are the + data points, dt_1 and dt_2 are the datetimes corresponding to the points. + delta_t is an optional paramter that changes the time_step in minutes. + ''' + if debug: print "smooth..." + + # KC: timestep changed to delta_t, made optional parameter + time_step = timedelta(minutes=delta_t) + + # create POSIX timestamp array corresponding to each dataset + ''' + times_1, times_2 = np.zeros(len(dt_1)), np.zeros(len(dt_2)) + for i in np.arange(times_1.size): + times_1[i] = time.mktime(dt_1[i].timetuple()) + for i in np.arange(times_2.size): + times_2[i] = time.mktime(dt_2[i].timetuple()) + ''' + + make_posix = lambda x: time.mktime(x.timetuple()) + times_1 = map(make_posix, dt_1) + times_2 = map(make_posix, dt_2) + time_1, times_2 = np.array(times_1), np.array(times_2) + + # choose smoothing interval + start = max(times_1[0], times_2[0]) + end = min(times_1[-1], times_2[-1]) + length = end - start + dt_start = max(dt_1[0], dt_2[0]) + + # grab number of steps and timestamp for start time + step_sec = time_step.total_seconds() + steps = int(length / step_sec) + + # sort times into bins + series_1, series_2 = np.zeros(steps - 1), np.zeros(steps - 1) + time_bins = np.arange(steps) * step_sec + start + inds_1 = np.digitize(times_1, time_bins) + inds_2 = np.digitize(times_2, time_bins) + + # identify bin vertices and take means + first_hit_1 = np.searchsorted(inds_1, np.arange(1, steps + 1)) + for j in xrange(steps - 1): + series_1[j] = np.nanmean(data_1[first_hit_1[j]:first_hit_1[j + 1]]) + first_hit_2 = np.searchsorted(inds_2, np.arange(1, steps + 1)) + for j in xrange(steps - 1): + series_2[j] = np.nanmean(data_2[first_hit_2[j]:first_hit_2[j + 1]]) + + ''' + # take averages at each step, create output data + series_1, series_2 = np.zeros(steps), np.zeros(steps) + for i in np.arange(steps): + start_buf = start + step_sec * i + end_buf = start + step_sec * (i + 1) + buf_1 = np.where((times_1 >= start_buf) & (times_1 < end_buf))[0] + buf_2 = np.where((times_2 >= start_buf) & (times_2 < end_buf))[0] + data_buf_1 = data_1[buf_1] + data_buf_2 = data_2[buf_2] + # communicate progress + #if (i % 1000 == 0): + # print 'Currently smoothing at step {} / {}'.format(i, steps) + # calculate mean of data subsets (in the buffers) + if (len(data_buf_1) != 0): + series_1[i] = np.mean(data_buf_1) + else: + series_1[i] = np.nan + if (len(data_buf_2) != 0): + series_2[i] = np.mean(data_buf_2) + else: + series_2[i] = np.nan + ''' + + if debug: print "...smooth done." + return (series_1, series_2, time_step, dt_start) diff --git a/build/lib/pyseidon_dvt/validationClass/tidalStats.py b/build/lib/pyseidon_dvt/validationClass/tidalStats.py new file mode 100644 index 0000000..4caa669 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/tidalStats.py @@ -0,0 +1,715 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +from scipy.stats import t, pearsonr +from datetime import datetime, timedelta +from scipy.interpolate import interp1d +from scipy.signal import correlate +import time +import pandas as pd +from sys import exit + +# local imports +from pyseidon_dvt.utilities.BP_tools import principal_axis + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +class TidalStats: + """ + An object representing a set of statistics on tidal heights used + to determine the skill of a model in comparison to observed data. + Standards are from NOAA's Standard Suite of Statistics. + + Instantiated with two arrays containing predicted and observed + data which have already been interpolated so they line up, the + time step between points, and the start time of the data. + + To remove NaNs in observed data, linear interpolation is performed to + fill gaps. Additionally, NaNs are trimmed from the start and end. + + Functions are used to calculate statistics and to output + visualizations and tables. + """ + def __init__(self, gear, model_data, observed_data, time_step, start_time, + model_u = [], observed_u = [], model_v= [], observed_v = [], + model_time = [], observed_time = [], phase_shift=False, + kind='', debug=False, debug_plot=False): + if debug: print "TidalStats initialisation..." + self._debug = debug + self._debug_plot = debug_plot + self.gear = gear # Type of measurement gear (drifter, adcp,...), str + self.model = np.asarray(model_data) + self.model = self.model.astype(np.float64) + self.observed = np.asarray(observed_data) + self.observed = self.observed.astype(np.float64) + self.model_u = np.asarray(model_u) + self.model_u = self.model_u.astype(np.float64) + self.observed_u = np.asarray(observed_u) + self.observed_u = self.observed_u.astype(np.float64) + self.model_v = np.asarray(model_v) + self.model_v = self.model_v.astype(np.float64) + self.observed_v = np.asarray(observed_v) + self.observed_v = self.observed_v.astype(np.float64) + self.kind = kind + self.step = time_step + self.length = model_data.size + + # TR: pass this step if dealing with Drifter's data + if not self.gear == 'Drifter': + try: + # TR: fix for interpolation pb when 0 index or -1 index array values = nan + if debug: print "...trim nans at start and end of data.." + start_index, end_index = 0, -1 + while np.isnan(self.observed[start_index]) or np.isnan(self.model[start_index]): + start_index += 1 + while np.isnan(self.observed[end_index]) or np.isnan(self.model[end_index]): + end_index -= 1 + except IndexError: # due too nans everywhere, itself due to no obs data at requested depth + raise PyseidonError("-No matching measurement-") + + # Correction for bound index call + if end_index == -1: + end_index = None + else: + end_index += 1 + if debug: print "Start index: ", start_index + if debug: print "End index: ", end_index + + m = self.model[start_index:end_index] + o = self.observed[start_index:end_index] + + setattr(self, 'model', m) + setattr(self, 'observed', o) + + # set up array of datetimes corresponding to the data (and timestamps) + self.times = start_time + np.arange(self.model.size) * time_step + timestamps = np.zeros(len(self.times)) + for j, jj in enumerate(self.times): + timestamps[j] = time.mktime(jj.timetuple()) + + if debug: print "...uses linear interpolation to eliminate any NaNs in the data..." + + if True in np.isnan(self.observed): + time_nonan = timestamps[np.where(~np.isnan(self.observed))[0]] + obs_nonan = self.observed[np.where(~np.isnan(self.observed))[0]] + func = interp1d(time_nonan, obs_nonan) + self.observed = func(timestamps) + if True in np.isnan(self.model): + time_nonan = timestamps[np.where(~np.isnan(self.model))[0]] + mod_nonan = self.model[np.where(~np.isnan(self.model))[0]] + func = interp1d(time_nonan, mod_nonan) + self.model = func(timestamps) + + # TR: pass this step if dealing with Drifter's data + else: + self.times = start_time + np.arange(self.model.size) * time_step # needed for plots, en seconds + #TR: those are not the real times though + + # Applying phase shift correction if needed + if phase_shift: + if debug: print "...applying phase shift..." + try: # Fix for Drifter's data + step_sec = time_step.seconds + except AttributeError: + step_sec = time_step * 24.0 * 60.0 * 60.0 # converts matlabtime to seconds + phase = self.getPhase() + phaseIndex = int(phase * 60.0 / step_sec) + if debug: print "Phase index = "+str(phaseIndex) + # create shifted data) + # if (phaseIndex < 0): + # # left shift + # iM = np.s_[-phaseIndex:] + # iO = np.s_[:self.length + phaseIndex] + # elif (phaseIndex > 0): + # # right shift + # iM = np.s_[:self.length - phaseIndex] + # iO = np.s_[phaseIndex:] + # else: # if (phaseIndex == 0): + # # no shift + # iM = np.s_[:] + # iO = np.s_[:] + # self.model = self.model[iM] + # self.observed = self.observed[iO] + # if not model_u == []: + # self.model_u = self.model_u[iM] + # if not model_v == []: + # self.model_v = self.model_v[iM] + # if not observed_u == []: + # self.observed_u = self.observed_v[iO] + # if not observed_v == []: + # self.observed_v = self.observed_v[iO] + self.model = np.roll(self.model, phaseIndex) + if not model_u == []: + self.model_u = np.roll(self.model_u, phaseIndex) + if not model_v == []: + self.model_v = np.roll(self.model_v, phaseIndex) + if debug: print "...TidalStats initialisation done." + + # Error attributes + if self.kind in ['cubic speed', 'velocity', 'direction']: + # TR: pass this step if dealing with Drifter's data + if not self.gear == 'Drifter': + # interpolate cubic speed, u and v on same time steps + model_timestamps = np.zeros(len(model_time)) + for j, jj in enumerate(model_time): + model_timestamps[j] = time.mktime(jj.timetuple()) + obs_timestamps = np.zeros(len(observed_time)) + for j, jj in enumerate(observed_time): + obs_timestamps[j] = time.mktime(jj.timetuple()) + func_u = interp1d(model_timestamps, model_u) + self.model_u = func_u(timestamps) + func_v = interp1d(model_timestamps, model_v) + self.model_v = func_v(timestamps) + func_u = interp1d(obs_timestamps, observed_u) + self.observed_u = func_u(timestamps) + func_v = interp1d(obs_timestamps, observed_v) + self.observed_v = func_v(timestamps) + + # if self.kind == 'cubic speed': + # # R.Karsten formula + # self.error = ((self.model_u**2.0 + self.model_v**2.0)**(3.0/2.0)) - \ + # ((self.observed_u**2.0 + self.observed_v**2.0)**(3.0/2.0)) + # else: + # self.error = self.observed - self.model + self.error = self.observed - self.model + elif self.kind in ['speed', 'elevation', 'u velocity', 'v velocity', 'Phase']: + self.error = self.observed - self.model + else: + print "---Data kind not supported---" + exit() + + # if debug: print "...establish limits as defined by NOAA standard..." + # if self.kind == 'velocity': + # self.ERROR_BOUND = 0.26 + # elif (self.kind == 'speed' or self.kind == 'velocity'): + # self.ERROR_BOUND = 0.26 + # elif (self.kind == 'elevation'): + # self.ERROR_BOUND = 0.15 + # elif (self.kind == 'direction'): + # self.ERROR_BOUND = 22.5 + # elif (self.kind == 'u velocity' or self.kind == 'v velocity'): + # self.ERROR_BOUND = 0.35 + # elif self.kind == 'cubic speed': + # self.ERROR_BOUND = 0.26**3.0 + # else: + # self.ERROR_BOUND = 0.5 + + # instead of using the NOAA errors, use 10% of the data range + obs_range = 0.1 * (np.nanmax(self.observed) - np.nanmin(self.observed)) + mod_range = 0.1 * (np.nanmax(self.model) - np.nanmin(self.model)) + self.ERROR_BOUND = (obs_range + mod_range) / 2. + + return + + def getRMSE(self, debug=False): + ''' + Returns the root mean squared error of the data. + ''' + if debug or self._debug: print "...getRMSE..." + if self.kind == 'velocity': + # Special definition of rmse - R.Karsten + rmse = np.sqrt(np.nanmean((self.model_u - self.observed_u)**2.0 + (self.model_v - self.observed_v)**2.0)) + else: + rmse = np.sqrt(np.nanmean(self.error**2)) + return rmse + + + def getNRMSE(self, debug=False): + """ + Returns the normalized root mean squared error between the model and + observed data in %. + """ + if debug or self._debug: print "...getNRMSE..." + if self.kind == 'velocity': + # Special definition of rmse - R.Karsten + rmse0 = np.sqrt(np.nanmean((self.observed_u)**2.0 + (self.observed_v)**2.0)) + else: + rmse0 = np.sqrt(np.mean(self.observed**2.0)) + # return 100. * self.getRMSE() / (max(self.observed) - min(self.observed)) + return 100. * self.getRMSE() / rmse0 + + def getSD(self, debug=False): + ''' + Returns the standard deviation of the error. + ''' + if debug or self._debug: print "...getSD..." + return np.sqrt(np.nanmean(abs(self.error - np.nanmean(self.error)**2))) + + def getBias(self, debug=False): + """ + Returns the bias of the model, a measure of over/under-estimation. + """ + if debug or self._debug: print "...getBias..." + return np.nanmean(self.error) + + def getSI(self, debug=False): + """ + Returns the scatter index of the model, a weighted measure of data + scattering. + """ + if debug or self._debug: print "...getSI..." + return self.getRMSE() / np.nanmean(self.observed) + + def getPBIAS(self, debug=False): + """ + Returns the percent bias between the model and the observed data. + + References: + Yapo P. O., Gupta H. V., Sorooshian S., 1996. + Automatic calibration of conceptual rainfall-runoff models: sensitivity to calibration data. + Journal of Hydrology. v181 i1-4. 23-48 + + Sorooshian, S., Q. Duan, and V. K. Gupta. 1993. + Calibration of rainfall-runoff models: Application of global optimization + to the Sacramento Soil Moisture Accounting Model. + Water Resources Research, 29 (4), 1185-1194, doi:10.1029/92WR02617. + """ + if debug or self._debug: print "...getPBIAS..." + + # if self.kind in ['elevation', 'direction', 'u velocity', 'v velocity', 'velocity']: + # norm_error = self.error / self.observed + # pbias = 100. * np.sum(norm_error) / norm_error.size + # else: + # norm_error = self.model - self.observed + # pbias = 100. * (np.sum(norm_error) / np.sum(self.observed)) + # # TR: this formula may lead to overly large values when used with sinusoidal signals + + pbias = 100. * (np.nansum(self.error) / np.nansum(self.observed)) + + return pbias + + + def getNSE(self, debug=False): + """ + Returns the Nash-Sutcliffe Efficiency coefficient of the model vs. + the observed data. Identifies if the model is better for + approximation than the mean of the observed data. + """ + SSE_mod = np.nansum((self.observed - self.model)**2) + SSE_mean = np.nansum((self.observed - np.nanmean(self.observed))**2) + return 1 - SSE_mod / SSE_mean + + def getCORR(self, debug=False): + """ + Returns the Pearson correlation coefficient for the model vs. + the observed data, a number between -1 and 1. -1 implies perfect + negative correlation, 1 implies perfect correlation. + """ + return pearsonr(self.observed, self.model)[0] + + def statsForDirection(self, debug=False): + """ + Special stats for direction + + Outputs: + - err = absolute error + - nerr = absolute error divided by standard deviation in % + """ + if debug: print "Computing special stats for direction..." + pr_axis_mod, pr_ax_var_mod = principal_axis(self.model_u, self.model_v) + pr_axis_obs, pr_ax_var_obs = principal_axis(self.observed_u, self.observed_v) + + # Defines intervals + dir_all_mod = self.model[:] + dir_all_obs = self.observed[:] + ind_mod = np.where(dir_all_mod<0) + ind_obs = np.where(dir_all_obs<0) + dir_all_mod[ind_mod] = dir_all_mod[ind_mod] + 360 + dir_all_obs[ind_obs] = dir_all_obs[ind_obs] + 360 + + # sign speed - eliminating wrap-around + dir_PA_mod = dir_all_mod - pr_axis_mod + dir_PA_mod[dir_PA_mod < -90] += 360 + dir_PA_mod[dir_PA_mod > 270] -= 360 + dir_PA_obs = dir_all_obs - pr_axis_obs + dir_PA_obs[dir_PA_obs < -90] += 360 + dir_PA_obs[dir_PA_obs > 270] -= 360 + + #general direction of flood passed as input argument + floodIndex_mod = np.where((dir_PA_mod >= -90) & (dir_PA_mod<90))[0] + ebbIndex_mod = np.arange(dir_PA_mod.shape[0]) + ebbIndex_mod = np.delete(ebbIndex_mod, floodIndex_mod[:]) + floodIndex_obs = np.where((dir_PA_obs >= -90) & (dir_PA_obs<90))[0] + ebbIndex_obs = np.arange(dir_PA_obs.shape[0]) + ebbIndex_obs = np.delete(ebbIndex_obs, floodIndex_obs[:]) + + # principal axis for ebb and flood + np.delete(floodIndex_mod, np.where(floodIndex_mod >= self.model_u.shape[0])) + np.delete(ebbIndex_mod, np.where(ebbIndex_mod >= self.model_u.shape[0])) + np.delete(floodIndex_obs, np.where(floodIndex_obs >= self.observed_u.shape[0])) + np.delete(ebbIndex_obs, np.where(ebbIndex_obs >= self.observed_u.shape[0])) + + pr_axis_mod_flood, pr_ax_var_mod_flood = principal_axis(self.model_u[floodIndex_mod], + self.model_v[floodIndex_mod]) + pr_axis_mod_ebb, pr_ax_var_mod_ebb = principal_axis(self.model_u[ebbIndex_mod], + self.model_v[ebbIndex_mod]) + pr_axis_obs_flood, pr_ax_var_obs_flood = principal_axis(self.observed_u[floodIndex_obs], + self.observed_v[floodIndex_obs]) + pr_axis_obs_ebb, pr_ax_var_obs_ebb = principal_axis(self.observed_u[ebbIndex_obs], + self.observed_v[ebbIndex_obs]) + err_flood = np.abs(pr_axis_mod_flood - pr_axis_obs_flood) + err_ebb = np.abs(pr_axis_mod_ebb - pr_axis_obs_ebb) + + if debug: print "...ebb direction error: " + str(err_ebb) + " degrees..." + if debug: print "...flood direction error: " + str(err_flood) + " degrees..." + + err = 0.5 * (err_flood + err_ebb) + + #std_flood = np.asarray(dir_all_obs[floodIndex_obs]).std() + #std_ebb = np.asarray(dir_all_obs[ebbIndex_obs]).std() + #std_flood = dir_all_obs[floodIndex_obs].max() - dir_all_obs[floodIndex_obs].min() + #std_ebb = dir_all_obs[ebbIndex_obs].max() - dir_all_obs[ebbIndex_obs].min() + #nerr = 0.5 * (np.abs(err_flood / std_flood) + np.abs(err_ebb / std_ebb)) + nerr = err / 90.0 + + return err, nerr * 100.0 + + + def getCF(self, debug=False): + ''' + Returns the central frequency of the data, i.e. the fraction of + errors that lie within the defined limit. + ''' + central_err = [i for i in self.error if abs(i) < self.ERROR_BOUND] + central_num = len(central_err) + if debug or self._debug: print "...getCF..." + return (float(central_num) / float(self.length)) * 100 + + def getPOF(self, debug=False): + ''' + Returns the positive outlier frequency of the data, i.e. the + fraction of errors that lie above the defined limit. + ''' + upper_err = [i for i in self.error if i > 2 * self.ERROR_BOUND] + upper_num = len(upper_err) + if debug or self._debug: print "...getPOF..." + return (float(upper_num) / float(self.length)) * 100 + + def getNOF(self, debug=False): + ''' + Returns the negative outlier frequency of the data, i.e. the + fraction of errors that lie below the defined limit. + ''' + lower_err = [i for i in self.error if i < -2 * self.ERROR_BOUND] + lower_num = len(lower_err) + if debug or self._debug: print "...getNOF..." + return (float(lower_num) / float(self.length)) * 100 + + def getMDPO(self, debug=False): + ''' + Returns the maximum duration of positive outliers, i.e. the + longest amount of time across the data where the model data + exceeds the observed data by a specified limit. + + Takes one parameter: the number of minutes between consecutive + data points. + ''' + try: #Fix for Drifter's data + timestep = self.step.seconds / 60 + except AttributeError: + timestep = self.step * 24.0 * 60.0 # converts matlabtime (in days) to minutes + + max_duration = 0 + current_duration = 0 + for i in np.arange(self.error.size): + if (self.error[i] > self.ERROR_BOUND): + current_duration += timestep + else: + if (current_duration > max_duration): + max_duration = current_duration + current_duration = 0 + if debug or self._debug: print "...getMDPO..." + return max(max_duration, current_duration) + + def getMDNO(self, debug=False): + ''' + Returns the maximum duration of negative outliers, i.e. the + longest amount of time across the data where the observed + data exceeds the model data by a specified limit. + + Takes one parameter: the number of minutes between consecutive + data points. + ''' + try: #Fix for Drifter's data + timestep = self.step.seconds / 60 + except AttributeError: + timestep = self.step * 24.0 * 60.0 # converts matlabtime (in days) to minutes + + max_duration = 0 + current_duration = 0 + for i in np.arange(self.error.size): + if (self.error[i] < -self.ERROR_BOUND): + current_duration += timestep + else: + if (current_duration > max_duration): + max_duration = current_duration + current_duration = 0 + if debug or self._debug: print "...getMDNO..." + return max(max_duration, current_duration) + + def getMSE(self, debug=False): + """ + Returns the mean square error (float) + """ + mse = np.nanmean(self.error**2.0) + if debug or self._debug: print "...getMSE..." + return mse + + def getNMSE(self, debug=False): + """ + Returns the normalized mean square error in % (float) + """ + mse0 = np.nanmean(self.observed**2.0) + nmse = 100. * self.getMSE() / mse0 + if debug or self._debug: print "...getNMSE..." + return nmse + + + def getWillmott(self, debug=False): + ''' + Returns the Willmott skill statistic. + ''' + + # start by calculating MSE + MSE = self.getMSE() + + # now calculate the rest of it + obs_mean = np.nanmean(self.observed) + skill = 1 - MSE / np.nanmean((abs(self.model - obs_mean) + + abs(self.observed - obs_mean))**2) + if debug or self._debug: print "...getWillmott..." + return skill + + def getPhase(self, max_phase=timedelta(hours=3), debug=False): + ''' + Attempts to find the phase shift between the model data and the + observed data. + + Iteratively tests different phase shifts, and calculates the RMSE + for each one. The shift with the smallest RMSE is returned. + + Argument max_phase is the span of time across which the phase shifts + will be tested. If debug is set to True, a plot of the RMSE for each + phase shift will be shown. + ''' + if debug or self._debug: print "getPhase..." + # grab the length of the timesteps in seconds + max_phase_sec = max_phase.seconds + try: # Fix for Drifter's data + step_sec = self.step.seconds + except AttributeError: + step_sec = self.step * 24.0 * 60.0 * 60.0 # converts matlabtime to seconds + + num_steps = max_phase_sec / step_sec + + if debug or self._debug: print "...iterate through the phase shifts and check RMSE..." + errors = [] + phases = np.arange(-num_steps, num_steps).astype(int) + for i in phases: + # create shifted data + shift_mod = np.roll(self.model, i) + # if (i < 0): + # # left shift + # #shift_mod = self.model[-i:] + # shift_obs = self.observed[:self.length + i] + # if (i > 0): + # # right shift + # shift_mod = self.model[:self.length - i] + # shift_obs = self.observed[i:] + # if (i == 0): + # # no shift + # shift_mod = self.model + # shift_obs = self.observed + + start = self.times[abs(i)] + step = self.times[1] - self.times[0] + + # create TidalStats class for shifted data and get the RMSE + #stats = TidalStats(self.gear, shift_mod, shift_obs, step, start, kind='Phase') + stats = TidalStats(self.gear, shift_mod, self.observed, step, start, kind='Phase') + nrms_error = stats.getNRMSE() + errors.append(nrms_error) + + if debug or self._debug: print "...find the minimum rmse, and thus the minimum phase..." + min_index = errors.index(min(errors)) + best_phase = phases[min_index] + phase_minutes = best_phase * step_sec / 60 + + return phase_minutes + + def altPhase(self, debug=False): + """ + Alternate version of lag detection using scipy's cross correlation function. + """ + if debug or self._debug: print "altPhase..." + # normalize arrays + mod = self.model + mod -= self.model.mean() + mod /= mod.std() + obs = self.observed + obs -= self.observed.mean() + obs /= obs.std() + + if debug or self._debug: print "...get cross correlation and find number of timesteps of shift..." + xcorr = correlate(mod, obs) + samples = np.arange(1 - self.length, self.length) + time_shift = samples[xcorr.argmax()] + + # find number of minutes in time shift + try: #Fix for Drifter's data + step_sec = self.step.seconds + except AttributeError: + step_sec = self.step * 24.0 * 60.0 * 60.0 # converts matlabtime (in days) to seconds + lag = time_shift * step_sec / 60 + + if debug or self._debug: print "...altPhase done." + + return lag + + def getStats(self, phase_shift=False, debug=False): + """ + Returns each of the statistics in a dictionary. + """ + + stats = {} + stats['gear'] = self.gear + stats['RMSE'] = self.getRMSE() + stats['CF'] = self.getCF() + stats['SD'] = self.getSD() + stats['POF'] = self.getPOF() + stats['NOF'] = self.getNOF() + stats['MDPO'] = self.getMDPO() + stats['MDNO'] = self.getMDNO() + stats['skill'] = self.getWillmott() + if not phase_shift: + stats['phase'] = self.getPhase(debug=debug) + else: + stats['phase'] = 0.0 + #stats['phase'] = self.getPhase() + stats['CORR'] = self.getCORR() + stats['NRMSE'] = self.getNRMSE() + stats['NSE'] = self.getNSE() + stats['bias'] = self.getBias() + stats['SI'] = self.getSI() + stats['pbias'] = self.getPBIAS() + stats['MSE'] = self.getMSE() + stats['NMSE'] = self.getNMSE() + + if debug or self._debug: print "...getStats..." + + return stats + + def linReg(self, alpha=0.05, debug=False): + ''' + Does linear regression on the model data vs. recorded data. + + Gives a 100(1-alpha)% confidence interval for the slope + ''' + if debug or self._debug: print "linReg..." + # set stuff up to make the code cleaner + obs = self.observed + mod = self.model + obs_mean = np.mean(obs) + mod_mean = np.mean(mod) + n = mod.size + df = n - 2 + + # calculate square sums + SSxx = np.sum(mod**2) - np.sum(mod)**2 / n + SSyy = np.sum(obs**2) - np.sum(obs)**2 / n + SSxy = np.sum(mod * obs) - np.sum(mod) * np.sum(obs) / n + SSE = SSyy - SSxy**2 / SSxx + MSE = SSE / df + + # estimate parameters + slope = SSxy / SSxx + intercept = obs_mean - slope * mod_mean + sd_slope = np.sqrt(MSE / SSxx) + r_squared = 1 - SSE / SSyy + + # calculate 100(1 - alpha)% CI for slope + width = t.isf(0.5 * alpha, df) * sd_slope + lower_bound = slope - width + upper_bound = slope + width + slope_CI = (lower_bound, upper_bound) + + # calculate 100(1 - alpha)% CI for intercept + lower_intercept = obs_mean - lower_bound * mod_mean + upper_intercept = obs_mean - upper_bound * mod_mean + intercept_CI = (lower_intercept, upper_intercept) + + # estimate 100(1 - alpha)% CI for predictands + predictands = slope * mod + intercept + sd_resid = np.std(obs - predictands) + y_CI_width = t.isf(0.5 * alpha, df) * sd_resid * \ + np.sqrt(1 - 1 / n) + + # return data in a dictionary + data = {} + data['slope'] = slope + data['intercept'] = intercept + data['r_2'] = r_squared + data['slope_CI'] = slope_CI + data['intercept_CI'] = intercept_CI + data['pred_CI_width'] = y_CI_width + data['conf_level'] = 100 * (1 - alpha) + + if debug or self._debug: print "...linReg done." + + return data + + def crossVal(self, alpha=0.05, debug=False): + ''' + Performs leave-one-out cross validation on the linear regression. + + i.e. removes one datum from the set, redoes linreg on the training + set, and uses the results to attempt to predict the missing datum. + ''' + if debug or self._debug: print "crossVal..." + cross_error = np.zeros(self.model.size) + cross_pred = np.zeros(self.model.size) + model_orig = self.model + obs_orig = self.observed + time_orig = self.time + + if debug or self._debug: print "...loop through each element, remove it..." + for i in np.arange(self.model.size): + train_mod = np.delete(model_orig, i) + train_obs = np.delete(obs_orig, i) + train_time = np.delete(time_orig, i) + train_stats = TidalStats(train_mod, train_obs, train_time) + + # redo the linear regression and get parameters + param = train_stats.linReg(alpha) + slope = param['slope'] + intercept = param['intercept'] + + # predict the missing observed value and calculate error + pred_obs = slope * model_orig[i] + intercept + cross_pred[i] = pred_obs + cross_error[i] = abs(pred_obs - obs_orig[i]) + + # calculate PRESS and PRRMSE statistics for predicted data + if debug or self._debug: print "...predicted residual sum of squares and predicted RMSE..." + PRESS = np.sum(cross_error**2) + PRRMSE = np.sqrt(PRESS) / self.model.size + + # return data in a dictionary + data = {} + data['PRESS'] = PRESS + data['PRRMSE'] = PRRMSE + data['cross_pred'] = cross_pred + + if debug or self._debug: print "...crossVal done." + + return data + + def save_data(self, path=''): + df = pd.DataFrame(data={'time': self.times.ravel(), + 'observed':self.observed.ravel(), + 'modeled':self.model.ravel() }) + df.to_csv(path+str(self.kind)+'.csv') diff --git a/build/lib/pyseidon_dvt/validationClass/valReport.py b/build/lib/pyseidon_dvt/validationClass/valReport.py new file mode 100644 index 0000000..0a20da0 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/valReport.py @@ -0,0 +1,402 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +import os +import datetime +import matplotlib.cm as cmap +import numpy as np +from decimal import Decimal +from reportlab.lib import colors +from reportlab.lib.enums import TA_JUSTIFY +from reportlab.lib.pagesizes import A4, landscape +from reportlab.platypus import BaseDocTemplate, Frame, Paragraph, PageTemplate +from reportlab.platypus import Spacer, Table, Image, NextPageTemplate, PageBreak +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch, cm +from reportlab.lib.utils import ImageReader + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + + +def write_report(valClass, report_title="validation_report.pdf", debug=False): + """ + Write automatically and dynamically a report based on the validation processed + + Args: + valClass (pyseidon.Validation): Validation object + + Kwargs: + report_title (str): report file name + debug (bool): debug flag + """ + # tests + if not (hasattr(valClass, "Benchmarks") or hasattr(valClass, "HarmonicBenchmarks")): + raise PyseidonError("-Run validation function first-") + + benchflag = False + if hasattr(valClass, "Benchmarks"): + benchflag = True + + harmoflag = False + if hasattr(valClass, "HarmonicBenchmarks"): + harmoflag = True + + # date + now = datetime.date.today() + # initiate doc + doc = BaseDocTemplate(report_title, + pagesize=A4, + title="Validation Report " + now.strftime("- %d %B %Y"), + rightMargin=72,leftMargin=72, + topMargin=72,bottomMargin=18) + + # default style and frames + styles = getSampleStyleSheet() + styles.add(ParagraphStyle(name='Justify', alignment=TA_JUSTIFY)) + pgtemp = [] + imNb = -1 + + #normal frame as for SimpleFlowDocument + frameP = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal') # portray frame + frameL = Frame(doc.leftMargin * 0.75, doc.bottomMargin, doc.height, doc.width, id='normal') # landscape frame + + #Two Columns + frame1 = Frame(doc.leftMargin, doc.bottomMargin, doc.width/2-6, doc.height, id='col1') + frame2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin, doc.width/2-6, doc.height, id='col2') + + # initialize "story" list + story = [] + + # Title + story.append(Spacer(doc.width, A4[1]/3.0)) + story.append(Paragraph("Validation benchmarks and methodologies for FVCOM Hydrodynamic Model results", + styles['Title'])) + story.append(Spacer(doc.width, A4[1]/3.0)) + story.append(Paragraph(now.strftime("%B, %Y"), styles['BodyText'])) + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # Introduction + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + story.append(Paragraph("Introduction", styles['Heading1'])) + story.append(Paragraph("The following report is a summary of validation standards and \ + methodologies usually performed on the FVCOM model output data in comparison \ + to observed data. This collection and analysis of a set of \ + statistics mostly adhere to the benchmarks defined as standards for \ + hydrodynamic model validation by NOAA [1]. Additional statistics have been \ + added to provide additional clarity on the skill of the model [2, 3, 4]." + , styles['Justify'])) # , styles['BodyText'])) + story.append(Paragraph("The present validation set is performed the following variables:" + , styles['BodyText'])) + if benchflag: + story.append(Paragraph("Hydrodynamic quantities", styles['Italic'])) + story.append(Paragraph("el: Elevation (deviation from mean sea level, m)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("cubic_speed: Signed cubic flow speed (m/s)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("u: normal velocity component (m/s)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("v: tangent velocity component (m/s)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("vel: signed flow velocity (m/s)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("speed: flow speed (m/s)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("dir: Current direction (between -180 and 180 degrees)" + , styles['Bullet'], bulletText='-')) + if harmoflag: + story.append(Paragraph("Harmonic coefficients", styles['Italic'])) + story.append(Paragraph("A & A_ci: Amplitude and associated 95% confidence interval" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("g & g_ci: Greenwich phase lag and associated 95% confidence interval" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("theta & theta_ci: Current ellipse orientation angle and associated 95% confidence interval" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("Lsmin & Lsmin_ci: Current ellipse minor axis length and associated 95% confidence interval" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("Lsmaj & Lsmaj_ci: Current ellipse major axis length and associated 95% confidence interval" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("constituent: tidal constituent's name" + , styles['Bullet'], bulletText='-')) + story.append(Spacer(1, 12)) + if benchflag and harmoflag: + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # Statistics + if benchflag and harmoflag: + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + + story.append(Paragraph("Statistics", styles['Heading1'])) + if benchflag: + story.append(Paragraph("Following is a list of the statistics used to evaluate model skill, separated \ + into two categories: NOAA's Standard Suite of Statistics (SSS), and those added \ + (Additional)" + , styles['BodyText'])) + story.append(Paragraph("SSS", styles['Heading2'])) + story.append(Paragraph("RMSE: Root Mean Squared Error" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("NRMSE: Normalized Root Mean Squared Error (in %)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("MSE: Mean Square Error" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("NMSE: Normalized Mean Square Error (in %)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("SD: Standard Deviation of Error" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("CF(X): Central Frequency; percentage of error values that fall within the \ + range (-X, X)" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("POF(X): Positive Outlier Frequency; percentage of error values that fall \ + above X", + styles['Bullet'], bulletText='-')) + story.append(Paragraph("NOF(X): Negative Outlier Frequency; percentage of error values that fall \ + below -X" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("MDPO(X): Maximum Duration of Positive Outliers; longest number of \ + minutes during which consecutive errors fall above X" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("MDNO(X): Maximum Duration of Negative Outliers; longest number of \ + minutes during which consecutive errors fall below X" + , styles['Bullet'], bulletText='-')) + + story.append(Paragraph("Additional", styles['Heading2'])) + story.append(Paragraph("Willmott Skill: A measure of model adherence to observed data between \ + 0 and 1, with 0 being absolutely no adherence, and 1 being perfect" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("Phase: The phase shift (minutes) of model data that minimizes RMSE \ + across a timespan of +/-3hr" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("R2 (i.e. coefficient of determination): Measure of the strength of the linear \ + correlation between the model data and the observed data between 0 \ + and 1, with 0 being no correlation, and 1 being perfect correlation" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("bias: bias of the model, a measure of over/under-estimation" + , styles['Bullet'], bulletText='-')) + story.append(Paragraph("Pbias: percent bias between the model and the observed data" + , styles['Bullet'], bulletText='-')) + + if harmoflag: + story.append(Paragraph("The statistics reported in the 'Harmonic Analysis' section, \ + can be defined as the normalised error (in %) between the observed and simulated \ + harmonic analysis coefficients described in the 'Introduction' section." + , styles['BodyText'])) + + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # Results + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + story.append(Paragraph("Results", styles['Heading1'])) + story.append(Paragraph("The simulated and measured data sets, used in this document to generate the validation \ + benchmarks, cover a " + valClass.History[1].lower() + ".", styles['Justify'])) # , styles['BodyText'])) + story.append(Paragraph("The following map displays the location(s) as well as the type(s) of the measurement(s) \ + used in this validation report.", styles['Justify'])) # , styles['BodyText'])) + story.append(Spacer(1, 12)) + # Map: measurement's locations + imNb += 1 + savename = 'tmp_'+str(imNb)+'_plot.png' + lonmax = -1.0 * np.inf + lonmin = np.inf + latmax = -1.0 * np.inf + latmin = np.inf + for ii, coor in enumerate(valClass._coordinates): + lon = coor[0] + lat = coor[1] + if lon > lonmax: lonmax = lon + if lat > latmax: latmax = lat + if lon < lonmin: lonmin = lon + if lat < latmin: latmin = lat + # redefine colorbar min/max + margin = 0.01 + if valClass._multi_sim: + for sim in valClass._simulated: + try: + if 'fvcom' in sim.__module__: + fvcom = sim + break + except AttributeError: + continue + else: + fvcom = valClass._simulated + indices = np.where(np.logical_and( + np.logical_and(fvcom.Grid.lon[:] < lonmax + margin, + fvcom.Grid.lon[:] > lonmin - margin), + np.logical_and(fvcom.Grid.lat[:] < latmax + margin, + fvcom.Grid.lat[:] > latmin - margin)))[0] + cmax = fvcom.Grid.h[indices].max() + cmin = fvcom.Grid.h[indices].min() + fvcom.Plots.colormap_var(fvcom.Grid.h, + title='Bathymetric Map & Measurement location(s)', + cmax=cmax, cmin=cmin, isoline='var', mesh=False) + # redefine frame + fvcom.Plots._ax.set_xlim([lonmin - margin, lonmax + margin]) + fvcom.Plots._ax.set_ylim([latmin - margin, latmax + margin]) + color = cmap.rainbow(np.linspace(0, 1, len(valClass._coordinates))) + for ii, coor in enumerate(valClass._coordinates): + lon = coor[0] + lat = coor[1] + name = coor[2] + txt = str(ii) + fvcom.Plots._ax.scatter(lon, lat, label=name, + lw=2, s=50, color=color[ii]) + # fvcom.Plots._ax.annotate(txt, (lon, lat), size=20) + fvcom.Plots._ax.legend() + fvcom.Plots._fig.savefig(savename, format='png', bbox_inches='tight') + fvcom.Plots._fig.clear() + # image = Image(savename, width=doc.width, height=doc.height / 1.5) + # story.append(image) + story.append(get_image(savename, width=16*cm)) + + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # table's style + ts = [('ALIGN', (1,1), (-1,-1), 'CENTER'), + ('LINEABOVE', (0,0), (-1,0), 1, colors.black), + ('LINEBELOW', (0,0), (-1,0), 1, colors.black), + ('FONT', (0,0), (-1,0), 'Times-Bold')] + + if benchflag: + df = valClass.Benchmarks + values = df.values.tolist() + # clean up + for ii, l in enumerate(values): + for jj, e in enumerate(l): + if type(e) == float: + values[ii][jj] = float(Decimal("%.2f" % e)) + # Adding id. numbers + for ii in range(len(values)): + values[ii] = [str(ii + 1)] + values[ii] + lista = [['Id.'] + df.columns[:,].values.astype(str).tolist()] + values + nb_pages = -(-len(lista)/20) + if nb_pages == 1: + story.append(NextPageTemplate('landscape')) + story.append(PageBreak()) + story.append(Paragraph("Validation Benchmarks", styles['Heading2'])) + table = Table(lista, style=ts) # , repeatRows=1, rowSplitRange=20) + story.append(table) + pgtemp.append(PageTemplate(id='landscape', frames=frameL, onPage=make_landscape)) + else: + for i in range(nb_pages): + story.append(NextPageTemplate('landscape')) + story.append(PageBreak()) + if i == 0: + story.append(Paragraph("Validation Benchmarks", styles['Heading2'])) + table = Table(lista[i*20:(i+1)*20], style=ts) # , repeatRows=1, rowSplitRange=20) + else: + table = Table([lista[0]]+lista[(i*20)+1:(i+1)*20], style=ts) # , repeatRows=1, rowSplitRange=20) + story.append(table) + pgtemp.append(PageTemplate(id='landscape', frames=frameL, onPage=make_landscape)) + if harmoflag: + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + if type(valClass.HarmonicBenchmarks.elevation) != str: + story.append(Paragraph("Harmonic Analysis - Elevation", styles['Heading2'])) + df = valClass.HarmonicBenchmarks.elevation + values = df.values.tolist() + # clean up + for ii, l in enumerate(values): + for jj, e in enumerate(l): + if type(e) == float: + values[ii][jj] = float(Decimal("%.2f" % e)) + # Adding id. numbers + for ii in range(len(values)): + values[ii] = [str(ii+1)]+values[ii] + lista = [['Id.'] + df.columns[:,].values.astype(str).tolist()] + values + table = Table(lista, style=ts) + story.append(table) + if type(valClass.HarmonicBenchmarks.velocity) != str: + story.append(Paragraph("Harmonic Analysis - Velocity", styles['Heading2'])) + df = valClass.HarmonicBenchmarks.velocity + values = df.values.tolist() + # clean up + for ii, l in enumerate(values): + for jj, e in enumerate(l): + if type(e) == float: + values[ii][jj] = float(Decimal("%.2f" % e)) + lista = [df.columns[:,].values.astype(str).tolist()] + values + table = Table(lista, style=ts) + story.append(table) + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # Taylor diagram + if benchflag: + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + story.append(Paragraph("Taylor Diagram", styles['Heading2'])) + story.append(Paragraph("The Taylor diagram below is a concise statistical summary of how well patterns match each \ + other in terms of their correlation, their root-mean-square difference and the ratio of \ + their variances.", styles['Justify'])) # , styles['BodyText'])) + story.append(Paragraph("Measurements' identification numbers (Id.) \ + can be found in the table above.", styles['Justify'])) # , styles['BodyText'])) + story.append(Spacer(1, 12)) + imNb += 1 + savename = 'tmp_'+str(imNb)+'_plot.png' + valClass.taylor_diagram(savepath="./", fname=savename, labels=False) + # image = Image(savename, width=doc.width , height=doc.height / 2.25) + # story.append(image) + story.append(get_image(savename, width=16*cm)) + + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # References + story.append(NextPageTemplate('OneCol')) + story.append(PageBreak()) + story.append(Paragraph("References", styles['Heading1'])) + story.append(Paragraph("K. W. Hess, T. F. Gross, R. A. Schmalz, J. G. Kelley, F. Aikman and E. Wei, \ + NOS standards for evaluating operational nowcast and forecst hydrodynamic model systems, \ + National Oceanic and Atmospheric Administration, Silver Srping, Maryland, 2003.", + styles['Italic'], bulletText='[1]')) + story.append(Paragraph("K. Gunn and C. Stock-Williams, On validating numerical hydrodynamic models of complex \ + tidal flow, International Journal of Marine Energy, Vols. 3-4, no. Special, pp. 82-97, \ + 2013.", + styles['Italic'], bulletText='[2]')) + story.append(Paragraph("N. Georgas and A. F. Blumberg, Establishing Confidence in Marine Forecast Systems: \ + The Design and Skill Assessment of the New York Harbor Observation and Prediction System, \ + Version 3 (NYHOPS v3), in 11th International Conference on Estuarine and Coastal Modeling, \ + Seattle, Washington, United States, 2010.", + styles['Italic'], bulletText='[3]')) + story.append(Paragraph("Y. Liu, P. MacCready, H. M. Barbara, E. P. Dever, M. Kosro and N. S. Banas, \ + Evaluation of a coastal ocean circulation model for the Columbia River plume in summer \ + 2004, Journal of Geophysical Research, vol. 114, no. C2, p. 1978–2012, 2009.", + styles['Italic'], bulletText='[4]')) + + pgtemp.append(PageTemplate(id='OneCol', frames=frameP, onPage=footpagenumber)) + + # start the construction of the pdf + doc.addPageTemplates(pgtemp) + doc.build(story) + # remove tmp plots + for ii in range(imNb+1): + savename = 'tmp_'+str(ii)+'_plot.png' + os.remove(savename) + + +def footpagenumber(canvas, doc): + """ + Foot note and page number + """ + canvas.saveState() + canvas.setFont('Times-Roman', 9) + canvas.drawString(A4[0]/2.0, 0.75 * inch, "%d" % doc.page) + canvas.restoreState() + +def make_landscape(canvas,doc): + """ + Foot note and page number plus landscape page formatting + """ + canvas.saveState() + canvas.setPageSize(landscape(A4)) + canvas.setFont('Times-Roman', 9) + canvas.drawString(A4[1]/2.0, 0.75 * inch, "%d" % doc.page) + canvas.restoreState() + +def get_image(path, width=1*cm): + """ + Read image from file and fit its size to the given width + """ + img = ImageReader(path) + iw, ih = img.getSize() + aspect = ih / float(iw) + return Image(path, width=width, height=(width * aspect)) \ No newline at end of file diff --git a/build/lib/pyseidon_dvt/validationClass/valTable.py b/build/lib/pyseidon_dvt/validationClass/valTable.py new file mode 100644 index 0000000..09fd58e --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/valTable.py @@ -0,0 +1,75 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 +import pandas as pd +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +# ALTERNATE VERSION FOR ANDY + +def valTable(struct, suites, save_path, filename, vars, save_csv=False, debug=False, debug_plot=False): + ''' + Takes validation data from the struct and saves it into a .csv file . + + Takes a single argument, a dictionary + ''' + # initialize lists + kind, name, RMSE, CF, SD, POF, NOF, MDPO, MDNO, skill, r2, phase = \ + [], [], [], [], [], [], [], [], [], [], [], [] + bias, pbias, NRMSE, NSE, MSE, NMSE, corr, SI, gear = [], [], [], [], [], [], [], [], [] + + # append to the lists the stats from each site for each variable + for var in vars: + (kind, name, RMSE, CF, SD, POF, NOF, MDPO, MDNO, skill, r2, phase, bias, pbias, NRMSE, NSE, MSE, NMSE, corr, SI, gear) \ + = siteStats(struct, suites, var, kind, name, RMSE, CF, SD, POF, NOF, MDPO, MDNO, skill, r2, phase, + bias, pbias, NRMSE, NSE, MSE, NMSE, corr, SI, gear, debug=False, debug_plot=False) + + # put stats into dict and create dataframe + val_dict = {'Type': kind, 'RMSE': RMSE, 'NRMSE': NRMSE, 'CF': CF, 'SD': SD, 'POF': POF, + 'NOF': NOF, 'MDPO': MDPO, 'MDNO': MDNO, 'skill': skill, 'r2': r2, 'phase': phase, + 'bias': bias, 'pbias': pbias, 'NSE': NSE, 'MSE': MSE, 'NMSE': NMSE, + 'corr': corr, 'SI': SI, 'gear': gear} + + table = pd.DataFrame(data=val_dict, index=name, columns=val_dict.keys()) + + # export as .csv file + if save_csv: + out_file = '{}{}_val.csv'.format(save_path, filename) + table.to_csv(out_file) + return table + +def siteStats(site, suites, variable, type, name, RMSE, CF, SD, POF, NOF, MDPO, MDNO, skill, r2, phase, + bias, pbias, NRMSE, NSE, MSE, NMSE, corr, SI, gear, debug=False, debug_plot=False): + """ + Takes in the run (an array of dictionaries) and the type of the run (a + string). Also takes in the list representing each statistic. + """ + if debug: print "siteStats..." + + stats = suites['{}'.format(variable)] + type.append(variable) + name.append(site['name'].split('/')[-1].split('.')[0]) + + + # add the statistics to the list, round to 2 decimal places + RMSE.append(round(stats['RMSE'], 2)) + CF.append(round(stats['CF'], 2)) + SD.append(round(stats['SD'], 2)) + POF.append(round(stats['POF'], 2)) + NOF.append(round(stats['NOF'], 2)) + MDPO.append(stats['MDPO']) + MDNO.append(stats['MDNO']) + skill.append(round(stats['skill'], 2)) + r2.append(round(stats['r_squared'], 2)) + phase.append(stats['phase']) + bias.append(round(stats['bias'], 2)) + pbias.append(round(stats['pbias'], 2)) + NRMSE.append(round(stats['NRMSE'], 2)) + NSE.append(round(stats['NSE'], 2)) + MSE.append(round(stats['MSE'], 2)) + NMSE.append(round(stats['NMSE'], 2)) + corr.append(round(stats['CORR'], 2)) + SI.append(round(stats['SI'], 2)) + gear.append(site['type']) + if debug: print "...siteStats done." + + return (type, name, RMSE, CF, SD, POF, NOF, MDPO, MDNO, skill, r2, phase, bias, pbias, NRMSE, NSE, MSE, NMSE, corr, SI, gear) diff --git a/build/lib/pyseidon_dvt/validationClass/validationClass.py b/build/lib/pyseidon_dvt/validationClass/validationClass.py new file mode 100644 index 0000000..27d0412 --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/validationClass.py @@ -0,0 +1,760 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division + +import numpy as np +import pandas as pd +import cPickle as pkl +from os import makedirs +from os.path import exists + +# Polar Plots +from math import * +from cmath import * + +# Quick fix +from scipy.io import savemat +from utide import solve + +# Local import +from compareData import * +from valTable import valTable +from variablesValidation import _load_validation +from pyseidon_dvt.utilities.interpolation_utils import * + +# Local import +from plotsValidation import taylorDiagram, benchmarksMap +from valReport import write_report +from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + + +class Validation: + """ + **Validation class/structure** + + Class structured as follows: :: + + _History = Quality Control metadata + |_Variables. = observed and simulated variables and quantities + |_validate_data = validation method/function against timeseries + Validation._|_validate_harmonics = validation method/function against + | harmonic coefficients + |_Save_as = "save as" function + + Inputs: + - observed = standalone or list of PySeidon measurement object (i.e. ADCP, TideGauge, Drifter,...) + - simulated = standalone or list of any PySeidon simulation object (i.e. FVCOM or Station) + Option: + - flow = impose flow comparison by surface flow ('sf'), depth-averaged flow ('daf') or at any depth (float) + , if negative = from sea surface downwards, if positive = from sea bottom upwards + - nn = if True then use the nearest location in the grid if the location is outside the grid. + - outpath = specify a path to save validation results (default = './') + """ + def __init__(self, observed, simulated, flow=[], nn=True, outpath='./', debug=False, debug_plot=False): + self._debug = debug + self._flow = flow + self._coordinates = [] + self._fig = None + self._ax = None + if type(observed) in [tuple, list]: + self._multi_meas = True + else: + self._multi_meas = False + if type(simulated) in [tuple, list]: + self._multi_sim = True + else: + self._multi_sim = False + self._debug_plot = debug_plot + if debug: print '-Debug mode on-' + self._nn=nn + if debug and nn: print '-Using nearest neighbour-' + # creates folder to store outputs + outpath=outpath.replace(" ","_") + if outpath[-1] is not '/': + self._outpath = outpath+'/' + else: + self._outpath = outpath + if not self._outpath == './': + while exists(self._outpath): + self._outpath = self._outpath[:-1] + '_bis/' + + #Metadata + if not self._multi_sim: + sim_origin = simulated._origin_file + else: + sim_origin = 'multiple simulation objects' + if not self._multi_meas: + self.History = ['Created from ' + observed._origin_file +\ + ' and ' + sim_origin] + else: + self.History = ['Created from multiple measurement sources' +\ + ' and ' + sim_origin] + if (not self._multi_meas) and (not self._multi_sim): + self._observed = observed + self._simulated = simulated + self.Variables = _load_validation(self._outpath, self._observed, self._simulated, flow=self._flow, nn=self._nn, debug=self._debug) + self._coordinates.append([np.mean(self.Variables.obs.lon), np.mean(self.Variables.obs.lat), self.Variables._obstype]) + + # -Append message to History field + start = mattime_to_datetime(self.Variables.obs.matlabTime[self.Variables._c[0]]) + end = mattime_to_datetime(self.Variables.obs.matlabTime[self.Variables._c[-1]]) + text = 'Temporal domain from ' + str(start) + ' to ' + str(end) + self.History.append(text) + elif (self._multi_meas) and (not self._multi_sim): + self._observed = observed + self._simulated = [simulated] + elif (not self._multi_meas) and (self._multi_sim): + self._observed = [observed] + self._simulated = simulated + else: + self._observed = observed + self._simulated = simulated + + # Creates folder once compatibility test passed + if not self._outpath == './': + makedirs(self._outpath) + if debug: print '-Saving results to {}-'.format(self._outpath) + return + + def _validate_data(self, filename=[], depth=[], slack_velo=0.1, plot=False, save_csv=False, phase_shift=False, + debug=False, debug_plot=False): + """ + This method computes series of standard validation benchmarks. + + Options: + - filename = file name of the .csv file to be saved, string. + - depth = depth at which the validation will be performed, float. + Only applicable for 3D simulations. + - slack_velo = slack water's velocity (m/s), float, everything below will be dumped out + - plot = plot series of validation graphs, boolean. + as well as associated plots in specific folder + - phase_shift = applies phase shift correction to model quantities + + + *References* + - NOAA. NOS standards for evaluating operational nowcast and + forecast hydrodynamic model systems, 2003. + + - K. Gunn, C. Stock-Williams. On validating numerical hydrodynamic + models of complex tidal flow, International Journal of Marine Energy, 2013 + + - N. Georgas, A. Blumberg. Establishing Confidence in Marine Forecast + Systems: The design and skill assessment of the New York Harbor Observation + and Prediction System, version 3 (NYHOPS v3), 2009 + + - Liu, Y., P. MacCready, B. M. Hickey, E. P. Dever, P. M. Kosro, and + N. S. Banas (2009), Evaluation of a coastal ocean circulation model for + the Columbia River plume in summer 2004, J. Geophys. Res., 114 + """ + debug = debug or self._debug + debug_plot = debug_plot or self._debug_plot + # User input + if filename == []: + filename = raw_input('Enter filename for csv file: ') + filename = str(filename) + if type(self._flow) == float: + depth = self._flow + if (depth == [] and self.Variables._3D): + depth = input('Depth from surface at which the validation will be performed: ') + depth = float(depth) + if depth == []: depth = 5.0 + + + # Harmonically reconstruct simulation properties at the original observed matlabTime if harmo is on + if self.Variables.harmo['On']: + observed = self.Variables.harmo['Observed'] + simulated = self.Variables.harmo['Simulated'] + # Harmonic Analysis + if self.Variables._simtype == 'station': + harmo_simEl = simulated.Util2D.Harmonic_analysis_at_point(self.Variables.harmo['nameSite'], elevation=True, velocity=False) + harmo_simVel = simulated.Util2D.Harmonic_analysis_at_point(self.Variables.harmo['nameSite'], elevation=False, velocity=True) + elif self.Variables._simtype == 'fvcom': + harmo_simEl = simulated.Util2D.Harmonic_analysis_at_point(observed.Variables.lon, observed.Variables.lat, elevation=True, velocity=False) + harmo_simVel = simulated.Util2D.Harmonic_analysis_at_point(observed.Variables.lon, observed.Variables.lat, elevation=False, velocity=True) + else: + raise PyseidonError("--Harmonic Analysis not possible with this type of measurement--") + # Reconstruction at recon_time + simEl = simulated.Util2D.Harmonic_reconstruction(harmo_simEl, recon_time=observed.Variables.matlabTime) + simVel = simulated.Util2D.Harmonic_reconstruction(harmo_simVel, recon_time=observed.Variables.matlabTime) + # Pass reconstructed timeseries to variables structure for validation + self.Variables.struct['mod_timeseries']['ua'] = simVel['u'] + self.Variables.struct['mod_timeseries']['va'] = simVel['v'] + self.Variables.struct['mod_timeseries']['el'] = simEl['h'] + self.Variables.struct['mod_time'] = observed.Variables.matlabTime + + #initialisation + vars = [] + self.Suites={} + threeD = self.Variables.sim._3D + if self._flow == 'daf': threeD = False + + if self.Variables.struct['type'] == 'ADCP': + suites = compareOBS(self.Variables.struct, self.Variables._save_path, threeD, + plot=plot, depth=depth, slack_velo=slack_velo, save_csv=save_csv, + phase_shift=phase_shift, debug=debug, debug_plot=debug_plot) + + for key in suites: + self.Suites[key] = suites[key] + vars.append(key) + + elif self.Variables.struct['type'] == 'TideGauge': + suites = compareOBS(self.Variables.struct, self.Variables._save_path, + plot=plot, slack_velo=slack_velo, save_csv=save_csv, + phase_shift=phase_shift, debug=debug, debug_plot=debug_plot) + for key in suites: + self.Suites[key] = suites[key] + vars.append(key) + + elif self.Variables.struct['type'] == 'Drifter': + suites = compareOBS(self.Variables.struct, self.Variables._save_path, self.Variables._3D, + depth=depth, plot=plot, slack_velo=slack_velo, save_csv=save_csv, + phase_shift=phase_shift, debug=debug, debug_plot=debug_plot) + + for key in suites: + self.Suites[key] = suites[key] + vars.append(key) + + else: + raise PyseidonError("-This kind of measurements is not supported yet-") + + # Make csv file + self._Benchmarks = valTable(self.Variables.struct, self.Suites, self.Variables._save_path, filename, vars, + save_csv=save_csv, debug=debug, debug_plot=debug_plot) + + # Display csv + print "---Validation benchmarks---" + pd.set_option('display.max_rows', len(self._Benchmarks)) + print(self._Benchmarks) + pd.reset_option('display.max_rows') + + # Reload _load_validation to return original values + # Is this even needed? -Jeremy + #print '-- Reloading Validation Variables --' + #self.Variables = _load_validation(self._outpath, self._observed, self._simulated, flow=self._flow, nn=self._nn, debug=self._debug) + + def _validate_harmonics(self, filename='', save_csv=False, debug=False, debug_plot=False): + """ + This method computes and store in a csv file the error in % + for each component of the harmonic analysis (i.e. *_error.csv). + + Options: + filename: file name of the .csv file to be saved, string. + save_csv: will save both observed and modeled harmonic + coefficients into *.csv files (i.e. *_harmo_coef.csv) + """ + # check if measurement object is a Drifter + if self.Variables.struct['type'] == 'Drifter': + print "--- Harmonic analysis does not work with Drifter's data ---" + return + # define attributes + if not hasattr(self, "_HarmonicBenchmarks"): + self._HarmonicBenchmarks = HarmonicBenchmarks() + else: + delattr(self, '_HarmonicBenchmarks') + self._HarmonicBenchmarks = HarmonicBenchmarks() + # User input + if filename==[]: + filename = raw_input('Enter filename for csv file: ') + filename = str(filename) + + hasEL=False + hasUV=False + commonlist_data = self.Variables.struct['_commonlist_data'] + if 'el' in commonlist_data: + hasEL=True + + ulist=[var for var in ['ua', 'u'] if var in commonlist_data ] + vlist=[var for var in ['va', 'v'] if var in commonlist_data ] + if len(ulist)>0 and len(vlist)>0: + hasUV=True + + # Harmonic analysis over matching & non-matching time + obs_time = self.Variables.struct['obs_time'] + obs_lat = self.Variables.struct['obs_lat'] + mod_time = self.Variables.struct['mod_time'] + mod_lat = self.Variables.struct['mod_lat'] + if hasEL: + obs_el = self.Variables.struct['obs_timeseries']['el'][:] + mod_el = self.Variables.struct['mod_timeseries']['el'][:] + self.Variables.obs.elCoef = solve(obs_time, obs_el, None, obs_lat, + constit='auto', trend=False, Rayleigh_min=0.95, + method='ols', conf_int='linear') + self.Variables.sim.elCoef = solve(mod_time, mod_el, None, mod_lat, + constit='auto', trend=False, Rayleigh_min=0.95, + method='ols', conf_int='linear') + + if hasUV: + obs_ua = self.Variables.struct['obs_timeseries']['ua'][:] + obs_va = self.Variables.struct['obs_timeseries']['va'][:] + mod_ua = self.Variables.struct['mod_timeseries']['ua'][:] + mod_va = self.Variables.struct['mod_timeseries']['va'][:] + self.Variables.obs.velCoef = solve(obs_time, obs_ua, obs_va, obs_lat, + constit='auto', trend=False, Rayleigh_min=0.95, + method='ols', conf_int='linear') + self.Variables.sim.velCoef = solve(mod_time, mod_ua, mod_va, mod_lat, + constit='auto', trend=False, Rayleigh_min=0.95, + method='ols', conf_int='linear') + + + # find matching and non-matching coef & hold their magnitude and phase for comparison + matchElCoef = [] # Coefficient Names + matchElCoefInd = [] # Coefficient Names' Indexes + matchEl_Adiff = [] # Coefficents' Maginitude Difference + matchEl_gdiff = [] # Coefficients' Phase Difference + for i1, key1 in enumerate(self.Variables.sim.elCoef['name']): + for i2, key2 in enumerate(self.Variables.obs.elCoef['name']): + if key1 == key2: + matchElCoefInd.append((i1,i2)) + matchElCoef.append(key1) + matchEl_Adiff.append(abs(self.Variables.sim.elCoef['A'][i1]*exp(1j*radians(self.Variables.sim.elCoef['g'][i1])) - + self.Variables.obs.elCoef['A'][i2]*exp(1j*radians(self.Variables.obs.elCoef['g'][i2])))) + matchEl_gdiff.append(abs(self.Variables.sim.elCoef['g'][i1] - self.Variables.obs.elCoef['g'][i2])) + matchElCoefInd=np.array(matchElCoefInd) + noMatchElCoef = np.delete(self.Variables.sim.elCoef['name'], matchElCoefInd[:,0]) + np.hstack((noMatchElCoef, np.delete(self.Variables.obs.elCoef['name'], matchElCoefInd[:,1]))) + + # Repeat for Velocity Coefficients + matchVelCoef = [] + matchVelCoefInd = [] + matchVel_LsmajDiff = [] + matchVel_LsminDiff = [] + matchVel_gdiff = [] + try: + for i1, key1 in enumerate(self.Variables.sim.velCoef['name']): + for i2, key2 in enumerate(self.Variables.obs.velCoef['name']): + if key1 == key2: + matchVelCoefInd.append((i1, i2)) + matchVelCoef.append(key1) + matchVel_LsmajDiff.append(abs(self.Variables.sim.velCoef['Lsmaj'][i1]*exp(1j*radians(self.Variables.sim.velCoef['g'][i1])) - + self.Variables.obs.velCoef['Lsmaj'][i2]*exp(1j*radians(self.Variables.obs.velCoef['g'][i2])))) + matchVel_LsminDiff.append(abs(self.Variables.sim.velCoef['Lsmin'][i1]*exp(1j*radians(self.Variables.sim.velCoef['g'][i1])) - + self.Variables.obs.velCoef['Lsmin'][i2]*exp(1j*radians(self.Variables.obs.velCoef['g'][i2])))) + matchVel_gdiff.append(abs(self.Variables.sim.velCoef['g'][i1] - self.Variables.obs.velCoef['g'][i2])) + matchVelCoefInd = np.array(matchVelCoefInd) + noMatchVelCoef = np.delete(self.Variables.sim.velCoef['name'], matchVelCoefInd[:, 0]) + np.hstack((noMatchVelCoef, np.delete(self.Variables.obs.velCoef['name'], matchVelCoefInd[:, 1]))) + except AttributeError: + pass + + # Compare largest ten obs. vs sim. coefficients visually on Polar plots + # I would prefer to have these plots saved to the directory created by validate_harmonics() + top10_el = zip(matchEl_Adiff, matchElCoef, matchEl_gdiff) + top10_el.sort() + top10_el = top10_el[-10:] + top10_el = zip(*top10_el) + plt.axes(polar=True) + plt.plot(top10_el[2], top10_el[0], 'r.') + for i, txt in enumerate(top10_el[1]): + plt.annotate(top10_el[1][i], (str(top10_el[2][i]), str(top10_el[0][i])), size=8) + plt.title('Harmonic Analysis Elevation Coefficient comparison') + plt.savefig('HarmoEl_coeffcompare', format='png') + plt.clf() + + top10_vel_Lsmaj = zip(matchVel_LsmajDiff, matchVelCoef, matchVel_gdiff) + top10_vel_Lsmaj.sort() + top10_vel_Lsmaj = top10_vel_Lsmaj[-10:] + top10_vel_Lsmaj = zip(*top10_vel_Lsmaj) + plt.axes(polar=True) + plt.plot(top10_vel_Lsmaj[2], top10_vel_Lsmaj[0], 'r.') + for i, txt in enumerate(top10_vel_Lsmaj[1]): + plt.annotate(top10_vel_Lsmaj[1][i], (str(top10_vel_Lsmaj[2][i]), str(top10_vel_Lsmaj[0][i])), size=8) + plt.title('Harmonic Analysis Lsmaj Velocity Coefficient comparison') + plt.savefig('HarmoVel_Lsmaj_coeffcompare', format='png') + plt.clf() + + top10_vel_Lsmin = zip(matchVel_LsminDiff, matchVelCoef, matchVel_gdiff) + top10_vel_Lsmin.sort() + top10_vel_Lsmin = top10_vel_Lsmin[-10:] + top10_vel_Lsmin = zip(*top10_vel_Lsmin) + plt.axes(polar=True) + plt.plot(top10_vel_Lsmin[2], top10_vel_Lsmin[0], 'r.') + for i, txt in enumerate(top10_vel_Lsmin[1]): + plt.annotate(top10_vel_Lsmin[1][i], (str(top10_vel_Lsmin[2][i]), str(top10_vel_Lsmin[0][i])), size=8) + plt.title('Harmonic Analysis Lsmin Velocity Coefficient comparison') + plt.savefig('HarmoVel_Lsmin_coeffcompare', format='png') + plt.clf() + + # Compare obs. vs. sim. elevation harmo coef + data = {} + columns = ['A', 'g', 'A_ci', 'g_ci'] + measname = self.Variables.struct['name'].split('/')[-1].split('.')[0] + + # Store harmonics in csv files + if save_csv and hasEL: + # observed elevation coefs + for key in columns: + data[key] = self.Variables.obs.elCoef[key] + data['constituent'] = self.Variables.obs.elCoef['name'] + measlist = [measname] * len(data['constituent']) + table = pd.DataFrame(data=data, index=measlist, + columns=columns + ['constituent']) + # export as .csv file + out_file = '{}{}_obs_el_harmo_coef.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + data = {} + + #modeled elevation coefs + for key in columns: + data[key] = self.Variables.sim.elCoef[key] + data['constituent'] = self.Variables.sim.elCoef['name'] + measlist = [measname] * len(data['constituent']) + table = pd.DataFrame(data=data, index=measlist, + columns=columns + ['constituent']) + # export as .csv file + out_file = '{}{}_sim_el_harmo_coef.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + data = {} + + # error in % + if hasEL: + if not matchElCoef==[]: + for key in columns: + b=self.Variables.sim.elCoef[key][matchElCoefInd[:,0]] + a=self.Variables.obs.elCoef[key][matchElCoefInd[:,1]] + err = abs((a-b)/a) * 100.0 + data[key] = err + data['constituent'] = matchElCoef + measlist = [measname] * len(matchElCoef) + ##create table + table = pd.DataFrame(data=data, index=measlist, columns=columns + ['constituent']) + ##export as .csv file + out_file = '{}{}_el_harmo_error.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + ##print non-matching coefs + if not noMatchElCoef.shape[0]==0: + print "Non-matching harmonic coefficients for elevation: ", noMatchElCoef + else: + print "-No matching harmonic coefficients for elevation-" + + # save dataframe in attribute + self._HarmonicBenchmarks.elevation = table + + #Compare obs. vs. sim. velocity harmo coef + data = {} + columns = ['Lsmaj', 'g', 'theta_ci', 'Lsmin_ci', + 'Lsmaj_ci', 'theta', 'g_ci'] + + #Store harmonics in csv files + if save_csv and hasUV: + #observed elevation coefs + for key in columns: + data[key] = self.Variables.obs.velCoef[key] + data['constituent'] = matchVelCoef + measlist = [measname] * len(data['constituent']) + table = pd.DataFrame(data=data, index=measlist, + columns=columns + ['constituent']) + ##export as .csv file + out_file = '{}{}_obs_velo_harmo_coef.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + data = {} + + #modeled elevation coefs + for key in columns: + data[key] = self.Variables.sim.velCoef[key] + data['constituent'] = matchVelCoef + measlist = [measname] * len(data['constituent']) + table = pd.DataFrame(data=data, index=measlist, + columns=columns + ['constituent']) + ##export as .csv file + out_file = '{}{}_sim_velo_harmo_coef.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + data = {} + + ##error in % + if hasUV: + if not matchVelCoef==[]: + for key in columns: + b=self.Variables.sim.velCoef[key][matchVelCoefInd[:,0]] + a=self.Variables.obs.velCoef[key][matchVelCoefInd[:,1]] + err = abs((a-b)/a) * 100.0 + data[key] = err + data['constituent'] = matchVelCoef + measlist = [measname] * len(matchVelCoef) + ##create table + table = pd.DataFrame(data=data, index=measlist, columns=columns + ['constituent']) + ##export as .csv file + out_file = '{}{}_vel0_harmo_error.csv'.format(self.Variables._save_path, filename) + table.to_csv(out_file) + ##print non-matching coefs + if not noMatchVelCoef.shape[0]==0: + print "Non-matching harmonic coefficients for velocity: ", noMatchVelCoef + else: + print "-No matching harmonic coefficients for velocity-" + + # save dataframe in attribute + self._HarmonicBenchmarks.velocity = table + + def validate_data(self, filename=[], depth=[], slack_velo=0.1, plot=False, save_csv=False, phase_shift=False, + debug=False, debug_plot=False): + """ + This method computes series of standard validation benchmarks. + + Options: + - filename = file name of the .csv file to be saved, string. + - depth = depth at which the validation will be performed, float. + Only applicable for 3D simulations. + If negative = from sea surface downwards, if positive = from sea bottom upwards + - slack_velo = slack water's velocity (m/s), float, everything below will be dumped out + - plot = plot series of validation graphs, boolean. + - save_csv = will save benchmark values into *.csv file + as well as associated plots in specific folder + - phase_shift = applies phase shift correction to model quantities + + *References* + - NOAA. NOS standards for evaluating operational nowcast and + forecast hydrodynamic model systems, 2003. + + - K. Gunn, C. Stock-Williams. On validating numerical hydrodynamic + models of complex tidal flow, International Journal of Marine Energy, 2013 + + - N. Georgas, A. Blumberg. Establishing Confidence in Marine Forecast + Systems: The design and skill assessment of the New York Harbor Observation + and Prediction System, version 3 (NYHOPS v3), 2009 + + - Liu, Y., P. MacCready, B. M. Hickey, E. P. Dever, P. M. Kosro, and + N. S. Banas (2009), Evaluation of a coastal ocean circulation model for + the Columbia River plume in summer 2004, J. Geophys. Res., 114 + """ + if (not self._multi_meas) and (not self._multi_sim): + self._validate_data(filename, depth, slack_velo, plot, save_csv, phase_shift, debug, debug_plot) + self.Benchmarks = self._Benchmarks + else: + I=0 + for sim in self._simulated: + for meas in self._observed: + try: + self.Variables = _load_validation(self._outpath, meas, sim, flow=self._flow, debug=self._debug) + + # -Append message to History field + start = mattime_to_datetime(self.Variables.obs.matlabTime[self.Variables._c[0]]) + end = mattime_to_datetime(self.Variables.obs.matlabTime[self.Variables._c[-1]]) + text = 'Temporal domain from ' + str(start) + ' to ' + str(end) + try: + self.History[1] = text + except IndexError: + self.History.append(text) + + self._validate_data(filename, depth, slack_velo, plot, save_csv, phase_shift, debug, debug_plot) + if I == 0: + self.Benchmarks = self._Benchmarks + I += 1 + else: + self.Benchmarks = pd.concat([self.Benchmarks, self._Benchmarks]) + + self._coordinates.append([np.mean(self.Variables.obs.lon), + np.mean(self.Variables.obs.lat), + self.Variables._obstype]) + except PyseidonError: + #except: # making it even more permissive + print "Error with measurement object "+meas.History[0] + continue + if save_csv: + #if self._multi_meas: + # savepath = self.Variables._save_path[:(self.Variables._save_path[:-1].rfind('/')+1)] + #else: + # savepath = self.Variables._save_path + savepath = self._outpath + #savepath = self.Variables._save_path + try: + out_file = '{}{}_benchmarks.csv'.format(savepath, filename) + self.Benchmarks.to_csv(out_file) + except AttributeError: + raise PyseidonError("-No matching measurement-") + + def validate_harmonics(self, filename=[], save_csv=False, debug=False, debug_plot=False): + """ + This method computes and store in a csv file the error in % + for each component of the harmonic analysis (i.e. *_error.csv). + + Options: + filename: file name of the .csv file to be saved, string. + save_csv: will save both observed and modeled harmonic + coefficients into *.csv files (i.e. *_harmo_coef.csv) + """ + if (not self._multi_meas) and (not self._multi_sim): + self.Variables = _load_validation(self._outpath, self._observed, self._simulated, flow=self._flow, debug=self._debug) + self._validate_harmonics(filename, save_csv, debug, debug_plot) + self.HarmonicBenchmarks = self._HarmonicBenchmarks + else: + I=0 + J=0 + for sim in self._simulated: + for meas in self._observed: + try: + self.Variables = _load_validation(self._outpath, meas, sim, flow=self._flow, debug=self._debug) + self._validate_harmonics(filename, save_csv, debug, debug_plot) + if I == 0 and J == 0: + self.HarmonicBenchmarks = HarmonicBenchmarks() + if I == 0 and type(self._HarmonicBenchmarks.elevation) != str: + self.HarmonicBenchmarks.elevation = self._HarmonicBenchmarks.elevation + I += 1 + elif I != 0 and type(self._HarmonicBenchmarks.elevation) != str: + self.HarmonicBenchmarks.elevation = pd.concat([self.HarmonicBenchmarks.elevation, + self._HarmonicBenchmarks.elevation]) + if J == 0 and type(self._HarmonicBenchmarks.velocity) != str: + self.HarmonicBenchmarks.velocity = self._HarmonicBenchmarks.velocity + J += 1 + elif J != 0 and type(self._HarmonicBenchmarks.velocity) != str: + self.HarmonicBenchmarks.velocity = pd.concat([self.HarmonicBenchmarks.velocity, + self._HarmonicBenchmarks.velocity]) + except PyseidonError: + pass + + # Drop duplicated lines + self.HarmonicBenchmarks.velocity.drop_duplicates(inplace=True) + self.HarmonicBenchmarks.elevation.drop_duplicates(inplace=True) + + if save_csv: + #if self._multi_meas: + # savepath = self.Variables._save_path[:(self.Variables._save_path[:-1].rfind('/')+1)] + #else: + # savepath = self.Variables._save_path + savepath = self._outpath + try: + try: + out_file = '{}{}_elevation_harmonic_benchmarks.csv'.format(savepath, filename) + self.HarmonicBenchmarks.elevation.to_csv(out_file) + except AttributeError: + pass + try: + out_file = '{}{}_velocity_harmonic_benchmarks.csv'.format(savepath, filename) + self.HarmonicBenchmarks.velocity.to_csv(out_file) + except AttributeError: + pass + except AttributeError: + raise PyseidonError("-No matching measurement-") + + def taylor_diagram(self, savepath='', fname="taylor_diagram", labels=True, debug=False): + """ + Plots Taylor diagram based on the results of 'validate_data' + + Options: + - savepath = folder path for saving plot, string + - fname = filename for saving plot, string + - labels = labels and legend, boolean + """ + try: + self._fig, self._ax = taylorDiagram(self.Benchmarks, + savepath=savepath, fname=fname, labels=labels, debug=debug) + except AttributeError: + raise PyseidonError("-validate_data needs to be run first-") + + def benchmarks_map(self, savepath='', fname="benchmarks_map", debug=False): + """ + Plots bathymetric map & model validation benchmarks + + Options: + - savepath = folder path for saving plot, string + - fname = filename for saving plot, string + + Note: this function shall work only if ADCP object(s) and FVCOM object + have been used as inputs + """ + if not self._simulated.__module__.split('.')[-1] == 'fvcomClass': + raise PyseidonError("---work only with a combination ADCP object(s) and FVCOM object---") + try: + benchmarksMap(self.Benchmarks, self._observed, self._simulated, savepath=savepath, fname=fname, debug=debug) + except AttributeError: + raise PyseidonError("---validate_data needs to be run first---") + + def save_as(self, filename, fileformat='pickle', debug=False): + """ + This method saves the current Validation structure as: + - *.p, i.e. python file + - *.mat, i.e. Matlab file + + Inputs: + - filename = path + name of the file to be saved, string + + Options: + - fileformat = format of the file to be saved, i.e. 'pickle' or 'matlab' + """ + debug = debug or self._debug + if debug: print 'Saving file...' + + #Save as different formats + if fileformat=='pickle': + filename = filename + ".p" + f = open(filename, "wb") + data = {} + data['History'] = self.History + try: + data['Benchmarks'] = self.Benchmarks + except AttributeError: + pass + data['Variables'] = self.Variables.__dict__ + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in data['Variables']: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(data['Variables'][key]).__name__==x for x in listkeys]): + if debug: + print "Force caching for " + key + data['Variables'][key] = data['Variables'][key][:] + #Save in pickle file + if debug: + print 'Dumping in pickle file...' + try: + pkl.dump(data, f, protocol=pkl.HIGHEST_PROTOCOL) + except (SystemError, MemoryError) as e: + print '---Data too large for machine memory---' + raise + + f.close() + elif fileformat=='matlab': + filename = filename + ".mat" + #TR comment: based on Mitchell O'Flaherty-Sproul's code + dtype = float + data = {} + Grd = {} + Var = {} + Bch = {} + + data['History'] = self.History + Bch = self.Benchmarks + for key in Bch: + data[key] = Bch[key] + Var = self.Variables.__dict__ + #TR: Force caching Variables otherwise error during loading + # with 'netcdf4.Variable' type (see above) + for key in Var: + listkeys=['Variable', 'ArrayProxy', 'BaseType'] + if any([type(Var[key]).__name__ == x for x in listkeys]): + if debug: + print "Force caching for " + key + Var[key] = Var[key][:] + #keyV = key + '-var' + #data[keyV] = Var[key] + data[key] = Var[key] + + #Save in mat file file + if debug: + print 'Dumping in matlab file...' + savemat(filename, data, oned_as='column') + else: + print "---Wrong file format---" + + def write_validation_report(self, report_title="validation_report.pdf", debug=False): + """ + This method writes a report (*.pdf) based on the validation methods' results + + Kwargs: + report_title (str): file name + debug (bool): debug flag + """ + debug = debug or self._debug + write_report(self, report_title=report_title, debug=debug) + +# utility classes +class HarmonicBenchmarks: + """ + Storage for hamonic benchmarks + """ + def __init__(self): + self.elevation = 'No harmonic benchmarks for the elevation yet. Run validate_harmonics' + self.velocity = 'No harmonic benchmarks for the velocity yet. Run validate_harmonics' + return diff --git a/build/lib/pyseidon_dvt/validationClass/variablesValidation.py b/build/lib/pyseidon_dvt/validationClass/variablesValidation.py new file mode 100644 index 0000000..a71e7fe --- /dev/null +++ b/build/lib/pyseidon_dvt/validationClass/variablesValidation.py @@ -0,0 +1,331 @@ +#!/usr/bin/python2.7 +# encoding: utf-8 + +from __future__ import division +import numpy as np +from datetime import datetime, timedelta +from os import makedirs +from os.path import exists + +# Local import +from pyseidon_dvt.utilities.interpolation_utils import * + +# Custom error +from pyseidon_dvt.utilities.pyseidon_error import PyseidonError + +class _load_validation: + """ + **'Variables' subset in Validation class** + + It contains the following items: :: + + _obs. = measurement/observational variables + Validation.Variables._|_sim. = simulated variables + |_struct. = dictionary structure for validation purposes + |_harmo. = dictionary stricture for harmonic analysis validation purposes + + """ + def __init__(self, outpath, observed, simulated, flow='sf', nn=True, debug=False, debug_plot=False): + if debug: print "..variables.." + self.obs = observed.Variables + self.sim = simulated.Variables + self._nn = nn + # Compatibility test + if (observed.__module__.split('.')[-1] == 'drifterClass' and + simulated.__module__.split('.')[-1] == 'stationClass'): + raise PyseidonError("---Station and Drifter are incompatible objects---") + + self.struct = np.array([]) + if flow == 'daf': + self._3D = False + else: + self._3D = simulated.Variables._3D + + try: + # Check if times coincide + obsMax = self.obs.matlabTime[~np.isnan(self.obs.matlabTime)].max() + obsMin = self.obs.matlabTime[~np.isnan(self.obs.matlabTime)].min() + simMax = self.sim.matlabTime.max() + simMin = self.sim.matlabTime.min() + absMin = max(obsMin, simMin) + absMax = min(obsMax, simMax) + A = set(np.where(self.sim.matlabTime[:] >= absMin)[0].tolist()) + B = set(np.where(self.sim.matlabTime[:] <= absMax)[0].tolist()) + C = list(A.intersection(B)) + # -Correction by J.Culina 2014- + C = sorted(C) + # -end- + self._C = np.asarray(C) + + a = set(np.where(self.obs.matlabTime[:] >= absMin)[0].tolist()) + b = set(np.where(self.obs.matlabTime[:] <= absMax)[0].tolist()) + c = list(a.intersection(b)) + # -Correction by J.Culina 2014- + c = sorted(c) + # -end- + self._c = np.asarray(c) + + if len(C) == 0: + print "---Time between simulation and measurement does not match up, only Harmonic Analysis can be done---" + self.harmo = {'On':True, 'Observed':observed, 'Simulated':simulated} + C = np.where(self.sim.matlabTime[:] >= simMin)[0].tolist() + c = np.where(self.obs.matlabTime[:] >= obsMin)[0].tolist() + self._C = np.asarray(C) + self._c = np.asarray(c) + else: + self.harmo = {'On':False} + except AttributeError: + raise PyseidonError("---Observations missing matlabTime comparison is impossible---") + + # Check which variables are available in the observations for comparison + self._obs_vars=dir(self.obs) + if ('lon' and 'lat') not in self._obs_vars: + raise PyseidonError("---Observations missing lon/lat comparison is impossible---") + + # Check what kind of simulated data it is + if simulated.__module__ .split('.')[-1] == 'stationClass': + self._simtype = 'station' + # Find closest point to ADCP + ind = closest_points([self.obs.lon], [self.obs.lat], + simulated.Grid.lon[:], + simulated.Grid.lat[:]) + try: + nameSite = ''.join(simulated.Grid.name[ind,:][0,:]) + except IndexError: # TR: quick fix + nameSite = ''.join(simulated.Grid.name[ind]) + self.harmo['nameSite'] = nameSite + print "Station site: " + nameSite + self.sim.lat = simulated.Grid.lat[ind] + el = self.sim.el[:, ind].ravel() + ua = self.sim.ua[:, ind].ravel() + va = self.sim.va[:, ind].ravel() + if self._3D: + u = np.squeeze(self.sim.u[:, :,ind]) + v = np.squeeze(self.sim.v[:, :,ind]) + sig = np.squeeze(simulated.Grid.siglay[:, ind]) + h = simulated.Grid.h[:] + + # Alternative simulation type + elif simulated.__module__.split('.')[-1] == 'fvcomClass': + self._simtype = 'fvcom' + # Different treatment measurements come from drifter + if not observed.__module__.split('.')[-1] == 'drifterClass': + if debug: print "...Interpolation at measurement location..." + el = simulated.Util2D.interpolation_at_point(self.sim.el, + self.obs.lon, self.obs.lat, nn=self._nn) + ua = simulated.Util2D.interpolation_at_point(self.sim.ua, + self.obs.lon, self.obs.lat, nn=self._nn) + va = simulated.Util2D.interpolation_at_point(self.sim.va, + self.obs.lon, self.obs.lat, nn=self._nn) + if self._3D: + u = simulated.Util3D.interpolation_at_point(self.sim.u, + self.obs.lon, self.obs.lat, nn=self._nn) + v = simulated.Util3D.interpolation_at_point(self.sim.v, + self.obs.lon, self.obs.lat, nn=self._nn) + sig = simulated.Util3D.interpolation_at_point(simulated.Grid.siglay, + self.obs.lon, self.obs.lat, nn=self._nn) + h = simulated.Util3D.interpolation_at_point(simulated.Grid.h[:], + self.obs.lon, self.obs.lat, nn=self._nn) + else: # Interpolation for drifter + if debug: print "...Interpolation at measurement locations & times..." + if self._3D: + lock=True + userInp = flow + while lock: + if userInp == 'daf': + if debug: + print 'flow comparison is depth-averaged' + # TR: temporary fix for proxy access + if self.sim._opendap: + uSim = np.zeros((self._C.shape[0], self.sim.ua.shape[1])) + vSim = np.zeros((self._C.shape[0], self.sim.va.shape[1])) + for i, j in enumerate(self._C): + uSim[i,:] = self.sim.ua[j,:] + vSim[i,:] = self.sim.va[j,:] + else: + uSim = np.squeeze(self.sim.ua[self._C,:]) + vSim = np.squeeze(self.sim.va[self._C,:]) + self._3D = False + lock = False + elif userInp == 'sf': + # Import only the surface velocities + # TR_comment: is surface vertical indice -1 or 0? + # KC : 0 is definitely the surface... + # TR: temporary fix for proxy access + if self.sim._opendap: + uSim = np.zeros((self._C.shape[0], self.sim.u.shape[2])) + vSim = np.zeros((self._C.shape[0], self.sim.v.shape[2])) + for i, j in enumerate(self._C): + uSim[i,:] = self.sim.u[j,0,:] + vSim[i,:] = self.sim.v[j,0,:] + else: + uSim = np.squeeze(self.sim.u[self._C,0,:]) + vSim = np.squeeze(self.sim.v[self._C,0,:]) + self._3D = False + lock = False + if debug: + print 'flow comparison at surface' + elif type(userInp) == float: + if debug: + print 'flow comparison at depth level ', float + # if userInp > 0.0: userInp = userInp*-1.0 + uInterp = simulated.Util3D.interp_at_depth(self.sim.u[:], userInp, debug=debug) + vInterp = simulated.Util3D.interp_at_depth(self.sim.v[:], userInp, debug=debug) + # TR: temporary fix for proxy access + if self.sim._opendap: + uSim = np.zeros((self._C.shape[0], uInterp.shape[1])) + vSim = np.zeros((self._C.shape[0], vInterp.shape[1])) + for i, j in enumerate(self._C): + uSim[i,:] = uInterp[j,:] + vSim[i,:] = vInterp[j,:] + else: + uSim = np.squeeze(uInterp[self._C,:]) + vSim = np.squeeze(vInterp[self._C,:]) + self._3D = False + lock = False + else: + userInp = input("compare flow by 'daf', 'sf' or a float number only!!!") + else: + # TR: temporary fix for proxy access + if self.sim._opendap: + uSim = np.zeros((self._C.shape[0], self.sim.ua.shape[1])) + vSim = np.zeros((self._C.shape[0], self.sim.va.shape[1])) + for i, j in enumerate(self._C): + uSim[i,:] = self.sim.ua[j,:] + vSim[i,:] = self.sim.va[j,:] + else: + uSim = np.squeeze(self.sim.ua[self._C,:]) + vSim = np.squeeze(self.sim.va[self._C,:]) + + # Finding the closest Drifter time to simulated data assuming measurement + # time step way faster than model one + indClosest = [] + for i in self._C: + ind = np.abs(self.obs.matlabTime[:]-self.sim.matlabTime[i]).argmin() + indClosest.append(ind) + # Keep only unique values to avoid sampling in measurement gaps + # TR: this doesn't not guarantee that the unique value kept is indeed + # the closest one among the values relative to the same indice!!! + uniqCloInd, uniqInd = np.unique(indClosest, return_index=True) + + # KC: Convert of matlabTime to datetime, smooth drifter temporally! + self.datetimes = [datetime.fromordinal(int(x))+timedelta(days=x%1) - + timedelta(days=366) for x in self.obs.matlabTime[self._c]] + + uObs = self.obs.u[uniqCloInd] + vObs = self.obs.v[uniqCloInd] + uSim = np.squeeze(uSim[uniqInd[:],:]) + vSim = np.squeeze(vSim[uniqInd[:],:]) + + # print 'uObs: \n', uObs, '\nvObs: \n', vObs + # print 'uSim: \n', vSim.shape, '\nuSim: \n', vSim.shape + + # Interpolation of timeseries at drifter's trajectory points + uSimInterp = np.zeros(len(uniqCloInd)) + vSimInterp = np.zeros(len(uniqCloInd)) + for i in range(len(uniqCloInd)): + uSimInterp[i]=simulated.Util2D.interpolation_at_point(uSim[i,:], + self.obs.lon[uniqCloInd[i]], + self.obs.lat[uniqCloInd[i]]) + vSimInterp[i]=simulated.Util2D.interpolation_at_point(vSim[i,:], + self.obs.lon[uniqCloInd[i]], + self.obs.lat[uniqCloInd[i]]) + # print 'vSimInterp: \n', vSimInterp, '\nuSimInterp: \n', uSimInterp + + else: + raise PyseidonError("-This type of simulations is not supported yet-") + + # Store in dict structure for compatibility purposes (except for drifters) + if not observed.__module__.split('.')[-1] == 'drifterClass': + if not self._3D: + sim_mod = {'ua': ua[C], 'va': va[C], 'el': el[C]} + else: + sim_mod = {'ua': ua[C], 'va': va[C], 'el': el[C], 'u': u[C,:], + 'v': v[C,:], 'siglay': sig[:], 'h': h} + + # Check what kind of observed data it is + if observed.__module__.split('.')[-1] == 'adcpClass' or observed.__module__ == 'adcpClass': + self._obstype = 'adcp' + obstype = 'ADCP' + # Alternative measurement type + elif observed.__module__.split('.')[-1] == 'tidegaugeClass': + self._obstype = 'tidegauge' + obstype = 'TideGauge' + else: + raise PyseidonError("---This type of measurements is not supported yet---") + + # This had to be split into two parts so that a time subset could be used. + # Specify list of data variables from observations that should be used. + dictlist=['ua','va','u','v','el'] + self._commonlist_data = [var for var in self._obs_vars if var in dictlist] + if debug: + print 'Data variables being used' + print self._commonlist_data + obs_mod={} + for key in self._commonlist_data: + obs_mod[key] = getattr(self.obs,key) + obs_mod[key] = obs_mod[key][c,] + + # Specify list of nondata variables that should be used. + dictlist = ['bins','data'] + self._commonlist_nondata = [var for var in self._obs_vars if var in dictlist] + if debug: + print 'Non data variables being used' + print self._commonlist_nondata + for key in self._commonlist_nondata: + obs_mod[key] = getattr(self.obs,key) + + else: + self._obstype = 'drifter' + obstype = 'Drifter' + + #Store in dict structure for compatibility purposes + #Common block for 'struct' + if not observed.__module__.split('.')[-1] == 'drifterClass': + self.struct = {'name': observed.History[0].split(' ')[-1], + 'type': obstype, + 'obs_lat': self.obs.lat, + 'obs_lon': self.obs.lon, + 'mod_lat': self.obs.lat, + 'mod_lon': self.obs.lon, + 'obs_timeseries': obs_mod, + 'mod_timeseries': sim_mod, + 'obs_time': self.obs.matlabTime[c], + 'mod_time': self.sim.matlabTime[C], + '_commonlist_data': self._commonlist_data, + '_commonlist_nondata': self._commonlist_nondata} + else: # Drifter's case + self.struct = {'name': observed.History[0].split(' ')[-1], + 'type': obstype, + 'lat': self.obs.lat[uniqCloInd], + 'lon': self.obs.lon[uniqCloInd], + 'obs_timeseries': {'u': uObs, 'v': vObs}, + 'mod_timeseries': {'u': uSimInterp, 'v': vSimInterp}, + 'obs_time': self.obs.matlabTime[uniqCloInd], + 'mod_time': self.sim.matlabTime[C], + '_commonlist_data': ['u', 'v']} + + if debug: print "..done" + + # find save_path + # self._save_path = '' + # for s in observed.History: + # if 'Create save_path ' not in s: + # continue + # else: + # self._save_path=s.split()[2] + # + # if '' is self._save_path: + # name = self.struct['name'] + # self._save_path = outpath+name.split('/')[-1].split('.')[0]+'/' + # while exists(self._save_path): + # self._save_path = self._save_path[:-1] + '_bis/' + # makedirs(self._save_path) + # observed.History.append('Create save_path {}'.format(self._save_path)) + name = self.struct['name'] + self._save_path = outpath+name.split('/')[-1].split('.')[0]+'/' + while exists(self._save_path): + self._save_path = self._save_path[:-1] + '_bis/' + makedirs(self._save_path) + + return diff --git a/dist/PySeidon_dvt-2.1-py2.7.egg b/dist/PySeidon_dvt-2.1-py2.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..f6c3d0ccc5cc0becde164d381ed0c18dc027d76e GIT binary patch literal 320841 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%0AYA?}6pm2k$aQLxVftb*gT;;%IGYtzEHF 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
I8xMrM-s! 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

y;1J(R>!0Osw1GmlH-C7WE6otydyh0G zl3}JpdSB`U4__s{_-pljTcJx(>&Fhc*jATN9Rw2=C*twB!LsADSZEv#) z*q3ircvtX5>z2%fRbx3(C1LY!kj9paSm>dJL)g1C^qrYNqa=6s&QuV%1V!&(v8KJ6 zdko){$vjTXKh&jcV!Ar8gFh5&N7&>?8KV8*yJA`HoNdn}OE__V?~kCULvgTHDr4yx z-+m%}Re(Lu0ayoipON{X)KJM|qcc196?dW7lFN5fpcD(h#00_0FA&Fu=&U>C2yU=s z3hy!EGV0*-aP%w`5VlMyNp%Al$^kGw?c_Q7D1gf+_ji_=N5!s5c=w-$tlS6k4HpQk zSP#l@hX>%d0p{m2mhyKgYuN*2Hf4ZY703n#ZMbj+ABwb9=(SOm;Yu#SWUJrL&*35_OZsGR;d8^Wg?IrlnrPT4y?q&}x5dp?f za1@rt6i!cJQ$eFZ_i-cIc|H8Ygve}f6BPS?Vp{1x^nr>tT2q}V!`3mcMZ9#-^5(}B zadLQNB%n}xi{NPXbt=Rcn{4_Hy8+`*7U{3&M~tlR!m>)mAVG)*D7c6)>vPy;{iOYo*~b9qV;FW3)A$HI̱)Oa!ZkU1WT9n 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

?NjWWSt~Yu4B6z@ytoPeTS+s#lKy9YbIHO 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 Ks(@6 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 IAh<#Q55N`x0RR91 literal 0 HcmV?d00001 diff --git a/pyseidon_dvt/drifterClass/variablesDrifter.py b/pyseidon_dvt/drifterClass/variablesDrifter.py index a2ad69d..1b2a43b 100644 --- a/pyseidon_dvt/drifterClass/variablesDrifter.py +++ b/pyseidon_dvt/drifterClass/variablesDrifter.py @@ -2,10 +2,11 @@ # encoding: utf-8 from __future__ import division -# import numpy as np +import numpy as np # from numpy.ma import MaskError # import h5py from pyseidon_dvt.utilities.miscellaneous import mattime_to_datetime +import sys class _load_drifter: """ @@ -24,22 +25,48 @@ def __init__(self,cls, History, debug=False): print 'Loading variables...' # Pointer to History setattr(self, '_History', History) - - self.matlabTime = cls.Data['velocity'].vel_time[:] - #Sorting values with increasing time step - sortedInd = self.matlabTime.argsort() - self.matlabTime.sort() - self.lat = cls.Data['velocity'].vel_lat[sortedInd] - self.lon = cls.Data['velocity'].vel_lon[sortedInd] - self.u = cls.Data['velocity'].u[sortedInd] - self.v = cls.Data['velocity'].v[sortedInd] + try: + self.matlabTime = cls.Data['velocity'].vel_time[:] + #Sorting values with increasing time step + sortedInd = self.matlabTime.argsort() + self.matlabTime.sort() + self.lat = cls.Data['velocity'].vel_lat[sortedInd] + self.lon = cls.Data['velocity'].vel_lon[sortedInd] + self.u = cls.Data['velocity'].u[sortedInd] + self.v = cls.Data['velocity'].v[sortedInd] + # Luna Ocean Consulting Ltd. new drifter format + except KeyError: + self.matlabTime = cls.Data['time'] + self.original = {} + self.original['lat'] = cls.Data['lat'] + self.original['lon'] = cls.Data['lon'] + self.original['u'] = cls.Data['u'] + self.original['v'] = cls.Data['v'] + self.original['drift_start'] = cls.Data['drift_start'] + self.original['drift_stop'] = cls.Data['drift_stop'] + self.smooth = {} + self.smooth['lat'] = cls.Data['lat_smooth'] + self.smooth['lon'] = cls.Data['lon_smooth'] + self.smooth['u'] = cls.Data['u_smooth'] + self.smooth['v'] = cls.Data['v_smooth'] + self.smooth['drift_start'] = cls.Data['drift_start_smooth'] + self.smooth['drift_stop'] = cls.Data['drift_stop_smooth'] + except: + sys.exit('Drifter file format incompatible') #-Append message to History field - start = mattime_to_datetime(self.matlabTime[0]) - end = mattime_to_datetime(self.matlabTime[-1]) - text = 'Temporal domain from ' + str(start) +\ - ' to ' + str(end) - self._History.append(text) + try: + start = mattime_to_datetime(self.matlabTime[0]) + end = mattime_to_datetime(self.matlabTime[-1]) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) + except ValueError: + start = mattime_to_datetime(np.nanmin(self.matlabTime)) + end = mattime_to_datetime(np.nanmax(self.matlabTime)) + text = 'Temporal domain from ' + str(start) +\ + ' to ' + str(end) + self._History.append(text) if debug: print '...Passed' From 174048716de069bf27b87ed4f50d2dc44e7c9920 Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:48:08 -0300 Subject: [PATCH 4/8] Delete PKG-INFO --- PySeidon_dvt.egg-info/PKG-INFO | 95 ---------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 PySeidon_dvt.egg-info/PKG-INFO diff --git a/PySeidon_dvt.egg-info/PKG-INFO b/PySeidon_dvt.egg-info/PKG-INFO deleted file mode 100644 index 9558b4c..0000000 --- a/PySeidon_dvt.egg-info/PKG-INFO +++ /dev/null @@ -1,95 +0,0 @@ -Metadata-Version: 1.0 -Name: PySeidon-dvt -Version: 2.1 -Summary: Suite of tools for tidal-energy and FVCOM-user communities -Home-page: https://github.com/GrumpyNounours/PySeidon -Author: Thomas Roc -Author-email: thomas.roc@acadiau.ca,wesley.bowman23@gmail.com,lavieenroux20@gmail.com -License: GNU Affero GPL v3.0 -Description: PySeidon_dvt - ================ - ###Warning### - ####This is the development branch of PySeidon and it is subject to daily changes!### - - ### Project description ### - * This project aims to meet multiple objectives of the [EcoEnergyII](http://tidalenergy.acadiau.ca/EcoEII.html) consortium - through the setting of a dedicated server and the development of Python - based packages. This project can be seen as two folded. On the one - hand, it aims to enhance data accessibility for all the partners of - the [EcoEII](http://tidalenergy.acadiau.ca/EcoEII.html) consortium thanks to simple client protocols. On the other - hand, it aims to develop a standardised numerical toolbox gathering - specific analysis functions for measured and simulated data (FVCOM model) - to the [EcoEII](http://tidalenergy.acadiau.ca/EcoEII.html) partners. - * Additionally, this project was the ideal opportunity to transport various - scripts and packages accumulated over the years into Python. These scripts - and packages have been extensively used by the tidal energy community for - more than a decade. The 'Contributors' section of this document is a - mere attempt to acknowledge the work of those who participated directly or - indirectly to the development of this tool box. We are consciously - standing on the shoulders of a multitude of giants...so please forgive us - if we forgot one of them. - * The present package is still a work in progress, so the more feedback, - the better - - ### Installation ### - Hydrodynamic model: - * This package has been primarily developed and designed for post-processing FVCOM outputs. One can download FVCOM from [here](http://fvcom.smast.umassd.edu/fvcom/) - - Requirements: - * This package has been designed for Python 2.7: one can download Python from [here](http://www.python.org/download) - * It is also recommended to install Anaconda beforehand: one can download Anaconda from [here](http://continuum.io/downloads#all) - * The HDF5 library is also needed for this package to work: one can download the HDF5 library from [here](https://www.hdfgroup.org/HDF5/) - - Dependencies: - Althought they should be automatically resolved during the installation, this package relies on the following dependencies: - * setuptools: One can download setuptools from [here](https://pypi.python.org/pypi/setuptools#installation-instructions) - * UTide: One can download UTide from [here](https://github.com/wesleybowman/UTide) - * Pydap: One can download Pydap from [here](http://www.pydap.org/) - * NetworkX: One can download NetworkX from [here](http://networkx.github.io/documentation/latest/install.html) - * Pandas: One can download Pandas from [here](http://pandas.pydata.org/pandas-docs/stable/install.html) - * Seaborn: One can download Seaborn from [here](http://web.stanford.edu/~mwaskom/software/seaborn/installing.html) - * netCDF4: One can download netCDF4 from [here](https://pypi.python.org/pypi/netCDF4/0.8.2) - * gdal: One can download gdal from [here](https://pypi.python.org/pypi/GDAL/) - - Installation: - * Step 1a: Download PySeidon package, save it on your machine and Unzip - * Step 1b: or clone the repository - * Step 2: from a shell, change directory to PySeidon-master folder - * Step 3: from the shell, as superuser/admin, type `python setup.py install` - or `python setup.py install --user` - * Step 4: choose to automatically resolve (y) or not (n) the dependencies - * Finally, in order to test the installation, type `from pyseidon_dvt import *` in Ipython shell. - - Up-dating: - * The code will evolve and improve with time. To up-date, simply "git pull" or download the package - and go through the installation procedure again. - - Recommendations: - * The tutorials and package functioning have been designed for use in IPython shell: One can download IPython from [here](http://ipython.org/) - - ### Documentation ### - Package's documentation can be found [here](http://grumpynounours.github.io/PySeidon/index.html) - - ### Contribution guidelines ### - * [Tutorial 0: First steps](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/master/PySeidon_tuto_0.ipynb) - * [Tutorial 1: FVCOM class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/master/PySeidon_tuto_1.ipynb) - * [Tutorial 2: Station class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_2.ipynb) - * [Tutorial 3: ADCP class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_3.ipynb) - * [Tutorial 4: TideGauge class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_4.ipynb) - * [Tutorial 5: Drifter class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_5.ipynb) - * [Tutorial 6: Validation class](http://nbviewer.ipython.org/github/GrumpyNounours/PySeidon/blob/development/PySeidon_tuto_6.ipynb) - - ### Contacts ### - * Project Leader: [Richard Karsten](richard.karsten@acadiau.ca) - * Repository Admin & Software Development Manager: [Thomas Roc](thomas.roc@acadiau.ca) - * Main Developers: [Thomas Roc](thomas.roc@acadiau.ca), [Jonathan Smith](https://github.com/LaVieEnRoux), [Wesley Bowman](https://github.com/wesleybowman), [Kody Crowell](https://github.com/TheKingInYellow) - - ### Contributors ### - Dr. Richard Karsten, [Aidan Bharath](https://github.com/Aidan-Bharath), Mitchell O'Flaherty-Sproul, Robie Hennigar, [Robert Covill](http://tekmap.ns.ca/), Dr. Joel Culina, Justine McMillan, Dr. Brian Polagye, [Dr. Kristen Thyng](https://github.com/kthyng)... - - ### Legal Information ### - * Original authorship attributed to Thomas Roc, Wesley Bowman and Jonathan Smith - * Copyright (c) 2014 [EcoEnergyII](http://tidalenergy.acadiau.ca/EcoEII.html) - * Licensed under an Affero GPL style license v3.0 (see License_PySeidon.txt) - -Platform: UNKNOWN From 1d42358b533fe1f03a011730370310644c48852c Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:48:15 -0300 Subject: [PATCH 5/8] Delete SOURCES.txt --- PySeidon_dvt.egg-info/SOURCES.txt | 61 ------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 PySeidon_dvt.egg-info/SOURCES.txt diff --git a/PySeidon_dvt.egg-info/SOURCES.txt b/PySeidon_dvt.egg-info/SOURCES.txt deleted file mode 100644 index da6ff47..0000000 --- a/PySeidon_dvt.egg-info/SOURCES.txt +++ /dev/null @@ -1,61 +0,0 @@ -README.txt -setup.py -PySeidon_dvt.egg-info/PKG-INFO -PySeidon_dvt.egg-info/SOURCES.txt -PySeidon_dvt.egg-info/dependency_links.txt -PySeidon_dvt.egg-info/not-zip-safe -PySeidon_dvt.egg-info/top_level.txt -pyseidon_dvt/__init__.py -pyseidon_dvt/adcpClass/__init__.py -pyseidon_dvt/adcpClass/adcpClass.py -pyseidon_dvt/adcpClass/functionsAdcp.py -pyseidon_dvt/adcpClass/plotsAdcp.py -pyseidon_dvt/adcpClass/rawADCPclass.py -pyseidon_dvt/adcpClass/variablesAdcp.py -pyseidon_dvt/drifterClass/__init__.py -pyseidon_dvt/drifterClass/drifterClass.py -pyseidon_dvt/drifterClass/functionsDrifter.py -pyseidon_dvt/drifterClass/plotsDrifter.py -pyseidon_dvt/drifterClass/variablesDrifter.py -pyseidon_dvt/fvcomClass/__init__.py -pyseidon_dvt/fvcomClass/functionsFvcom.py -pyseidon_dvt/fvcomClass/functionsFvcomThreeD.py -pyseidon_dvt/fvcomClass/fvcomClass.py -pyseidon_dvt/fvcomClass/plotsFvcom.py -pyseidon_dvt/fvcomClass/variablesFvcom.py -pyseidon_dvt/stationClass/__init__.py -pyseidon_dvt/stationClass/functionsStation.py -pyseidon_dvt/stationClass/functionsStationThreeD.py -pyseidon_dvt/stationClass/plotsStation.py -pyseidon_dvt/stationClass/stationClass.py -pyseidon_dvt/stationClass/variablesStation.py -pyseidon_dvt/tidegaugeClass/__init__.py -pyseidon_dvt/tidegaugeClass/functionsTidegauge.py -pyseidon_dvt/tidegaugeClass/plotsTidegauge.py -pyseidon_dvt/tidegaugeClass/tidegaugeClass.py -pyseidon_dvt/tidegaugeClass/variablesTidegauge.py -pyseidon_dvt/utilities/BP_tools.py -pyseidon_dvt/utilities/__init__.py -pyseidon_dvt/utilities/createNC.py -pyseidon_dvt/utilities/interpolation_utils.py -pyseidon_dvt/utilities/miscellaneous.py -pyseidon_dvt/utilities/object_from_dict.py -pyseidon_dvt/utilities/pyseidon2matlab.py -pyseidon_dvt/utilities/pyseidon2netcdf.py -pyseidon_dvt/utilities/pyseidon2pickle.py -pyseidon_dvt/utilities/pyseidon_error.py -pyseidon_dvt/utilities/regioner.py -pyseidon_dvt/utilities/save_FlowFile_BPFormat.py -pyseidon_dvt/utilities/shortest_element_path.py -pyseidon_dvt/utilities/windrose.py -pyseidon_dvt/validationClass/__init__.py -pyseidon_dvt/validationClass/compareData.py -pyseidon_dvt/validationClass/depthInterp.py -pyseidon_dvt/validationClass/interpolate.py -pyseidon_dvt/validationClass/plotsValidation.py -pyseidon_dvt/validationClass/smooth.py -pyseidon_dvt/validationClass/tidalStats.py -pyseidon_dvt/validationClass/valReport.py -pyseidon_dvt/validationClass/valTable.py -pyseidon_dvt/validationClass/validationClass.py -pyseidon_dvt/validationClass/variablesValidation.py \ No newline at end of file From e21cf8d9f3f620ac9f7790e036369ec9f9f62a1c Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:48:23 -0300 Subject: [PATCH 6/8] Delete dependency_links.txt --- PySeidon_dvt.egg-info/dependency_links.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 PySeidon_dvt.egg-info/dependency_links.txt diff --git a/PySeidon_dvt.egg-info/dependency_links.txt b/PySeidon_dvt.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/PySeidon_dvt.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - From 12700c78492dd221cbd0cb92c243432d18d61376 Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:48:31 -0300 Subject: [PATCH 7/8] Delete not-zip-safe --- PySeidon_dvt.egg-info/not-zip-safe | 1 - 1 file changed, 1 deletion(-) delete mode 100644 PySeidon_dvt.egg-info/not-zip-safe diff --git a/PySeidon_dvt.egg-info/not-zip-safe b/PySeidon_dvt.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/PySeidon_dvt.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - From 09e623708a174abf769f82617c907a8eb15df10d Mon Sep 17 00:00:00 2001 From: Jeremy Locke Date: Tue, 27 Jun 2017 22:48:40 -0300 Subject: [PATCH 8/8] Delete top_level.txt --- PySeidon_dvt.egg-info/top_level.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 PySeidon_dvt.egg-info/top_level.txt diff --git a/PySeidon_dvt.egg-info/top_level.txt b/PySeidon_dvt.egg-info/top_level.txt deleted file mode 100644 index c91d302..0000000 --- a/PySeidon_dvt.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pyseidon_dvt