Skip to content

Conversation

@huchangyang
Copy link
Contributor

@huchangyang huchangyang commented Dec 30, 2025

Summary

This PR adds a new timeaxis mode for coherence matrix plotting and enhances the plot_network.py to display both standard and timeaxis coherence matrices.

Changes

1. Enhanced plot_coherence_matrix.py - Split coherence matrix into two views

image
  • Fig 1: plot_coherence_matrix in two views.

2. Added timeaxis mode for coherence matrix plotting

A new timeaxis mode has been implemented that displays coherence matrices with a continuous time axis instead of discrete date indices. This provides better visualization for temporal coherence patterns.

image
  • Fig 2: Standard coherence matrix (existing functionality)
image - **Fig 3**: Coherence matrix with continuous time axis (new timeaxis mode)

3. Added timeaxis-coherence matrix in plot_network.py

This allows users to compare both visualization approaches side by side when running plot_network.py.

Files Changed

  • src/mintpy/cli/plot_coherence_matrix.py: Added --time-axis CLI option
  • src/mintpy/plot_coherence_matrix.py: Implemented timeaxis mode support
  • src/mintpy/plot_network.py: Added timeaxis coherence matrix figure
  • src/mintpy/utils/plot.py: Added plot_coherence_matrix_time_axis() function

Usage

In plot_coherence_matrix.py:

plot_coherence_matrix.py ifgramStack.h5 --time-axis

In plot_network.py:

The timeaxis coherence matrix is automatically included as Fig 3 when coherence data is available.

Summary by Sourcery

Add a continuous time-axis coherence matrix visualization alongside the existing date-indexed matrix and wire it into both the standalone viewer and network plotting workflow.

New Features:

  • Introduce plot_coherence_matrix_time_axis() utility to render coherence matrices on a continuous time axis with month and year labeling.
  • Add a time-axis mode to the coherence matrix viewer, including a new CLI flag to toggle it and dual-window interaction between image and matrix views.
  • Extend network plotting to generate and save an additional coherence-matrix figure using the new time-axis visualization.

Enhancements:

  • Refactor coherence matrix plotting into separate image and matrix figures with clearer window titles and tighter layouts.
  • Improve image-window interactivity by adding and updating a marker on the selected pixel when updating the coherence matrix.

- Refactor timeaxis plotting logic to utils/plot.py
- Add black diagonal cells in timeaxis mode
- Set default colormap to RdBu_truncate (same as normal mode)
- Fix colorbar vlim to use [cmap_vlist[0], cmap_vlist[-1]]
- Ensure upper triangle shows only kept pairs, lower triangle shows all pairs
- Add timeaxis coherence matrix plot to plot_network.py
@welcome
Copy link

welcome bot commented Dec 30, 2025

💖 Thanks for opening this pull request! Please check out our contributing guidelines. 💖
Keep in mind that all new features should be documented. It helps to write the comments next to the code or below your functions describing all arguments, and return types before writing the code. This will help you think about your code design and usually results in better code.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 30, 2025

Reviewer's Guide

Implements a new continuous time-axis coherence matrix plotting function and integrates it into both the standalone coherence-matrix viewer and the network plotting workflow, including a new CLI flag and figure layout changes.

Sequence diagram for time-axis coherence matrix interaction in viewer

