diff --git a/src/mintpy/cli/plot_coherence_matrix.py b/src/mintpy/cli/plot_coherence_matrix.py index 6626137f4..9817b7ba5 100755 --- a/src/mintpy/cli/plot_coherence_matrix.py +++ b/src/mintpy/cli/plot_coherence_matrix.py @@ -60,6 +60,8 @@ def create_parser(subparsers=None): help='temporal coherence file.') parser.add_argument('-t','--template', dest='template_file', help='temporal file.') + parser.add_argument('--time-axis', dest='time_axis', action='store_true', + help='Use continuous time axis instead of date indices for coherence matrix') parser.add_argument('--save', dest='save_fig', action='store_true', help='save the figure') diff --git a/src/mintpy/plot_coherence_matrix.py b/src/mintpy/plot_coherence_matrix.py index db4391271..d41f204aa 100644 --- a/src/mintpy/plot_coherence_matrix.py +++ b/src/mintpy/plot_coherence_matrix.py @@ -72,6 +72,8 @@ def __init__(self, inps): self.fig_mat = None self.ax_mat = None + self.time_axis = getattr(inps, 'time_axis', False) + # copy inps to self object for key, value in inps.__dict__.items(): setattr(self, key, value) @@ -151,10 +153,78 @@ def plot_init_image(self): self.fig_img.tight_layout() return + def plot_coherence_matrix4pixel_time_axis(self, yx): + """Plot coherence matrix with continuous time axis for one pixel.""" + self.ax_mat.cla() + + box = (yx[1], yx[0], yx[1]+1, yx[0]+1) + coh = readfile.read(self.ifgram_file, datasetName='coherence', box=box)[0] + + ex_date12_list = self.ex_date12_list[:] + if self.min_coh_used > 0.: + ex_date12_list += np.array(self.date12_list)[coh < self.min_coh_used].tolist() + ex_date12_list = sorted(list(set(ex_date12_list))) + + plotDict = {} + plotDict['fig_title'] = f'Y = {yx[0]}, X = {yx[1]}' + if self.tcoh is not None: + tcoh = self.tcoh[yx[0], yx[1]] + plotDict['fig_title'] += f', tcoh = {tcoh:.2f}' + plotDict['colormap'] = self.colormap + if len(self.cmap_vlist) >= 2: + plotDict['vlim'] = [self.cmap_vlist[0], self.cmap_vlist[-1]] + else: + plotDict['vlim'] = [0.0, 1.0] + plotDict['cbar_label'] = 'Coherence' + plotDict['disp_legend'] = False + + pp.plot_coherence_matrix_time_axis( + self.ax_mat, + date12List=self.date12_list, + cohList=coh.tolist(), + date12List_drop=ex_date12_list, + p_dict=plotDict, + ) + + self.ax_mat.annotate('ifgrams\navailable', xy=(0.05, 0.05), xycoords='axes fraction', fontsize=12) + self.ax_mat.annotate('ifgrams\nused', ha='right', xy=(0.95, 0.85), xycoords='axes fraction', fontsize=12) + + msg = f'pixel in yx = {tuple(yx)}, ' + msg += f'min/max spatial coherence: {np.nanmin(coh):.2f} / {np.nanmax(coh):.2f}, ' + if self.tcoh is not None: + msg += f'temporal coherence: {tcoh:.2f}' + vprint(msg) + + self.fig_mat.canvas.manager.set_window_title(self.figname_mat) + if not hasattr(self, "_mat_tight_layout_done"): + self.fig_mat.tight_layout() + self._mat_tight_layout_done = True + + self.fig_mat.canvas.draw_idle() + self.fig_mat.canvas.flush_events() + + # plot/update marker on the image window + if self.fig_coord == 'geo': + lat, lon = self.coord.yx2lalo(yx[0], yx[1]) + mx = lon if self.fig_coord == 'geo' else yx[1] + my = lat if self.fig_coord == 'geo' else yx[0] + if self._marker_artist is None: + self._marker_artist = self.ax_img.plot( + mx, my, 'r^', markersize=6, markeredgecolor='black' + )[0] + else: + self._marker_artist.set_data([mx], [my]) + + self.fig_img.canvas.draw_idle() + return + def plot_coherence_matrix4pixel(self, yx): """Plot coherence matrix for one pixel Parameters: yx : list of 2 int """ + if self.time_axis: + return self.plot_coherence_matrix4pixel_time_axis(yx) + self.ax_mat.cla() # read coherence diff --git a/src/mintpy/plot_network.py b/src/mintpy/plot_network.py index 31911f7bb..a0df3831b 100644 --- a/src/mintpy/plot_network.py +++ b/src/mintpy/plot_network.py @@ -173,7 +173,7 @@ def plot_network(inps): # figure names ext = 'Ion.pdf' if os.path.basename(inps.file).startswith('ion') else '.pdf' fig_names = { - 'coherence' : [i+ext for i in ['pbaseHistory', 'coherenceHistory', 'coherenceMatrix', 'network']], + 'coherence' : [i+ext for i in ['pbaseHistory', 'coherenceHistory', 'coherenceMatrix', 'coherenceMatrixTimeAxis', 'network']], 'offsetSNR' : [i+ext for i in ['pbaseHistory', 'SNRHistory', 'SNRMatrix', 'network']], 'tbase' : [i+ext for i in ['pbaseHistory', 'tbaseHistory', 'tbaseMatrix', 'network']], 'pbase' : [i+ext for i in ['pbaseHistory', 'pbaseRangeHistory', 'pbaseMatrix', 'network']], @@ -204,7 +204,7 @@ def plot_network(inps): ) if inps.save_fig: fig.savefig(fig_names[1], **kwargs) - print(f'save figure to {fig_names[2]}') + print(f'save figure to {fig_names[1]}') # Fig 3 - Coherence Matrix fig_size3 = np.mean(inps.fig_size) @@ -218,9 +218,25 @@ def plot_network(inps): )[0] if inps.save_fig: fig.savefig(fig_names[2], **kwargs) - print(f'save figure to {fig_names[1]}') + print(f'save figure to {fig_names[2]}') + + # Fig 4 - Coherence Matrix with Time Axis + fig_size4 = np.mean(inps.fig_size) + fig, ax = plt.subplots(figsize=[fig_size4, fig_size4]) + ax = pp.plot_coherence_matrix_time_axis( + ax, + inps.date12List, + inps.cohList, + inps.date12List_drop, + p_dict=vars(inps), + )[0] + fig.tight_layout() + if inps.save_fig: + fig.savefig(fig_names[3], **kwargs) + print(f'save figure to {fig_names[3]}') - # Fig 4 - Interferogram Network + # Fig 5 - Interferogram Network (or Fig 4 if cohList is None) + fig_idx = 4 if inps.cohList is not None else 3 fig, ax = plt.subplots(figsize=inps.fig_size) ax = pp.plot_network( ax, @@ -231,8 +247,8 @@ def plot_network(inps): inps.date12List_drop, ) if inps.save_fig: - fig.savefig(fig_names[3], **kwargs) - print(f'save figure to {fig_names[3]}') + fig.savefig(fig_names[fig_idx], **kwargs) + print(f'save figure to {fig_names[fig_idx]}') if inps.disp_fig: print('showing ...') diff --git a/src/mintpy/utils/plot.py b/src/mintpy/utils/plot.py index 5d267cfd8..1446ac2c6 100644 --- a/src/mintpy/utils/plot.py +++ b/src/mintpy/utils/plot.py @@ -959,6 +959,172 @@ def plot_coherence_matrix(ax, date12List, cohList, date12List_drop=[], p_dict={} return ax, coh_mat, im +def plot_coherence_matrix_time_axis(ax, date12List, cohList, date12List_drop=[], p_dict={}): + """Plot Coherence Matrix with continuous time axis + Parameters: ax : matplotlib.pyplot.Axes, + date12List : list of date12 in YYYYMMDD_YYYYMMDD format + cohList : list of float, coherence value + date12List_drop : list of date12 for date12 marked as dropped + p_dict : dict of plot setting + Returns: ax : matplotlib.pyplot.Axes + coh_mat : 2D np.array in size of [num_date, num_date] + mesh : matplotlib.collections.QuadMesh object + """ + # Figure Setting + if 'ds_name' not in p_dict.keys(): p_dict['ds_name'] = 'Coherence' + if 'fontsize' not in p_dict.keys(): p_dict['fontsize'] = 12 + if 'disp_title' not in p_dict.keys(): p_dict['disp_title'] = True + if 'fig_title' not in p_dict.keys(): p_dict['fig_title'] = '{} Matrix'.format(p_dict['ds_name']) + if 'colormap' not in p_dict.keys(): p_dict['colormap'] = 'RdBu_truncate' + if 'cbar_label' not in p_dict.keys(): p_dict['cbar_label'] = p_dict['ds_name'] + if 'vlim' not in p_dict.keys(): p_dict['vlim'] = (0.2, 1.0) + if 'disp_cbar' not in p_dict.keys(): p_dict['disp_cbar'] = True + if 'legend_loc' not in p_dict.keys(): p_dict['legend_loc'] = 'best' + if 'disp_legend' not in p_dict.keys(): p_dict['disp_legend'] = True + + # support input colormap: string for colormap name, or colormap object directly + if isinstance(p_dict['colormap'], str): + cmap = ColormapExt(p_dict['colormap']).colormap + elif isinstance(p_dict['colormap'], mpl.colors.LinearSegmentedColormap): + cmap = p_dict['colormap'] + else: + raise ValueError('unrecognized colormap input: {}'.format(p_dict['colormap'])) + + date12List = ptime.yyyymmdd_date12(date12List) + coh_mat = pnet.coherence_matrix(date12List, cohList) + + m_dates = [i.split('_')[0] for i in date12List] + s_dates = [i.split('_')[1] for i in date12List] + dateList = ptime.yyyymmdd(sorted(list(set(m_dates + s_dates)))) + dateList_dt = [dt.datetime.strptime(i, '%Y%m%d') for i in dateList] + + if date12List_drop: + date12List_drop = ptime.yyyymmdd_date12(date12List_drop) + for date12 in date12List_drop: + idx1, idx2 = (dateList.index(i) for i in date12.split('_')) + coh_mat[idx1, idx2] = np.nan + + if len(dateList_dt) == 1: + half_width = dt.timedelta(days=15) + grid_points = [dateList_dt[0] - half_width, dateList_dt[0] + half_width] + else: + grid_points = [dateList_dt[0] - (dateList_dt[1] - dateList_dt[0])] + for date1, date2 in zip(dateList_dt[:-1], dateList_dt[1:]): + grid_points.append(date1 + (date2 - date1) / 2) + grid_points.append(dateList_dt[-1] + (dateList_dt[-1] - dateList_dt[-2])) + + grid_nums = mdates.date2num(grid_points) + X, Y = np.meshgrid(grid_nums, grid_nums) + + # Show diagonal value as black, to be distinguished from un-selected interferograms + diag_mat = np.diag(np.ones(coh_mat.shape[0])) + diag_mat[diag_mat == 0.] = np.nan + ax.pcolormesh(X, Y, diag_mat, cmap='gray_r', vmin=0.0, vmax=1.0, shading='auto', zorder=1) + + cmap_plot = cmap.copy() + cmap_plot.set_bad('white') + mesh = ax.pcolormesh( + X, Y, coh_mat, + cmap=cmap_plot, + vmin=p_dict['vlim'][0], + vmax=p_dict['vlim'][1], + shading='auto', + zorder=0, + ) + + # numeric month/year axis labels + fs = p_dict['fontsize'] + min_date, max_date = min(dateList_dt), max(dateList_dt) + if min_date.day > 1: + if min_date.month == 12: + tick_start = min_date.replace(year=min_date.year + 1, month=1, day=1) + else: + tick_start = min_date.replace(month=min_date.month + 1, day=1) + else: + tick_start = min_date.replace(day=1) + + tick_dates = [] + current_date = tick_start + while current_date <= max_date: + tick_dates.append(current_date) + if current_date.month == 12: + current_date = current_date.replace(year=current_date.year + 1, month=1) + else: + current_date = current_date.replace(month=current_date.month + 1) + + tick_positions = mdates.date2num(tick_dates) + major_ticks = [p for p, d in zip(tick_positions, tick_dates) if d.month == 1] + minor_ticks = [p for p, d in zip(tick_positions, tick_dates) if d.month != 1] + ax.set_xticks(major_ticks) + ax.set_xticks(minor_ticks, minor=True) + ax.set_yticks(major_ticks) + ax.set_yticks(minor_ticks, minor=True) + ax.set_xticklabels([''] * len(major_ticks)) + ax.set_xticklabels([''] * len(minor_ticks), minor=True) + ax.set_yticklabels([''] * len(major_ticks)) + ax.set_yticklabels([''] * len(minor_ticks), minor=True) + ax.tick_params(which='major', direction='out', length=6, width=1.1, + bottom=True, top=True, left=True, right=True) + ax.tick_params(which='minor', direction='out', length=3, width=1, + bottom=True, top=True, left=True, right=True) + + offset = (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.03 + year_offset = offset * 2.2 + for i in range(len(tick_dates) - 1): + if tick_dates[i].month % 2 == 1: + pos = (tick_positions[i] + tick_positions[i + 1]) / 2 + label = str(tick_dates[i].month) + ax.text(pos, ax.get_ylim()[1] + offset, label, ha='center', va='top', fontsize=fs) + ax.text(ax.get_xlim()[0] - offset, pos, label, ha='right', va='center', fontsize=fs) + + year_groups = {} + for i, date in enumerate(tick_dates): + year_groups.setdefault(date.year, []).append(tick_positions[i]) + for year, positions in sorted(year_groups.items()): + pos = (positions[0] + positions[-1]) / 2 + ax.text(pos, ax.get_ylim()[1] + year_offset, str(year), ha='center', va='top', fontsize=fs) + ax.text(ax.get_xlim()[0] - year_offset, pos, str(year), + ha='right', va='center', fontsize=fs, rotation=90) + + ax.set_xlabel('Time', fontsize=fs, labelpad=18) + ax.set_ylabel('Time', fontsize=fs, labelpad=18) + ax.set_aspect('equal', adjustable='box') + ax.invert_yaxis() + + if p_dict['disp_title']: + ax.set_title(p_dict['fig_title'], fontsize=p_dict['fontsize']) + + # Colorbar + if p_dict['disp_cbar']: + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", "3%", pad="3%") + cbar = ax.figure.colorbar(mesh, cax=cax) + cbar.set_label(p_dict['cbar_label'], fontsize=p_dict['fontsize']) + + # Legend + if date12List_drop and p_dict['disp_legend']: + ax.plot([], [], label='Upper: Ifgrams used') + ax.plot([], [], label='Lower: Ifgrams all') + ax.legend(loc=p_dict['legend_loc'], handlelength=0) + + # Status bar + def format_coord(x, y): + col = np.searchsorted(grid_nums, x, side='right') - 1 + row = np.searchsorted(grid_nums, y, side='right') - 1 + if 0 <= row < len(dateList_dt) and 0 <= col < len(dateList_dt): + date1 = dateList_dt[col].strftime('%Y-%m-%d') + date2 = dateList_dt[row].strftime('%Y-%m-%d') + coh_val = coh_mat[row, col] + if not np.isnan(coh_val): + return f'x={date1}, y={date2}, v={coh_val:.3f}' + return f'x={date1}, y={date2}, v=NaN' + return '' + + ax.format_coord = format_coord + + return ax, coh_mat, mesh + + def plot_num_triplet_with_nonzero_integer_ambiguity(fname, disp_fig=False, font_size=12, fig_size=[9,3]): """Plot the histogram for the number of triplets with non-zero integer ambiguity.