Skip to content

Commit 22b4fa0

Browse files
cassiestuurmanRui WeiCassie Stuurman
authored and
GitHub Enterprise
committed
Delivery 1.3.1 updates (#92)
* Compute and output node-level WSE uncertainties from Bayes reconst * Fix wse and slope random uncertainties/unit conversions * Move unc scaling factors into b and c vectors * Keep degraded area pixels that overlie prior water mask * Move prior water check up to pixel mask * Fix logic for finding degraded_and_no_prior_water * Add w_opt and w_opt_r_u to river product * Output wse_opt only for populated nodes * Fix reconst random uncert comment * Update area flg to use all pixels * Do not merge non-contiguous land-near-water pix into dominant label * Consider degraded pixels for area flag * Change default scaling to 5 instead of 20 if ext_dist is missing * Reduce initial pixassgn search width to 1/3 max width * KCV-465_area_wse_fix (#91) Add the computation of the total area of pixels that were used in the WSE calculation for area_wse * Add specular ringing frac to product * put >0 inside the parentheses for is_area_suspect mask * Pull updated reach plotters from pix subset * Use reach mask when computing reach width * Do not change degraded class qual to suspect where there's prior water * Do not publish sring_frac or w_opt to product * Check that mask has any good to avoid warning --------- Co-authored-by: Rui Wei <rui.wei@jpl.nasa.gov> Co-authored-by: Cassie Stuurman <stuurman@frode.jpl.nasa.gov>
1 parent a00e81f commit 22b4fa0

File tree

4 files changed

+195
-62
lines changed

4 files changed

+195
-62
lines changed

src/RiverObs/RiverNode.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,11 @@ def percentile(self, var, q, goodvar='good'):
203203
def sum(self, var, goodvar='good'):
204204
"""Return the sum of all variable values (e.g., for area)."""
205205
good = getattr(self, goodvar)
206-
Sum = np.sum(getattr(self, var)[good])
207-
return Sum
206+
if good.any():
207+
Sum = np.sum(getattr(self, var)[good])
208+
return Sum
209+
else:
210+
return 0
208211

209212
def cdf(self, var, goodvar='good'):
210213
"""Get the cdf for a variable."""

src/RiverObs/RiverObs.py

+27-21
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ def flag_out_channel_and_label(
160160
that are unconnected to the dominant label but within the original
161161
max_dist threshold.
162162
163-
If no extreme distance value is given it will use a very wide threshold
164-
of 20x the input max_width for all nodes.
163+
If no extreme distance value is given it will use a threshold of 5x
164+
the input max_width for all nodes.
165165
166166
Parameters
167167
----------
@@ -201,17 +201,18 @@ def flag_out_channel_and_label(
201201
# Find the dominant label and include pixels up to the extreme dist
202202
self.dominant_label = None
203203
if seg_label is not None and self.in_channel.any():
204-
class_mask = np.logical_and(self.in_channel, seg_label > 0)
205-
if class_mask.any():
206-
# find the largest (i.e. dominant) label within the
207-
# max_distance bounds
204+
# remove unattached land-near-water pixels
205+
self.in_channel = np.logical_and(self.in_channel, seg_label > 0)
206+
# find the largest (i.e. dominant) label within the
207+
# max_distance bounds
208+
if self.in_channel.any():
208209
try:
209210
dominant_label = scipy.stats.mode(
210-
seg_label[class_mask], keepdims=False)[0]
211+
seg_label[self.in_channel], keepdims=False)[0]
211212
except TypeError:
212213
# Try previous syntax if TypeError raised (older scipy)
213214
dominant_label = scipy.stats.mode(
214-
seg_label[class_mask])[0][0]
215+
seg_label[self.in_channel])[0][0]
215216

216217
self.dominant_label = dominant_label
217218

@@ -229,7 +230,7 @@ def flag_out_channel_and_label(
229230

230231
# search for segment in this node which has smallest
231232
# absolute value of n coordinate
232-
min_n_seg_label, min_n = -1, 9999999999999
233+
min_n_seg_label, min_n = 0, 9999999999999
233234
for this_seg_label in np.unique(
234235
seg_label[this_node_mask]):
235236

@@ -244,7 +245,8 @@ def flag_out_channel_and_label(
244245

245246
# merge seg label of segment with min abs n coordinate
246247
# with dominant label
247-
if min_n_seg_label != -1:
248+
if min_n_seg_label != 0 \
249+
and min_n_seg_label != dominant_label:
248250
seg_label[seg_label == min_n_seg_label] = (
249251
dominant_label)
250252

@@ -258,9 +260,6 @@ def flag_out_channel_and_label(
258260
dst0 <= extreme_dist,
259261
abs(self.n) <= extreme_dist)))
260262

261-
else:
262-
self.in_channel = class_mask
263-
264263
self.index = self.index[self.in_channel]
265264
self.d = self.d[self.in_channel]
266265
self.x = self.x[self.in_channel]
@@ -291,16 +290,19 @@ def get_ext_dist_threshold(self, max_width, ext_dist_coef):
291290
lakes
292291
"""
293292
if np.iterable(max_width):
294-
max_distance = max_width[self.index] / 2.
293+
max_distance = max_width[self.index] / 3. # was 2
295294
else:
296-
max_distance = max_width / 2.
295+
max_distance = max_width / 3. # was 2
297296

298297
node_spacing = abs(self.ds[self.index])
299298
if ext_dist_coef is None:
300-
scale_factor = 20.0
299+
scale_factor = 5.0
301300
else:
302301
scale_factor = ext_dist_coef[self.index]
303-
extreme_dist = scale_factor * np.maximum(node_spacing, max_distance)
302+
303+
extreme_dist = scale_factor * np.maximum(
304+
node_spacing, max_distance * 3/2
305+
) # includes 3/2 to make ext_dist operate on river half-width
304306
return max_distance, extreme_dist
305307

306308
def flag_out_channel(self, max_width):
@@ -309,9 +311,9 @@ def flag_out_channel(self, max_width):
309311
and remove the points from the list of observations.
310312
"""
311313
if np.iterable(max_width):
312-
max_distance = max_width[self.index] / 2.
314+
max_distance = max_width[self.index] / 3. # was 2
313315
else:
314-
max_distance = max_width / 2.
316+
max_distance = max_width / 3. # was 2
315317

316318
self.in_channel = np.abs(self.n) <= max_distance
317319

@@ -485,8 +487,8 @@ def get_node_agg(
485487
"""
486488
outputs = {key: [] for key in [
487489
'h', 'h_std', 'h_u', 'lat_u', 'lon_u', 'area', 'area_u',
488-
'area_det', 'area_det_u', 'width_area', 'width_area_u', 'sig0',
489-
'sig0_u', 'sig0_std']}
490+
'area_det', 'area_det_u', 'area_of_ht', 'area_of_ht_u',
491+
'width_area', 'width_area_u', 'sig0', 'sig0_u', 'sig0_std']}
490492

491493
for node in self.all_nodes:
492494
if node in self.populated_nodes:
@@ -502,6 +504,10 @@ def get_node_agg(
502504
river_node.area_with_uncert(
503505
method=area_method, goodvar=goodvar_area)
504506

507+
area_of_ht, _, area_of_ht_u, _, _, _ = \
508+
river_node.area_with_uncert(
509+
method = area_method, goodvar = goodvar_wse)
510+
505511
local_vars = locals()
506512
for key in outputs:
507513
value = local_vars[key]

src/SWOTRiver/SWOTRiverEstimator.py

+88-32
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,19 @@ def __init__(self,
420420
mask, class_qual_area_bad & classification_qual > 0,
421421
geo_qual_wse_bad & geolocation_qual > 0])
422422

423+
# Remove degraded class_qual where there is no prior water
424+
PIXC_CLASS_QUAL_NO_PRIOR = 2**2
425+
degraded_class_mask = class_qual_area_degraded \
426+
& classification_qual > 0
427+
no_prior_water_mask = classification_qual \
428+
& PIXC_CLASS_QUAL_NO_PRIOR > 0
429+
degraded_and_no_prior_water = degraded_class_mask & no_prior_water_mask
430+
degraded_and_prior_water = degraded_class_mask & ~no_prior_water_mask
431+
# uncomment to make make degraded class_qual good anywhere there is
432+
# prior water
433+
# degraded_class_mask[degraded_and_prior_water] = False
434+
mask = np.logical_or(mask, degraded_and_no_prior_water)
435+
423436
# HACK in avoidance of bad water_frac values
424437
water_frac = self.get(fractional_inundation_kwd)
425438
mask = np.logical_or(mask, water_frac.mask)
@@ -429,7 +442,6 @@ def __init__(self,
429442
mask = np.logical_or(mask, bright_land_flag > 0)
430443

431444
# skip NaNs in dheight_dphase
432-
433445
good = ~mask
434446
if good.sum() == 0:
435447
LOGGER.warning("No usable pixels found in input PIXC file")
@@ -491,15 +503,18 @@ def __init__(self,
491503
# later in node aggregation.
492504
PIXC_GEO_QUAL_XOVR_SUSPECT = 2**6
493505
PIXC_GEO_QUAL_XOVR_BAD = 2**23
506+
PIXC_CLASS_QUAL_RINGING = 2**19
494507

495508
if self.classification_qual is not None:
496-
self.is_area_degraded = (
497-
class_qual_area_degraded & self.classification_qual > 0)
509+
self.is_area_degraded = degraded_class_mask[good]
498510
self.is_area_suspect = (
499-
class_qual_area_suspect & self.classification_qual > 0)
511+
class_qual_area_suspect & self.classification_qual > 0)
512+
self.sring_flg = (
513+
self.classification_qual & PIXC_CLASS_QUAL_RINGING) > 0
500514
else:
501515
self.is_area_degraded = np.zeros(self.lat.shape, dtype='bool')
502516
self.is_area_suspect = np.zeros(self.lat.shape, dtype='bool')
517+
self.sring_flg = np.zeros(self.lat.shape, dtype='bool')
503518

504519
if self.geolocation_qual is not None:
505520
self.is_wse_degraded = (
@@ -1265,7 +1280,7 @@ def process_node(self,
12651280
# self.river_obs.
12661281
dsets = [
12671282
'h_noise', 'h_flg', 'wse_class_flg', 'area_flg', 'sig0_flg', 'lon',
1268-
'lat', 'inundated_area', 'klass', 'pixel_area',
1283+
'lat', 'inundated_area', 'klass', 'pixel_area', 'sring_flg',
12691284
'xtrack', 'sig0', 'sig0_uncert', 'water_frac', 'water_frac_uncert',
12701285
'ifgram', 'power1', 'power2', 'phase_noise_std', 'dh_dphi',
12711286
'dlat_dphi', 'dlon_dphi', 'num_rare_looks', 'num_med_looks',
@@ -1398,6 +1413,10 @@ def process_node(self,
13981413
self.river_obs.get_node_stat(
13991414
'sum', 'inundated_area', goodvar='good')
14001415
)[~mask_good_sus_area]
1416+
# compute the area of all pixels flagged specular ringing
1417+
area_sring = np.asarray(
1418+
self.river_obs.get_node_stat('sum', 'inundated_area',
1419+
goodvar='sring_flg'))
14011420

14021421
# area of pixels used to compute heights
14031422
with warnings.catch_warnings():
@@ -1461,7 +1480,7 @@ def process_node(self,
14611480
area_u = node_aggs['area_u']
14621481
area_det = node_aggs['area_det']
14631482
area_det_u = node_aggs['area_det_u']
1464-
area_of_ht = node_aggs['area']
1483+
area_of_ht = node_aggs['area_of_ht']
14651484

14661485
# Use degraded pix as well when not enough good/suspect pix
14671486
width_area[~mask_good_sus_area] = node_aggs_w_degraded[
@@ -1477,7 +1496,7 @@ def process_node(self,
14771496
area_det_u[~mask_good_sus_area] = node_aggs_w_degraded[
14781497
'area_det_u'][~mask_good_sus_area]
14791498
area_of_ht[~mask_good_sus_area] = node_aggs_w_degraded[
1480-
'area'][~mask_good_sus_area]
1499+
'area_of_ht'][~mask_good_sus_area]
14811500

14821501
if self.height_agg_method != 'orig':
14831502
wse = node_aggs['h']
@@ -1494,8 +1513,6 @@ def process_node(self,
14941513
wse_r_u[~mask_good_sus_wse] = node_aggs_w_degraded[
14951514
'h_u'][~mask_good_sus_wse]
14961515

1497-
area_of_ht = area
1498-
14991516
# geoid heights and tide corrections weighted by height uncertainty
15001517
try:
15011518
geoid_hght = np.asarray(
@@ -1624,6 +1641,9 @@ def process_node(self,
16241641
dark_frac = MISSING_VALUE_FLT * np.ones(area.shape)
16251642
dark_frac[area > 0] = 1 - area_det[area > 0] / area[area > 0]
16261643
dark_frac[np.logical_and(dark_frac > 1, area > 0)] = 1 # clip to <= 1
1644+
sring_frac = MISSING_VALUE_FLT * np.ones(area.shape)
1645+
sring_frac[area > 0] = area_sring[area > 0] / area[area > 0]
1646+
sring_frac[np.logical_and(sring_frac > 1, area > 0)] = 1
16271647

16281648
# Compute flow direction relative to along-track
16291649
tangent = self.river_obs.centerline.tangent[
@@ -1677,6 +1697,12 @@ def process_node(self,
16771697
n_pix_area_degraded = np.array(self.river_obs.get_node_stat(
16781698
'sum', 'is_area_degraded'))
16791699

1700+
# create empty arrays for the reconst WSE & uncertainty (for later)
1701+
w_opt = np.ones(wse.shape,
1702+
dtype=np.float64) * self.river_obs.missing_value
1703+
w_opt_r_u = np.ones(wse.shape,
1704+
dtype=np.float64) * self.river_obs.missing_value
1705+
16801706
# Create node_q_b quality bitwise flag
16811707
node_q_b = np.zeros(lat_median.shape, dtype='i4')
16821708

@@ -1822,9 +1848,12 @@ def process_node(self,
18221848
'area_det': area_det.astype('float64'),
18231849
'area_det_u': area_det_u.astype('float64'),
18241850
'area_of_ht': area_of_ht.astype('float64'),
1851+
'area_sring': area_sring.astype('float64'),
18251852
'wse': wse.astype('float64'),
18261853
'wse_std': wse_std.astype('float64'),
18271854
'wse_r_u': wse_r_u.astype('float64'),
1855+
'w_opt': w_opt.astype('float64'),
1856+
'w_opt_r_u': w_opt_r_u.astype('float64'),
18281857
'nobs': nobs.astype('int32'),
18291858
'nobs_h': nobs_h.astype('int32'),
18301859
'n_good_pix': n_pix_wse.astype('int32'),
@@ -1842,6 +1871,7 @@ def process_node(self,
18421871
'pole_tide': pole_tide.astype('float64'),
18431872
'node_blocked': is_blocked.astype('uint8'),
18441873
'dark_frac': dark_frac,
1874+
'sring_frac': sring_frac,
18451875
'x_prior': x_prior.astype('float64'),
18461876
'y_prior': y_prior.astype('float64'),
18471877
'lon_prior': lon_prior.astype('float64'),
@@ -1939,9 +1969,17 @@ def process_reach(self, river_reach_collection, river_reach, reach,
19391969

19401970
reach_stats['area_of_ht'] = np.sum(river_reach.area_of_ht)
19411971

1942-
reach_stats['width'] = np.sum(river_reach.area)/reach_stats['length']
1943-
reach_stats['width_u'] = np.sqrt(np.sum(
1944-
river_reach.area_u**2)) / reach_stats['length']
1972+
pct_unmasked = 100*np.sum(river_reach.mask) / len(river_reach.mask)
1973+
if pct_unmasked >= self.reach_pct_good_sus_thresh:
1974+
reach_stats['width'] = np.sum(river_reach.area[river_reach.mask])\
1975+
/np.sum(river_reach.p_length[river_reach.mask])
1976+
reach_stats['width_u'] = np.sqrt(
1977+
np.sum(river_reach.area_u[river_reach.mask]**2)) / np.sum(
1978+
river_reach.p_length[river_reach.mask])
1979+
else:
1980+
reach_stats['width'] = reach_stats['area'] / reach_stats['length']
1981+
reach_stats['width_u'] = np.sqrt(
1982+
np.sum(river_reach.area_u**2))/ reach_stats['length']
19451983
reach_stats['layovr_val'] = np.sqrt(np.sum(
19461984
river_reach.layovr_val[river_reach.mask]**2))
19471985

@@ -2020,20 +2058,28 @@ def process_reach(self, river_reach_collection, river_reach, reach,
20202058
hh_opt, wse_r_u_opt, mask_opt, ss_opt = \
20212059
hh, wse_r_u, mask, ss
20222060
# get the optimal reconstruction (Bayes estimate)
2023-
wse_opt, height_u, slope_u = self.optimal_reconstruct(
2024-
river_reach_collection,
2025-
river_reach, reach_id,
2026-
ss_opt, hh_opt,
2027-
wse_r_u_opt, mask_opt,
2028-
min_fit_points,
2029-
method='Bayes',
2030-
)
2061+
wse_opt, wse_opt_r_u, height_u, slope_u, = \
2062+
self.optimal_reconstruct(
2063+
river_reach_collection,
2064+
river_reach, reach_id,
2065+
ss_opt, hh_opt,
2066+
wse_r_u_opt, mask_opt,
2067+
min_fit_points,
2068+
method='Bayes',
2069+
)
2070+
# Store the reconstructed node heights in river reach object.
2071+
# Currently, only store populated nodes (node_indx clipped to
2072+
# nodes with minobs observations as specified by L2_HR_Param
2073+
# file).
2074+
river_reach.w_opt = wse_opt[self.river_obs.populated_nodes]
2075+
river_reach.w_opt_r_u = wse_opt_r_u[
2076+
self.river_obs.populated_nodes]
20312077
# Use reconstruction height and slope for reach outputs
20322078
dx = ss_opt[0] - ss_opt[-1] # along-reach dist
20332079
reach_stats['slope'] = (wse_opt[0] - wse_opt[-1]) / dx
20342080
reach_stats['height'] = np.mean(wse_opt)
2035-
reach_stats['slope_r_u'] = slope_u * 0.0001 # m/m
2036-
reach_stats['height_r_u'] = height_u * 0.01 # m
2081+
reach_stats['slope_r_u'] = slope_u
2082+
reach_stats['height_r_u'] = height_u
20372083
reach_stats['slope_u'] = np.sqrt(
20382084
SLOPE_SYS_UNCERT**2 + reach_stats['slope_r_u']**2)
20392085
reach_stats['height_u'] = np.sqrt(
@@ -2101,6 +2147,9 @@ def process_reach(self, river_reach_collection, river_reach, reach,
21012147
dark_frac = (
21022148
1-np.sum(river_reach.area_det)/np.sum(river_reach.area))
21032149
reach_stats['dark_frac'] = min(dark_frac, 1) # clip to <= 1
2150+
sring_frac = (
2151+
np.sum(river_reach.area_sring)/np.sum(river_reach.area))
2152+
reach_stats['sring_frac'] = min(sring_frac, 1) # clip to <= 1
21042153

21052154
reach_stats['n_reach_up'] = (reach_stats['rch_id_up'] > 0).sum()
21062155
reach_stats['n_reach_dn'] = (reach_stats['rch_id_dn'] > 0).sum()
@@ -2529,7 +2578,12 @@ def get_reach_mask(self, ss, hh, ww, node_q, min_fit_points,
25292578
mask = np.zeros(len(hh), dtype=bool)
25302579

25312580
wse_outlier_mask = mask.copy()
2532-
pct_good_sus = 100 * (node_q[mask] < 2).sum() / mask.sum()
2581+
2582+
if mask.sum() > 0:
2583+
pct_good_sus = 100 * (node_q[mask] < 2).sum() / mask.sum()
2584+
else:
2585+
pct_good_sus = 0
2586+
25332587
if pct_good_sus > self.reach_pct_good_sus_thresh:
25342588
mask[node_q >= 2] = False
25352589

@@ -2904,9 +2958,11 @@ def optimal_reconstruct(
29042958
# define vectors b and c for uncertainty estimates later
29052959
this_reach_mask_b = np.zeros_like(ss)
29062960
this_reach_mask_b[first_node:first_node+this_len] = 1
2961+
this_reach_mask_b = this_reach_mask_b / np.sum(this_reach_mask_b)
29072962
first_and_last_node_c = np.zeros_like(ss)
2908-
first_and_last_node_c[first_node] = -1
2909-
first_and_last_node_c[first_node+this_len-1] = 1
2963+
reach_length = ss[first_node + this_len - 1] - ss[first_node]
2964+
first_and_last_node_c[first_node] = -1 / reach_length
2965+
first_and_last_node_c[first_node+this_len-1] = 1 / reach_length
29102966

29112967
# create a wse prior if flagged
29122968
if self.prior_wse_method == 'fit':
@@ -2983,15 +3039,15 @@ def optimal_reconstruct(
29833039
wse_out0 = np.matmul(K, wse_reg)
29843040
# apply the prior term
29853041
wse_out = wse_out0 + np.matmul(K_bar, prior_wse)
2986-
height_u = this_reach_mask_b @ A_inv @ np.atleast_2d(
2987-
this_reach_mask_b).T
2988-
slope_u = first_and_last_node_c @ A_inv @ np.atleast_2d(
2989-
first_and_last_node_c).T
3042+
wse_out_std = np.sqrt(np.diag(A_inv)) # node-level height uncertainty
3043+
height_u = np.sqrt(this_reach_mask_b @ A_inv @ this_reach_mask_b.T)
3044+
slope_u = np.sqrt(first_and_last_node_c @ A_inv @ np.atleast_2d(
3045+
first_and_last_node_c).T)
29903046
if self.use_multiple_reaches:
29913047
wse_out = wse_out[first_node:first_node+this_len]
2992-
2993-
# Return height_u and slope_u as scalars
2994-
return wse_out, height_u.item(), slope_u.item()
3048+
wse_out_std = wse_out_std[first_node:first_node+this_len]
3049+
# Return node WSEs & stdevs, and reach height_u and slope_u as scalars
3050+
return wse_out, wse_out_std, height_u.item(), slope_u.item()
29953051

29963052
@staticmethod
29973053
def compute_bayes_estimator(Ry, Rv, H):

0 commit comments

Comments
 (0)