Skip to content

Commit 7ba6b04

Browse files
committed
Updat of object oriented identification
1 parent 1f8f5d1 commit 7ba6b04

File tree

11 files changed

+259
-141
lines changed

11 files changed

+259
-141
lines changed

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
packages=find_packages('src'),
1717
package_dir={'': 'src'},
1818
scripts=[
19+
'src/scripts/EddyId',
1920
'src/scripts/EddyIdentification',
2021
'src/scripts/EddyTracking',
2122
'src/scripts/EddyFinalTracking',
@@ -44,5 +45,6 @@
4445
'shapely',
4546
'pyyaml',
4647
'pyproj',
48+
'pint'
4749
],
4850
)

src/py_eddy_tracker/__init__.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ def parse_args(self, *args, **kwargs):
101101
reference_description="Julian date on Jan 1, 1992",
102102
)
103103
),
104+
time_jj=dict(
105+
attr_name='time',
106+
nc_name='time',
107+
nc_type='int32',
108+
nc_dims=('Nobs',),
109+
nc_attr=dict(
110+
standard_name='time',
111+
units='days since 1950-01-01 00:00:00',
112+
calendar='proleptic_gregorian',
113+
axis='T',
114+
long_name='time of gridded file',
115+
description='date of this observation',
116+
)
117+
),
104118
ocean_time=dict(
105119
attr_name=None,
106120
nc_name='ocean_time',
@@ -186,20 +200,6 @@ def parse_args(self, *args, **kwargs):
186200
'around the contour defining the eddy perimeter',
187201
)
188202
),
189-
#~ radius=dict(
190-
#~ attr_name=None,
191-
#~ nc_name='L',
192-
#~ nc_type='float32',
193-
#~ nc_dims=('Nobs',),
194-
#~ scale_factor=1e-3,
195-
#~ nc_attr=dict(
196-
#~ long_name='speed radius scale',
197-
#~ units='km',
198-
#~ description='radius of a circle whose area is equal to that '
199-
#~ 'enclosed by the contour of maximum circum-average'
200-
#~ ' speed',
201-
#~ )
202-
#~ ),
203203
speed_radius=dict(
204204
attr_name='speed_radius',
205205
scale_factor=100,

src/py_eddy_tracker/dataset/grid.py

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
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, nan, ceil, sinc, float64
7+
linspace, errstate, int_, column_stack, interp, meshgrid, nan, ceil, sinc, float64, isnan
8+
from datetime import datetime
89
from scipy.special import j1
9-
from scipy.signal import convolve2d
1010
from netCDF4 import Dataset
1111
from scipy.ndimage import gaussian_filter, convolve
12-
from scipy.interpolate import RectBivariateSpline
12+
from scipy.interpolate import RectBivariateSpline, interp1d
1313
from scipy.spatial import cKDTree
14+
from scipy.signal import welch, convolve2d
1415
from matplotlib.path import Path as BasePath
1516
from matplotlib.contour import QuadContourSet as BaseQuadContourSet
1617
from pyproj import Proj
18+
from pint import UnitRegistry
1719
from ..tools import fit_circle_c, distance_vector, winding_number_poly, poly_contain_poly, \
1820
distance, distance_point_vector
1921
from ..observations import EddiesObservations
2022
from ..eddy_feature import Amplitude, Contours
23+
from .. import VAR_DESCR
2124

2225

2326
def raw_resample(datas, fixed_size):
@@ -314,6 +317,12 @@ def is_circular(self):
314317
"""
315318
return False
316319

320+
def units(self, varname):
321+
with Dataset(self.filename) as h:
322+
var = h.variables[varname]
323+
if hasattr(var, 'units'):
324+
return var.units
325+
317326
def grid(self, varname):
318327
"""give grid required
319328
"""
@@ -351,8 +360,10 @@ def bounds(self):
351360
"""
352361
return self.x_bounds.min(), self.x_bounds.max(), self.y_bounds.min(), self.y_bounds.max()
353362

354-
def eddy_identification(self, grid_height, uname, vname, step=0.005, shape_error=55,
363+
def eddy_identification(self, grid_height, uname, vname, date, step=0.005, shape_error=55,
355364
array_sampling=50, pixel_limit=None, bbox_surface_min_degree=.125**2):
365+
if not isinstance(date, datetime):
366+
raise Exception('Date argument be a datetime object')
356367
# The inf limit must be in pixel and sup limit in surface
357368
if pixel_limit is None:
358369
pixel_limit = (4, 1000)
@@ -459,8 +470,7 @@ def eddy_identification(self, grid_height, uname, vname, step=0.005, shape_error
459470
# Instantiate new EddyObservation object
460471
properties = EddiesObservations(
461472
size=1,
462-
track_extra_variables=['shape_error_e', 'shape_error_s', 'height_max_speed_contour',
463-
'height_external_contour', 'height_inner_contour', 'nb_contour_selected'],
473+
track_extra_variables=[ 'height_max_speed_contour', 'height_external_contour', 'height_inner_contour'],
464474
track_array_variables=array_sampling,
465475
array_variables=['contour_lon_e', 'contour_lat_e', 'contour_lon_s', 'contour_lat_s', 'uavg_profile']
466476
)
@@ -492,7 +502,23 @@ def eddy_identification(self, grid_height, uname, vname, step=0.005, shape_error
492502
eddies.append(properties)
493503
# To reserve definitively the area
494504
data.mask[i_x_in, i_y_in] = True
495-
a_and_c.append(EddiesObservations.concatenate(eddies))
505+
if len(eddies) == 0:
506+
eddies_collection = EddiesObservations()
507+
else:
508+
eddies_collection = EddiesObservations.concatenate(eddies)
509+
eddies_collection.sign_type = 1 if anticyclonic_search else -1
510+
eddies_collection.obs['time'] = (date - datetime(1950, 1, 1)).total_seconds() / 86400.
511+
512+
a_and_c.append(eddies_collection)
513+
h_units = self.units(grid_height)
514+
units = UnitRegistry()
515+
in_unit = units.parse_expression(h_units)
516+
if in_unit is not None:
517+
for name in ['amplitude', 'height_max_speed_contour', 'height_external_contour', 'height_inner_contour']:
518+
out_unit = units.parse_expression(VAR_DESCR[name]['nc_attr']['units'])
519+
factor, _ = in_unit.to(out_unit).to_tuple()
520+
a_and_c[0].obs[name] *= factor
521+
a_and_c[1].obs[name] *= factor
496522
return a_and_c
497523

498524
def get_uavg(self, all_contours, centlon_e, centlat_e, original_contour, anticyclonic_search, level_start,
@@ -528,7 +554,6 @@ def get_uavg(self, all_contours, centlon_e, centlat_e, original_contour, anticyc
528554
level_contour.pixels_in(self)
529555
if pixel_min > level_contour.nb_pixel:
530556
break
531-
532557
# Interpolate uspd to seglon, seglat, then get mean
533558
level_average_speed = self.speed_coef(level_contour).mean()
534559
speed_array.append(level_average_speed)
@@ -851,6 +876,10 @@ def kernel_bessel(self, lat, wave_length, order=1):
851876
# Estimate size of kernel
852877
step_y_km = self.ystep * distance(0, 0, 0, 1) / 1000
853878
step_x_km = self.xstep * distance(0, lat, 1, lat) / 1000
879+
min_wave_length = max(step_x_km * 2, step_y_km * 2)
880+
if wave_length < min_wave_length:
881+
logging.error('Wave_length to short for resolution, must be > %d km', ceil(min_wave_length))
882+
raise Exception()
854883
# half size will be multiply with by order
855884
half_x_pt, half_y_pt = ceil(wave_length / step_x_km).astype(int), ceil(wave_length / step_y_km).astype(int)
856885

@@ -876,7 +905,7 @@ def kernel_bessel(self, lat, wave_length, order=1):
876905
kernel[dist_norm > order] = 0
877906
return kernel
878907

879-
def convolve_filter_with_dynamic_kernel(self, grid_name, kernel_func, lat_max, **kwargs_func):
908+
def convolve_filter_with_dynamic_kernel(self, grid_name, kernel_func, lat_max=85, **kwargs_func):
880909
logging.warning('No filtering above %f degrees of latitude', lat_max)
881910
data = self.grid(grid_name).copy()
882911
# Matrix for result
@@ -943,6 +972,14 @@ def lanczos_low_filter(self, grid_name, wave_length, order=1, lat_max=85):
943972
grid_name, self.kernel_lanczos, lat_max=lat_max, wave_length=wave_length, order=order)
944973
self.vars[grid_name] = data_out
945974

975+
def bessel_band_filter(self, grid_name, wave_length_inf, wave_length_sup, **kwargs):
976+
data_out = self.convolve_filter_with_dynamic_kernel(
977+
grid_name, self.kernel_bessel, wave_length=wave_length_inf, **kwargs)
978+
self.vars[grid_name] = data_out
979+
data_out = self.convolve_filter_with_dynamic_kernel(
980+
grid_name, self.kernel_bessel, wave_length=wave_length_sup, **kwargs)
981+
self.vars[grid_name] -= data_out
982+
946983
def bessel_high_filter(self, grid_name, wave_length, order=1, lat_max=85):
947984
data_out = self.convolve_filter_with_dynamic_kernel(
948985
grid_name, self.kernel_bessel, lat_max=lat_max, wave_length=wave_length, order=order)
@@ -953,6 +990,62 @@ def bessel_low_filter(self, grid_name, wave_length, order=1, lat_max=85):
953990
grid_name, self.kernel_bessel, lat_max=lat_max, wave_length=wave_length, order=order)
954991
self.vars[grid_name] = data_out
955992

993+
def spectrum_lonlat(self, grid_name, area=None, ref=None, **kwargs):
994+
if area is None:
995+
area = dict(llcrnrlon=190, urcrnrlon=280, llcrnrlat=-62, urcrnrlat=8)
996+
scaling = kwargs.pop('scaling', 'density')
997+
x0, y0 = self.nearest_grd_indice(area['llcrnrlon'], area['llcrnrlat'])
998+
x1, y1 = self.nearest_grd_indice(area['urcrnrlon'], area['urcrnrlat'])
999+
1000+
data = self.grid(grid_name)[x0:x1,y0:y1]
1001+
1002+
# Lat spectrum
1003+
pws = list()
1004+
step_y_km = self.ystep * distance(0, 0, 0, 1) / 1000
1005+
nb_invalid = 0
1006+
for i, _ in enumerate(self.x_c[x0:x1]):
1007+
f, pw = welch(data[i,:], 1 / step_y_km, scaling=scaling, **kwargs)
1008+
if isnan(pw).any():
1009+
nb_invalid += 1
1010+
continue
1011+
pws.append(pw)
1012+
if nb_invalid:
1013+
logging.warning('%d/%d columns invalid', nb_invalid, i + 1)
1014+
lat_content = 1 / f, array(pws).mean(axis=0)
1015+
1016+
# Lon spectrum
1017+
fs, pws = list(), list()
1018+
f_min, f_max = None, None
1019+
nb_invalid = 0
1020+
for i, lat in enumerate(self.y_c[y0:y1]):
1021+
step_x_km = self.xstep * distance(0, lat, 1, lat) / 1000
1022+
f, pw = welch(data[:,i], 1 / step_x_km, scaling=scaling, **kwargs)
1023+
if isnan(pw).any():
1024+
nb_invalid += 1
1025+
continue
1026+
if f_min is None:
1027+
f_min = f.min()
1028+
f_max = f.max()
1029+
else:
1030+
f_min = max(f_min, f.min())
1031+
f_max = min(f_max, f.max())
1032+
fs.append(f)
1033+
pws.append(pw)
1034+
if nb_invalid:
1035+
logging.warning('%d/%d lines invalid', nb_invalid, i + 1)
1036+
f_interp = linspace(f_min, f_max, f.shape[0])
1037+
pw_m = array(
1038+
[interp1d(f, pw, fill_value=0., bounds_error=False)(f_interp) for f, pw in zip(fs, pws)]).mean(axis=0)
1039+
lon_content = 1 / f_interp, pw_m
1040+
if ref is None:
1041+
return lon_content, lat_content
1042+
else:
1043+
ref_lon_content, ref_lat_content = ref.spectrum_lonlat(grid_name, area, **kwargs)
1044+
return (lon_content[0], lon_content[1] / ref_lon_content[1]), \
1045+
(lat_content[0], lat_content[1] / ref_lat_content[1])
1046+
1047+
1048+
9561049
def add_uv(self, grid_height):
9571050
"""Compute a u and v grid
9581051
"""

src/py_eddy_tracker/eddy_feature.py

Lines changed: 22 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class Amplitude(object):
4949
'iy',
5050
'interval',
5151
'amplitude',
52-
'local_extrema',
5352
'local_extrema_inds',
5453
'mle',
5554
)
@@ -68,9 +67,10 @@ def __init__(self, i_contour_x, i_contour_y, contour_height, data, interval):
6867
self.iy -= self.slice_y.start
6968
# Link on original grid (local view)
7069
self.data = data[self.slice_x, self.slice_y]
70+
# => maybe replace pixel out of contour by nan?
71+
7172
# Amplitude which will be provide
7273
self.amplitude = 0
73-
self.local_extrema = None
7474
self.local_extrema_inds = None
7575
# Maximum local extrema accepted
7676
self.mle = 1
@@ -101,23 +101,16 @@ def all_pixels_below_h0(self, level):
101101
return False
102102
else:
103103
self._set_local_extrema(1)
104-
if 0 < self.local_extrema <= self.mle:
105-
self._set_cyc_amplitude()
104+
lmi_i, lmi_j = where(self.local_extrema_inds)
105+
nb_real_extrema = ((level - self.data[lmi_i, lmi_j]) >= 2 * self.interval).sum()
106+
if nb_real_extrema > self.mle:
106107
return False
107-
elif self.local_extrema > self.mle:
108-
lmi_i, lmi_j = where(self.local_extrema_inds)
109-
levnm2 = level - (2 * self.interval)
110-
slamin = 1e5
111-
for i, j in zip(lmi_i, lmi_j):
112-
if slamin >= self.data[i, j]:
113-
slamin = self.data[i, j]
114-
imin, jmin = i, j
115-
if self.data[i, j] >= levnm2:
116-
self._set_cyc_amplitude()
117-
# Prevent further calls to_set_cyc_amplitude
118-
levnm2 = 1e5
119-
return imin + self.slice_x.start, jmin + self.slice_y.start
120-
return False
108+
amp_min = level - (2 * self.interval)
109+
index = self.data[lmi_i, lmi_j].argmin()
110+
jmin_, imin_ = lmi_j[index], lmi_i[index]
111+
if self.data[imin_, jmin_] <= amp_min:
112+
self._set_cyc_amplitude()
113+
return imin_ + self.slice_x.start, jmin_ + self.slice_y.start
121114

122115
def all_pixels_above_h0(self, level):
123116
"""
@@ -130,39 +123,23 @@ def all_pixels_above_h0(self, level):
130123
return False
131124
else:
132125
self._set_local_extrema(-1)
133-
# If we have a number of extrema avoid, we compute amplitude
134-
if 0 < self.local_extrema <= self.mle:
135-
self._set_acyc_amplitude()
126+
lmi_i, lmi_j = where(self.local_extrema_inds)
127+
nb_real_extrema = ((self.data[lmi_i, lmi_j] - level) >= 2 * self.interval).sum()
128+
if nb_real_extrema > self.mle:
136129
return False
137-
138-
# More than avoid
139-
elif self.local_extrema > self.mle:
140-
# index of extrema
141-
lmi_i, lmi_j = where(self.local_extrema_inds)
142-
levnp2 = level + (2 * self.interval)
143-
slamax = -1e5
144-
# Iteration on extrema
145-
for i, j in zip(lmi_i, lmi_j):
146-
# We iterate on max and search the first sup of slamax
147-
if slamax <= self.data[i, j]:
148-
slamax = self.data[i, j]
149-
imax, jmax = i, j
150-
151-
if self.data[i, j] <= levnp2:
152-
self._set_acyc_amplitude()
153-
# Prevent further calls to_set_acyc_amplitude
154-
levnp2 = -1e5
155-
return imax + self.slice_x.start, jmax + self.slice_y.start
156-
return False
130+
amp_min = level + (2 * self.interval)
131+
index = self.data[lmi_i, lmi_j].argmax()
132+
jmin_, imin_ = lmi_j[index], lmi_i[index]
133+
if self.data[imin_, jmin_] >= amp_min:
134+
self._set_cyc_amplitude()
135+
return imin_ + self.slice_x.start, jmin_ + self.slice_y.start
157136

158137
def _set_local_extrema(self, sign):
159138
"""
160139
Set count of local SLA maxima/minima within eddy
161140
"""
162141
# mask of minima
163142
self.local_extrema_inds = self.detect_local_minima(self.data * sign)
164-
# nb of minima
165-
self.local_extrema = self.local_extrema_inds.sum()
166143

167144
@staticmethod
168145
def detect_local_minima(grid):
@@ -174,8 +151,8 @@ def detect_local_minima(grid):
174151
"""
175152
# Equivalent
176153
neighborhood = ones((3, 3), dtype='bool')
177-
# ~ neighborhood = generate_binary_structure(grid.ndim, 2)
178-
154+
if hasattr(grid, 'mask'):
155+
grid[grid.mask] = 2e10
179156
# Get local mimima
180157
detected_minima = minimum_filter(
181158
grid, footprint=neighborhood) == grid

src/py_eddy_tracker/grid/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@
4343

4444

4545
def browse_dataset_in(data_dir, files_model, date_regexp, date_model,
46-
start_date=None, end_date=None, sub_sampling_step=1):
47-
pattern_regexp = re_compile('.*/' + date_regexp)
48-
full_path = join_path(data_dir, files_model)
49-
logging.info('Search files : %s', full_path)
46+
start_date=None, end_date=None, sub_sampling_step=1,
47+
files=None):
48+
if files is not None:
49+
pattern_regexp = re_compile('.*/' + date_regexp)
50+
filenames = bytes_(files)
51+
else:
52+
pattern_regexp = re_compile('.*/' + date_regexp)
53+
full_path = join_path(data_dir, files_model)
54+
logging.info('Search files : %s', full_path)
55+
filenames = bytes_(glob(full_path))
5056

51-
filenames = bytes_(glob(full_path))
5257
dataset_list = empty(len(filenames),
5358
dtype=[('filename', 'S500'),
5459
('date', 'datetime64[D]'),

0 commit comments

Comments
 (0)