sequenceDiagram
    actor User
    participant CLI_plot_coherence_matrix
    participant coherenceMatrixViewer
    participant PlotUtils as PlotUtils
    participant Matplotlib

    User->>CLI_plot_coherence_matrix: run with args (including optional time_axis)
    CLI_plot_coherence_matrix->>coherenceMatrixViewer: create(inps)
    CLI_plot_coherence_matrix->>coherenceMatrixViewer: open()
    CLI_plot_coherence_matrix->>coherenceMatrixViewer: plot()
    coherenceMatrixViewer->>Matplotlib: plt.subplots(figname_img)
    coherenceMatrixViewer->>coherenceMatrixViewer: plot_init_image()
    coherenceMatrixViewer->>Matplotlib: plt.subplots(figname_mat)
    coherenceMatrixViewer->>Matplotlib: mpl_connect(button_press_event, update_coherence_matrix)
    Matplotlib-->>User: display image and matrix windows

    User->>Matplotlib: click on image pixel
    Matplotlib->>coherenceMatrixViewer: update_coherence_matrix(event)
    coherenceMatrixViewer->>coherenceMatrixViewer: compute yx from event
    alt time_axis is True
        coherenceMatrixViewer->>coherenceMatrixViewer: plot_coherence_matrix4pixel_time_axis(yx)
        coherenceMatrixViewer->>PlotUtils: plot_coherence_matrix_time_axis(ax_mat, date12List, cohList, date12List_drop, p_dict)
        PlotUtils-->>coherenceMatrixViewer: ax_mat, Z, mesh
    else time_axis is False
        coherenceMatrixViewer->>coherenceMatrixViewer: plot_coherence_matrix4pixel(yx)
        coherenceMatrixViewer->>PlotUtils: plot_coherence_matrix(ax_mat, date12List, cohList, date12List_drop, p_dict)
        PlotUtils-->>coherenceMatrixViewer: ax_mat, coh_mat, im
    end
    coherenceMatrixViewer->>coherenceMatrixViewer: update_image_marker(yx)
    coherenceMatrixViewer->>Matplotlib: draw_idle(), flush_events()
    Matplotlib-->>User: updated coherence matrix and marker
Loading

Sequence diagram for plot_network adding time-axis coherence matrix figure

sequenceDiagram
    participant Caller as plot_network_caller
    participant plot_network
    participant PlotUtils as PlotUtils
    participant Matplotlib

    Caller->>plot_network: call(inps)
    plot_network->>Matplotlib: subplots() for pbaseHistory
    plot_network->>Matplotlib: subplots() for coherenceHistory
    plot_network->>PlotUtils: plot_coherence_matrix(ax, inps.date12List, inps.cohList, inps.date12List_drop, p_dict)
    PlotUtils-->>plot_network: ax, coh_mat, im

    alt inps.cohList is not None
        plot_network->>Matplotlib: subplots() for coherenceMatrixTimeAxis
        plot_network->>PlotUtils: plot_coherence_matrix_time_axis(ax, inps.date12List, inps.cohList, inps.date12List_drop, p_dict)
        PlotUtils-->>plot_network: ax, Z, mesh
    end

    plot_network->>Matplotlib: subplots() for network
    plot_network->>PlotUtils: plot_network(ax, inps.pbaseDict, inps.date12List, inps.ifgIndexDict, inps.dropIfgIndexDict, inps.date12List_drop)
    PlotUtils-->>plot_network: ax

    opt inps.save_fig
        plot_network->>Matplotlib: savefig(fig_names)
    end
Loading

Class diagram for coherenceMatrixViewer and time-axis plotting utilities

classDiagram
    class coherenceMatrixViewer {
        - figname_img
        - figsize_img
        - fig_img
        - ax_img
        - cbar_img
        - img
        - figname_mat
        - figsize_mat
        - fig_mat
        - ax_mat
        - time_axis
        + open()
        + plot()
        + plot_init_image()
        + plot_coherence_matrix4pixel(yx)
        + plot_coherence_matrix4pixel_time_axis(yx)
        + update_coherence_matrix(event)
        + update_image_marker(yx)
    }

    class PlotUtils {
        + plot_coherence_matrix(ax, date12List, cohList, date12List_drop, p_dict)
        + plot_coherence_matrix_time_axis(ax, date12List, cohList, date12List_drop, p_dict)
    }

    class plot_network {
        + plot_network(inps)
    }

    coherenceMatrixViewer ..> PlotUtils : uses
    plot_network ..> PlotUtils : uses
Loading

File-Level Changes

