Skip to content

Commit 1f8f5d1

Browse files
committed
- Add a bessel filter(work in kilometer and not in degrees)
- Remove warning - Close contour which are almost closed - clean contour collection from tiny one
1 parent 5f4e47e commit 1f8f5d1

File tree

3 files changed

+235
-86
lines changed

3 files changed

+235
-86
lines changed

src/py_eddy_tracker/dataset/grid.py

Lines changed: 200 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,35 @@
44
import logging
55
from 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
810
from netCDF4 import Dataset
911
from scipy.ndimage import gaussian_filter, convolve
1012
from scipy.interpolate import RectBivariateSpline
1113
from scipy.spatial import cKDTree
1214
from matplotlib.path import Path as BasePath
1315
from matplotlib.contour import QuadContourSet as BaseQuadContourSet
1416
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
1619
from ..observations import EddiesObservations
17-
from ..eddy_feature import Amplitude, get_uavg, Contours
20+
from ..eddy_feature import Amplitude, Contours
1821

1922

2023
def 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

2530
def 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

Comments
 (0)