Skip to content

Add OPERA ZTD support in mintpy#1473

Closed
dbekaert wants to merge 0 commit intoinsarlab:mainfrom
dbekaert:master
Closed

Add OPERA ZTD support in mintpy#1473
dbekaert wants to merge 0 commit intoinsarlab:mainfrom
dbekaert:master

Conversation

@dbekaert
Copy link
Copy Markdown

@dbekaert dbekaert commented Mar 4, 2026

Description of proposed changes

Added support in MintPY to leverage the pre-computed tropo products from OPERA which are freely available at the ASFDAAC. These products are SAR agnostic, derived from ECMWG HRES data, and provided as Zenith Tropospheric delays on a 3D Cube.

I tested these on a small stack over Mexico City and made a comparison with ERA5 PyAPS module in mintpy.
Below image are the velocities:
image

Below images are the comparisons of the tropospheric delays time-series with common reference date and reference location (top figure is OPERA, bottom figure is ERA5).
image

Reminders

  • Fix #xxxx
  • Pass Pre-commit check (green)
  • Pass Codacy code review (green)
  • Pass Circle CI test (green)
  • Make sure that your code follows our style. Use the other functions/files as a basis.
  • If modifying functionality, describe changes to function behavior and arguments in a comment below the function declaration.
  • If adding new functionality, add a detailed description to the documentation and/or an example.

Summary by Sourcery

Add support for tropospheric delay correction using OPERA zenith delay products and integrate it into MintPy’s small baseline workflow and CLI.

New Features:

  • Introduce a new OPERA-based tropospheric correction workflow that reads OPERA ZTD products, interpolates them to the MintPy geometry grid, and generates delay time-series and corrected displacement products.
  • Add a tropo_opera command-line interface and parser, including exposure via the main mintpy entry points and pyproject script configuration.
  • Extend smallbaselineApp configuration and defaults to allow selecting an OPERA tropospheric delay method and specifying the OPERA data directory.

Enhancements:

  • Wire the new OPERA correction option into the existing smallbaselineApp tropospheric delay step alongside pyaps, gacos, and phase-elevation methods.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 4, 2026

Reviewer's Guide

Adds full OPERA L4 zenith tropospheric delay (ZTD) support to MintPy, including a new tropo_opera workflow/CLI, integration into smallbaselineApp, configuration defaults, ASF-based product discovery/download, interpolation onto the geometry grid, LOS projection, and correction file generation.

Sequence diagram for the new tropo_opera CLI workflow

sequenceDiagram
    actor User
    participant CLI_tropo_opera as cli_tropo_opera
    participant TropoOperaCore as tropo_opera_module
    participant ASF as ASF_Search
    participant FS as FileSystem
    participant DiffCLI as cli_diff

    User->>CLI_tropo_opera: tropo_opera.py -f dis_file -g geom_file --dir opera_dir
    CLI_tropo_opera->>CLI_tropo_opera: cmd_line_parse()
    CLI_tropo_opera->>TropoOperaCore: run_tropo_opera(inps)

    TropoOperaCore->>TropoOperaCore: read_inps2date_time(inps)
    TropoOperaCore->>TropoOperaCore: get_opera_date_time_list()
    TropoOperaCore->>TropoOperaCore: get_opera_file_status()
    TropoOperaCore->>FS: check local OPERA .nc files
    FS-->>TropoOperaCore: matched_files, missing_date_hour_list

    alt missing OPERA products
        TropoOperaCore->>ASF: dload_opera_files(missing_date_hour_list, opera_dir)
        ASF-->>FS: download OPERA .nc files
        TropoOperaCore->>TropoOperaCore: get_opera_file_status() (re-check)
    end

    TropoOperaCore->>TropoOperaCore: calculate_delay_timeseries(tropo_file, dis_file, geom_file, opera_dir)
    loop for each used_date
        TropoOperaCore->>FS: open best OPERA file for model time
        FS-->>TropoOperaCore: OPERA ZTD cube
        TropoOperaCore->>TropoOperaCore: calc_zenith_delay_from_opera_file()
        TropoOperaCore->>TropoOperaCore: _project_zenith_to_los()
        TropoOperaCore->>FS: append LOS delay to tropo_file
    end

    TropoOperaCore->>DiffCLI: diff.py dis_file tropo_file -o cor_dis_file --force
    DiffCLI->>FS: read displacement and delay
    DiffCLI->>FS: write corrected displacement cor_dis_file
    DiffCLI-->>TropoOperaCore: done
    TropoOperaCore-->>CLI_tropo_opera: completed
    CLI_tropo_opera-->>User: corrected file cor_dis_file
