44import logging
55from numpy import concatenate , int32 , empty , maximum , where , array , \
66 sin , deg2rad , pi , ones , cos , ma , int8 , histogram2d , arange , float_ , \
7- linspace , errstate , int_ , column_stack , interp , meshgrid , unique , nan
7+ linspace , errstate , int_ , column_stack , interp , meshgrid , nan , ceil , sinc , float64
8+ from scipy .special import j1
9+ from scipy .signal import convolve2d
810from netCDF4 import Dataset
911from scipy .ndimage import gaussian_filter , convolve
1012from scipy .interpolate import RectBivariateSpline
1113from scipy .spatial import cKDTree
1214from matplotlib .path import Path as BasePath
1315from matplotlib .contour import QuadContourSet as BaseQuadContourSet
1416from pyproj import Proj
15- from ..tools import fit_circle_c , distance_vector
17+ from ..tools import fit_circle_c , distance_vector , winding_number_poly , poly_contain_poly , \
18+ distance , distance_point_vector
1619from ..observations import EddiesObservations
17- from ..eddy_feature import Amplitude , get_uavg , Contours
20+ from ..eddy_feature import Amplitude , Contours
1821
1922
2023def raw_resample (datas , fixed_size ):
2124 nb_value = datas .shape [0 ]
25+ if nb_value == 1 :
26+ raise Exception ()
2227 return interp (arange (fixed_size ), arange (nb_value ) * (fixed_size - 1 ) / (nb_value - 1 ) , datas )
2328
2429
2530def contour_iter (self , anticyclonic_search ):
2631 for coll in self .collections [::1 if anticyclonic_search else - 1 ]:
2732 yield coll
2833
29- BaseQuadContourSet .iter_ = contour_iter
30-
31- @property
32- def isvalid (self ):
33- return False not in (self .vertices [0 ] == self .vertices [- 1 ]
34- ) and len (self .vertices ) > 2
35-
3634
37- BasePath . isvalid = isvalid
35+ BaseQuadContourSet . iter_ = contour_iter
3836
3937
4038@property
@@ -117,7 +115,9 @@ def _fit_circle_path(self):
117115 self ._circle_params = centlon_e , centlat_e , eddy_radius_e , aerr
118116 except ZeroDivisionError :
119117 # Some time, edge is only a dot of few coordinates
120- if len (unique (self .lon )) == 1 and len (unique (self .lat )) == 1 :
118+ d_lon = self .lon .max () - self .lon .min ()
119+ d_lat = self .lat .max () - self .lat .min ()
120+ if d_lon < 1e-7 and d_lat < 1e-7 :
121121 logging .warning ('An edge is only define in one position' )
122122 logging .debug ('%d coordinates %s,%s' , len (self .lon ), self .lon ,
123123 self .lat )
@@ -351,11 +351,11 @@ def bounds(self):
351351 """
352352 return self .x_bounds .min (), self .x_bounds .max (), self .y_bounds .min (), self .y_bounds .max ()
353353
354- def eddy_identification (self , grid_height , uname , vname ,
355- step = 0.005 , shape_error = 55 , array_sampling = 50 , pixel_limit = None ):
354+ def eddy_identification (self , grid_height , uname , vname , step = 0.005 , shape_error = 55 ,
355+ array_sampling = 50 , pixel_limit = None , bbox_surface_min_degree = .125 ** 2 ):
356356 # The inf limit must be in pixel and sup limit in surface
357357 if pixel_limit is None :
358- pixel_limit = (8 , 1000 )
358+ pixel_limit = (4 , 1000 )
359359
360360 # Compute an interpolator for eke
361361 self .init_speed_coef (uname , vname )
@@ -371,7 +371,7 @@ def eddy_identification(self, grid_height, uname, vname,
371371 x , y = self .vars [self .coordinates [0 ]], self .vars [self .coordinates [1 ]]
372372
373373 # Compute ssh contour
374- contours = Contours (x , y , data , levels )
374+ contours = Contours (x , y , data , levels , bbox_surface_min_degree = bbox_surface_min_degree )
375375
376376 # Compute cyclonic and anticylonic research:
377377 a_and_c = list ()
@@ -397,10 +397,6 @@ def eddy_identification(self, grid_height, uname, vname,
397397 for current_contour in contour_paths :
398398 if current_contour .used :
399399 continue
400- # Filter for closed contours
401- if not current_contour .isvalid :
402- continue
403-
404400 centlon_e , centlat_e , eddy_radius_e , aerr = current_contour .fit_circle ()
405401 # Filter for shape
406402 if aerr < 0 or aerr > shape_error :
@@ -446,7 +442,7 @@ def eddy_identification(self, grid_height, uname, vname,
446442
447443 # centlat_e and centlon_e must be index of maximum, we will loose some inner contour, if it's not
448444 max_average_speed , speed_contour , inner_contour , speed_array , i_max_speed , i_inner = \
449- get_uavg (self , contours , centlon_e , centlat_e , current_contour , anticyclonic_search , corrected_coll_index )
445+ self . get_uavg (contours , centlon_e , centlat_e , current_contour , anticyclonic_search , corrected_coll_index )
450446
451447 # Use azimuth equal projection for radius
452448 proj = Proj ('+proj=aeqd +ellps=WGS84 +lat_0={1} +lon_0={0}' .format (* inner_contour .mean_coordinates ))
@@ -474,17 +470,10 @@ def eddy_identification(self, grid_height, uname, vname,
474470 properties .obs ['height_inner_contour' ] = contours .cvalues [i_inner ]
475471 array_size = speed_array .shape [0 ]
476472 properties .obs ['nb_contour_selected' ] = array_size
477- properties .obs ['uavg_profile' ] = raw_resample (speed_array , array_sampling )
478- # from matplotlib import pyplot as plt
479- # if array_size > 10:
480- # plt.figure()
481- # plt.plot(linspace(properties.obs['height_external_contour'],properties.obs['height_inner_contour'], speed_array.shape[0]), speed_array, 'b')
482- # plt.axvline(properties.obs['height_inner_contour'], color='g')
483- # plt.axvline(properties.obs['height_max_speed_contour'], color='r')
484- # plt.axvline(properties.obs['height_external_contour'], color='k')
485- # plt.title('%d' % array_size)
486- # plt.ylim(0,None)
487- # plt.show()
473+ if speed_array .shape [0 ] == 1 :
474+ properties .obs ['uavg_profile' ][:] = speed_array [0 ]
475+ else :
476+ properties .obs ['uavg_profile' ] = raw_resample (speed_array , array_sampling )
488477 properties .obs ['amplitude' ] = amp .amplitude
489478 properties .obs ['radius_s' ] = eddy_radius_s / 1000
490479 properties .obs ['speed_radius' ] = max_average_speed
@@ -506,6 +495,56 @@ def eddy_identification(self, grid_height, uname, vname,
506495 a_and_c .append (EddiesObservations .concatenate (eddies ))
507496 return a_and_c
508497
498+ def get_uavg (self , all_contours , centlon_e , centlat_e , original_contour , anticyclonic_search , level_start ,
499+ pixel_min = 3 ):
500+ """
501+ Calculate geostrophic speed around successive contours
502+ Returns the average
503+ """
504+ max_average_speed = self .speed_coef (original_contour ).mean ()
505+ speed_array = [max_average_speed ]
506+ pixel_min = 1
507+
508+ eddy_contours = [original_contour ]
509+ inner_contour = selected_contour = original_contour
510+ # Must start only on upper or lower contour, no need to test the two part
511+ step = 1 if anticyclonic_search else - 1
512+ i_inner = i_max_speed = - 1
513+
514+ for i , coll in enumerate (all_contours .iter (start = level_start + step , step = step )):
515+ level_contour = coll .get_nearest_path_bbox_contain_pt (centlon_e , centlat_e )
516+ # Leave loop if no contours at level
517+ if level_contour is None :
518+ break
519+ # 1. Ensure polygon_i contains point centlon_e, centlat_e (Maybe we loose some inner contour if eddy
520+ # core are not centered)
521+ # if winding_number_poly(centlon_e, centlat_e, level_contour.vertices) == 0:
522+ # break
523+ # 2. Ensure polygon_i is within polygon_e
524+ if not poly_contain_poly (original_contour .vertices , level_contour .vertices ):
525+ break
526+ # 3. Respect size range
527+ # nb_pixel properties need call of pixels_in before with a grid of pixel
528+ level_contour .pixels_in (self )
529+ if pixel_min > level_contour .nb_pixel :
530+ break
531+
532+ # Interpolate uspd to seglon, seglat, then get mean
533+ level_average_speed = self .speed_coef (level_contour ).mean ()
534+ speed_array .append (level_average_speed )
535+ if level_average_speed >= max_average_speed :
536+ max_average_speed = level_average_speed
537+ i_max_speed = i
538+ selected_contour = level_contour
539+ inner_contour = level_contour
540+ eddy_contours .append (level_contour )
541+ i_inner = i
542+ for contour in eddy_contours :
543+ contour .used = True
544+ i_max_speed = level_start + step + step * i_max_speed
545+ i_inner = level_start + step + step * i_inner
546+ return max_average_speed , selected_contour , inner_contour , array (speed_array ), i_max_speed , i_inner
547+
509548 @staticmethod
510549 def _gaussian_filter (data , sigma , mode = 'reflect' ):
511550 """Standard gaussian filter
@@ -770,6 +809,115 @@ def is_circular(self):
770809 """
771810 return abs ((self .x_bounds [0 ] % 360 ) - (self .x_bounds [- 1 ] % 360 )) < 0.0001
772811
812+ def kernel_lanczos (self , lat , wave_length , order = 1 ):
813+ # Not really operational
814+ # wave_length in km
815+ # order must be int
816+ if order < 1 :
817+ logging .warning ('order must be superior to 0' )
818+ order = ceil (order ).astype (int )
819+ # Estimate size of kernel
820+ step_y_km = self .ystep * distance (0 , 0 , 0 , 1 ) / 1000
821+ step_x_km = self .xstep * distance (0 , lat , 1 , lat ) / 1000
822+ # half size will be multiply with by order
823+ half_x_pt , half_y_pt = ceil (wave_length / step_x_km ).astype (int ), ceil (wave_length / step_y_km ).astype (int )
824+
825+ y = arange (
826+ lat - self .ystep * half_y_pt * order ,
827+ lat + self .ystep * half_y_pt * order + 0.01 * self .ystep ,
828+ self .ystep )
829+ x = arange (
830+ - self .xstep * half_x_pt * order ,
831+ self .xstep * half_x_pt * order + 0.01 * self .xstep ,
832+ self .xstep )
833+
834+ y , x = meshgrid (y , x )
835+ out_shape = x .shape
836+ dist = empty (out_shape , dtype = float64 ).flatten ()
837+ distance_point_vector (0 , lat , x .astype (float64 ).flatten (), y .astype (float64 ).flatten (), dist )
838+ dist_norm = dist .reshape (out_shape ) / 1000. / wave_length
839+
840+ # sinc(d_x) and sinc(d_y) are windows and bessel function give an equivalent of sinc for lanczos filter
841+ kernel = sinc (dist_norm / order ) * sinc (dist_norm )
842+ kernel [dist_norm > order ] = 0
843+ return kernel
844+
845+ def kernel_bessel (self , lat , wave_length , order = 1 ):
846+ # wave_length in km
847+ # order must be int
848+ if order < 1 :
849+ logging .warning ('order must be superior to 0' )
850+ order = ceil (order ).astype (int )
851+ # Estimate size of kernel
852+ step_y_km = self .ystep * distance (0 , 0 , 0 , 1 ) / 1000
853+ step_x_km = self .xstep * distance (0 , lat , 1 , lat ) / 1000
854+ # half size will be multiply with by order
855+ half_x_pt , half_y_pt = ceil (wave_length / step_x_km ).astype (int ), ceil (wave_length / step_y_km ).astype (int )
856+
857+ y = arange (
858+ lat - self .ystep * half_y_pt * order ,
859+ lat + self .ystep * half_y_pt * order + 0.01 * self .ystep ,
860+ self .ystep )
861+ x = arange (
862+ - self .xstep * half_x_pt * order ,
863+ self .xstep * half_x_pt * order + 0.01 * self .xstep ,
864+ self .xstep )
865+
866+ y , x = meshgrid (y , x )
867+ out_shape = x .shape
868+ dist = empty (out_shape , dtype = float64 ).flatten ()
869+ distance_point_vector (0 , lat , x .astype (float64 ).flatten (), y .astype (float64 ).flatten (), dist )
870+ dist_norm = dist .reshape (out_shape ) / 1000. / wave_length
871+
872+ # sinc(d_x) and sinc(d_y) are windows and bessel function give an equivalent of sinc for lanczos filter
873+ with errstate (invalid = 'ignore' ):
874+ kernel = sinc (dist_norm / order ) * j1 (2 * pi * dist_norm ) / dist_norm
875+ kernel [half_x_pt * order ,half_y_pt * order ] = pi
876+ kernel [dist_norm > order ] = 0
877+ return kernel
878+
879+ def convolve_filter_with_dynamic_kernel (self , grid_name , kernel_func , lat_max , ** kwargs_func ):
880+ logging .warning ('No filtering above %f degrees of latitude' , lat_max )
881+ data = self .grid (grid_name ).copy ()
882+ # Matrix for result
883+ data_out = ma .zeros (data .shape )
884+ data_out .mask = ones (data_out .shape , dtype = bool )
885+ for i , lat in enumerate (self .y_c ):
886+ if abs (lat ) > lat_max :
887+ data_out .mask [:, i ] = True
888+ continue
889+ # Get kernel
890+ kernel = kernel_func (lat , ** kwargs_func )
891+ # Kernel shape
892+ k_shape = kernel .shape
893+ # Half size, k_shape must be always impair
894+ d_lat = int ((k_shape [1 ] - 1 ) / 2 )
895+ d_lon = int ((k_shape [0 ] - 1 ) / 2 )
896+ # Temporary matrix to have exact shape at outuput
897+ tmp_matrix = ma .zeros ((2 * d_lon + data .shape [0 ], k_shape [1 ]))
898+ tmp_matrix .mask = ones (tmp_matrix .shape , dtype = bool )
899+ # Slice to apply on input data
900+ sl_lat_data = slice (max (0 , i - d_lat ), min (i + d_lat , data .shape [1 ]))
901+ # slice to apply on temporary matrix to store input data
902+ sl_lat_in = slice (d_lat - (i - sl_lat_data .start ), d_lat + (sl_lat_data .stop - i ))
903+ # If global => manual wrapping
904+ if self .is_circular ():
905+ tmp_matrix [:d_lon , sl_lat_in ] = data [- d_lon :, sl_lat_data ]
906+ tmp_matrix [- d_lon :, sl_lat_in ] = data [:d_lon , sl_lat_data ]
907+ # Copy data
908+ tmp_matrix [d_lon :- d_lon , sl_lat_in ] = data [:, sl_lat_data ]
909+ # Convolution
910+ m = ~ tmp_matrix .mask
911+ tmp_matrix [~ m ] = 0
912+ values_sum = convolve2d (tmp_matrix , kernel , mode = 'valid' )[:,0 ]
913+
914+ kernel_sum = convolve2d ((m ).astype (float ), kernel , mode = 'valid' )[:,0 ]
915+ with errstate (invalid = 'ignore' ):
916+ data_out [:, i ] = values_sum / kernel_sum
917+ data_out = ma .array (data_out , mask = data .mask + data_out .mask )
918+
919+ return data_out
920+
773921 def _low_filter (self , grid_name , x_cut , y_cut ):
774922 """low filtering
775923 """
@@ -785,6 +933,26 @@ def _low_filter(self, grid_name, x_cut, y_cut):
785933 (i_x , i_y ),
786934 mode = 'wrap' if self .is_circular () else 'reflect' )
787935
936+ def lanczos_high_filter (self , grid_name , wave_length , order = 1 , lat_max = 85 ):
937+ data_out = self .convolve_filter_with_dynamic_kernel (
938+ grid_name , self .kernel_lanczos , lat_max = lat_max , wave_length = wave_length , order = order )
939+ self .vars [grid_name ] -= data_out
940+
941+ def lanczos_low_filter (self , grid_name , wave_length , order = 1 , lat_max = 85 ):
942+ data_out = self .convolve_filter_with_dynamic_kernel (
943+ grid_name , self .kernel_lanczos , lat_max = lat_max , wave_length = wave_length , order = order )
944+ self .vars [grid_name ] = data_out
945+
946+ def bessel_high_filter (self , grid_name , wave_length , order = 1 , lat_max = 85 ):
947+ data_out = self .convolve_filter_with_dynamic_kernel (
948+ grid_name , self .kernel_bessel , lat_max = lat_max , wave_length = wave_length , order = order )
949+ self .vars [grid_name ] -= data_out
950+
951+ def bessel_low_filter (self , grid_name , wave_length , order = 1 , lat_max = 85 ):
952+ data_out = self .convolve_filter_with_dynamic_kernel (
953+ grid_name , self .kernel_bessel , lat_max = lat_max , wave_length = wave_length , order = order )
954+ self .vars [grid_name ] = data_out
955+
788956 def add_uv (self , grid_height ):
789957 """Compute a u and v grid
790958 """
0 commit comments