Skip to content

Commit f5222bc

Browse files
mirzaeesmirzaees
authored andcommitted
Cli timeseries (isce-framework#279)
* add timeseries step to cli * pre-commit fix --------- Co-authored-by: mirzaees <[email protected]>
1 parent bdb0458 commit f5222bc

File tree

6 files changed

+309
-102
lines changed

6 files changed

+309
-102
lines changed

src/dolphin/_cli_timeseries.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import argparse
2+
from pathlib import Path
3+
from typing import TYPE_CHECKING, Any, Optional
4+
5+
from dolphin.workflows import CallFunc
6+
7+
if TYPE_CHECKING:
8+
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
9+
else:
10+
_SubparserType = Any
11+
12+
13+
def get_parser(subparser=None, subcommand_name="timeseries") -> argparse.ArgumentParser:
14+
"""Set up the command line interface."""
15+
metadata = {
16+
"description": "Create a configuration file for a displacement workflow.",
17+
"formatter_class": argparse.ArgumentDefaultsHelpFormatter,
18+
# https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars
19+
"fromfile_prefix_chars": "@",
20+
}
21+
if subparser:
22+
# Used by the subparser to make a nested command line interface
23+
parser = subparser.add_parser(subcommand_name, **metadata)
24+
else:
25+
parser = argparse.ArgumentParser(**metadata) # type: ignore[arg-type]
26+
27+
# parser._action_groups.pop()
28+
parser.add_argument(
29+
"-o",
30+
"--output-dir",
31+
default=Path(),
32+
help="Path to output directory to store results",
33+
)
34+
parser.add_argument(
35+
"--unwrapped-paths",
36+
nargs=argparse.ZERO_OR_MORE,
37+
help=(
38+
"List the paths of all unwrapped interferograms. Can pass a "
39+
"newline delimited file with @ifg_filelist.txt"
40+
),
41+
)
42+
parser.add_argument(
43+
"--conncomp-paths",
44+
nargs=argparse.ZERO_OR_MORE,
45+
help=(
46+
"List the paths of all connected component files. Can pass a "
47+
"newline delimited file with @conncomp_filelist.txt"
48+
),
49+
)
50+
parser.add_argument(
51+
"--corr-paths",
52+
nargs=argparse.ZERO_OR_MORE,
53+
help=(
54+
"List the paths of all correlation files. Can pass a newline delimited"
55+
" file with @cor_filelist.txt"
56+
),
57+
)
58+
parser.add_argument(
59+
"--condition-file",
60+
help=(
61+
"A file with the same size as each raster, like amplitude dispersion or"
62+
"temporal coherence to find reference point. default: amplitude dispersion"
63+
),
64+
)
65+
parser.add_argument(
66+
"--condition",
67+
type=CallFunc,
68+
default=CallFunc.MIN,
69+
help="A condition to apply to condition file to find the reference point"
70+
"Options are [min, max]. default=min",
71+
)
72+
parser.add_argument(
73+
"--num-threads",
74+
type=int,
75+
default=5,
76+
help="Number of threads for the inversion",
77+
)
78+
parser.add_argument(
79+
"--run-velocity",
80+
action="store_true",
81+
help="Run the velocity estimation from the phase time series",
82+
)
83+
parser.add_argument(
84+
"--reference-point",
85+
type=Optional[tuple[int, int]],
86+
default=None,
87+
help="Reference point (row, col) used if performing a time series inversion. "
88+
"If not provided, a point will be selected from a consistent connected "
89+
"component with low amplitude dispersion or high temporal coherence.",
90+
)
91+
parser.add_argument(
92+
"--correlation-threshold",
93+
type=float,
94+
default=0.2,
95+
choices=range(1),
96+
metavar="[0-1]",
97+
help="Pixels with correlation below this value will be masked out.",
98+
)
99+
100+
parser.set_defaults(run_func=_run_timeseries)
101+
102+
return parser
103+
104+
105+
def _run_timeseries(*args, **kwargs):
106+
"""Run `dolphin.timeseries.run`.
107+
108+
Wrapper for the dolphin.timeseries to invert and create velocity.
109+
"""
110+
from dolphin import timeseries
111+
112+
return timeseries.run(*args, **kwargs)
113+
114+
115+
def main(args=None):
116+
"""Get the command line arguments for timeseries inversion."""
117+
from dolphin import timeseries
118+
119+
parser = get_parser()
120+
parsed_args = parser.parse_args(args)
121+
122+
timeseries.run(**vars(parsed_args))
123+
124+
125+
if __name__ == "__main__":
126+
main()

src/dolphin/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import sys
33

4+
import dolphin._cli_timeseries
45
import dolphin._cli_unwrap
56
import dolphin.workflows._cli_config
67
import dolphin.workflows._cli_run
@@ -20,6 +21,7 @@ def main(args=None):
2021
dolphin.workflows._cli_run.get_parser(subparser, "run")
2122
dolphin.workflows._cli_config.get_parser(subparser, "config")
2223
dolphin._cli_unwrap.get_parser(subparser, "unwrap")
24+
dolphin._cli_timeseries.get_parser(subparser, "timeseries")
2325
parsed_args = parser.parse_args(args=args)
2426

2527
arg_dict = vars(parsed_args)

src/dolphin/timeseries.py

Lines changed: 158 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import logging
1+
from __future__ import annotations
2+
3+
import contextlib
24
from pathlib import Path
35
from tempfile import NamedTemporaryFile
4-
from typing import Callable, Protocol, Sequence, TypeVar
6+
from typing import Callable, Optional, Protocol, Sequence, TypeVar
57

68
import jax.numpy as jnp
79
import numpy as np
@@ -10,31 +12,151 @@
1012
from opera_utils import get_dates
1113
from scipy import ndimage
1214

13-
from dolphin import DateOrDatetime, io
15+
from dolphin import DateOrDatetime, io, utils
16+
from dolphin._log import get_log, log_runtime
1417
from dolphin._overviews import ImageType, create_overviews
1518
from dolphin._types import PathOrStr, ReferencePoint
1619
from dolphin.utils import flatten, format_dates
17-
18-
__all__ = [
19-
"invert_stack",
20-
"get_incidence_matrix",
21-
"estimate_velocity",
22-
"create_velocity",
23-
"create_temporal_average",
24-
"invert_unw_network",
25-
"correlation_to_variance",
26-
"select_reference_point",
27-
]
20+
from dolphin.workflows import CallFunc
2821

2922
T = TypeVar("T")
3023

31-
logger = logging.getLogger(__name__)
24+
logger = get_log(__name__)
25+
26+
__all__ = ["run"]
3227

3328

3429
class ReferencePointError(ValueError):
3530
pass
3631

3732

33+
@log_runtime
34+
def run(
35+
unwrapped_paths: Sequence[PathOrStr],
36+
conncomp_paths: Sequence[PathOrStr],
37+
corr_paths: Sequence[PathOrStr],
38+
condition_file: PathOrStr,
39+
condition: CallFunc,
40+
output_dir: PathOrStr,
41+
run_velocity: bool = False,
42+
velocity_file: Optional[PathOrStr] = None,
43+
correlation_threshold: float = 0.2,
44+
num_threads: int = 5,
45+
reference_point: Optional[ReferencePoint] = None,
46+
) -> list[Path]:
47+
"""Invert the unwrapped interferograms, estimate timeseries and phase velocity.
48+
49+
Parameters
50+
----------
51+
unwrapped_paths : Sequence[Path]
52+
Sequence unwrapped interferograms to invert.
53+
corr_paths : Sequence[Path]
54+
Sequence interferometric correlation files, one per file in `unwrapped_paths`
55+
conncomp_paths : Sequence[Path]
56+
Sequence connected component files, one per file in `unwrapped_paths`
57+
condition_file: PathOrStr
58+
A file with the same size as each raster, like amplitude dispersion or
59+
temporal coherence
60+
condition: CallFunc
61+
The function to apply to the condition file,
62+
for example numpy.argmin which finds the pixel with lowest value
63+
the options are [min, max]
64+
output_dir : Path
65+
Path to the output directory.
66+
run_velocity : bool
67+
Whether to run velocity estimation on the inverted phase series
68+
velocity_file : Path, Optional
69+
The output velocity file
70+
correlation_threshold : float
71+
Pixels with correlation below this value will be masked out
72+
num_threads : int
73+
The parallel blocks to process at once.
74+
Default is 5.
75+
reference_point : tuple[int, int], optional
76+
Reference point (row, col) used if performing a time series inversion.
77+
If not provided, a point will be selected from a consistent connected
78+
component with low amplitude dispersion or high temporal coherence.
79+
80+
Returns
81+
-------
82+
inverted_phase_paths : list[Path]
83+
list of Paths to inverted interferograms (single reference phase series).
84+
85+
"""
86+
condition_func = argmax_index if condition == CallFunc.MAX else argmin_index
87+
88+
Path(output_dir).mkdir(exist_ok=True, parents=True)
89+
90+
# First we find the reference point for the unwrapped interferograms
91+
if reference_point is None:
92+
reference = select_reference_point(
93+
conncomp_paths,
94+
condition_file,
95+
output_dir=Path(output_dir),
96+
condition_func=condition_func,
97+
)
98+
else:
99+
reference = reference_point
100+
101+
ifg_date_pairs = [get_dates(f) for f in unwrapped_paths]
102+
sar_dates = sorted(set(utils.flatten(ifg_date_pairs)))
103+
# if we did single-reference interferograms, for `n` sar dates, we will only have
104+
# `n-1` interferograms. Any more than n-1 ifgs means we need to invert
105+
needs_inversion = len(unwrapped_paths) > len(sar_dates) - 1
106+
# check if we even need to invert, or if it was single reference
107+
inverted_phase_paths: list[Path] = []
108+
if needs_inversion:
109+
logger.info("Selecting a reference point for unwrapped interferograms")
110+
111+
logger.info("Inverting network of %s unwrapped ifgs", len(unwrapped_paths))
112+
inverted_phase_paths = invert_unw_network(
113+
unw_file_list=unwrapped_paths,
114+
reference=reference,
115+
output_dir=output_dir,
116+
num_threads=num_threads,
117+
)
118+
else:
119+
logger.info(
120+
"Skipping inversion step: only single reference interferograms exist."
121+
)
122+
# Symlink the unwrapped paths to `timeseries/`
123+
for p in unwrapped_paths:
124+
target = Path(output_dir) / Path(p).name
125+
with contextlib.suppress(FileExistsError):
126+
target.symlink_to(p)
127+
inverted_phase_paths.append(target)
128+
# Make extra "0" raster so that the number of rasters matches len(sar_dates)
129+
ref_raster = Path(output_dir) / (
130+
utils.format_dates(sar_dates[0], sar_dates[0]) + ".tif"
131+
)
132+
io.write_arr(
133+
arr=None, output_name=ref_raster, like_filename=inverted_phase_paths[0]
134+
)
135+
inverted_phase_paths.append(ref_raster)
136+
137+
if run_velocity:
138+
# We can't pass the correlations after an inversion- the numbers don't match
139+
# TODO:
140+
# Is there a better weighting then?
141+
cor_file_list = (
142+
corr_paths if len(corr_paths) == len(inverted_phase_paths) else None
143+
)
144+
logger.info("Estimating phase velocity")
145+
if velocity_file is None:
146+
velocity_file = Path(output_dir) / "velocity.tif"
147+
create_velocity(
148+
unw_file_list=inverted_phase_paths,
149+
output_file=velocity_file,
150+
reference=reference,
151+
date_list=sar_dates,
152+
cor_file_list=cor_file_list,
153+
cor_threshold=correlation_threshold,
154+
num_threads=num_threads,
155+
)
156+
157+
return inverted_phase_paths
158+
159+
38160
def argmin_index(arr: ArrayLike) -> tuple[int, ...]:
39161
"""Get the index tuple of the minimum value of the array.
40162
@@ -55,6 +177,26 @@ def argmin_index(arr: ArrayLike) -> tuple[int, ...]:
55177
return np.unravel_index(np.argmin(arr), np.shape(arr))
56178

57179

180+
def argmax_index(arr: ArrayLike) -> tuple[int, ...]:
181+
"""Get the index tuple of the maximum value of the array.
182+
183+
If multiple occurrences of the maximum value exist, returns
184+
the index of the first such occurrence in the flattened array.
185+
186+
Parameters
187+
----------
188+
arr : array_like
189+
The input array.
190+
191+
Returns
192+
-------
193+
tuple of int
194+
The index of the maximum value.
195+
196+
"""
197+
return np.unravel_index(np.argmax(arr), np.shape(arr))
198+
199+
58200
@jit
59201
def weighted_lstsq_single(
60202
A: ArrayLike,
@@ -509,7 +651,7 @@ def invert_unw_network(
509651
The number of looks used to form the input correlation data, used
510652
to convert correlation to phase variance.
511653
Default is 1.
512-
num_threads : int, optional
654+
num_threads : int
513655
The parallel blocks to process at once.
514656
Default is 5.
515657
add_overviews : bool, optional

src/dolphin/workflows/config/_enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__all__ = [
44
"ShpMethod",
55
"UnwrapMethod",
6+
"CallFunc",
67
]
78

89

@@ -23,3 +24,10 @@ class UnwrapMethod(str, Enum):
2324
SNAPHU = "snaphu"
2425
ICU = "icu"
2526
PHASS = "phass"
27+
28+
29+
class CallFunc(str, Enum):
30+
"""Call function for the timeseries method to find reference point."""
31+
32+
MIN = "min"
33+
MAX = "max"

0 commit comments

Comments
 (0)