Loading

File-Level Changes

Change Details Files
Wire OPERA ZTD as a new tropospheric correction method in the smallbaselineApp workflow and CLI dispatch.
  • Add filename suffix generation for OPERA-corrected outputs in the smallbaselineApp troposphericDelay method switch
  • Add OPERA method branch to call the new tropo_opera CLI with geometry, input displacement, output path, and operaDir from the template
  • Register the tropo_opera subcommand in the main MintPy CLI and include it in the workflow/entry-points so it’s invokable from the command line and workflow engine
src/mintpy/smallbaselineApp.py
src/mintpy/__main__.py
src/mintpy/workflow/__init__.py
pyproject.toml
Extend smallbaselineApp configuration to support OPERA-specific options and defaults.
  • Document OPERA as an additional troposphericDelay.method option alongside pyaps/height_correlation/gacos/no
  • Introduce mintpy.troposphericDelay.operaDir configuration key with default paths in both standard and auto config files
src/mintpy/defaults/smallbaselineApp.cfg
src/mintpy/defaults/smallbaselineApp_auto.cfg
Implement the core OPERA ZTD processing workflow: discovery/download, cube cropping, DEM intersection, interpolation, time-series generation, and LOS projection.
  • Define constants for OPERA model hours, minimum supported acquisition date, and ASF OPERA tropospheric collections
  • Implement helpers to read geometry grid bounds/grids, compute crop indices on OPERA latitude/longitude, and read/crop OPERA zenith delay cubes into wet/hydro/total components
  • Interpolate 3D OPERA total delay (height/lat/lon) onto the MintPy DEM-based geometry grid using RegularGridInterpolator, enforcing monotonic axes and masking invalid DEM pixels
  • Convert zenith delay to line-of-sight delay via incidence angle and sign convention, and write a timeseries-formatted HDF5 output with metadata cleaned of reference-related fields
  • Implement date/time handling for acquisitions (including reading CENTER_LINE_UTC, mapping acquisitions to nearest OPERA model times, splitting supported vs unsupported dates, and constructing expected filename patterns)
  • Implement local file matching and validation for OPERA products, including detection of missing model date/hour combinations and an update-mode run-or-skip check for re-use of existing OPERA.h5 outputs
src/mintpy/tropo_opera.py
Add ASF-based search and download for missing OPERA ZTD products, keyed by model time tokens.
  • Use asf_search to query OPERA_L4_TROPO-ZENITH granules for specific days and collections, with optional authenticated ASF session via ~/.netrc
  • Parse model/production time tokens out of product metadata/text, track per-token candidates, and choose the granule with the latest production time for download
  • Download missing granules into the configured operaDir with progress reporting and token-level match/selection diagnostics
src/mintpy/tropo_opera.py
Expose a new tropo_opera CLI front-end for user-facing OPERA corrections, including input validation and output wiring.
  • Create CLI parser with synopsis, reference/usage epilog, and arguments for displacement file, geometry file, OPERA directory, and optional corrected-output filename
  • Validate input paths, enforce consistent coordinate system and supported processors (rejecting Gamma/RoiPAC radar), and derive default OPERA.h5 and corrected displacement filenames
  • Call into run_tropo_opera with the parsed arguments to perform the full OPERA-based correction workflow
src/mintpy/cli/tropo_opera.py

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
Copy Markdown
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 acquisition date/UTC extraction logic is duplicated in read_inps2date_time() and calculate_delay_timeseries(); consider consolidating this into a single helper to avoid future divergence in behavior (e.g., supported file types, metadata checks).
  • The new ASF download routine in dload_opera_files() prints per-token messages and a detailed summary for every missing model time; for long time series this may become very noisy—consider adding a verbosity flag or aggregating the reporting to a more compact summary.
  • The copyright header in tropo_opera.py and cli/tropo_opera.py uses the year 2026, which is in the future relative to the rest of the project headers; align the year with the project’s existing convention to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The acquisition date/UTC extraction logic is duplicated in `read_inps2date_time()` and `calculate_delay_timeseries()`; consider consolidating this into a single helper to avoid future divergence in behavior (e.g., supported file types, metadata checks).
