@@ -33,10 +33,39 @@ def rename_bands(ds, old_string, new_string):
33
33
return ds_renamed
34
34
35
35
36
+ def tidal_thresholds (
37
+ tides_highres ,
38
+ threshold_lowtide = 0.15 ,
39
+ threshold_hightide = 0.85 ,
40
+ min_obs = 0 ,
41
+ ):
42
+ # Calculate per-pixel integer rankings for each tide height
43
+ rank_n = tides_highres .rank (dim = "time" )
44
+
45
+ # Calculate low and high ranking thresholds from total rankings.
46
+ # Low threshold needs to be rounded up ("ceil"), and high tide
47
+ # rounded down ("floor") to ensure we capture all matching values.
48
+ rank_max = rank_n .max (dim = "time" )
49
+ rank_thresh_low = np .ceil (rank_max * threshold_lowtide )
50
+ rank_thresh_high = np .floor (rank_max * threshold_hightide )
51
+
52
+ # Update thresholds to ensure minimum number of valid observations
53
+ if min_obs > 0 :
54
+ rank_thresh_low = np .maximum (rank_thresh_low , min_obs )
55
+ rank_thresh_high = np .minimum (rank_thresh_high , rank_max - min_obs )
56
+
57
+ # Calculate tide thresholds by masking tides by ranking threshold
58
+ tide_thresh_low = tides_highres .where (rank_n <= rank_thresh_low ).max (dim = "time" )
59
+ tide_thresh_high = tides_highres .where (rank_n >= rank_thresh_high ).min (dim = "time" )
60
+
61
+ return tide_thresh_low , tide_thresh_high
62
+
63
+
36
64
def tidal_composites (
37
65
satellite_ds ,
38
- threshold_lowtide = 0.2 ,
39
- threshold_hightide = 0.8 ,
66
+ threshold_lowtide = 0.15 ,
67
+ threshold_hightide = 0.85 ,
68
+ min_obs = 0 ,
40
69
eps = 1e-4 ,
41
70
cpus = None ,
42
71
max_iters = 10000 ,
@@ -55,7 +84,7 @@ def tidal_composites(
55
84
to filter satellite data to low and high tide images prior to
56
85
loading it into memory, allowing more efficient processing.
57
86
58
- Based on the method described in:
87
+ Pixel-based implementation of the method originally published in:
59
88
60
89
Sagar, S., Phillips, C., Bala, B., Roberts, D., & Lymburner, L.
61
90
(2018). Generating Continental Scale Pixel-Based Surface Reflectance
@@ -67,9 +96,12 @@ def tidal_composites(
67
96
satellite_ds : xarray.Dataset
68
97
A satellite data time series containing spectral bands.
69
98
threshold_lowtide : float, optional
70
- Quantile used to identify low tide observations, by default 0.2 .
99
+ Quantile used to identify low tide observations, by default 0.15 .
71
100
threshold_hightide : float, optional
72
- Quantile used to identify high tide observations, by default 0.8.
101
+ Quantile used to identify high tide observations, by default 0.85.
102
+ min_obs : int, optional
103
+ Minimum number of clear observations to enforce when calculating tide
104
+ height thresholds. Defaults to 0, which will not apply any minimum.
73
105
eps: float, optional
74
106
Termination criteria passed on to the geomedian algorithm.
75
107
cpus: int, optional
@@ -134,14 +166,22 @@ def tidal_composites(
134
166
tides_highres = tides_highres .where (nodata_array )
135
167
136
168
# Calculate low and high tide thresholds from masked tide data
137
- log .info (f"{ run_id } : Calculating low and high tide thresholds" )
138
- threshold_ds = xr_quantile (
139
- src = tides_highres .to_dataset (),
140
- quantiles = [threshold_lowtide , threshold_hightide ],
141
- nodata = np .nan ,
169
+ log .info (
170
+ f"{ run_id } : Calculating low and high tide thresholds with minimum { min_obs } observations"
171
+ )
172
+ # threshold_ds = xr_quantile(
173
+ # src=tides_highres.to_dataset(),
174
+ # quantiles=[threshold_lowtide, threshold_hightide],
175
+ # nodata=np.nan,
176
+ # )
177
+ # low_threshold = threshold_ds.isel(quantile=0).tide_height.drop("quantile")
178
+ # high_threshold = threshold_ds.isel(quantile=-1).tide_height.drop("quantile")
179
+ low_threshold , high_threshold = tidal_thresholds (
180
+ tides_highres = tides_highres ,
181
+ threshold_lowtide = threshold_lowtide ,
182
+ threshold_hightide = threshold_hightide ,
183
+ min_obs = min_obs ,
142
184
)
143
- low_threshold = threshold_ds .isel (quantile = 0 ).tide_height .drop ("quantile" )
144
- high_threshold = threshold_ds .isel (quantile = - 1 ).tide_height .drop ("quantile" )
145
185
146
186
# Create masks for selecting satellite observations below and above the
147
187
# low and high tide thresholds
@@ -270,14 +310,21 @@ def tidal_composites(
270
310
@click .option (
271
311
"--threshold_lowtide" ,
272
312
type = float ,
273
- default = 0.2 ,
274
- help = "The quantile used to identify low tide observations. " " Defaults to 0.2 ." ,
313
+ default = 0.15 ,
314
+ help = "The quantile used to identify low tide observations. Defaults to 0.15 ." ,
275
315
)
276
316
@click .option (
277
317
"--threshold_hightide" ,
278
318
type = float ,
279
- default = 0.8 ,
280
- help = "The quantile used to identify high tide observations. " "Defaults to 0.8." ,
319
+ default = 0.85 ,
320
+ help = "The quantile used to identify high tide observations. Defaults to 0.85." ,
321
+ )
322
+ @click .option (
323
+ "--min_obs" ,
324
+ type = int ,
325
+ default = 0 ,
326
+ help = "Minimum number of clear observations to enforce when calculating tide "
327
+ "height thresholds. Defaults to 0, which will not apply any minimum." ,
281
328
)
282
329
@click .option (
283
330
"--mask_sunglint" ,
@@ -355,6 +402,7 @@ def tidal_composites_cli(
355
402
resolution ,
356
403
threshold_lowtide ,
357
404
threshold_hightide ,
405
+ min_obs ,
358
406
mask_sunglint ,
359
407
include_coastal_aerosol ,
360
408
eps ,
@@ -429,7 +477,7 @@ def tidal_composites_cli(
429
477
)
430
478
431
479
# Fail early if not enough observations
432
- if len (satellite_ds .time ) < 20 :
480
+ if len (satellite_ds .time ) < 50 :
433
481
raise Exception (
434
482
"Insufficient satellite data available to process composites; skipping."
435
483
)
@@ -440,6 +488,7 @@ def tidal_composites_cli(
440
488
satellite_ds = satellite_ds ,
441
489
threshold_lowtide = threshold_lowtide ,
442
490
threshold_hightide = threshold_hightide ,
491
+ min_obs = min_obs ,
443
492
eps = eps ,
444
493
cpus = cpus ,
445
494
max_iters = max_iters ,
0 commit comments