Change Details Files
Add plot_coherence_matrix_time_axis utility to generate coherence matrices on a continuous time axis with custom ticks, labels, and colorbar handling.
  • Introduce plot_coherence_matrix_time_axis that converts date12 pairs and coherence values into a time-grid QuadMesh using pcolormesh.
  • Normalize and parse date strings into datetime objects, tracking kept vs dropped interferograms and mapping them into a NaN-initialized matrix Z.
  • Create continuous time grid boundaries, map dates to grid cells, and fill upper/lower triangle values while preserving dropped-pair behavior.
  • Render a black diagonal mask, configure colormap/NaN handling, add month and year tick marks/labels on both axes, and invert Y for matrix orientation.
  • Attach a custom format_coord handler to show human-readable dates and coherence at cursor location, and optionally add a colorbar and title based on p_dict options.
src/mintpy/utils/plot.py
Refactor coherenceMatrixViewer to use separate image and matrix figures and add a time-axis plotting mode controlled by CLI.
  • Split single fig into fig_img and fig_mat with separate sizes and window titles, and compute default figure sizes for image vs matrix based on dataset shape and interferogram count.
  • Introduce time_axis flag on the viewer, choose default colormap (RdBu_truncate vs viridis) and vlim based on cmap_vlist, and precompute a ColormapExt instance.
  • Update plot() to create two windows (image and coherence matrix), call a new plot_init_image helper, and connect button_press_event handlers on both canvases to update the matrix.
  • Implement plot_coherence_matrix4pixel_time_axis to read per-pixel coherence, apply min_coh and ex_date12_list masking, build plotDict, and delegate to plot_coherence_matrix_time_axis, including annotations and window/title updates.
  • Modify plot_coherence_matrix4pixel to dispatch to the time-axis variant when enabled, and adjust draw/flush calls to operate on fig_mat; add update_image_marker to show the currently selected pixel as a red triangle in the image view and extend update_coherence_matrix to update markers and accept clicks from either window.
src/mintpy/plot_coherence_matrix.py
Extend network plotting to generate and save an additional coherence matrix figure using the new time-axis mode, and fix save-logging messages.
  • Expand coherence fig_names list to include a new coherenceMatrixTimeAxis PDF before network, and adjust final network fig index to account for optional coherence data.
  • Add a new figure block that calls plot_coherence_matrix_time_axis with inps.date12List, cohList, date12List_drop, and p_dict=vars(inps), saving to the new fig_names slot when save_fig is set.
  • Correct print statements after saving coherence history and coherence matrix figures so they reference the correct filename indices and add logging for the new time-axis figure.
src/mintpy/plot_network.py
Expose a CLI flag to enable time-axis mode in the coherence matrix viewer.
  • Add --time-axis boolean flag to plot_coherence_matrix CLI parser, setting inps.time_axis to switch coherenceMatrixViewer into continuous time-axis mode while keeping existing behavior as default.
src/mintpy/cli/plot_coherence_matrix.py

Possibly linked issues

  • #use time as axis tick in the coherence matrix plot: PR implements the requested time-axis coherence matrix option in plot_coherence_matrix.py and wires it into plot_network.
  • #Rotated coherence matrix in plot_coherence_matrix.py: PR implements rotated/time-axis coherence matrix and splits coherence plotting into two figures, fulfilling the feature request.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The month tick label formatting in plot_coherence_matrix_time_axis uses strftime('%-m'), which is not portable on Windows; consider using strftime('%m').lstrip('0') instead to avoid platform-specific behavior.
  • In update_image_marker, removing existing markers by scanning all children for a '^' marker is brittle if other plots use the same marker; it would be more robust to keep a reference to the specific marker artist and update/remove that directly.
  • The date-to-grid index mapping in plot_coherence_matrix_time_axis does a nested search over grid_points for each date, which is O(n^2); you could precompute bin edges and use np.searchsorted or a similar vectorized approach to simplify and speed up this section.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The month tick label formatting in `plot_coherence_matrix_time_axis` uses `strftime('%-m')`, which is not portable on Windows; consider using `strftime('%m').lstrip('0')` instead to avoid platform-specific behavior.