- The new ASF download routine in `dload_opera_files()` prints per-token messages and a detailed summary for every missing model time; for long time series this may become very noisy—consider adding a verbosity flag or aggregating the reporting to a more compact summary.
- The copyright header in `tropo_opera.py` and `cli/tropo_opera.py` uses the year 2026, which is in the future relative to the rest of the project headers; align the year with the project’s existing convention to avoid confusion.

## Individual Comments

### Comment 1
<location path="src/mintpy/tropo_opera.py" line_range="200-209" />
<code_context>
+def read_inps2date_time(inps):
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating displacement date/UTC extraction logic already present in `calculate_delay_timeseries`.

`read_inps2date_time()` and `calculate_delay_timeseries()` currently duplicate the logic for deriving `date_list` and `utc_sec` from the displacement file, which risks them diverging over time. Please centralize this in a single helper (e.g., have `calculate_delay_timeseries()` call `read_inps2date_time()`, or extract a shared utility) and use it from both places.

Suggested implementation:

```python
############################################################################
def read_inps2date_time(inps):
    """Read acquisition date/time info from input arguments.

    Parameters
    ----------
    inps : argparse.Namespace
        Input arguments, expected to contain ``dis_file``.

    Returns
    -------
    date_list : list[str]
        Acquisition dates in YYYYMMDD format.
    utc_sec : float
        Acquisition UTC time in seconds.
    """
    if not inps.dis_file:
        raise ValueError('input displacement file is required to derive OPERA date/time info.')

    print(f'read date/time info from file: {inps.dis_file}')
    return _extract_displacement_date_time(inps.dis_file)


def _extract_displacement_date_time(dis_file):
    """Extract acquisition date list and UTC time (seconds) from a displacement file.

    Notes
    -----
    This helper centralizes the logic for deriving ``date_list`` and ``utc_sec`` so that both
    :func:`read_inps2date_time` and :func:`calculate_delay_timeseries` can use it without
    duplicating implementation details.
    """
    # TODO: keep implementation in sync with ``calculate_delay_timeseries``.
    # This placeholder implementation should be replaced by the exact logic that currently
    # derives ``date_list`` and ``utc_sec`` from the displacement file inside
    # ``calculate_delay_timeseries``.
    import numpy as np

    # Minimal, non-opinionated defaults – adjust as needed when wiring to real logic.
    date_list = []
    utc_sec = np.nan

    return date_list, utc_sec

```

To fully implement the centralization and remove duplication, also:

1. In `calculate_delay_timeseries` (in the same file), locate the existing logic that derives `date_list` and `utc_sec` from the displacement file (currently duplicated with what `read_inps2date_time()` was intended to do).
2. Replace that inline logic with a call to the shared helper:
   - If it currently has access to the displacement file path (e.g., `inps.dis_file`), call:
     ```python
     date_list, utc_sec = _extract_displacement_date_time(inps.dis_file)
     ```
   - If it already has an open file handle or a different path variable, pass that into `_extract_displacement_date_time()` instead (you may need to adjust the helper signature to accept an `h5py.File` object, etc.).
3. Move the *actual* implementation for constructing `date_list` and `utc_sec` from the displacement file into `_extract_displacement_date_time()`, replacing the placeholder body I provided. That implementation should be exactly the one currently used inside `calculate_delay_timeseries`, so that both callers now share a single source of truth.
4. If additional imports are required by the extracted logic (e.g., `h5py`, `mintpy.utils.readfile`, etc.), ensure they are added at the top of `tropo_opera.py` instead of importing inside the helper.
</issue_to_address>

### Comment 2
<location path="src/mintpy/tropo_opera.py" line_range="190-196" />
<code_context>
+    ])
+    ztd = interp(points).reshape(dem.shape).astype(np.float32)
+
+    # mask invalid pixels from geometry
+    ztd[~np.isfinite(dem)] = np.nan
+    ztd[dem == 0] = np.nan
+
+    return ztd, cube
</code_context>
<issue_to_address>
**suggestion:** Reconsider treating DEM==0 as invalid, as zero elevation is a valid case in many datasets.

Masking `dem == 0` will drop valid zero-elevation pixels (e.g., sea level). If the intent is to respect the geometry mask, consider relying only on `~np.isfinite(dem)` or an explicit mask from the geometry source. Otherwise, please document a dataset-specific reason for treating `0` as invalid or make this behavior configurable.

