Skip to content

GMTSAR S1 backend trial#4

Open
dunyuliu wants to merge 13 commits into
jldz9:mainfrom
dunyuliu:main
Open

GMTSAR S1 backend trial#4
dunyuliu wants to merge 13 commits into
jldz9:mainfrom
dunyuliu:main

Conversation

@dunyuliu

Copy link
Copy Markdown

GMTSAT S1 backend is developed in InSAR Hub workflow style. First trial.

dunyuliu and others added 13 commits July 21, 2026 00:28
…ssing

New processor/gmtsar_s1.py + GMTSAR_S1_Config, implementing the real
LocalProcessor interface (submit/refresh/retry/watch/save), matching
ISCE_S1's conventions (status constants, job JSON, per-pair concurrent
execution via ThreadPoolExecutor instead of ISCE_S1's per-step model,
since p2p_processing is one-pair-per-invocation).

Verified against real sources: p2p_processing's actual CLI contract
(SAT ref sec [config.py]), and MintPy's own prep_gmtsar.py -- confirmed
it consumes GMTSAR's native intf/<ref>_<sec>/ output layout directly,
so no output-normalization step is needed before a Mintpy analyzer can
read a GMTSAR_S1 case.

Real-tested in this repo (not just import-shimmed against the public
GMTSAR_S1 docstring): imports cleanly, registers in the real Processor
registry, constructs correctly, and correctly raises NotImplementedError
for the one genuinely-unimplemented path (bbox-driven DEM auto-fetch --
GMTSAR has no GLO-30-style auto-download equivalent to ISCE_S1 yet)
instead of silently pretending to work.

