Skip to content

Commit ba49f2e

Browse files
committed
- Correction on gradient compute
- improvement along uavg profile - better mangement of coast on u/v grid - Allow to not provide u/V grid to identify eddies
1 parent ddd41cc commit ba49f2e

File tree

3 files changed

+139
-45
lines changed

3 files changed

+139
-45
lines changed

src/py_eddy_tracker/dataset/grid.py

Lines changed: 124 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from numpy import concatenate, int32, empty, where, array, \
66
sin, deg2rad, pi, ones, cos, ma, int8, histogram2d, arange, float_, \
77
linspace, errstate, int_, interp, meshgrid, nan, ceil, sinc, isnan, \
8-
percentile, zeros, arctan2, arcsin, round_
8+
percentile, zeros, arctan2, arcsin, round_, nanmean
99
from datetime import datetime
1010
from scipy.special import j1
1111
from netCDF4 import Dataset
@@ -71,10 +71,14 @@ def value_on_regular_contour(x_g, y_g, z_g, m_g, vertices, num_fac=2, fixed_size
7171

7272

7373
@njit(cache=True)
74-
def mean_on_regular_contour(x_g, y_g, z_g, m_g, vertices, num_fac=2, fixed_size=None):
74+
def mean_on_regular_contour(x_g, y_g, z_g, m_g, vertices, num_fac=2, fixed_size=None, nan_remove=False):
7575
x_val, y_val = vertices[:, 0], vertices[:, 1]
7676
x_new, y_new = uniform_resample(x_val, y_val, num_fac, fixed_size)
77-
return interp2d_geo(x_g, y_g, z_g, m_g, x_new[1:], y_new[1:]).mean()
77+
values = interp2d_geo(x_g, y_g, z_g, m_g, x_new[1:], y_new[1:])
78+
if nan_remove:
79+
return nanmean(values)
80+
else:
81+
return values.mean()
7882

7983

8084
def fit_circle_path(self):
@@ -415,7 +419,7 @@ def bounds(self):
415419
return self.x_bounds.min(), self.x_bounds.max(), self.y_bounds.min(), self.y_bounds.max()
416420

417421
def eddy_identification(self, grid_height, uname, vname, date, step=0.005, shape_error=55,
418-
array_sampling=50, pixel_limit=None, bbox_surface_min_degree=.125**2):
422+
array_sampling=50, pixel_limit=None):
419423
if not isinstance(date, datetime):
420424
raise Exception('Date argument be a datetime object')
421425
# The inf limit must be in pixel and sup limit in surface
@@ -446,7 +450,7 @@ def eddy_identification(self, grid_height, uname, vname, date, step=0.005, shape
446450
x, y = self.x_c, self.y_c
447451

448452
# Compute ssh contour
449-
self.contours = Contours(x, y, data, levels, bbox_surface_min_degree=bbox_surface_min_degree, wrap_x=self.is_circular())
453+
self.contours = Contours(x, y, data, levels, wrap_x=self.is_circular())
450454

451455
track_extra_variables = ['height_max_speed_contour', 'height_external_contour', 'height_inner_contour']
452456
array_variables = ['contour_lon_e', 'contour_lat_e', 'contour_lon_s', 'contour_lat_s', 'uavg_profile']
@@ -525,7 +529,7 @@ def eddy_identification(self, grid_height, uname, vname, date, step=0.005, shape
525529
# centlat_e and centlon_e must be index of maximum, we will loose some inner contour, if it's not
526530
max_average_speed, speed_contour, inner_contour, speed_array, i_max_speed, i_inner = \
527531
self.get_uavg(self.contours, centlon_e, centlat_e, current_contour, anticyclonic_search,
528-
corrected_coll_index)
532+
corrected_coll_index, pixel_min=pixel_limit[0])
529533

530534
# Use azimuth equal projection for radius
531535
proj = Proj('+proj=aeqd +ellps=WGS84 +lat_0={1} +lon_0={0}'.format(*inner_contour.mean_coordinates))
@@ -606,7 +610,6 @@ def get_uavg(self, all_contours, centlon_e, centlat_e, original_contour, anticyc
606610
"""
607611
max_average_speed = self.speed_coef_mean(original_contour)
608612
speed_array = [max_average_speed]
609-
pixel_min = 1
610613

611614
eddy_contours = [original_contour]
612615
inner_contour = selected_contour = original_contour
@@ -619,22 +622,16 @@ def get_uavg(self, all_contours, centlon_e, centlat_e, original_contour, anticyc
619622
# Leave loop if no contours at level
620623
if level_contour is None:
621624
break
622-
# 1. Ensure polygon_i contains point centlon_e, centlat_e (Maybe we loose some inner contour if eddy
623-
# core are not centered)
624-
# if winding_number_poly(centlon_e, centlat_e, level_contour.vertices) == 0:
625-
# break
626-
# 2. Ensure polygon_i is within polygon_e
625+
# Ensure polygon_i is within polygon_e
627626
if not poly_contain_poly(original_contour.vertices, level_contour.vertices):
628627
break
629-
# 3. Respect size range
628+
# 3. Respect size range (for max speed)
630629
# nb_pixel properties need call of pixels_in before with a grid of pixel
631630
level_contour.pixels_in(self)
632-
if pixel_min > level_contour.nb_pixel:
633-
break
634631
# Interpolate uspd to seglon, seglat, then get mean
635632
level_average_speed = self.speed_coef_mean(level_contour)
636633
speed_array.append(level_average_speed)
637-
if level_average_speed >= max_average_speed:
634+
if pixel_min < level_contour.nb_pixel and level_average_speed >= max_average_speed:
638635
max_average_speed = level_average_speed
639636
i_max_speed = i
640637
selected_contour = level_contour
@@ -1065,19 +1062,118 @@ def spectrum_lonlat(self, grid_name, area=None, ref=None, **kwargs):
10651062
return (lon_content[0], lon_content[1] / ref_lon_content[1]), \
10661063
(lat_content[0], lat_content[1] / ref_lat_content[1])
10671064

1068-
def add_uv(self, grid_height):
1065+
def compute_grad(self, data, stencil_halfwidth=4, mode='reflect', vertical=False):
1066+
stencil_halfwidth = max(min(int(stencil_halfwidth), 4), 1)
1067+
logging.debug('Stencil half width apply : %d', stencil_halfwidth)
1068+
# output
1069+
grad = None
1070+
1071+
weights = [
1072+
array((3, -32, 168, -672, 0, 672, -168, 32, -3)) / 840.,
1073+
array((-1, 9, -45, 0, 45, -9, 1)) / 60.,
1074+
array((1, -8, 0, 8, -1)) / 12.,
1075+
array((-1, 0, 1)) / 2.,
1076+
# like array((0, -1, 1))
1077+
array((-1, 1)),
1078+
# like array((-1, 1, 0))
1079+
(1, array((-1, 1))),
1080+
]
1081+
# reduce to stencil selected
1082+
weights = weights[4-stencil_halfwidth:]
1083+
if vertical:
1084+
data = data.T
1085+
# Iteration from larger stencil to smaller (to fill matrix)
1086+
for weight in weights:
1087+
if isinstance(weight, tuple):
1088+
# In the cas of unbalanced diff
1089+
shift, weight = weight
1090+
data_ = data.copy()
1091+
data_[shift:] = data[:-shift]
1092+
if not vertical:
1093+
data_[:shift] = data[-shift:]
1094+
else:
1095+
data_ = data
1096+
# Delta h
1097+
d_h = convolve(data_, weights=weight.reshape((-1, 1)), mode=mode)
1098+
mask = convolve(int8(data_.mask), weights=ones(weight.shape).reshape((-1, 1)), mode=mode)
1099+
d_h = ma.array(d_h, mask=mask != 0)
1100+
1101+
# Delta d
1102+
if vertical:
1103+
d_h = d_h.T
1104+
d = self.EARTH_RADIUS * 2 * pi / 360 * convolve(self.y_c, weight)
1105+
else:
1106+
if mode == 'wrap':
1107+
# Along x axis, we need to close
1108+
# we will compute in two part
1109+
x = self.x_c % 360
1110+
d_degrees = convolve(x, weight, mode=mode)
1111+
d_degrees_180 = convolve((x + 180) % 360 - 180, weight, mode=mode)
1112+
# Arbitrary, to be sure to be far far away of bound
1113+
m = (x < 50) + (x > 310)
1114+
d_degrees[m] = d_degrees_180[m]
1115+
d_degrees = d_degrees.reshape((-1, 1))
1116+
else:
1117+
d_degrees = convolve(self.x_c, weight, mode=mode).reshape((-1, 1))
1118+
d = self.EARTH_RADIUS * 2 * pi / 360 * d_degrees * cos(deg2rad(self.y_c))
1119+
if grad is None:
1120+
# First Gradient
1121+
grad = d_h / d
1122+
else:
1123+
# Fill hole
1124+
grad[grad.mask] = (d_h / d)[grad.mask]
1125+
return grad
1126+
1127+
def add_uv_lagerloef(self, grid_height, uname='u', vname='v', latmax=5):
1128+
self.add_uv(grid_height, uname, vname)
1129+
logging.info('Modified u/v with lagerloef 99 method abov %f', latmax)
1130+
data = self.grid(grid_height)
1131+
# Divide by sideral day
1132+
gof = sin(deg2rad(self.y_c)) * ones((self.x_c.shape[0], 1)) * 4. * pi / (23 * 3600 + 56 * 60 + 4.1)
1133+
with errstate(divide='ignore'):
1134+
gof = self.GRAVITY / (gof * ones((self.x_c.shape[0], 1)))
1135+
1136+
def add_uv2(self, grid_height, uname='u2', vname='v2'):
10691137
"""Compute a u and v grid
1070-
"""
1138+
"""
1139+
logging.info('Add u/v variable with stencil method')
10711140
data = self.grid(grid_height)
10721141
h_dict = self.variables_description[grid_height]
1073-
for variable in ('u', 'v'):
1142+
for variable in (uname, vname):
10741143
self.variables_description[variable] = dict(
10751144
infos=h_dict['infos'].copy(),
10761145
attrs=h_dict['attrs'].copy(),
10771146
args=tuple((variable, *h_dict['args'][1:])),
10781147
kwargs=h_dict['kwargs'].copy(),
10791148
)
1149+
if 'units' in self.variables_description[variable]['attrs']:
1150+
self.variables_description[variable]['attrs']['units'] += '/s'
1151+
if 'long_name' in self.variables_description[variable]['attrs']:
1152+
self.variables_description[variable]['attrs']['long_name'] += ' gradient'
1153+
# Divide by sideral day
1154+
gof = sin(deg2rad(self.y_c)) * ones((self.x_c.shape[0], 1)) * 4. * pi / (23 * 3600 + 56 * 60 + 4.1)
1155+
with errstate(divide='ignore'):
1156+
gof = self.GRAVITY / (gof * ones((self.x_c.shape[0], 1)))
10801157

1158+
# Compute v
1159+
mode = 'wrap' if self.is_circular() else 'reflect'
1160+
self.vars[vname] = self.compute_grad(data, mode=mode) * gof
1161+
# Compute u
1162+
self.vars[uname] = -self.compute_grad(data, vertical=True) * gof
1163+
1164+
def add_uv(self, grid_height, uname='u', vname='v'):
1165+
"""Compute a u and v grid
1166+
"""
1167+
logging.info('Add u/v variable with stencil method')
1168+
data = self.grid(grid_height)
1169+
h_dict = self.variables_description[grid_height]
1170+
for variable in (uname, vname):
1171+
self.variables_description[variable] = dict(
1172+
infos=h_dict['infos'].copy(),
1173+
attrs=h_dict['attrs'].copy(),
1174+
args=tuple((variable, *h_dict['args'][1:])),
1175+
kwargs=h_dict['kwargs'].copy(),
1176+
)
10811177
self.variables_description[variable]['attrs']['units'] += '/s'
10821178
# Divide by sideral day
10831179
gof = sin(deg2rad(self.y_c)) * ones((self.x_c.shape[0], 1)) * 4. * pi / (23 * 3600 + 56 * 60 + 4.1)
@@ -1094,7 +1190,7 @@ def add_uv(self, grid_height):
10941190
]
10951191

10961192
# Compute v
1097-
self.vars['v'] = None
1193+
self.vars[vname] = None
10981194
mode = 'wrap' if self.is_circular() else 'reflect'
10991195
for m_x in w:
11001196
if isinstance(m_x, tuple):
@@ -1112,13 +1208,13 @@ def add_uv(self, grid_height):
11121208
d_x_degrees = (d_x_degrees + 180) % 360 - 180
11131209
d_x = self.EARTH_RADIUS * 2 * pi / 360 * d_x_degrees * cos(deg2rad(self.y_c))
11141210
v = d_hx / d_x * gof
1115-
if self.vars['v'] is None:
1116-
self.vars['v'] = v
1211+
if self.vars[vname] is None:
1212+
self.vars[vname] = v
11171213
else:
1118-
self.vars['v'][self.vars['v'].mask] = v[self.vars['v'].mask]
1214+
self.vars[vname][self.vars[vname].mask] = v[self.vars[vname].mask]
11191215

11201216
# Compute u
1121-
self.vars['u'] = None
1217+
self.vars[uname] = None
11221218
for m_y in w:
11231219
if isinstance(m_y, tuple):
11241220
shift, m_y = m_y
@@ -1134,19 +1230,17 @@ def add_uv(self, grid_height):
11341230
d_y = self.EARTH_RADIUS * 2 * pi / 360 * convolve(self.y_c, m_y)
11351231

11361232
u = - d_hy / d_y * gof
1137-
if self.vars['u'] is None:
1138-
self.vars['u'] = u
1233+
if self.vars[uname] is None:
1234+
self.vars[uname] = u
11391235
else:
1140-
self.vars['u'][self.vars['u'].mask] = u[self.vars['u'].mask]
1236+
self.vars[uname][self.vars[uname].mask] = u[self.vars[uname].mask]
11411237

11421238
def speed_coef_mean(self, contour):
11431239
"""some nan can be compute over contour if we are near border,
11441240
something to explore
11451241
"""
11461242
return mean_on_regular_contour(
1147-
self.x_c, self.y_c,
1148-
self._speed_ev, self._speed_ev.mask,
1149-
contour.vertices)
1243+
self.x_c, self.y_c, self._speed_ev, self._speed_ev.mask, contour.vertices, nan_remove=True)
11501244

