diff --git a/src/mintpy/cli/plot_coherence_matrix.py b/src/mintpy/cli/plot_coherence_matrix.py index 6626137f4..c95f9d2c2 100755 --- a/src/mintpy/cli/plot_coherence_matrix.py +++ b/src/mintpy/cli/plot_coherence_matrix.py @@ -25,6 +25,7 @@ # right: matrix view # show color jump same as the coherence threshold in network inversion with pixel-wised masking plot_coherence_matrix.py inputs/ifgramStack.h5 --cmap-vlist 0 0.4 1 + plot_coherence_matrix.py inputs/ifgramStack.h5 --axis-format time --yx 216 310 """ @@ -42,6 +43,11 @@ def create_parser(subparsers=None): help='Point of interest in lat/lon') parser.add_argument('--lookup','--lut', dest='lookup_file', help='Lookup file to convert lat/lon into y/x') + + # format + parser.add_argument('--ax-fmt', '--axis-format', dest='axis_format', + choices=['index', 'time'], default='time', + help='Coherence matrix axis format: index or time (default: %(default)s).') parser.add_argument('-c','--cmap', dest='cmap_name', default='RdBu_truncate', help='Colormap for coherence matrix.\nDefault: RdBu_truncate') parser.add_argument('--cmap-vlist', dest='cmap_vlist', type=float, nargs=3, default=[0.0, 0.7, 1.0], @@ -61,6 +67,7 @@ def create_parser(subparsers=None): parser.add_argument('-t','--template', dest='template_file', help='temporal file.') + # output parser.add_argument('--save', dest='save_fig', action='store_true', help='save the figure') parser.add_argument('--nodisplay', dest='disp_fig', diff --git a/src/mintpy/cli/plot_network.py b/src/mintpy/cli/plot_network.py index 6bc5dc2f0..a25dff7ed 100755 --- a/src/mintpy/cli/plot_network.py +++ b/src/mintpy/cli/plot_network.py @@ -74,6 +74,9 @@ def create_parser(subparsers=None): # Figure Setting fig = parser.add_argument_group('Figure', 'Figure settings for display') + fig.add_argument('--ax-fmt', '--axis-format', dest='axis_format', + choices=['index', 'time'], default='time', + help='Coherence matrix axis format: index or time (default: %(default)s).') fig.add_argument('--fs', '--fontsize', type=int, default=12, help='font size in points') fig.add_argument('--lw', '--linewidth', dest='linewidth', diff --git a/src/mintpy/plot_coherence_matrix.py b/src/mintpy/plot_coherence_matrix.py index db4391271..b366b0164 100644 --- a/src/mintpy/plot_coherence_matrix.py +++ b/src/mintpy/plot_coherence_matrix.py @@ -179,25 +179,26 @@ def plot_coherence_matrix4pixel(self, yx): plotDict['disp_legend'] = False # plot - coh_mat = pp.plot_coherence_matrix( - self.ax_mat, - date12List=self.date12_list, - cohList=coh.tolist(), - date12List_drop=ex_date12_list, - p_dict=plotDict, - )[1] + if self.axis_format == 'time': + pp.plot_coherence_matrix_time_axis( + self.ax_mat, + date12List=self.date12_list, + cohList=coh.tolist(), + date12List_drop=ex_date12_list, + p_dict=plotDict, + ) + else: + pp.plot_coherence_matrix( + 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) - # status bar - def format_coord(x, y): - row, col = int(y+0.5), int(x+0.5) - date12 = sorted([self.date_list[row], self.date_list[col]]) - date12 = [f'{i[0:4]}-{i[4:6]}-{i[6:8]}' for i in date12] - return f'x={date12[0]}, y={date12[1]}, v={coh_mat[row, col]:.3f}' - self.ax_mat.format_coord = format_coord - # info msg = f'pixel in yx = {tuple(yx)}, ' if self.fig_coord == 'geo': diff --git a/src/mintpy/plot_network.py b/src/mintpy/plot_network.py index 31911f7bb..ed54f4f36 100644 --- a/src/mintpy/plot_network.py +++ b/src/mintpy/plot_network.py @@ -204,21 +204,31 @@ 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 3 - Coherence Matrix (index or time axis) fig_size3 = np.mean(inps.fig_size) fig, ax = plt.subplots(figsize=[fig_size3, fig_size3]) - ax = pp.plot_coherence_matrix( - ax, - inps.date12List, - inps.cohList, - inps.date12List_drop, - p_dict=vars(inps), - )[0] + if inps.axis_format == 'time': + ax = pp.plot_coherence_matrix_time_axis( + ax, + inps.date12List, + inps.cohList, + inps.date12List_drop, + p_dict=vars(inps), + )[0] + fig.tight_layout() + else: + ax = pp.plot_coherence_matrix( + ax, + inps.date12List, + inps.cohList, + inps.date12List_drop, + p_dict=vars(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 - Interferogram Network fig, ax = plt.subplots(figsize=inps.fig_size) diff --git a/src/mintpy/utils/plot.py b/src/mintpy/utils/plot.py index 1f44958b6..3cda332f5 100644 --- a/src/mintpy/utils/plot.py +++ b/src/mintpy/utils/plot.py @@ -905,11 +905,11 @@ def plot_coherence_matrix(ax, date12List, cohList, date12List_drop=[], p_dict={} date12List = ptime.yyyymmdd_date12(date12List) coh_mat = pnet.coherence_matrix(date12List, cohList) + # Date Convert (also used by status bar below) + m_dates = [i.split('_')[0] for i in date12List] + s_dates = [i.split('_')[1] for i in date12List] + dateList = sorted(list(set(m_dates + s_dates))) if date12List_drop: - # Date Convert - m_dates = [i.split('_')[0] for i in date12List] - s_dates = [i.split('_')[1] for i in date12List] - dateList = sorted(list(set(m_dates + s_dates))) # Set dropped pairs' value to nan, in upper triangle only. for date12 in date12List_drop: idx1, idx2 = (dateList.index(i) for i in date12.split('_')) @@ -956,9 +956,166 @@ def plot_coherence_matrix(ax, date12List, cohList, date12List_drop=[], p_dict={} ax.plot([], [], label='Lower: Ifgrams all') ax.legend(loc=p_dict['legend_loc'], handlelength=0) + # Status bar + def format_coord(x, y): + row, col = int(y + 0.5), int(x + 0.5) + if 0 <= row < len(dateList) and 0 <= col < len(dateList): + date12 = sorted([dateList[row], dateList[col]]) + date12 = [f'{i[0:4]}-{i[4:6]}-{i[6:8]}' for i in date12] + return f'x={date12[0]}, y={date12[1]}, v={coh_mat[row, col]:.3f}' + return '' + + ax.format_coord = format_coord + 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)))) + dates = [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 + + # Plotting strategy for the time-axis coherence matrix: + # 1) Build a date-centered grid: each acquisition sits at the midpoint between + # neighboring cell edges; the first/last edges extend outward by half of the + # adjacent interval so edge cells match the in-network cell width. + # 2) Convert grid edges to matplotlib date numbers via mdates.date2num() because + # pcolormesh() requires numeric vertex coordinates for datetime axes. + grid_points = [dates[0] - (dates[1] - dates[0]) / 2] + for date1, date2 in zip(dates[:-1], dates[1:]): + grid_points.append(date1 + (date2 - date1) / 2) + grid_points.append(dates[-1] + (dates[-1] - dates[-2]) / 2) + + grid_nums = mdates.date2num(grid_points) + X, Y = np.meshgrid(grid_nums, grid_nums) + + # Plot diagonal grids (gray/black, zorder=1): mark acquisition dates for visual reference, and + # to distinguish them from un-selected / dropped interferograms (off-diagonal NaNs). + 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) + + # Plot off-diagonal grids (zorder=0): coherence of each ifgram pair; upper triangle may exclude + # dropped pairs (NaN -> white via set_bad) while lower triangle keeps the full network. + cmap.set_bad('white') + mesh = ax.pcolormesh( + X, Y, coh_mat, + cmap=cmap, + vmin=p_dict['vlim'][0], + vmax=p_dict['vlim'][1], + shading='auto', + zorder=0, + ) + + # axis format + # x-axis: reuse auto_adjust_xaxis_date() year labels + # y-axis: copy the same locators/formatters from the x-axis to be consistent + ax.set_aspect('equal', adjustable='box') + ax = auto_adjust_xaxis_date(ax, dates, buffer_year=None, fontsize=p_dict['fontsize'])[0] + + # for short span (<=1.5 yr), use same-line labels — year at Jan, odd month num at others + span_years = (dates[-1] - dates[0]).days / 365.25 + if span_years <= 1.5: + def _month_or_year(x, pos=None): + d = mdates.num2date(x).replace(tzinfo=None) + if d.month == 1: + return str(d.year) + return str(d.month) + for axis in [ax.xaxis, ax.yaxis]: + axis.set_major_locator(mdates.MonthLocator(bymonth=range(1, 13, 2))) + axis.set_major_formatter(ticker.FuncFormatter(_month_or_year)) + axis.set_minor_locator(mdates.MonthLocator()) + else: + # Sync y-axis tick locators/formatters with the x-axis (after auto_adjust) + ax.yaxis.set_major_locator(ax.xaxis.get_major_locator()) + ax.yaxis.set_major_formatter(ax.xaxis.get_major_formatter()) + ax.yaxis.set_minor_locator(ax.xaxis.get_minor_locator()) + + # Invert y-axis so early dates are at the top (same visual layout as the index matrix) + ax.set_ylim(ax.get_xlim()[::-1]) + ax.set_xlabel('Time', fontsize=p_dict['fontsize']) + ax.set_ylabel('Time', fontsize=p_dict['fontsize']) + # Rotate y tick labels 90 deg for readable date/month labels along the left edge + for label in ax.get_yticklabels(): + label.set_rotation(90) + label.set_va('center') + ax.tick_params(which='both', direction='out', + bottom=True, top=True, left=True, right=True) + + if p_dict['disp_title']: + ax.set_title(p_dict['fig_title']) + + # 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(dates) and 0 <= col < len(dates): + date1 = dates[col].strftime('%Y-%m-%d') + date2 = dates[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.