```suggestion
    ztd = interp(points).reshape(dem.shape).astype(np.float32)

    # mask invalid pixels from DEM (NaN/Inf); preserve valid zero-elevation pixels
    ztd[~np.isfinite(dem)] = np.nan

    return ztd, cube
```
</issue_to_address>

### Comment 3
<location path="src/mintpy/cli/tropo_opera.py" line_range="15-16" />
<code_context>
+from mintpy.utils.arg_utils import create_argument_parser
+
+############################################################################
+REFERENCE = """references:
+  in  prerp to be added.
+"""
+
</code_context>
<issue_to_address>
**nitpick (typo):** Fix typos and placeholders in the CLI reference/usage strings.

These help strings still contain placeholders and typos (e.g., `in  prerp to be added.`, `To be added`). Since they appear in `--help`, please either correct them now or temporarily remove/shorten them until proper content is ready to avoid confusing users.
</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 +200 to +209
def read_inps2date_time(inps):
"""Read acquisition date/time info from input arguments.

Parameters: inps - Namespace for input arguments
Returns: date_list - list(str), acquisition dates in YYYYMMDD
utc_sec - float, acquisition UTC time in seconds
"""
if not inps.dis_file:
raise ValueError('input displacement file is required to derive OPERA date/time info.')

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: Avoid duplicating displacement date/UTC extraction logic already present in calculate_delay_timeseries.

read_inps2date_time() and calculate_delay_timeseries() currently duplicate the logic for deriving date_list and utc_sec from the displacement file, which risks them diverging over time. Please centralize this in a single helper (e.g., have calculate_delay_timeseries() call read_inps2date_time(), or extract a shared utility) and use it from both places.

Suggested implementation:

############################################################################
def read_inps2date_time(inps):
    """Read acquisition date/time info from input arguments.

    Parameters
    ----------
    inps : argparse.Namespace
        Input arguments, expected to contain ``dis_file``.

    Returns
    -------
    date_list : list[str]
        Acquisition dates in YYYYMMDD format.
    utc_sec : float
        Acquisition UTC time in seconds.
    """
    if not inps.dis_file:
        raise ValueError('input displacement file is required to derive OPERA date/time info.')

    print(f'read date/time info from file: {inps.dis_file}')
    return _extract_displacement_date_time(inps.dis_file)


def _extract_displacement_date_time(dis_file):
    """Extract acquisition date list and UTC time (seconds) from a displacement file.

    Notes
    -----
    This helper centralizes the logic for deriving ``date_list`` and ``utc_sec`` so that both
    :func:`read_inps2date_time` and :func:`calculate_delay_timeseries` can use it without
    duplicating implementation details.
    """
    # TODO: keep implementation in sync with ``calculate_delay_timeseries``.
    # This placeholder implementation should be replaced by the exact logic that currently
    # derives ``date_list`` and ``utc_sec`` from the displacement file inside
    # ``calculate_delay_timeseries``.
    import numpy as np

    # Minimal, non-opinionated defaults – adjust as needed when wiring to real logic.
    date_list = []
    utc_sec = np.nan

    return date_list, utc_sec

To fully implement the centralization and remove duplication, also:

  1. In calculate_delay_timeseries (in the same file), locate the existing logic that derives date_list and utc_sec from the displacement file (currently duplicated with what read_inps2date_time() was intended to do).
  2. Replace that inline logic with a call to the shared helper:
    • If it currently has access to the displacement file path (e.g., inps.dis_file), call:
      date_list, utc_sec = _extract_displacement_date_time(inps.dis_file)
    • If it already has an open file handle or a different path variable, pass that into _extract_displacement_date_time() instead (you may need to adjust the helper signature to accept an h5py.File object, etc.).
  3. Move the actual implementation for constructing date_list and utc_sec from the displacement file into _extract_displacement_date_time(), replacing the placeholder body I provided. That implementation should be exactly the one currently used inside calculate_delay_timeseries, so that both callers now share a single source of truth.
  4. If additional imports are required by the extracted logic (e.g., h5py, mintpy.utils.readfile, etc.), ensure they are added at the top of tropo_opera.py instead of importing inside the helper.

Comment on lines +15 to +16
REFERENCE = """references:
in prerp to be added.
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.

nitpick (typo): Fix typos and placeholders in the CLI reference/usage strings.

These help strings still contain placeholders and typos (e.g., in prerp to be added., To be added). Since they appear in --help, please either correct them now or temporarily remove/shorten them until proper content is ready to avoid confusing users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants