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