Skip to content

Commit 9eba4b3

Browse files
committed
update object oriented, give eddy detection, but not validated
1 parent 9b5aa7e commit 9eba4b3

File tree

3 files changed

+200
-147
lines changed

3 files changed

+200
-147
lines changed

src/py_eddy_tracker/dataset/grid.py

Lines changed: 161 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from scipy.interpolate import interp1d
66
from numpy import concatenate, int32, empty, maximum, where, array, \
77
sin, deg2rad, pi, ones, cos, ma, int8, histogram2d, arange, float_, \
8-
linspace, errstate, round_, int_, column_stack, interp
8+
linspace, errstate, int_, column_stack, interp, meshgrid, unique, nan
99
from netCDF4 import Dataset
1010
from scipy.ndimage import gaussian_filter, convolve
1111
from scipy.interpolate import RectBivariateSpline
@@ -277,6 +277,7 @@ def load(self):
277277
self.vars[y_name] = h.variables[y_name][:]
278278

279279
if self.is_centered:
280+
logging.info('Grid center')
280281
self.x_c = self.vars[x_name]
281282
self.y_c = self.vars[y_name]
282283

@@ -346,122 +347,142 @@ def bounds(self):
346347
"""
347348
return self.x_bounds.min(), self.x_bounds.max(), self.y_bounds.min(), self.y_bounds.max()
348349

349-
def eddy_identification(self, grid_height, step=0.005, shape_error=55, array_sampling=50, pixel_limit=None):
350+
def eddy_identification(self, grid_height, uname, vname,
351+
step=0.005, shape_error=55, array_sampling=50, pixel_limit=None):
352+
# The inf limit must be in pixel and sup limit in surface
350353
if pixel_limit is None:
351354
pixel_limit = (8, 1000)
352355

353-
self.init_speed_coef()
356+
# Compute an interpolator for eke
357+
self.init_speed_coef(uname, vname)
354358

359+
# Get h grid
355360
data = self.grid(grid_height)
356-
z_min, z_max = data.min(), data.max()
357361

362+
# Compute levels for ssh
363+
z_min, z_max = data.min(), data.max()
358364
levels = arange(z_min - z_min % step, z_max - z_max % step + 2 * step, step)
359365

366+
# Get x and y values
360367
x, y = self.vars[self.coordinates[0]], self.vars[self.coordinates[1]]
361368

362-
eddies = list()
369+
# Compute ssh contour
363370
contours = Contours(x, y, data, levels)
364371

365-
anticyclonic_search = True
366-
iterator = 1 if anticyclonic_search else -1
367-
368-
# Loop over each collection
369-
for coll_ind, coll in enumerate(contours.iter(step=iterator)):
370-
corrected_coll_index = coll_ind
371-
if iterator == -1:
372-
corrected_coll_index = - coll_ind - 1
373-
374-
contour_paths = coll.get_paths()
375-
nb_paths = len(contour_paths)
376-
if nb_paths == 0:
377-
continue
378-
cvalues = contours.cvalues[corrected_coll_index]
379-
logging.debug('doing collection %s, contour value %.4f, %d paths',
380-
corrected_coll_index, cvalues, nb_paths)
381-
382-
# Loop over individual c_s contours (i.e., every eddy in field)
383-
for current_contour in contour_paths:
384-
if current_contour.used:
385-
continue
386-
# Filter for closed contours
387-
if not current_contour.isvalid:
372+
# Compute cyclonic and anticylonic research:
373+
a_and_c = list()
374+
for anticyclonic_search in [True, False]:
375+
eddies = list()
376+
iterator = 1 if anticyclonic_search else -1
377+
378+
# Loop over each collection
379+
for coll_ind, coll in enumerate(contours.iter(step=iterator)):
380+
corrected_coll_index = coll_ind
381+
if iterator == -1:
382+
corrected_coll_index = - coll_ind - 1
383+
384+
contour_paths = coll.get_paths()
385+
nb_paths = len(contour_paths)
386+
if nb_paths == 0:
388387
continue
389-
centlon_e, centlat_e, eddy_radius_e, aerr = current_contour.fit_circle()
390-
# Filter for shape
391-
if aerr < 0 or aerr > shape_error:
392-
continue
393-
# Get indices of centroid
394-
# Give only 1D array of lon and lat not 2D data
395-
i_x, i_y = self.nearest_grd_indice(centlon_e, centlat_e)
388+
cvalues = contours.cvalues[corrected_coll_index]
389+
logging.debug('doing collection %s, contour value %.4f, %d paths',
390+
corrected_coll_index, cvalues, nb_paths)
396391

397-
# Check if centroid is on define value
398-
if hasattr(data, 'mask') and data.mask[i_x, i_y]:
399-
continue
400-
# Test to know cyclone or anticyclone
401-
acyc_not_cyc = data[i_x, i_y] >= cvalues
402-
if anticyclonic_search != acyc_not_cyc:
403-
continue
392+
# Loop over individual c_s contours (i.e., every eddy in field)
393+
for current_contour in contour_paths:
394+
if current_contour.used:
395+
continue
396+
# Filter for closed contours
397+
if not current_contour.isvalid:
398+
continue
399+
400+
centlon_e, centlat_e, eddy_radius_e, aerr = current_contour.fit_circle()
401+
# Filter for shape
402+
if aerr < 0 or aerr > shape_error:
403+
continue
404+
# Get indices of centroid
405+
# Give only 1D array of lon and lat not 2D data
406+
i_x, i_y = self.nearest_grd_indice(centlon_e, centlat_e)
404407

405-
i_x_in, i_y_in = current_contour.pixels_in(self)
408+
# Check if centroid is on define value
409+
if hasattr(data, 'mask') and data.mask[i_x, i_y]:
410+
continue
411+
# Test to know cyclone or anticyclone
412+
acyc_not_cyc = data[i_x, i_y] >= cvalues
413+
if anticyclonic_search != acyc_not_cyc:
414+
continue
406415

407-
# Maybe limit max must be replace with a maximum of surface
408-
if current_contour.nb_pixel < pixel_limit[0] or current_contour.nb_pixel > pixel_limit[1]:
409-
continue
416+
# Find all pixels in the contour
417+
i_x_in, i_y_in = current_contour.pixels_in(self)
410418

411-
reset_centroid, amp = self.get_amplitude(i_x_in, i_y_in, cvalues, data,
412-
anticyclonic_search=anticyclonic_search, level=contours.levels[corrected_coll_index], step=step)
419+
# Maybe limit max must be replace with a maximum of surface
420+
if current_contour.nb_pixel < pixel_limit[0] or current_contour.nb_pixel > pixel_limit[1]:
421+
continue
413422

414-
# If we have a valid amplitude
415-
if (not amp.within_amplitude_limits()) or (amp.amplitude == 0):
416-
continue
423+
# Compute amplitude
424+
reset_centroid, amp = self.get_amplitude(i_x_in, i_y_in, cvalues, data,
425+
anticyclonic_search=anticyclonic_search,
426+
level=contours.levels[corrected_coll_index], step=step)
417427

418-
if reset_centroid:
419-
centi = reset_centroid[0]
420-
centj = reset_centroid[1]
421-
centlon_e = x[centi, centj]
422-
centlat_e = y[centi, centj]
423-
424-
average_speed, speed_contour, inner_contour, speed_array = \
425-
get_uavg(self, contours, centlon_e, centlat_e, current_contour, anticyclonic_search, corrected_coll_index)
426-
427-
# Use azimuth equal projection for radius
428-
proj = Proj('+proj=aeqd +ellps=WGS84 +lat_0={1} +lon_0={0}'.format(*inner_contour.mean_coordinates))
429-
# First, get position based on innermost
430-
# contour
431-
c_x, c_y = proj(inner_contour.lon, inner_contour.lat)
432-
centx_s, centy_s, _, _ = fit_circle_c(c_x, c_y)
433-
centlon_s, centlat_s = proj(centx_s, centy_s, inverse=True)
434-
# Second, get speed-based radius based on
435-
# contour of max uavg
436-
c_x, c_y = proj(speed_contour.lon, speed_contour.lat)
437-
_, _, eddy_radius_s, aerr_s = fit_circle_c(c_x, c_y)
438-
439-
# Instantiate new EddyObservation object
440-
properties = EddiesObservations(
441-
size=1,
442-
track_extra_variables=['shape_error_e', 'shape_error_s'],
443-
track_array_variables=array_sampling,
444-
array_variables=['contour_lon_e', 'contour_lat_e', 'contour_lon_s', 'contour_lat_s']
445-
)
446-
properties.obs['amplitude'] = amp.amplitude
447-
properties.obs['radius_s'] = eddy_radius_s / 1000
448-
properties.obs['speed_radius'] = average_speed
449-
properties.obs['radius_e'] = eddy_radius_e / 1000
450-
properties.obs['shape_error_e'] = aerr
451-
properties.obs['shape_error_s'] = aerr_s
452-
properties.obs['lon'] = centlon_s
453-
properties.obs['lat'] = centlat_s
454-
properties.obs['contour_lon_e'], properties.obs['contour_lat_e'] = uniform_resample(
455-
current_contour.lon, current_contour.lat, fixed_size=array_sampling)
456-
properties.obs['contour_lon_s'], properties.obs['contour_lat_s'] = uniform_resample(
457-
speed_contour.lon, speed_contour.lat, fixed_size=array_sampling)
458-
if aerr > 99.9 or aerr_s > 99.9:
459-
logging.warning('Strange shape at this step! shape_error : %f, %f', aerr, aerr_s)
460-
461-
eddies.append(properties)
462-
# To reserve definitively the area
463-
data.mask[i_x_in, i_y_in] = True
464-
return eddies
428+
# If we have a valid amplitude
429+
if (not amp.within_amplitude_limits()) or (amp.amplitude == 0):
430+
continue
431+
432+
if reset_centroid:
433+
centi = reset_centroid[0]
434+
centj = reset_centroid[1]
435+
# To move in regular and unregular grid
436+
if len(x.shape) == 1:
437+
centlon_e = x[centi]
438+
centlat_e = y[centj]
439+
else:
440+
centlon_e = x[centi, centj]
441+
centlat_e = y[centi, centj]
442+
443+
# centlat_e and centlon_e must be index of maximum, we will loose some inner contour, if it's not
444+
average_speed, speed_contour, inner_contour, speed_array = \
445+
get_uavg(self, contours, centlon_e, centlat_e, current_contour, anticyclonic_search, corrected_coll_index)
446+
447+
# Use azimuth equal projection for radius
448+
proj = Proj('+proj=aeqd +ellps=WGS84 +lat_0={1} +lon_0={0}'.format(*inner_contour.mean_coordinates))
449+
# First, get position based on innermost
450+
# contour
451+
c_x, c_y = proj(inner_contour.lon, inner_contour.lat)
452+
centx_s, centy_s, _, _ = fit_circle_c(c_x, c_y)
453+
centlon_s, centlat_s = proj(centx_s, centy_s, inverse=True)
454+
# Second, get speed-based radius based on
455+
# contour of max uavg
456+
c_x, c_y = proj(speed_contour.lon, speed_contour.lat)
457+
_, _, eddy_radius_s, aerr_s = fit_circle_c(c_x, c_y)
458+
459+
# Instantiate new EddyObservation object
460+
properties = EddiesObservations(
461+
size=1,
462+
track_extra_variables=['shape_error_e', 'shape_error_s'],
463+
track_array_variables=array_sampling,
464+
array_variables=['contour_lon_e', 'contour_lat_e', 'contour_lon_s', 'contour_lat_s']
465+
)
466+
properties.obs['amplitude'] = amp.amplitude
467+
properties.obs['radius_s'] = eddy_radius_s / 1000
468+
properties.obs['speed_radius'] = average_speed
469+
properties.obs['radius_e'] = eddy_radius_e / 1000
470+
properties.obs['shape_error_e'] = aerr
471+
properties.obs['shape_error_s'] = aerr_s
472+
properties.obs['lon'] = centlon_s
473+
properties.obs['lat'] = centlat_s
474+
properties.obs['contour_lon_e'], properties.obs['contour_lat_e'] = uniform_resample(
475+
current_contour.lon, current_contour.lat, fixed_size=array_sampling)
476+
properties.obs['contour_lon_s'], properties.obs['contour_lat_s'] = uniform_resample(
477+
speed_contour.lon, speed_contour.lat, fixed_size=array_sampling)
478+
if aerr > 99.9 or aerr_s > 99.9:
479+
logging.warning('Strange shape at this step! shape_error : %f, %f', aerr, aerr_s)
480+
481+
eddies.append(properties)
482+
# To reserve definitively the area
483+
data.mask[i_x_in, i_y_in] = True
484+
a_and_c.append(EddiesObservations.concatenate(eddies))
485+
return a_and_c
465486

466487
@staticmethod
467488
def _gaussian_filter(data, sigma, mode='reflect'):
@@ -471,7 +492,7 @@ def _gaussian_filter(data, sigma, mode='reflect'):
471492
local_data[data.mask] = 0
472493

473494
v = gaussian_filter(local_data, sigma=sigma, mode=mode)
474-
w = gaussian_filter(float_(-data.mask), sigma=sigma, mode=mode)
495+
w = gaussian_filter(float_(~data.mask), sigma=sigma, mode=mode)
475496

476497
with errstate(invalid='ignore'):
477498
return ma.array(v / w, mask=w == 0)
@@ -492,7 +513,6 @@ def get_amplitude(i_x_in, i_y_in, contour_height, data, anticyclonic_search=True
492513

493514
if anticyclonic_search:
494515
reset_centroid = amp.all_pixels_above_h0(level)
495-
496516
else:
497517
reset_centroid = amp.all_pixels_below_h0(level)
498518

@@ -597,16 +617,48 @@ class RegularGridDataset(GridDataset):
597617
"""Class only for regular grid
598618
"""
599619

600-
__slots__ = ()
620+
__slots__ = (
621+
'_speed_ev',
622+
)
601623

602624
def init_pos_interpolator(self):
603625
"""Create function to have a quick index interpolator
604626
"""
605-
self.xinterp = interp1d(self.x_bounds, range(self.x_bounds.shape[0]), assume_sorted=True)
606-
self.yinterp = interp1d(self.y_bounds, range(self.y_bounds.shape[0]), assume_sorted=True)
627+
self.xinterp = arange(self.x_bounds.shape[0])
628+
self.yinterp = arange(self.y_bounds.shape[0])
629+
630+
def bbox_indice(self, vertices):
631+
lon, lat = vertices.T
632+
lon_min, lon_max = lon.min(), lon.max()
633+
lat_min, lat_max = lat.min(), lat.max()
634+
i_x0, i_y0 = self.nearest_grd_indice(lon_min, lat_min)
635+
i_x1, i_y1 = self.nearest_grd_indice(lon_max, lat_max)
636+
slice_x = slice(i_x0 - self.N, i_x1 + self.N + 1)
637+
slice_y = slice(i_y0 - self.N, i_y1 + self.N + 1)
638+
return slice_x, slice_y
639+
640+
def get_pixels_in(self, contour):
641+
slice_x, slice_y = self.bbox_indice(contour.vertices)
642+
x, y = meshgrid(self.x_c[slice_x], self.y_c[slice_y])
643+
pts = array((x.reshape(-1), y.reshape(-1))).T
644+
mask = contour.contains_points(pts).reshape(x.shape)
645+
i_x, i_y = where(mask.T)
646+
i_x += slice_x.start
647+
i_y += slice_y.start
648+
return i_x, i_y
607649

608650
def nearest_grd_indice(self, x, y):
609-
return int_(round_(self.xinterp(x))), int_(round_(self.yinterp(y)))
651+
"""
652+
Can use this version, which are faster without check
653+
from numpy.core.multiarray import interp
654+
Args:
655+
x:
656+
y:
657+
658+
Returns:
659+
660+
"""
661+
return round(interp(x, self.x_bounds, self.xinterp)), round(interp(y, self.y_bounds, self.yinterp))
610662

611663
@property
612664
def xstep(self):
@@ -761,8 +813,12 @@ def add_uv(self, grid_height):
761813
d_x = self.EARTH_RADIUS * 2 * pi / 360 * d_x_degrees * cos(deg2rad(self.y_c))
762814
self.vars['v'] = d_hx / d_x * gof
763815

816+
def speed_coef(self, contour):
817+
lon, lat = contour.regular_coordinates[1:].T
818+
return self._speed_ev(lon, lat)
819+
764820
def init_speed_coef(self, uname='u', vname='v'):
765821
"""Draft
766822
"""
767823
uspd = (self.grid(uname) ** 2 + self.grid(vname) ** 2) ** .5
768-
self.speed_coef = RectBivariateSpline(self.xc, self.yc, uspd, kx=1, ky=1).ev
824+
self._speed_ev = RectBivariateSpline(self.x_c, self.y_c, uspd, kx=1, ky=1).ev

0 commit comments

Comments
 (0)