4
4
import logging
5
5
from numpy import concatenate , int32 , empty , maximum , where , array , \
6
6
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
8
10
from netCDF4 import Dataset
9
11
from scipy .ndimage import gaussian_filter , convolve
10
12
from scipy .interpolate import RectBivariateSpline
11
13
from scipy .spatial import cKDTree
12
14
from matplotlib .path import Path as BasePath
13
15
from matplotlib .contour import QuadContourSet as BaseQuadContourSet
14
16
from 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
16
19
from ..observations import EddiesObservations
17
- from ..eddy_feature import Amplitude , get_uavg , Contours
20
+ from ..eddy_feature import Amplitude , Contours
18
21
19
22
20
23
def raw_resample (datas , fixed_size ):
21
24
nb_value = datas .shape [0 ]
25
+ if nb_value == 1 :
26
+ raise Exception ()
22
27
return interp (arange (fixed_size ), arange (nb_value ) * (fixed_size - 1 ) / (nb_value - 1 ) , datas )
23
28
24
29
25
30
def contour_iter (self , anticyclonic_search ):
26
31
for coll in self .collections [::1 if anticyclonic_search else - 1 ]:
27
32
yield coll
28
33
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
-
36
34
37
- BasePath . isvalid = isvalid
35
+ BaseQuadContourSet . iter_ = contour_iter
38
36
39
37
40
38
@property
@@ -117,7 +115,9 @@ def _fit_circle_path(self):
117
115
self ._circle_params = centlon_e , centlat_e , eddy_radius_e , aerr
118
116
except ZeroDivisionError :
119
117
# 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 :
121
121
logging .warning ('An edge is only define in one position' )
122
122
logging .debug ('%d coordinates %s,%s' , len (self .lon ), self .lon ,
123
123
self .lat )
@@ -351,11 +351,11 @@ def bounds(self):
351
351
"""
352
352
return self .x_bounds .min (), self .x_bounds .max (), self .y_bounds .min (), self .y_bounds .max ()
353
353
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 ):
356
356
# The inf limit must be in pixel and sup limit in surface
357
357
if pixel_limit is None :
358
- pixel_limit = (8 , 1000 )
358
+ pixel_limit = (4 , 1000 )
359
359
360
360
# Compute an interpolator for eke
361
361
self .init_speed_coef (uname , vname )
@@ -371,7 +371,7 @@ def eddy_identification(self, grid_height, uname, vname,
371
371
x , y = self .vars [self .coordinates [0 ]], self .vars [self .coordinates [1 ]]
372
372
373
373
# 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 )
375
375
376
376
# Compute cyclonic and anticylonic research:
377
377
a_and_c = list ()
@@ -397,10 +397,6 @@ def eddy_identification(self, grid_height, uname, vname,
397
397
for current_contour in contour_paths :
398
398
if current_contour .used :
399
399
continue
400
- # Filter for closed contours
401
- if not current_contour .isvalid :
402
- continue
403
-
404
400
centlon_e , centlat_e , eddy_radius_e , aerr = current_contour .fit_circle ()
405
401
# Filter for shape
406
402
if aerr < 0 or aerr > shape_error :
@@ -446,7 +442,7 @@ def eddy_identification(self, grid_height, uname, vname,
446
442
447
443
# centlat_e and centlon_e must be index of maximum, we will loose some inner contour, if it's not
448
444
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 )
450
446
451
447
# Use azimuth equal projection for radius
452
448
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,
474
470
properties .obs ['height_inner_contour' ] = contours .cvalues [i_inner ]
475
471
array_size = speed_array .shape [0 ]
476
472
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 )
488
477
properties .obs ['amplitude' ] = amp .amplitude
489
478
properties .obs ['radius_s' ] = eddy_radius_s / 1000
490
479
properties .obs ['speed_radius' ] = max_average_speed
@@ -506,6 +495,56 @@ def eddy_identification(self, grid_height, uname, vname,
506
495
a_and_c .append (EddiesObservations .concatenate (eddies ))
507
496
return a_and_c
508
497
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
+
509
548
@staticmethod
510
549
def _gaussian_filter (data , sigma , mode = 'reflect' ):
511
550
"""Standard gaussian filter
@@ -770,6 +809,115 @@ def is_circular(self):
770
809
"""
771
810
return abs ((self .x_bounds [0 ] % 360 ) - (self .x_bounds [- 1 ] % 360 )) < 0.0001
772
811
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
+
773
921
def _low_filter (self , grid_name , x_cut , y_cut ):
774
922
"""low filtering
775
923
"""
@@ -785,6 +933,26 @@ def _low_filter(self, grid_name, x_cut, y_cut):
785
933
(i_x , i_y ),
786
934
mode = 'wrap' if self .is_circular () else 'reflect' )
787
935
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
+
788
956
def add_uv (self , grid_height ):
789
957
"""Compute a u and v grid
790
958
"""
0 commit comments