- In `update_image_marker`, removing existing markers by scanning all children for a `'^'` marker is brittle if other plots use the same marker; it would be more robust to keep a reference to the specific marker artist and update/remove that directly.
- The date-to-grid index mapping in `plot_coherence_matrix_time_axis` does a nested search over `grid_points` for each date, which is O(n^2); you could precompute bin edges and use `np.searchsorted` or a similar vectorized approach to simplify and speed up this section.

## Individual Comments

### Comment 1
<location> `src/mintpy/utils/plot.py:987-992` </location>
<code_context>
+    if 'disp_cbar'   not in p_dict.keys():   p_dict['disp_cbar']   = 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']))
+
+    # Normalize date12 format
</code_context>

<issue_to_address>
**issue:** Broaden the accepted colormap types to handle generic Matplotlib colormap instances.

`p_dict['colormap']` may be a generic Matplotlib colormap (e.g. `ListedColormap` like `viridis`), but the code only accepts strings or `LinearSegmentedColormap`, causing valid colormaps to raise `ValueError`. To support all Matplotlib colormap instances passed in directly (including custom ones), consider checking against the base `mpl.colors.Colormap` instead of just `LinearSegmentedColormap`.
</issue_to_address>

### Comment 2
<location> `src/mintpy/utils/plot.py:1189` </location>
<code_context>
+        # Only add label for odd months
+        if tick_dates[i].month % 2 == 1:
+            label_positions.append(mid_point)
+            month_labels.append(tick_dates[i].strftime('%-m'))
+            is_january.append(tick_dates[i].month == 1)
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid using `strftime('%-m')` since it is not portable across platforms (e.g., Windows).

On platforms like Windows, `%-m` isn’t supported (it uses `%#m` instead), which can raise a `ValueError`. To keep this portable, derive the month number directly instead of via `strftime`, e.g.:

```python
month_labels.append(str(tick_dates[i].month))
```

This preserves the intended month labels without relying on platform-specific behavior.
</issue_to_address>

### Comment 3
<location> `src/mintpy/utils/plot.py:963` </location>
<code_context>
     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,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring `plot_coherence_matrix_time_axis` by extracting helpers for date parsing, grid construction, tick layout, and coordinate formatting while removing unused structures to keep behavior but greatly simplify the function.

You can keep all functionality but significantly reduce complexity and duplication by extracting a few helpers and simplifying some data paths. Two concrete wins:

1. **Remove unused structures & redundant parsing**
2. **Factor grid mapping & ticks into small helpers**

### 1. Remove `coh_dict` and redundant date parsing

`coh_dict` is built but never used; only `coh_dict_ordered` and `excluded_pairs` are used when filling `Z`. Also, the “ensure we have datetime objects” block re-parses dates that could be normalized once.

You can simplify the coherence dictionary creation to a single pass that:

- Normalizes strings to canonical `%Y%m%d` once
- Converts to `datetime` once using a shared helper
- Populates `coh_dict_ordered` and `excluded_pairs` only

Example:

```python
def _parse_date_yyyymmdd(date_str: str, cache: dict) -> datetime:
    if date_str in cache:
        return cache[date_str]
    # normalize with ptime first
    norm = ptime.yyyymmdd([date_str])[0]
    try:
        dt_obj = datetime.strptime(norm, "%Y%m%d")
    except ValueError:
        if len(norm) == 6:
            dt_obj = datetime.strptime("20" + norm, "%Y%m%d")
        else:
            raise
    cache[date_str] = dt_obj
    cache[norm] = dt_obj
    return dt_obj
```

Then in `plot_coherence_matrix_time_axis`:

```python
date_cache = {}
coh_dict_ordered = {}
excluded_pairs = set()

for date12, coh_val in zip(date12List, cohList):
    d1_str, d2_str = date12.split("_")
    d1 = _parse_date_yyyymmdd(d1_str, date_cache)
    d2 = _parse_date_yyyymmdd(d2_str, date_cache)

    coh_dict_ordered[(d1, d2)] = float(coh_val)
    pair_norm = (min(d1, d2), max(d1, d2))
    if date12 in date12List_drop:
        excluded_pairs.add(pair_norm)
```

