Skip to content

Commit 36a0835

Browse files
authored
wrap up for version 1.4.1 (#829)
+ version: add Tag for version 1.4.1 + readfile.read_hdf5_file(): speedup the 3D matrix reading when slicing a small fraction of the 1st dimension, by using integer indexing for 3D h5 dataset, instead of 1D boolean array indexing. + view.read_data4figure(): bugfix for referencing unwrapPhase while plotting mixed dset types + move the following plotting functions to utils.plot.py for a more compact module import, to simplify the UNAVCO notebook: - unwrap_error_phase_closure.plot_num_triplet_with_nonzero_integer_ambiguity() - timeseries_rms.plot_rms_bar() - objects.insar_vs_gps.plot_insar_vs_gps_scatter() + plot.plot_insar_vs_gps_scatter(): add preliminary outlier detection
1 parent 47bb7f6 commit 36a0835

File tree

8 files changed

+361
-310
lines changed

8 files changed

+361
-310
lines changed

mintpy/objects/insar_vs_gps.py

Lines changed: 1 addition & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -13,144 +13,14 @@
1313
from scipy.interpolate import griddata
1414
from datetime import datetime as dt
1515
from dateutil.relativedelta import relativedelta
16-
from matplotlib import pyplot as plt
1716

1817
from mintpy.objects import timeseries, giantTimeseries
19-
from mintpy.utils import ptime, readfile, plot as pp, utils as ut
18+
from mintpy.utils import readfile, plot as pp, utils as ut
2019
from mintpy.objects.gps import GPS
2120
from mintpy.defaults.plot import *
2221

2322

2423

25-
############################## utilities functions ##########################################
26-
27-
def plot_insar_vs_gps_scatter(vel_file, csv_file='gps_enu2los.csv', msk_file=None, ref_gps_site=None,
28-
xname='InSAR', vlim=None, ex_gps_sites=[], display=True):
29-
"""Scatter plot to compare the velocities between SAR/InSAR and GPS.
30-
31-
Parameters: vel_file - str, path of InSAR LOS velocity HDF5 file.
32-
ref_gps_site - str, reference GNSS site name
33-
csv_file - str, path of GNSS CSV file, generated after running view.py --gps-comp
34-
msk_file - str, path of InSAR mask file.
35-
xname - str, xaxis label
36-
vlim - list of 2 float, display value range in the unit of cm/yr
37-
Default is None to grab from data
38-
If set, the range will be used to prune the SAR and GPS observations
39-
ex_gps_sites - list of str, exclude GNSS sites for analysis and plotting.
40-
Example:
41-
from mintpy.objects.insar_vs_gps import plot_insar_vs_gps_scatter
42-
csv_file = os.path.join(work_dir, 'geo/gps_enu2los.csv')
43-
vel_file = os.path.join(work_dir, 'geo/geo_velocity.h5')
44-
msk_file = os.path.join(work_dir, 'geo/geo_maskTempCoh.h5')
45-
plot_insar_vs_gps_scatter(vel_file, ref_gps_site='CACT', csv_file=csv_file, msk_file=msk_file, vlim=[-2.5, 2])
46-
"""
47-
48-
disp_unit = 'cm/yr'
49-
unit_fac = 100.
50-
51-
# read GPS velocity from CSV file (generated by gps.get_gps_los_obs())
52-
col_names = ['Site', 'Lon', 'Lat', 'Displacement', 'Velocity']
53-
num_col = len(col_names)
54-
col_types = ['U10'] + ['f8'] * (num_col - 1)
55-
56-
print('read GPS velocity from file: {}'.format(csv_file))
57-
fc = np.genfromtxt(csv_file, dtype=col_types, delimiter=',', names=True)
58-
sites = fc['Site']
59-
lats = fc['Lat']
60-
lons = fc['Lon']
61-
gps_obs = fc[col_names[-1]] * unit_fac
62-
63-
if ex_gps_sites:
64-
ex_flag = np.array([x in ex_gps_sites for x in sites], dtype=np.bool_)
65-
if np.sum(ex_flag) > 0:
66-
sites = sites[~ex_flag]
67-
lats = lats[~ex_flag]
68-
lons = lons[~ex_flag]
69-
gps_obs = gps_obs[~ex_flag]
70-
71-
# read InSAR velocity
72-
print('read InSAR velocity from file: {}'.format(vel_file))
73-
atr = readfile.read_attribute(vel_file)
74-
length, width = int(atr['LENGTH']), int(atr['WIDTH'])
75-
coord = ut.coordinate(atr)
76-
ys, xs = coord.geo2radar(lats, lons)[:2]
77-
78-
msk = readfile.read(msk_file)[0] if msk_file else np.ones((length, width), dtype=np.bool_)
79-
80-
num_site = sites.size
81-
insar_obs = np.zeros(num_site, dtype=np.float32) * np.nan
82-
prog_bar = ptime.progressBar(maxValue=num_site)
83-
for i in range(num_site):
84-
x, y = xs[i], ys[i]
85-
if (0 <= x < width) and (0 <= y < length) and msk[y, x]:
86-
box = (x, y, x+1, y+1)
87-
insar_obs[i] = readfile.read(vel_file, datasetName='velocity', box=box)[0] * unit_fac
88-
prog_bar.update(i+1, suffix='{}/{} {}'.format(i+1, num_site, sites[i]))
89-
prog_bar.close()
90-
91-
off_med = np.nanmedian(insar_obs - gps_obs)
92-
print(f'median offset between InSAR and GPS [before common referencing]: {off_med:.2f} cm/year')
93-
94-
# reference site
95-
if ref_gps_site:
96-
print(f'referencing both InSAR and GPS data to site: {ref_gps_site}')
97-
ref_ind = sites.tolist().index(ref_gps_site)
98-
gps_obs -= gps_obs[ref_ind]
99-
insar_obs -= insar_obs[ref_ind]
100-
101-
# remove NaN value
102-
print('removing sites with NaN values in GPS or {}'.format(xname))
103-
flag = np.multiply(~np.isnan(insar_obs), ~np.isnan(gps_obs))
104-
if vlim is not None:
105-
print('pruning sites with value range: {} {}'.format(vlim, disp_unit))
106-
flag *= gps_obs >= vlim[0]
107-
flag *= gps_obs <= vlim[1]
108-
flag *= insar_obs >= vlim[0]
109-
flag *= insar_obs <= vlim[1]
110-
111-
gps_obs = gps_obs[flag]
112-
insar_obs = insar_obs[flag]
113-
sites = sites[flag]
114-
115-
# stats
116-
print('GPS min/max: {:.2f} / {:.2f}'.format(np.nanmin(gps_obs), np.nanmax(gps_obs)))
117-
print('InSAR min/max: {:.2f} / {:.2f}'.format(np.nanmin(insar_obs), np.nanmax(insar_obs)))
118-
119-
rmse = np.sqrt(np.sum((insar_obs - gps_obs)**2) / (gps_obs.size - 1))
120-
r2 = stats.linregress(insar_obs, gps_obs)[2]
121-
print('RMSE = {:.1f} cm'.format(rmse))
122-
print('R^2 = {:.2f}'.format(r2))
123-
124-
# plot
125-
if display:
126-
plt.rcParams.update({'font.size': 12})
127-
if vlim is None:
128-
vlim = [np.min(insar_obs), np.max(insar_obs)]
129-
buffer = (vlim[1] - vlim[0]) * 0.1
130-
vlim = [vlim[0] - buffer, vlim[1] + buffer]
131-
132-
fig, ax = plt.subplots(figsize=[4, 4])
133-
ax.plot((vlim[0], vlim[1]), (vlim[0], vlim[1]), 'k--')
134-
ax.plot(insar_obs, gps_obs, '.', ms=15)
135-
136-
# axis format
137-
ax.set_xlim(vlim)
138-
ax.set_ylim(vlim)
139-
ax.set_xlabel(f'{xname} [{disp_unit}]')
140-
ax.set_ylabel(f'GNSS [{disp_unit}]')
141-
ax.set_aspect('equal', 'box')
142-
fig.tight_layout()
143-
144-
# output
145-
out_fig = '{}_vs_gps_scatter.pdf'.format(xname.lower())
146-
plt.savefig(out_fig, bbox_inches='tight', transparent=True, dpi=300)
147-
print('save figure to file', out_fig)
148-
plt.show()
149-
150-
return sites, insar_obs, gps_obs
151-
152-
153-
15424
############################## beginning of insar_vs_gps class ##############################
15525
class insar_vs_gps:
15626
""" Comparing InSAR time-series with GPS time-series in LOS direction

mintpy/timeseries2velocity.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ def run_timeseries2time_func(inps):
487487
# Bootstrapping is a resampling method which can be used to estimate properties
488488
# of an estimator. The method relies on independently sampling the data set with
489489
# replacement.
490-
print('estimating time function STD with bootstrap resampling ({} times) ...'.format(
490+
print('estimating time functions STD with bootstrap resampling ({} times) ...'.format(
491491
inps.bootstrapCount))
492492

493493
# calc model of all bootstrap sampling
@@ -559,7 +559,7 @@ def run_timeseries2time_func(inps):
559559
# TO DO: save the full covariance matrix of the time function parameters
560560
# only the STD is saved right now
561561
covar_flag = True if len(ts_cov.shape) == 3 else False
562-
msg = 'estimating time function STD from time-serries '
562+
msg = 'estimating time functions STD from time-serries '
563563
msg += 'covariance pixel-by-pixel ...' if covar_flag else 'variance pixel-by-pixel ...'
564564
print(msg)
565565

@@ -583,7 +583,7 @@ def run_timeseries2time_func(inps):
583583

584584
elif inps.uncertaintyQuantification == 'residue':
585585
# option 2.3 - assume obs errors following normal dist. in time
586-
print('estimating time function STD from time-series fitting residual ...')
586+
print('estimating time functions STD from time-series fitting residual ...')
587587
G_inv = linalg.inv(np.dot(G.T, G))
588588
m_var = e2.reshape(1, -1) / (num_date - num_param)
589589
m_std[:, mask] = np.sqrt(np.dot(np.diag(G_inv).reshape(-1, 1), m_var))

mintpy/timeseries_rms.py

Lines changed: 26 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import os
1010
import sys
1111
import numpy as np
12-
import matplotlib.pyplot as plt
13-
from mpl_toolkits.axes_grid1 import make_axes_locatable
12+
1413
from mintpy.defaults.template import get_template_content
15-
from mintpy.utils import readfile, ptime, utils as ut, plot as pp
14+
from mintpy.utils import readfile, utils as ut, plot as pp
1615
from mintpy.utils.arg_utils import create_argument_parser
1716

1817

@@ -103,15 +102,14 @@ def analyze_rms(date_list, rms_list, inps):
103102
print('save date to file: '+ref_date_file)
104103

105104
# exclude date(s) - outliers
106-
try:
107-
rms_threshold = ut.median_abs_deviation_threshold(rms_list, center=0., cutoff=inps.cutoff)
108-
except:
109-
# equivalent calculation using numpy assuming Gaussian distribution
110-
rms_threshold = np.median(rms_list) / .6745 * inps.cutoff
105+
# equivalent calculation using numpy assuming Gaussian distribution as:
106+
# rms_threshold = np.median(rms_list) / .6745 * inps.cutoff
107+
rms_threshold = ut.median_abs_deviation_threshold(rms_list, center=0., cutoff=inps.cutoff)
111108

112109
ex_idx = [rms_list.index(i) for i in rms_list if i > rms_threshold]
113-
print(('-'*50+'\ndate(s) with RMS > {} * median RMS'
114-
' ({:.4f})'.format(inps.cutoff, rms_threshold)))
110+
print('-'*50)
111+
print(f'date(s) with RMS > {inps.cutoff} * median RMS ({rms_threshold:.4f})')
112+
115113
ex_date_file = 'exclude_date.txt'
116114
if ex_idx:
117115
# print
@@ -127,110 +125,37 @@ def analyze_rms(date_list, rms_list, inps):
127125
if os.path.isfile(ex_date_file):
128126
os.remove(ex_date_file)
129127

130-
# plot bar figure and save
131-
fig_file = os.path.splitext(inps.rms_file)[0]+'.pdf'
132-
fig, ax = plt.subplots(figsize=inps.fig_size)
133-
print('create figure in size:', inps.fig_size)
134-
ax = plot_rms_bar(ax, date_list, np.array(rms_list)*1000., cutoff=inps.cutoff)
135-
fig.savefig(fig_file, bbox_inches='tight', transparent=True)
136-
print('save figure to file: '+fig_file)
137128
return inps
138129

139130

140-
def plot_rms_bar(ax, date_list, rms, cutoff=3., font_size=12,
141-
tick_year_num=1, legend_loc='best',
142-
disp_legend=True, disp_side_plot=True, disp_thres_text=False,
143-
ylabel='Residual phase RMS [mm]'):
144-
""" Bar plot Phase Residual RMS
145-
Parameters: ax : Axes object
146-
date_list : list of string in YYYYMMDD format
147-
rms : 1D np.array of float for RMS value in mm
148-
cutoff : cutoff value of MAD outlier detection
149-
tick_year_num : int, number of years per major tick
150-
legend_loc : 'upper right' or (0.5, 0.5)
151-
Returns: ax : Axes object
152-
"""
153-
dates, datevector = ptime.date_list2vector(date_list)
154-
dates = np.array(dates)
155-
try:
156-
bar_width = min(ut.most_common(np.diff(dates).tolist(), k=2))*3/4
157-
except:
158-
bar_width = np.min(np.diff(dates).tolist())*3/4
159-
rms = np.array(rms)
160-
161-
# Plot all dates
162-
ax.bar(dates, rms, bar_width.days, color=pp.mplColors[0])
163-
164-
# Plot reference date
165-
ref_idx = np.argmin(rms)
166-
ax.bar(dates[ref_idx], rms[ref_idx], bar_width.days, color=pp.mplColors[1], label='Reference date')
167-
168-
# Plot exclude dates
169-
rms_threshold = ut.median_abs_deviation_threshold(rms, center=0., cutoff=cutoff)
170-
ex_idx = rms > rms_threshold
171-
if not np.all(ex_idx==False):
172-
ax.bar(dates[ex_idx], rms[ex_idx], bar_width.days, color='darkgray', label='Exclude date')
173-
174-
# Plot rms_threshold line
175-
(ax, xmin, xmax) = pp.auto_adjust_xaxis_date(ax, datevector, font_size, every_year=tick_year_num)
176-
ax.plot(np.array([xmin, xmax]), np.array([rms_threshold, rms_threshold]), '--k',
177-
label='Median Abs Dev * {}'.format(cutoff))
178-
179-
# axis format
180-
ax = pp.auto_adjust_yaxis(ax, np.append(rms, rms_threshold), font_size, ymin=0.0)
181-
#ax.set_xlabel('Time [years]', fontsize=font_size)
182-
ax.set_ylabel(ylabel, fontsize=font_size)
183-
ax.tick_params(which='both', direction='in', labelsize=font_size,
184-
bottom=True, top=True, left=True, right=True)
185-
186-
# 2nd axes for circles
187-
if disp_side_plot:
188-
divider = make_axes_locatable(ax)
189-
ax2 = divider.append_axes("right", "10%", pad="2%")
190-
ax2.plot(np.ones(rms.shape, np.float32) * 0.5, rms, 'o', mfc='none', color=pp.mplColors[0])
191-
ax2.plot(np.ones(rms.shape, np.float32)[ref_idx] * 0.5, rms[ref_idx], 'o', mfc='none', color=pp.mplColors[1])
192-
if not np.all(ex_idx==False):
193-
ax2.plot(np.ones(rms.shape, np.float32)[ex_idx] * 0.5, rms[ex_idx], 'o', mfc='none', color='darkgray')
194-
ax2.plot(np.array([0, 1]), np.array([rms_threshold, rms_threshold]), '--k')
195-
196-
ax2.set_ylim(ax.get_ylim())
197-
ax2.set_xlim([0, 1])
198-
ax2.tick_params(which='both', direction='in', labelsize=font_size,
199-
bottom=True, top=True, left=True, right=True)
200-
ax2.get_xaxis().set_ticks([])
201-
ax2.get_yaxis().set_ticklabels([])
202-
203-
if disp_legend:
204-
ax.legend(loc=legend_loc, frameon=False, fontsize=font_size)
205-
206-
# rms_threshold text
207-
if disp_thres_text:
208-
ymin, ymax = ax.get_ylim()
209-
yoff = (ymax - ymin) * 0.1
210-
if (rms_threshold - ymin) > 0.5 * (ymax - ymin):
211-
yoff *= -1.
212-
ax.annotate('Median Abs Dev * {}'.format(cutoff),
213-
xy=(xmin + (xmax-xmin)*0.05, rms_threshold + yoff ),
214-
color='k', xycoords='data', fontsize=font_size)
215-
return ax
216-
217-
218131
######################################################################################################
219132
def main(iargs=None):
220-
plt.switch_backend('Agg') # Backend setting
221133

134+
# read inputs
222135
inps = cmd_line_parse(iargs)
223136
if inps.template_file:
224137
inps = read_template2inps(inps.template_file, inps)
225138

226139
# calculate timeseries of residual Root Mean Square
227-
(inps.rms_list,
228-
inps.date_list,
229-
inps.rms_file) = ut.get_residual_rms(inps.timeseries_file,
230-
mask_file=inps.maskFile,
231-
ramp_type=inps.deramp)
140+
inps.rms_list, inps.date_list, inps.rms_file = ut.get_residual_rms(
141+
inps.timeseries_file,
142+
mask_file=inps.maskFile,
143+
ramp_type=inps.deramp,
144+
)
232145

146+
# analyze RMS: generate reference/exclude_date.txt files
233147
analyze_rms(inps.date_list, inps.rms_list, inps)
148+
149+
# plot RMS
150+
pp.plot_timeseries_rms(
151+
rms_file=inps.rms_file,
152+
cutoff=inps.cutoff,
153+
out_fig=os.path.splitext(inps.rms_file)[0]+'.pdf',
154+
disp_fig=False,
155+
fig_size=inps.fig_size,
156+
tick_year_num=inps.tick_year_num,
157+
)
158+
234159
return
235160

236161

0 commit comments

Comments
 (0)