Known gaps before this is real, tracked for the next pass:
- Whether p2p_processing internally preprocesses raw .SAFE/.zip input
  or expects already-focused SLCs (biggest open unknown, needs tracing
  p2p_stages.py's stage 1 against real S1 data).
- DEM auto-fetch (dem_path=None currently raises, doesn't auto-download).
- No HPC/SLURM support yet (ISCE_S1 has a real hpc_mode path; this is
  local ThreadPoolExecutor only, deliberately out of scope for v0).
- Never run against real Sentinel-1 data end-to-end yet.

Deliberately kept as subprocess-per-pair (not in-process calls into
GMTSAR's own p2p_stages.py): separate conda environments with different
numpy/GDAL stacks make in-process imports risky, most wall-clock is in
C binaries either way so it wouldn't be faster, and this matches both
ISCE_S1's own external-process pattern and GMTSAR's own test harness
design (case_runner.py deliberately isolates each case in its own
subprocess).
…irs signature bug found

Traced p2p_stages.py::P2P1Preprocess directly: it calls pre_proc
internally on raw/ input, resolving the README's 'biggest open unknown'
(_stage_case()'s raw-staging assumption was correct).

Found a real bug while tracing this: GMTSAR_S1's pairs=[(ref,sec),...]
signature copied ISCE_S1's plain-date-tuple convention, but S1_TOPS's
actual p2p_processing master/aligned args are raw per-subswath product
basename stems, not YYYYMMDD dates (confirmed against a real recipe,
tests/recipes/README_S1_Ridgecrest_EQ.txt in the GMTSAR repo). Also
found a second, different entry point (p2p_S1_TOPS_Frame) for
multi-subswath cases with its own distinct signature.

Not fixing the signature yet -- it's a real design decision (which S1
variant to target, or both) that needs a human call, not a unilateral
autonomous rewrite of the public interface. Documented in
docs/gmtsar_s1_notes/OPEN_ISSUES.md for the next session.
Two real bugs found and fixed via actual end-to-end testing against
real cached S1_Ridgecrest_EQ data, not just code review:

1. pairs=[(ref,sec),...] copied ISCE_S1's date-tuple convention, but
   GMTSAR's real S1_TOPS args are raw per-subswath product stems, not
   YYYYMMDD dates -- confirmed against a real recipe. Now supports BOTH
   real GMTSAR entry points via config.frame_mode: single-subswath
   p2p_processing (default) and multi-subswath p2p_S1_TOPS_Frame.
   _build_cmd() output verified identical to the real recipe lines for
   both modes, not just reviewed.

2. Real end-to-end run revealed GMTSAR's own Python stages (e.g.
   dem2topo_ra) shell out to the standalone `gmt` binary, which comes
   from GMTSAR's own conda env, not from GMTSAR's bin/ dir. InSARHub
   runs in a separate conda env without `gmt` on PATH at all --
   confirmed directly (`which gmt`: not found in InSARHub's env).
   Without this fix every GMTSAR subprocess call failed in ~1s with no
   useful error surfaced. Fixed: GMTSAR_S1_Config gained gmtsar_root /
   gmtsar_env_bin; _subprocess_env() builds an explicit PATH for every
   GMTSAR subprocess call, independent of the caller's own environment.

Also hardened _stage_case() against a real broken symlink loop found in
real GMTSAR test data (a stray self-referencing .EOF) -- now skipped
with a warning instead of crashing staging entirely.

A real end-to-end validation run (Frame mode, with the env fix) is in
progress in the background as of this commit -- see
docs/gmtsar_s1_notes/OPEN_ISSUES.md for status; do not trust this as
"done" until that lands.
Real run against S1_Ridgecrest_EQ data completed rc=0 through preprocessing,
per-subswath SLC/topo/intf/filter, merge, unwrap, and geocode, producing
real phasefilt_ll.grd/corr_ll.grd products. proc.watch() reported SUCCEEDED.
Also documents the remaining known gap: single-subswath mode is not yet
fixed for the H_res per-subswath extraction step.
…t config validation

Both frame_mode settings now take the same pairs = [(ref_safe, ref_eof,
sec_safe, sec_eof), ...] shape. Single-subswath mode (frame_mode=False)
previously required callers to hand-derive raw per-subswath product
stems themselves, which was correct per GMTSAR's CLI contract but bad
UX and untested. GMTSAR_S1 now does that extraction itself
(_extract_subswath_stem), mirroring GMTSAR's own H_res/raw/ single-
subswath test fixture layout. Both modes are now real end-to-end
validated against real Sentinel-1 data (see OPEN_ISSUES.md).

Audit fixes: dem_path/gmtsar_root/gmtsar_env_bin are now validated at
construction time instead of failing deep inside a background staging
thread; _status_dir() no longer raises a masking KeyError for pairs
that were never staged (returns a not-yet-existing path, reading as
PENDING); added test coverage for the FAILED (nonzero returncode) path
and frame_mode=True staging via submit(), both previously unguarded.

Also: added CHANGELOG.md entry, GMTSAR_S1_Config docstring gap fix,
GMTSAR_S1 docs tab in docs/advanced/processor.md matching the existing
ISCE_S1/Hyp3_S1 pattern.
…ath mode

Real bug found via MintPy integration testing: p2p_processing's actual
output directory is named by GMTSAR's own Julian-date pair (e.g.
intf/2019184_2019196/, derived from each SLC's SC_clock_start), NOT
intf/<ref_stem>_<sec_stem>/ as the code and docs previously claimed. The
wrong assumption was masked because our own status-marker write mkdir'd
its own directory regardless of whether GMTSAR actually wrote output
there, so SUCCEEDED still reported correctly -- but downstream consumers
(MintPy) pointed at the documented path would find nothing, since
prep_gmtsar.py derives DATE12 by parsing the real Julian-date dirname.

_run_one_pair() now diffs intf/'s contents before/after each real run
and records whichever new directory matches GMTSAR's \d{7}_\d{7} naming;
_status_dir() uses that discovered directory once known, falling back to
a stem-based sentinel (which can never collide with a real directory)
before a run completes. Updated all docstring/doc claims accordingly and
added test coverage for both the pre-run and post-run status_dir paths.

Confirmed against a real single-subswath run with unwrapping enabled
(threshold_snaphu=0.1): real unwrap_ll.grd landed under the correct
Julian-date directory this fix discovers.
Found via real MintPy integration testing: gdal.Open() on GMTSAR's
.grd (netCDF) output returned None in the insarhub_test env (pip-
installed GDAL, missing the netCDF driver plugin) -- prep_gmtsar.py
crashed with AttributeError before reaching any real logic. Pinning
libgdal-netcdf explicitly (rather than assuming gdal >=3.8 always
bundles it) makes this reproducible for anyone rebuilding the env.
Confirmed real fixes for ALOOKS/RLOOKS, HEADING, and the missing GDAL
netCDF driver against real single-subswath output. Documents the real
remaining blocker: read_baseline_table() needs a stack-level
baseline_table.dat across multiple SLC dates, which our one-pair
validation run doesn't produce. Not fabricated -- logged as an open
item pending either a real multi-pair run or an explicit decision to
build a synthetic stand-in.
… reload

Real CLI gaps found by actually running GMTSAR_S1 through the CLI
(refresh against real saved job state), not just reading source:

- _find_jobs_file() hardcoded "isce_jobs*.json" + an ISCE-only
  subdirectory lookup at all 4 call sites (refresh/retry/cancel/watch),
  so it could never find GMTSAR_S1's gmtsar_jobs.json. Added
  JOBS_FILE/JOBS_SUBDIR class attributes to LocalProcessor (set on
  ISCE_Base and GMTSAR_S1) and a _jobs_glob() helper deriving the
  search pattern/subdir generically per processor.
- _proc_local_submit hardcoded 2-tuple pairs ((p[0], p[1])), silently
  truncating GMTSAR_S1's required 4-tuples. Now preserves full arity.
- _load_local_processor's saved-job reload assumed ISCE's
  {"jobs": {...}} wrapper and step-based pairs ((j["step"], j["step"])).
  GMTSAR_S1.save() writes the jobs dict directly at the top level and
  stores the real pair under "pair" -- now handles both shapes.

Confirmed via a real CLI refresh against GMTSAR_S1's actual saved
gmtsar_jobs.json: discovery, pairs-arity, and reload-shape now all work
end-to-end. Surfaced the next real gap in the process: GMTSAR_S1's
refresh()/watch()/cancel() don't share ISCE_Base's method signatures
(ls=, refresh_interval=, cancel() doesn't exist) -- not fixed here,
logged as a follow-up.
…ing reload

Real gaps found by actually running refresh/watch/cancel against
GMTSAR_S1 through the CLI as separate process invocations (the normal
CLI usage pattern -- submit in one call, refresh/watch/cancel later in
another):

- cli/main.py: refresh(ls=...)/watch(refresh_interval=...) assumed every
  local processor shares ISCE_Base's exact method signature; crashed on
  GMTSAR_S1. Added _call_if_supported() -- calls each processor's method
  with only the kwargs its real signature (via inspect.signature)
  accepts. cancel() has no GMTSAR_S1 equivalent at all; now a clean
  error instead of AttributeError.

- gmtsar_s1.py: a freshly CLI-reconstructed GMTSAR_S1 had self.jobs
  permanently empty (refresh() only updates pre-existing entries), so a
  reloaded processor printed nothing regardless of real on-disk status.
  For frame_mode=False this went deeper: self._stems/_real_intf_dirs
  are populated only in-memory during the ORIGINAL submitting process,
  never persisted. Fixed with read-only rediscovery at construction
  time: _rediscover_stem() (matches already-extracted raw/*.tiff
  symlinks against a pair's real .SAFE name) and
  _rediscover_real_intf_dir() (recomputes the Julian-date intf/ dirname
  from real SC_clock_start in the already-present .PRM files, same
  formula _run_one_pair() uses live). frame_mode=True needed none of
  this -- its _status_dir() is purely path-based.

Confirmed via real CLI refresh/watch/cancel runs against the actual
completed single-subswath job, in a fresh process with no submit()
call: refresh and watch both correctly report SUCCEEDED; cancel
correctly refuses instead of crashing.
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