This removes:

- `coh_dict`
- The “if date1_str not in date_objs / if date1 is None” re-checks
- Multiple `ptime.yyyymmdd` calls in different branches

### 2. Simplify grid index mapping with a helper

The manual nested loop assigning dates to grid cells can be replaced with a small helper that uses numeric days and `np.searchsorted`, which will be easier to reason about and reuse (e.g., for the `format_coord` function).

```python
def _build_time_grid(dates: list[datetime]) -> tuple[np.ndarray, dict]:
    dates_sorted = np.array(sorted(dates))
    base = dates_sorted.min()
    # grid_points as before
    grid_points = [dates_sorted[0]]
    for i in range(len(dates_sorted) - 1):
        mid = dates_sorted[i] + (dates_sorted[i + 1] - dates_sorted[i]) / 2
        grid_points.append(mid)
    grid_points.append(dates_sorted[-1])
    grid_points = np.array(grid_points)

    days_grid = (grid_points - base).astype("timedelta64[D]").astype(int)
    date_days = (dates_sorted - base).astype("timedelta64[D]").astype(int)

    # indices: date falls into bin returned by searchsorted-1
    idx = np.searchsorted(days_grid, date_days, side="right") - 1
    date_to_idx = {d: int(i) for d, i in zip(dates_sorted, idx)}
    return days_grid, date_to_idx
```

Usage inside `plot_coherence_matrix_time_axis`:

```python
days_grid, date_to_grid_idx = _build_time_grid(date_list)
X, Y = np.meshgrid(days_grid, days_grid)
Z = np.full((len(days_grid) - 1, len(days_grid) - 1), np.nan)
```

This replaces the manual `for date in date_list: for grid_idx in range(...):` block and also gives you `days_grid` for both plotting and `format_coord`.

### 3. Extract tick/label layout into a helper

The month/year tick logic is correct but verbose. Moving it into a helper keeps the main plot function focused on building `Z` and the mesh.

```python
def _setup_month_year_ticks(ax, base_date: datetime, dates: list[datetime]):
    min_date = min(dates)
    max_date = max(dates)

    # compute tick_dates exactly as now
    # ...

    tick_positions = [(d - base_date).days for d in tick_dates]
    # compute major/minor ticks & labels as now
    # ...

    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)

    # month and year labels via ax.text as now
    # ...
```

Then in the main function:

```python
base_date = min(date_list)
# ... build grid / Z ...
_setup_month_year_ticks(ax, base_date, date_list)
```

This doesn’t change behavior, but it shortens the main function and makes the “plot layout” responsibility clearly separated.

### 4. Extract formatter to a factory

The inline `format_coord` closure adds to the size of the main function and re-creates the “nearest grid index from days” logic. Once you have `days_grid` and `Z`, you can move this to a small factory:

```python
def make_coh_matrix_formatter(days_grid: np.ndarray, grid_points, Z: np.ndarray):
    days_grid = np.asarray(days_grid)

    def _formatter(x, y):
        x_idx = np.argmin(np.abs(days_grid - x))
        y_idx = np.argmin(np.abs(days_grid - y))
        x_idx = np.clip(x_idx, 0, len(grid_points) - 1)
        y_idx = np.clip(y_idx, 0, len(grid_points) - 1)

        d1 = grid_points[x_idx]
        d2 = grid_points[y_idx]
        val = np.nan
        if x_idx < len(grid_points) - 1 and y_idx < len(grid_points) - 1:
            val = Z[y_idx, x_idx]
        if not np.isnan(val):
            return f"x={d1:%Y-%m-%d}, y={d2:%Y-%m-%d}, v={val:.3f}"
        return f"x={d1:%Y-%m-%d}, y={d2:%Y-%m-%d}, v=NaN"

    return _formatter
```

Then in `plot_coherence_matrix_time_axis`:

```python
ax.format_coord = make_coh_matrix_formatter(days_grid, grid_points, Z)
```

---

These changes keep all behavior intact, but:

- Remove unused structures (`coh_dict`) and repeated date parsing logic.
- Encapsulate the most intricate pieces (grid construction, tick labeling, formatter) into helpers, reducing the monolithic size and cognitive load of `plot_coherence_matrix_time_axis`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +987 to +992
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']))
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Broaden the accepted colormap types to handle generic Matplotlib colormap instances.

p_dict['colormap'] may be a generic Matplotlib colormap (e.g. ListedColormap like viridis), but the code only accepts strings or LinearSegmentedColormap, causing valid colormaps to raise ValueError. To support all Matplotlib colormap instances passed in directly (including custom ones), consider checking against the base mpl.colors.Colormap instead of just LinearSegmentedColormap.

# Only add label for odd months
if tick_dates[i].month % 2 == 1:
label_positions.append(mid_point)
month_labels.append(tick_dates[i].strftime('%-m'))
Copy link
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 strftime('%-m') since it is not portable across platforms (e.g., Windows).

On platforms like Windows, %-m isn’t supported (it uses %#m instead), which can raise a ValueError. To keep this portable, derive the month number directly instead of via strftime, e.g.:

month_labels.append(str(tick_dates[i].month))

This preserves the intended month labels without relying on platform-specific behavior.

return ax, coh_mat, im


def plot_coherence_matrix_time_axis(ax, date12List, cohList, date12List_drop=[], p_dict={}):
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring plot_coherence_matrix_time_axis by extracting helpers for date parsing, grid construction, tick layout, and coordinate formatting while removing unused structures to keep behavior but greatly simplify the function.

You can keep all functionality but significantly reduce complexity and duplication by extracting a few helpers and simplifying some data paths. Two concrete wins:

  1. Remove unused structures & redundant parsing
  2. Factor grid mapping & ticks into small helpers

1. Remove coh_dict and redundant date parsing

coh_dict is built but never used; only coh_dict_ordered and excluded_pairs are used when filling Z. Also, the “ensure we have datetime objects” block re-parses dates that could be normalized once.

You can simplify the coherence dictionary creation to a single pass that:

  • Normalizes strings to canonical %Y%m%d once
  • Converts to datetime once using a shared helper
  • Populates coh_dict_ordered and excluded_pairs only

Example:

def _parse_date_yyyymmdd(date_str: str, cache: dict) -> datetime:
    if date_str in cache:
        return cache[date_str]
    # normalize with ptime first
    norm = ptime.yyyymmdd([date_str])[0]
    try:
        dt_obj = datetime.strptime(norm, "%Y%m%d")
    except ValueError:
        if len(norm) == 6:
            dt_obj = datetime.strptime("20" + norm, "%Y%m%d")
        else:
            raise
    cache[date_str] = dt_obj
    cache[norm] = dt_obj
    return dt_obj

Then in plot_coherence_matrix_time_axis:

date_cache = {}
coh_dict_ordered = {}
excluded_pairs = set()

for date12, coh_val in zip(date12List, cohList):
    d1_str, d2_str = date12.split("_")
    d1 = _parse_date_yyyymmdd(d1_str, date_cache)
    d2 = _parse_date_yyyymmdd(d2_str, date_cache)

    coh_dict_ordered[(d1, d2)] = float(coh_val)
    pair_norm = (min(d1, d2), max(d1, d2))
    if date12 in date12List_drop:
        excluded_pairs.add(pair_norm)

This removes:

  • coh_dict
  • The “if date1_str not in date_objs / if date1 is None” re-checks
  • Multiple ptime.yyyymmdd calls in different branches

2. Simplify grid index mapping with a helper

The manual nested loop assigning dates to grid cells can be replaced with a small helper that uses numeric days and np.searchsorted, which will be easier to reason about and reuse (e.g., for the format_coord function).

def _build_time_grid(dates: list[datetime]) -> tuple[np.ndarray, dict]:
    dates_sorted = np.array(sorted(dates))
    base = dates_sorted.min()
    # grid_points as before
    grid_points = [dates_sorted[0]]
    for i in range(len(dates_sorted) - 1):
        mid = dates_sorted[i] + (dates_sorted[i + 1] - dates_sorted[i]) / 2
        grid_points.append(mid)
    grid_points.append(dates_sorted[-1])
    grid_points = np.array(grid_points)

    days_grid = (grid_points - base).astype("timedelta64[D]").astype(int)
    date_days = (dates_sorted - base).astype("timedelta64[D]").astype(int)

    # indices: date falls into bin returned by searchsorted-1
    idx = np.searchsorted(days_grid, date_days, side="right") - 1
    date_to_idx = {d: int(i) for d, i in zip(dates_sorted, idx)}
    return days_grid, date_to_idx

Usage inside plot_coherence_matrix_time_axis:

days_grid, date_to_grid_idx = _build_time_grid(date_list)
X, Y = np.meshgrid(days_grid, days_grid)
Z = np.full((len(days_grid) - 1, len(days_grid) - 1), np.nan)

This replaces the manual for date in date_list: for grid_idx in range(...): block and also gives you days_grid for both plotting and format_coord.

3. Extract tick/label layout into a helper

The month/year tick logic is correct but verbose. Moving it into a helper keeps the main plot function focused on building Z and the mesh.

def _setup_month_year_ticks(ax, base_date: datetime, dates: list[datetime]):
    min_date = min(dates)
    max_date = max(dates)

    # compute tick_dates exactly as now
    # ...

    tick_positions = [(d - base_date).days for d in tick_dates]
    # compute major/minor ticks & labels as now
    # ...

    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)

    # month and year labels via ax.text as now
    # ...

Then in the main function:

base_date = min(date_list)
# ... build grid / Z ...
_setup_month_year_ticks(ax, base_date, date_list)

This doesn’t change behavior, but it shortens the main function and makes the “plot layout” responsibility clearly separated.

4. Extract formatter to a factory

The inline format_coord closure adds to the size of the main function and re-creates the “nearest grid index from days” logic. Once you have days_grid and Z, you can move this to a small factory:

def make_coh_matrix_formatter(days_grid: np.ndarray, grid_points, Z: np.ndarray):
    days_grid = np.asarray(days_grid)

    def _formatter(x, y):
        x_idx = np.argmin(np.abs(days_grid - x))
        y_idx = np.argmin(np.abs(days_grid - y))
        x_idx = np.clip(x_idx, 0, len(grid_points) - 1)
        y_idx = np.clip(y_idx, 0, len(grid_points) - 1)

        d1 = grid_points[x_idx]
        d2 = grid_points[y_idx]
        val = np.nan
        if x_idx < len(grid_points) - 1 and y_idx < len(grid_points) - 1:
            val = Z[y_idx, x_idx]
        if not np.isnan(val):
            return f"x={d1:%Y-%m-%d}, y={d2:%Y-%m-%d}, v={val:.3f}"
        return f"x={d1:%Y-%m-%d}, y={d2:%Y-%m-%d}, v=NaN"

    return _formatter

Then in plot_coherence_matrix_time_axis:

ax.format_coord = make_coh_matrix_formatter(days_grid, grid_points, Z)

These changes keep all behavior intact, but:

  • Remove unused structures (coh_dict) and repeated date parsing logic.
  • Encapsulate the most intricate pieces (grid construction, tick labeling, formatter) into helpers, reducing the monolithic size and cognitive load of plot_coherence_matrix_time_axis.

@yunjunz yunjunz changed the title Add timeaxis mode for coherence matrix plotting coherence matrix plot: add timeaxis mode to better support irregular temporal sampling Dec 31, 2025
@yunjunz yunjunz self-assigned this Dec 31, 2025
huchangyang and others added 7 commits January 4, 2026 15:11
…version

Fix the issue where inv_quality[idx] assignment fails when inv_quali
is returned as a 1D array instead of a scalar for single pixel processing.
Use np.atleast_1d(inv_quali)[0] to ensure scalar value extraction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants