Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/mintpy/cli/plot_coherence_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
70 changes: 70 additions & 0 deletions src/mintpy/plot_coherence_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider unifying the two pixel plotting paths into a single parameterized plot_coherence_matrix4pixel function that selects the appropriate plotting routine based on time_axis while sharing all common setup and marker logic.

You can remove most of the duplication by parameterizing the plot function and keeping a single plot_coherence_matrix4pixel implementation. That also lets you unify the layout and marker logic.

For example:

def plot_coherence_matrix4pixel(self, yx):
    """Plot coherence matrix for one pixel."""
    # choose plotting function based on time_axis
    plot_func = (
        pp.plot_coherence_matrix_time_axis
        if self.time_axis
        else pp.plot_coherence_matrix
    )

    self.ax_mat.cla()

    # read coherence
    box = (yx[1], yx[0], yx[1] + 1, yx[0] + 1)
    coh = readfile.read(self.ifgram_file, datasetName='coherence', box=box)[0]

    # ex_date for pixel-wise masking during network inversion
    ex_date12_list = self.ex_date12_list[:]  # local copy
    if self.min_coh_used > 0.0:
        ex_date12_list += np.array(self.date12_list)[coh < self.min_coh_used].tolist()
        ex_date12_list = sorted(set(ex_date12_list))

    # common plot config
    plotDict = {
        "fig_title": f"Y = {yx[0]}, X = {yx[1]}",
        "colormap": self.colormap,
        "cbar_label": "Coherence",
        "disp_legend": False,
    }
    if self.tcoh is not None:
        tcoh = self.tcoh[yx[0], yx[1]]
        plotDict["fig_title"] += f", tcoh = {tcoh:.2f}"
    if len(self.cmap_vlist) >= 2:
        plotDict["vlim"] = [self.cmap_vlist[0], self.cmap_vlist[-1]]
    else:
        plotDict["vlim"] = [0.0, 1.0]

    # call the selected plotting function
    plot_func(
        self.ax_mat,
        date12List=self.date12_list,
        cohList=coh.tolist(),
        date12List_drop=ex_date12_list,
        p_dict=plotDict,
    )

    # common annotations
    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()

    # common marker logic
    if self.fig_coord == "geo":
        lat, lon = self.coord.yx2lalo(yx[0], yx[1])
        mx, my = lon, lat
    else:
        mx, my = yx[1], 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()

Actionable steps:

  1. Remove plot_coherence_matrix4pixel_time_axis entirely.
  2. Fold its unique behavior into plot_coherence_matrix4pixel by:
    • Selecting plot_func based on self.time_axis.
    • Reusing shared logic for:
      • reading coh / building ex_date12_list
      • constructing plotDict (including tcoh)
      • annotations, logging, and window title
      • tight layout (_mat_tight_layout_done) for both paths
      • marker update on ax_img.
  3. Keep the existing public API (plot_coherence_matrix4pixel signature) and the time_axis flag, so no external behavior changes.

"""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
Expand Down
28 changes: 22 additions & 6 deletions src/mintpy/plot_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']],
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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 ...')
Expand Down
166 changes: 166 additions & 0 deletions src/mintpy/utils/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={}):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Avoid using a mutable dict as the default value for p_dict.

{} as a default argument makes all calls share the same dict instance, which can cause subtle bugs if p_dict is mutated. Prefer p_dict=None and then inside: p_dict = {} if p_dict is None else dict(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
Comment on lines +1001 to +1005
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Dropped interferograms are only masked on one triangle of the symmetric matrix.

Because coherence_matrix is symmetric, updating only coh_mat[idx1, idx2] leaves coh_mat[idx2, idx1] unmasked, which can produce inconsistent plots. Please mask both [idx1, idx2] and [idx2, idx1] to keep the visualization symmetric.

Suggested change
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 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
coh_mat[idx2, idx1] = 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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Inverting the y-axis after placing month/year labels likely moves them away from the intended edges.

Because the month/year positions use ax.get_ylim()[1] and ax.get_xlim()[0] computed before invert_yaxis(), those values no longer map to the top/left edges after inversion, so the labels can shift toward the bottom or misalign. Either invert the y-axis before computing offset/placing the labels, or place them in axis coordinates (transform=ax.transAxes) so they stay fixed to the edges regardless of axis direction.


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.

Expand Down