11511245
def init_speed_coef(self, uname='u', vname='v'):
11521246
"""Draft

src/py_eddy_tracker/eddy_feature.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def find_wrapcut_path_and_join(self, x0, x1):
331331
collection._paths = paths_out
332332
logging.info('%d contours close over the bounds', poly_solve)
333333

334-
def __init__(self, x, y, z, levels, bbox_surface_min_degree, wrap_x=False, keep_unclose=False):
334+
def __init__(self, x, y, z, levels, wrap_x=False, keep_unclose=False):
335335
"""
336336
c_i : index to contours
337337
l_i : index to levels
@@ -369,13 +369,6 @@ def __init__(self, x, y, z, levels, bbox_surface_min_degree, wrap_x=False, keep_
369369
# Contour with less vertices than 4 are popped
370370
if contour.vertices.shape[0] < 4:
371371
continue
372-
# Check if side of bbox is greater than ... => Avoid tiny shape
373-
x_min, y_min = contour.vertices.min(axis=0)
374-
x_max, y_max = contour.vertices.max(axis=0)
375-
d_x, d_y = x_max - x_min, y_max - y_min
376-
square_root = bbox_surface_min_degree ** .5
377-
if d_x <= square_root or d_y <= square_root:
378-
continue
379372
if keep_unclose:
380373
keep_path.append(contour)
381374
continue
@@ -391,6 +384,8 @@ def __init__(self, x, y, z, levels, bbox_surface_min_degree, wrap_x=False, keep_
391384
closed_contours += 1
392385
contour.vertices[-1] = contour.vertices[0]
393386
# Store to use latter
387+
x_min, y_min = contour.vertices.min(axis=0)
388+
x_max, y_max = contour.vertices.max(axis=0)
394389
contour.xmin = x_min
395390
contour.xmax = x_max
396391
contour.ymin = y_min
@@ -486,15 +481,17 @@ def get_index_nearest_path_bbox_contain_pt(self, level, xpt, ypt):
486481
else:
487482
return self.contours.collections[level]._paths[index]
488483

489-
def display(self, ax, **kwargs):
484+
def display(self, ax, step=1, **kwargs):
490485
from matplotlib.collections import LineCollection
491-
for collection in self.contours.collections:
486+
for collection in self.contours.collections[::step]:
492487
ax.add_collection(LineCollection(
493488
(i.vertices for i in collection.get_paths()),
494489
color=collection.get_color(),
495490
**kwargs
496491
))
497-
ax.update_datalim([self.contours._mins, self.contours._maxs])
492+
493+
if hasattr(self.contours, '_mins'):
494+
ax.update_datalim([self.contours._mins, self.contours._maxs])
498495
ax.autoscale_view()
499496

500497

src/scripts/EddyId

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ if __name__ == '__main__':
2323
args = id_parser().parse_args()
2424

2525
h = RegularGridDataset(args.filename, args.longitude, args.latitude)
26-
h.bessel_high_filter(args.h, 500, order=3)
27-
2826
date = datetime.strptime(args.datetime, '%Y%m%d')
29-
a, c = h.eddy_identification(
30-
args.h, args.u, args.v, date, 0.002, pixel_limit=(5, 2000), shape_error=55, bbox_surface_min_degree=.125**2)
27+
if args.u == 'None' and args.v == 'None':
28+
h.add_uv(args.h)
29+
u, v = 'u', 'v'
30+
else:
31+
u, v = args.u, args.v
32+
h.bessel_high_filter(args.h, 500, order=3)
33+
a, c = h.eddy_identification(args.h, u, v, date, 0.002, pixel_limit=(5, 2000), shape_error=55)
3134
with Dataset(args.path_out + date.strftime('/Anticyclonic_%Y%m%d.nc'), 'w') as h:
3235
a.to_netcdf(h)
3336
with Dataset(args.path_out + date.strftime('/Cyclonic_%Y%m%d.nc'), 'w') as h:

0 commit comments

Comments
 (0)