Skip to content

unwrap

UnwrapMethod

Bases: str, Enum

Phase unwrapping method.

Source code in src/dolphin/workflows/config/_enums.py
19
20
21
22
23
24
25
26
class UnwrapMethod(str, Enum):
    """Phase unwrapping method."""

    SNAPHU = "snaphu"
    ICU = "icu"
    PHASS = "phass"
    SPURT = "spurt"
    WHIRLWIND = "whirlwind"

full_suffix(filename)

Get the full suffix of a filename, including multiple dots.

Parameters:

Name Type Description Default
filename str or Path

path to file

required

Returns:

Type Description
str

The full suffix, including multiple dots.

Examples:

>>> full_suffix('test.tif')
'.tif'
>>> full_suffix('test.tar.gz')
'.tar.gz'
Source code in src/dolphin/utils.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def full_suffix(filename: Filename):
    """Get the full suffix of a filename, including multiple dots.

    Parameters
    ----------
    filename : str or Path
        path to file

    Returns
    -------
    str
        The full suffix, including multiple dots.

    Examples
    --------
        >>> full_suffix('test.tif')
        '.tif'
        >>> full_suffix('test.tar.gz')
        '.tar.gz'

    """
    fpath = Path(filename)
    return "".join(fpath.suffixes)

get_2pi_ambiguities(unw, round_decimals=4)

Find the number of 2pi offsets from [-pi, pi) at each pixel of unw.

Source code in src/dolphin/unwrap/_post_process.py
32
33
34
35
36
37
38
def get_2pi_ambiguities(
    unw: NDArray[np.floating], round_decimals: int = 4
) -> NDArray[np.floating]:
    """Find the number of 2pi offsets from [-pi, pi) at each pixel of `unw`."""
    mod_2pi_image = np.mod(np.pi + unw, TWOPI) - np.pi
    re_wrapped = np.round(mod_2pi_image, round_decimals)
    return np.round((unw - re_wrapped) / (TWOPI), round_decimals - 1)

grow_conncomp_snaphu(unw_filename, corr_filename, nlooks, mask_filename=None, ccl_nodata=DEFAULT_CCL_NODATA, cost='smooth', min_conncomp_frac=0.0001, scratchdir=None)

Compute connected component labels using SNAPHU.

Parameters:

Name Type Description Default
unw_filename Filename

Path to output unwrapped phase file.

required
corr_filename Filename

Path to input correlation file.

required
nlooks float

Effective number of looks used to form the input correlation data.

required
mask_filename Filename

Path to binary byte mask file, by default None. Assumes that 1s are valid pixels and 0s are invalid.

None
ccl_nodata float

Nodata value for the connected component labels.

DEFAULT_CCL_NODATA
cost str

Statistical cost mode. Default = "smooth"

'smooth'
min_conncomp_frac float

Minimum size of a single connected component, as a fraction of the total number of pixels in the array. Defaults to 0.0001.

0.0001
scratchdir Filename

If provided, uses a scratch directory to save the intermediate files during unwrapping.

None

Returns:

Name Type Description
conncomp_path Filename

Path to output connected component label file.

Source code in src/dolphin/unwrap/_snaphu_py.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def grow_conncomp_snaphu(
    unw_filename: Filename,
    corr_filename: Filename,
    nlooks: float,
    mask_filename: Optional[Filename] = None,
    ccl_nodata: Optional[int] = DEFAULT_CCL_NODATA,
    cost: str = "smooth",
    min_conncomp_frac: float = 0.0001,
    scratchdir: Optional[Filename] = None,
) -> Path:
    """Compute connected component labels using SNAPHU.

    Parameters
    ----------
    unw_filename : Filename
        Path to output unwrapped phase file.
    corr_filename : Filename
        Path to input correlation file.
    nlooks : float
        Effective number of looks used to form the input correlation data.
    mask_filename : Filename, optional
        Path to binary byte mask file, by default None.
        Assumes that 1s are valid pixels and 0s are invalid.
    ccl_nodata : float, optional
        Nodata value for the connected component labels.
    cost : str
        Statistical cost mode.
        Default = "smooth"
    min_conncomp_frac : float, optional
        Minimum size of a single connected component, as a fraction of the total number
        of pixels in the array. Defaults to 0.0001.
    scratchdir : Filename, optional
        If provided, uses a scratch directory to save the intermediate files
        during unwrapping.

    Returns
    -------
    conncomp_path : Filename
        Path to output connected component label file.

    """
    import snaphu

    unw_suffix = full_suffix(unw_filename)
    cc_filename = str(unw_filename).replace(unw_suffix, CONNCOMP_SUFFIX)

    with ExitStack() as stack:
        if mask_filename is not None:
            # Zero masked regions in temporary copies rather than modifying the
            # input files in-place. Masked pixels can have large/garbage phase
            # values (e.g. after timeseries inversion) that inflate the cost
            # network's maximum flow and cause integer overflow.
            zeroed_unw_file, zeroed_corr_file = _zero_from_mask(
                unw_filename, corr_filename, mask_filename
            )
            unw = stack.enter_context(snaphu.io.Raster(zeroed_unw_file))
            corr = stack.enter_context(snaphu.io.Raster(zeroed_corr_file))
            mask = stack.enter_context(snaphu.io.Raster(mask_filename))
        else:
            unw = stack.enter_context(snaphu.io.Raster(unw_filename))
            corr = stack.enter_context(snaphu.io.Raster(corr_filename))
            mask = None

        conncomp = stack.enter_context(
            snaphu.io.Raster.create(
                cc_filename,
                like=unw,
                nodata=ccl_nodata,
                dtype="u2",
                **DEFAULT_TIFF_OPTIONS_RIO,
            )
        )

        snaphu.grow_conncomps(
            unw=unw,
            corr=corr,
            nlooks=nlooks,
            mask=mask,
            cost=cost,
            min_conncomp_frac=min_conncomp_frac,
            scratchdir=scratchdir,
            conncomp=conncomp,
        )

    return Path(cc_filename)

interpolate_masked_gaps(unw, ifg)

Perform phase unwrapping using nearest neighbor interpolation of ambiguities.

Overwrites unw's masked pixels with the interpolated values.

This function takes an input unwrapped phase array containing NaNs at masked pixel. It calculates the phase ambiguity, K, at the attempted unwrapped pixels, then interpolates the ambiguities to fill the gaps. The masked pixels get the value of the original wrapped phase + 2pi*K.

Parameters:

Name Type Description Default
unw NDArray[float]

Input unwrapped phase array with NaN values for masked areas.

required
ifg NDArray[complex64]

Corresponding wrapped interferogram phase

required

Returns:

Type Description
ndarray

Fully unwrapped phase array with interpolated values for previously masked areas.

Source code in src/dolphin/unwrap/_post_process.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def interpolate_masked_gaps(
    unw: NDArray[np.float64], ifg: NDArray[np.complex64]
) -> None:
    """Perform phase unwrapping using nearest neighbor interpolation of ambiguities.

    Overwrites `unw`'s masked pixels with the interpolated values.

    This function takes an input unwrapped phase array containing NaNs at masked pixel.
    It calculates the phase ambiguity, K, at the attempted unwrapped pixels, then
    interpolates the ambiguities to fill the gaps.
    The masked pixels get the value of the original wrapped phase + 2pi*K.

    Parameters
    ----------
    unw : NDArray[np.float]
        Input unwrapped phase array with NaN values for masked areas.
    ifg : NDArray[np.complex64]
        Corresponding wrapped interferogram phase

    Returns
    -------
    np.ndarray
        Fully unwrapped phase array with interpolated values for previously
        masked areas.

    """
    # Create masks for valid areas
    ifg_valid = ~np.isnan(ifg) & (ifg != 0)
    unw_valid = ~np.isnan(unw)

    # Identify areas to interpolate: where ifg is valid but unw is not
    interpolate_mask = ifg_valid & ~unw_valid

    # If there's nothing to interpolate, we're done
    if not np.any(interpolate_mask):
        return

    # Calculate ambiguities for valid unwrapped pixels
    valid_pixels = ifg_valid & unw_valid
    # Work with a subsampled version for the nearest interpolator to speed it up
    sub = 4
    ambiguities = get_2pi_ambiguities(unw[::sub, ::sub][valid_pixels[::sub, ::sub]])

    # Get coordinates for valid pixels and pixels to interpolate
    valid_coords = np.array(np.where(valid_pixels[::sub, ::sub])).T
    interp_coords = np.array(np.where(interpolate_mask[::sub, ::sub])).T

    # Create and apply the interpolator
    interpolator = NearestNDInterpolator(valid_coords, ambiguities)
    interpolated_ambiguities = interpolator(interp_coords)

    # Apply interpolated ambiguities to the wrapped phase
    # Fill in the subsampled version, then resize it to full
    interpolated_sub = np.zeros(ifg[::sub, ::sub].shape, dtype=int)
    interpolated_sub[interpolate_mask[::sub, ::sub]] = interpolated_ambiguities

    unw[interpolate_mask] = np.angle(ifg[interpolate_mask]) + (
        TWOPI
        * np.round(_resize(interpolated_sub, ifg.shape)).astype(int)[interpolate_mask]
    )

multiscale_unwrap(ifg_filename, corr_filename, unw_filename, downsample_factor, ntiles, nlooks, mask_file=None, zero_where_masked=False, unwrap_method=UnwrapMethod.SNAPHU, unwrap_callback=None, unw_nodata=DEFAULT_UNW_NODATA, ccl_nodata=DEFAULT_CCL_NODATA, init_method='mst', cost='smooth', scratchdir=None, log_to_file=True)

Unwrap an interferogram using at multiple scales using tophu.

Parameters:

Name Type Description Default
ifg_filename Filename

Path to input interferogram.

required
corr_filename Filename

Path to input correlation file.

required
unw_filename Filename

Path to output unwrapped phase file.

required
downsample_factor tuple[int, int]

Downsample the interferograms by this factor to unwrap faster, then upsample

required
ntiles tuple[int, int]

Number of tiles to split for full image into for high res unwrapping. If ntiles is an int, will use (ntiles, ntiles)

required
nlooks float

Effective number of looks used to form the input correlation data.

required
mask_file Filename

Path to binary byte mask file, by default None. Assumes that 1s are valid pixels and 0s are invalid.

None
zero_where_masked bool

Set wrapped phase/correlation to 0 where mask is 0 before unwrapping. If not mask is provided, this is ignored. By default False.

False
unwrap_method UnwrapMethod or str

Choice of unwrapping algorithm to use. Choices: {"snaphu", "icu", "phass"}

= "snaphu"
unwrap_callback UnwrapCallback

Alternative to unwrap_method: directly provide a callable function usable in tophu. See [tophu.UnwrapCallback] docs for interface.

None
unw_nodata float, optional.

If providing unwrap_callback, provide the nodata value for your unwrapping function.

DEFAULT_UNW_NODATA
ccl_nodata float

Nodata value for the connected component labels.

DEFAULT_CCL_NODATA
init_method str, choices = {"mcf", "mst"}

SNAPHU initialization method, by default "mst"

'mst'
cost str, choices = {"smooth", "defo", "p-norm",}

SNAPHU cost function, by default "smooth"

'smooth'
scratchdir Filename

Path to scratch directory to hold intermediate files. If None, uses tophu's /tmp/... default.

None
log_to_file bool

Redirect isce3's logging output to file, by default True

True

Returns:

Name Type Description
unw_path Path

Path to output unwrapped phase file.

conncomp_path Path

Path to output connected component label file.

Source code in src/dolphin/unwrap/_tophu.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def multiscale_unwrap(
    ifg_filename: Filename,
    corr_filename: Filename,
    unw_filename: Filename,
    downsample_factor: tuple[int, int],
    ntiles: tuple[int, int],
    nlooks: float,
    mask_file: Filename | None = None,
    zero_where_masked: bool = False,
    unwrap_method: UnwrapMethod = UnwrapMethod.SNAPHU,
    unwrap_callback=None,  # type is `tophu.UnwrapCallback`
    unw_nodata: float | None = DEFAULT_UNW_NODATA,
    ccl_nodata: int | None = DEFAULT_CCL_NODATA,
    init_method: str = "mst",
    cost: str = "smooth",
    scratchdir: Filename | None = None,
    log_to_file: bool = True,
) -> tuple[Path, Path]:
    """Unwrap an interferogram using at multiple scales using `tophu`.

    Parameters
    ----------
    ifg_filename : Filename
        Path to input interferogram.
    corr_filename : Filename
        Path to input correlation file.
    unw_filename : Filename
        Path to output unwrapped phase file.
    downsample_factor : tuple[int, int]
        Downsample the interferograms by this factor to unwrap faster, then upsample
    ntiles : tuple[int, int]
        Number of tiles to split for full image into for high res unwrapping.
        If `ntiles` is an int, will use `(ntiles, ntiles)`
    nlooks : float
        Effective number of looks used to form the input correlation data.
    mask_file : Filename, optional
        Path to binary byte mask file, by default None.
        Assumes that 1s are valid pixels and 0s are invalid.
    zero_where_masked : bool, optional
        Set wrapped phase/correlation to 0 where mask is 0 before unwrapping.
        If not mask is provided, this is ignored.
        By default False.
    unwrap_method : UnwrapMethod or str, optional, default = "snaphu"
        Choice of unwrapping algorithm to use.
        Choices: {"snaphu", "icu", "phass"}
    unwrap_callback : tophu.UnwrapCallback
        Alternative to `unwrap_method`: directly provide a callable
        function usable in `tophu`. See [tophu.UnwrapCallback] docs for interface.
    unw_nodata : float, optional.
        If providing `unwrap_callback`, provide the nodata value for your
        unwrapping function.
    ccl_nodata : float, optional
        Nodata value for the connected component labels.
    init_method : str, choices = {"mcf", "mst"}
        SNAPHU initialization method, by default "mst"
    cost : str, choices = {"smooth", "defo", "p-norm",}
        SNAPHU cost function, by default "smooth"
    scratchdir : Filename, optional
        Path to scratch directory to hold intermediate files.
        If None, uses `tophu`'s `/tmp/...` default.
    log_to_file : bool, optional
        Redirect isce3's logging output to file, by default True

    Returns
    -------
    unw_path : Path
        Path to output unwrapped phase file.
    conncomp_path : Path
        Path to output connected component label file.

    """
    import rasterio as rio
    import tophu

    def _get_rasterio_crs_transform(filename: Filename):
        with rio.open(filename) as src:
            return src.crs, src.transform

    def _get_cb_and_nodata(unwrap_method, unwrap_callback, nodata):
        if unwrap_callback is not None:
            # Pass through what the user gave
            return unwrap_callback, nodata
        # Otherwise, set defaults depending on the method
        unwrap_method = UnwrapMethod(unwrap_method)
        if unwrap_method == UnwrapMethod.ICU:
            unwrap_callback = tophu.ICUUnwrap()
            nodata = 0  # TODO: confirm this?
        elif unwrap_method == UnwrapMethod.PHASS:
            unwrap_callback = tophu.PhassUnwrap()
            nodata = -10_000
        elif unwrap_method == UnwrapMethod.SNAPHU:
            unwrap_callback = tophu.SnaphuUnwrap(
                cost=cost,
                init_method=init_method,
            )
            nodata = 0
        else:
            msg = f"Unknown {unwrap_method = }"
            raise ValueError(msg)
        return unwrap_callback, nodata

    # Used to track if we can redirect logs or not
    _user_gave_callback = unwrap_callback is not None
    unwrap_callback, unw_nodata = _get_cb_and_nodata(
        unwrap_method, unwrap_callback, unw_nodata
    )
    # Used to track if we can redirect logs or not

    width, height = io.get_raster_xysize(ifg_filename)
    crs, transform = _get_rasterio_crs_transform(ifg_filename)

    unw_suffix = full_suffix(unw_filename)
    conncomp_filename = str(unw_filename).replace(unw_suffix, CONNCOMP_SUFFIX)

    # SUFFIX=ADD
    # Convert to something rasterio understands
    logger.debug(f"Saving conncomps to {conncomp_filename}")
    conncomp_rb = tophu.RasterBand(
        conncomp_filename,
        height=height,
        width=width,
        dtype=np.uint16,
        driver="GTiff",
        crs=crs,
        transform=transform,
        nodata=ccl_nodata,
        **DEFAULT_TIFF_OPTIONS_RIO,
    )
    unw_rb = tophu.RasterBand(
        unw_filename,
        height=height,
        width=width,
        dtype=np.float32,
        crs=crs,
        transform=transform,
        nodata=unw_nodata,
        **DEFAULT_TIFF_OPTIONS_RIO,
    )

    if zero_where_masked and mask_file is not None:
        logger.info(f"Zeroing phase/corr of pixels masked in {mask_file}")
        zeroed_ifg_file, zeroed_corr_file = _zero_from_mask(
            ifg_filename, corr_filename, mask_file
        )
        igram_rb = tophu.RasterBand(zeroed_ifg_file)
        coherence_rb = tophu.RasterBand(zeroed_corr_file)
    else:
        igram_rb = tophu.RasterBand(ifg_filename)
        coherence_rb = tophu.RasterBand(corr_filename)

    if log_to_file and not _user_gave_callback:
        # Note that if they gave an arbitrary callback, we don't know the logger
        _redirect_unwrapping_log(unw_filename, unwrap_method.value)

    tophu.multiscale_unwrap(
        unw_rb,
        conncomp_rb,
        igram_rb,
        coherence_rb,
        nlooks=nlooks,
        unwrap_func=unwrap_callback,
        downsample_factor=downsample_factor,
        ntiles=ntiles,
        scratchdir=scratchdir,
    )
    if zero_where_masked and mask_file is not None:
        logger.info(f"Zeroing unw/conncomp of pixels masked in {mask_file}")
        return _zero_from_mask(unw_filename, conncomp_filename, mask_file)
    elif unwrap_method == UnwrapMethod.PHASS:
        # Fill in the nan pixels with the nearest ambiguities
        from ._post_process import interpolate_masked_gaps

        with (
            rio.open(unw_filename, mode="r+") as u_src,
            rio.open(igram_rb.filepath) as i_src,
        ):
            unw = u_src.read(1)
            ifg = i_src.read(1)
            # nodata_mask = i_src.read_masks(1) != 0
            interpolate_masked_gaps(unw, ifg)
            u_src.write(unw, 1)

    return Path(unw_filename), Path(conncomp_filename)

rewrap_to_twopi(arr)

Rewrap arr to be between [-pi and pi].

Parameters:

Name Type Description Default
arr ArrayLike

Unwrapped floating point phase

required

Returns:

Type Description
ndarray

Array of phases between -pi and pi

Source code in src/dolphin/unwrap/_post_process.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def rewrap_to_twopi(arr: ArrayLike) -> np.ndarray:
    """Rewrap `arr` to be between [-pi and pi].

    Parameters
    ----------
    arr : ArrayLike
        Unwrapped floating point phase

    Returns
    -------
    np.ndarray
        Array of phases between -pi and pi

    """
    return np.mod(np.pi + arr, TWOPI) - np.pi

run(ifg_filenames, cor_filenames, output_path, *, unwrap_options=DEFAULT_OPTIONS, nlooks=5, temporal_coherence_filename=None, similarity_filename=None, mask_filename=None, unw_nodata=DEFAULT_UNW_NODATA, ccl_nodata=DEFAULT_CCL_NODATA, scratchdir=None, delete_intermediate=True, overwrite=False)

Run snaphu on all interferograms in a directory.

Parameters:

Name Type Description Default
ifg_filenames Sequence[Filename]

Paths to input interferograms.

required
cor_filenames Sequence[Filename]

Paths to input correlation files. Order must match ifg_filenames.

required
output_path Filename

Path to output directory.

required
unwrap_options UnwrapOptions

UnwrapOptions config object with parameters and settings for unwrapping.

DEFAULT_OPTIONS
nlooks int

Effective number of looks used to form the input correlation data.

5
temporal_coherence_filename Filename

Path to temporal coherence file from phase linking.

None
similarity_filename Filename

Path to phase cosine similarity file from phase linking.

None
mask_filename Filename

Path to binary byte mask file, by default None. Assumes that 1s are valid pixels and 0s are invalid.

None
unw_nodata float , optional.

Requested nodata value for the unwrapped phase. Default = 0

DEFAULT_UNW_NODATA
ccl_nodata float

Requested nodata value for connected component labels. Default = max value of UInt16 (65535)

DEFAULT_CCL_NODATA
scratchdir Filename

Path to scratch directory to hold intermediate files. If None, uses the unwrapper's default.

None
delete_intermediate bool

Delete the temporary files made in the scratchdir after completion. If True, will make separate folders inside scratchdir for cleaner removals (in case scratchdir) has other contents. Must specify scratchdir for this option to be used.

= True
overwrite bool

Overwrite existing unwrapped files.

= False

Returns:

Name Type Description
unw_paths list[Path]

list of unwrapped files names

conncomp_paths list[Path]

list of connected-component-label files names

Source code in src/dolphin/unwrap/_unwrap.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def run(
    ifg_filenames: Sequence[PathOrStr],
    cor_filenames: Sequence[PathOrStr],
    output_path: PathOrStr,
    *,
    unwrap_options: UnwrapOptions = DEFAULT_OPTIONS,
    nlooks: float = 5,
    temporal_coherence_filename: PathOrStr | None = None,
    similarity_filename: PathOrStr | None = None,
    mask_filename: PathOrStr | None = None,
    unw_nodata: float | None = DEFAULT_UNW_NODATA,
    ccl_nodata: int | None = DEFAULT_CCL_NODATA,
    scratchdir: PathOrStr | None = None,
    delete_intermediate: bool = True,
    overwrite: bool = False,
) -> tuple[list[Path], list[Path]]:
    """Run snaphu on all interferograms in a directory.

    Parameters
    ----------
    ifg_filenames : Sequence[Filename]
        Paths to input interferograms.
    cor_filenames : Sequence[Filename]
        Paths to input correlation files. Order must match `ifg_filenames`.
    output_path : Filename
        Path to output directory.
    unwrap_options : UnwrapOptions, optional
        [`UnwrapOptions`][dolphin.workflows.config.UnwrapOptions] config object
        with parameters and settings for unwrapping.
    nlooks : int, optional
        Effective number of looks used to form the input correlation data.
    temporal_coherence_filename : Filename, optional
        Path to temporal coherence file from phase linking.
    similarity_filename : Filename, optional
        Path to phase cosine similarity file from phase linking.
    mask_filename : Filename, optional
        Path to binary byte mask file, by default None.
        Assumes that 1s are valid pixels and 0s are invalid.
    unw_nodata : float , optional.
        Requested nodata value for the unwrapped phase.
        Default = 0
    ccl_nodata : float, optional
        Requested nodata value for connected component labels.
        Default = max value of UInt16 (65535)
    scratchdir : Filename, optional
        Path to scratch directory to hold intermediate files.
        If None, uses the unwrapper's default.
    delete_intermediate : bool, default = True
        Delete the temporary files made in the scratchdir after completion.
        If True, will make separate folders inside `scratchdir` for cleaner
        removals (in case `scratchdir`) has other contents.
        Must specify `scratchdir` for this option to be used.
    overwrite : bool, optional, default = False
        Overwrite existing unwrapped files.

    Returns
    -------
    unw_paths : list[Path]
        list of unwrapped files names
    conncomp_paths : list[Path]
        list of connected-component-label files names

    """
    if scratchdir is None:
        delete_intermediate = False

    if len(cor_filenames) != len(ifg_filenames):
        msg = (
            "Number of correlation files does not match number of interferograms."
            f" Found {len(cor_filenames)} correlation files and"
            f" {len(ifg_filenames)} interferograms."
        )
        raise ValueError(msg)

    output_path = Path(output_path)
    if unwrap_options.unwrap_method == UnwrapMethod.SPURT:
        if temporal_coherence_filename is None:
            # TODO: we should make this a mask, instead of requiring this.
            # we'll need to change spurt
            raise ValueError("temporal coherence required for spurt unwrapping")
        unw_paths, conncomp_paths = unwrap_spurt(
            ifg_filenames=ifg_filenames,
            output_path=output_path,
            temporal_coherence_filename=temporal_coherence_filename,
            similarity_filename=similarity_filename,
            # cor_filenames=cor_filenames,
            mask_filename=mask_filename,
            options=unwrap_options.spurt_options,
            scratchdir=scratchdir,
        )
        for f in unw_paths:
            io.set_raster_units(f, "radians")
        return unw_paths, conncomp_paths

    ifg_suffixes = [full_suffix(f) for f in ifg_filenames]
    all_out_files = [
        (output_path / Path(f).name.replace(suf, UNW_SUFFIX))
        for f, suf in zip(ifg_filenames, ifg_suffixes, strict=False)
    ]
    in_files, out_files = [], []
    for inf, outf in zip(ifg_filenames, all_out_files, strict=False):
        if Path(outf).exists() and not overwrite:
            logger.info(f"{outf} exists. Skipping.")
            continue

        in_files.append(inf)
        out_files.append(outf)
    logger.info(f"{len(out_files)} left to unwrap")

    if mask_filename:
        mask_filename = Path(mask_filename).resolve()

    if delete_intermediate:
        assert scratchdir is not None  # can't be none from the previous check
        scratch_dirs: list[Path | None] = [
            Path(scratchdir) / f"scratch-{Path(ifg_file).stem}" for ifg_file in in_files
        ]
    else:
        scratch_dirs = itertools.repeat(scratchdir)  # type: ignore[assignment]
    # This keeps it from spawning a new process for a single job.
    max_jobs = _resolve_n_parallel_jobs(unwrap_options)
    if unwrap_options.unwrap_method == UnwrapMethod.WHIRLWIND:
        _propagate_whirlwind_thread_settings(unwrap_options)

    Executor = ThreadPoolExecutor if max_jobs > 1 else DummyProcessPoolExecutor
    with Executor(max_workers=max_jobs) as exc:
        futures = [
            exc.submit(
                unwrap,
                ifg_filename=ifg_file,
                corr_filename=cor_file,
                unw_filename=out_file,
                nlooks=nlooks,
                mask_filename=mask_filename,
                similarity_filename=similarity_filename,
                unwrap_options=unwrap_options,
                unw_nodata=unw_nodata,
                ccl_nodata=ccl_nodata,
                scratchdir=cur_scratch,
                delete_scratch=delete_intermediate,
            )
            for ifg_file, out_file, cor_file, cur_scratch in zip(
                in_files, out_files, cor_filenames, scratch_dirs, strict=False
            )
        ]
        for fut in tqdm(as_completed(futures)):
            # We're not passing all the unw files in, so we need to tally up below
            _unw_path, _cc_path = fut.result()

    if unwrap_options.zero_where_masked and mask_filename is not None:
        all_out_files = [
            Path(str(outf).replace(UNW_SUFFIX, UNW_SUFFIX_ZEROED))
            for outf in all_out_files
        ]
        conncomp_files = [
            Path(str(outf).replace(UNW_SUFFIX_ZEROED, CONNCOMP_SUFFIX_ZEROED))
            for outf in all_out_files
        ]
    else:
        conncomp_files = [
            Path(str(outf).replace(UNW_SUFFIX, CONNCOMP_SUFFIX))
            for outf in all_out_files
        ]
    for f in all_out_files:
        io.set_raster_units(f, "radians")
    return all_out_files, conncomp_files

unwrap(ifg_filename, corr_filename, unw_filename, nlooks, mask_filename=None, similarity_filename=None, unwrap_options=DEFAULT_OPTIONS, log_to_file=True, unw_nodata=DEFAULT_UNW_NODATA, ccl_nodata=DEFAULT_CCL_NODATA, scratchdir=None, delete_scratch=False)

Unwrap a single interferogram.

Parameters:

Name Type Description Default
ifg_filename Filename

Path to input interferogram.

required
corr_filename Filename

Path to input correlation file.

required
unw_filename Filename

Path to output unwrapped phase file.

required
unwrap_options UnwrapOptions

UnwrapOptions config object with parameters and settings for unwrapping.

DEFAULT_OPTIONS
nlooks float

Effective number of looks used to form the input correlation data.

required
mask_filename Filename

Path to binary byte mask file, by default None. Assumes that 1s are valid pixels and 0s are invalid.

None
similarity_filename Filename

Path to phase cosine similarity file from phase linking.

None
log_to_file bool

Redirect isce3 logging output to file, by default True

True
unw_nodata float , optional.

Requested nodata value for the unwrapped phase. Default = 0

DEFAULT_UNW_NODATA
ccl_nodata float

Requested nodata value for connected component labels. Default = max value of UInt16 (65535)

DEFAULT_CCL_NODATA
scratchdir Filename

Path to scratch directory to hold intermediate files. If None, uses tophu's /tmp/... default.

None
delete_scratch bool

After unwrapping, delete the contents inside scratchdir.

= False

Returns:

Name Type Description
unw_path Path

Path to output unwrapped phase file.

conncomp_path Path

Path to output connected component label file.

Source code in src/dolphin/unwrap/_unwrap.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
def unwrap(
    ifg_filename: Filename,
    corr_filename: Filename,
    unw_filename: Filename,
    nlooks: float,
    mask_filename: Optional[Filename] = None,
    similarity_filename: Optional[Filename] = None,
    unwrap_options: UnwrapOptions = DEFAULT_OPTIONS,
    log_to_file: bool = True,
    unw_nodata: float | None = DEFAULT_UNW_NODATA,
    ccl_nodata: int | None = DEFAULT_CCL_NODATA,
    scratchdir: Optional[Filename] = None,
    delete_scratch: bool = False,
) -> tuple[Path, Path]:
    """Unwrap a single interferogram.

    Parameters
    ----------
    ifg_filename : Filename
        Path to input interferogram.
    corr_filename : Filename
        Path to input correlation file.
    unw_filename : Filename
        Path to output unwrapped phase file.
    unwrap_options : UnwrapOptions, optional
        [`UnwrapOptions`][dolphin.workflows.config.UnwrapOptions] config object
        with parameters and settings for unwrapping.
    nlooks : float
        Effective number of looks used to form the input correlation data.
    mask_filename : Filename, optional
        Path to binary byte mask file, by default None.
        Assumes that 1s are valid pixels and 0s are invalid.
    similarity_filename : Filename, optional
        Path to phase cosine similarity file from phase linking.
    log_to_file : bool, optional
        Redirect isce3 logging output to file, by default True
    unw_nodata : float , optional.
        Requested nodata value for the unwrapped phase.
        Default = 0
    ccl_nodata : float, optional
        Requested nodata value for connected component labels.
        Default = max value of UInt16 (65535)
    scratchdir : Filename, optional
        Path to scratch directory to hold intermediate files.
        If None, uses `tophu`'s `/tmp/...` default.
    delete_scratch : bool, default = False
        After unwrapping, delete the contents inside `scratchdir`.

    Returns
    -------
    unw_path : Path
        Path to output unwrapped phase file.
    conncomp_path : Path
        Path to output connected component label file.

    """
    unwrap_method = unwrap_options.unwrap_method
    preproc_options = unwrap_options.preprocess_options
    if scratchdir is None:
        # Let the unwrappers handle the scratch if we don't specify.
        delete_scratch = False
    else:
        Path(scratchdir).mkdir(parents=True, exist_ok=True)

    # Check for a nodata mask
    if io.get_raster_nodata(ifg_filename) is None or mask_filename is None:
        # With no marked `nodata`, just use the passed in mask
        combined_mask_file = mask_filename
    else:
        combined_mask_file = Path(ifg_filename).with_suffix(".mask.tif")
        create_combined_mask(
            mask_filename=mask_filename,
            image_filename=ifg_filename,
            output_filename=combined_mask_file,
        )

    unwrapper_ifg_filename = Path(ifg_filename)
    unwrapper_unw_filename = Path(unw_filename)
    unwrapper_corr_filename = Path(corr_filename)
    name_change = "."

    ifg = io.load_gdal(ifg_filename, masked=True)
    if unwrap_options.run_goldstein:
        suf = Path(unw_filename).suffix
        if suf == ".tif":
            driver = "GTiff"
            opts = list(io.DEFAULT_TIFF_OPTIONS)
        else:
            driver = "ENVI"
            opts = list(io.DEFAULT_ENVI_OPTIONS)

        name_change = ".filt" + name_change
        # If we're running Goldstein filtering, the intermediate
        # filtered/unwrapped rasters are temporary rasters in the scratch dir.
        filt_ifg_filename = Path(scratchdir or ".") / (
            Path(ifg_filename).stem.split(".")[0] + (name_change + "int" + suf)
        )
        filt_unw_filename = Path(
            str(unw_filename).split(".")[0] + (name_change + "unw" + suf)
        )

        logger.info(f"Goldstein filtering {ifg_filename} -> {filt_ifg_filename}")
        modified_ifg = goldstein(ifg.filled(0), alpha=preproc_options.alpha)
        logger.info(f"Writing filtered output to {filt_ifg_filename}")
        io.write_arr(
            arr=modified_ifg,
            output_name=filt_ifg_filename,
            like_filename=ifg_filename,
            driver=driver,
            options=opts,
        )
        unwrapper_ifg_filename = filt_ifg_filename
        unwrapper_unw_filename = filt_unw_filename

    if unwrap_options.run_interpolation:
        suf = Path(ifg_filename).suffix
        if suf == ".tif":
            driver = "GTiff"
            opts = list(io.DEFAULT_TIFF_OPTIONS)
        else:
            driver = "ENVI"
            opts = list(io.DEFAULT_ENVI_OPTIONS)

        pre_interp_ifg_filename = unwrapper_ifg_filename
        pre_interp_unw_filename = unwrapper_unw_filename
        name_change = ".interp" + name_change

        # temporarily storing the intermediate interpolated rasters in the scratch dir.
        interp_ifg_filename = Path(scratchdir or ".") / (
            pre_interp_ifg_filename.stem.split(".")[0] + (name_change + "int" + suf)
        )
        interp_unw_filename = Path(
            str(pre_interp_unw_filename).split(".")[0] + (name_change + "unw" + suf)
        )

        pre_interp_ifg = io.load_gdal(pre_interp_ifg_filename, masked=True).filled(0)

        corr = io.load_gdal(corr_filename, masked=True).filled(0)
        cutoff = preproc_options.interpolation_cor_threshold
        logger.info(f"Masking pixels with correlation below {cutoff}")
        coherent_pixel_mask = corr >= cutoff
        if similarity_filename and (
            sim_cutoff := preproc_options.interpolation_similarity_threshold
        ):
            logger.info(f"Masking pixels with similarity below {sim_cutoff}")
            sim = io.load_gdal(similarity_filename, masked=True).filled(0)
            coherent_pixel_mask &= sim >= sim_cutoff

        logger.info(f"Interpolating {pre_interp_ifg_filename} -> {interp_ifg_filename}")
        modified_ifg = interpolate(
            ifg=pre_interp_ifg,
            weights=coherent_pixel_mask,
            weight_cutoff=cutoff,
            max_radius=preproc_options.max_radius,
        )
        if preproc_options.zero_correlation_where_interpolating:
            # Set the pixels we masked to have 0 correlation so SNAHPU ignores them
            masked_corr_filename = Path(scratchdir or ".") / (
                Path(corr_filename).stem.split(".")[0] + ".masked.cor.tif"
            )
            io.write_arr(
                arr=np.where(coherent_pixel_mask, corr, 0),
                output_name=masked_corr_filename,
            )
            unwrapper_corr_filename = masked_corr_filename

        logger.info(f"Writing interpolated output to {interp_ifg_filename}")
        io.write_arr(
            arr=modified_ifg,
            output_name=interp_ifg_filename,
            like_filename=ifg_filename,
            driver=driver,
            options=opts,
        )
        unwrapper_ifg_filename = interp_ifg_filename
        unwrapper_unw_filename = interp_unw_filename

    if unwrap_method == UnwrapMethod.SNAPHU:
        snaphu_opts = unwrap_options.snaphu_options
        # Pass everything to snaphu-py
        unw_path, conncomp_path = unwrap_snaphu_py(
            unwrapper_ifg_filename,
            unwrapper_corr_filename,
            unwrapper_unw_filename,
            nlooks,
            ntiles=snaphu_opts.ntiles,
            tile_overlap=snaphu_opts.tile_overlap,
            mask_file=combined_mask_file,
            nproc=snaphu_opts.n_parallel_tiles,
            zero_where_masked=unwrap_options.zero_where_masked,
            unw_nodata=unw_nodata,
            ccl_nodata=ccl_nodata,
            init_method=snaphu_opts.init_method,
            cost=snaphu_opts.cost,
            single_tile_reoptimize=snaphu_opts.single_tile_reoptimize,
            scratchdir=scratchdir,
        )
    elif unwrap_method == UnwrapMethod.WHIRLWIND:
        ww_opts = unwrap_options.whirlwind_options
        unw_path, conncomp_path = unwrap_whirlwind(
            unwrapper_ifg_filename,
            corr_filename,
            unwrapper_unw_filename,
            nlooks,
            mask_file=combined_mask_file,
            zero_where_masked=unwrap_options.zero_where_masked,
            unw_nodata=unw_nodata,
            ccl_nodata=ccl_nodata,
            interpolate=ww_opts.interpolate,
            interp_cutoff=ww_opts.interp_cutoff,
            interp_num_neighbors=ww_opts.interp_num_neighbors,
            interp_max_radius=ww_opts.interp_max_radius,
            interp_min_radius=ww_opts.interp_min_radius,
            interp_alpha=ww_opts.interp_alpha,
            cost_threshold=ww_opts.cost_threshold,
            conncomp_sigma=ww_opts.conncomp_sigma,
            conncomp_cycle_prob=ww_opts.conncomp_cycle_prob,
            min_size_px=ww_opts.min_size_px,
            max_ncomps=ww_opts.max_ncomps,
        )
    elif (unwrap_method == UnwrapMethod.ICU) or (unwrap_method == UnwrapMethod.PHASS):
        tophu_opts = unwrap_options.tophu_options
        unw_path, conncomp_path = multiscale_unwrap(
            unwrapper_ifg_filename,
            corr_filename,
            unwrapper_unw_filename,
            tophu_opts.downsample_factor,
            ntiles=tophu_opts.ntiles,
            nlooks=nlooks,
            mask_file=combined_mask_file,
            zero_where_masked=unwrap_options.zero_where_masked,
            unw_nodata=unw_nodata,
            ccl_nodata=ccl_nodata,
            init_method=tophu_opts.init_method,
            cost=tophu_opts.cost,
            unwrap_method=unwrap_method,
            scratchdir=scratchdir,
            log_to_file=log_to_file,
        )
    else:
        # Should be unreachable.
        raise AssertionError(f"unexpected unwrap method {unwrap_method}")

    # post-processing steps go here:

    # Transfer ambiguity numbers from filtered/interpolated unwrapped interferogram
    # back to original interferogram
    if unwrap_options.run_goldstein or unwrap_options.run_interpolation:
        logger.info(
            "Transferring ambiguity numbers from filtered/interpolated"
            f" ifg {unwrapper_unw_filename}"
        )
        unw_arr = io.load_gdal(unwrapper_unw_filename, masked=True).filled(unw_nodata)

        final_arr = transfer_ambiguities(np.angle(ifg), unw_arr)
        final_arr[ifg.mask] = unw_nodata

        io.write_arr(
            arr=final_arr,
            output_name=unw_filename,
            like_filename=unwrapper_unw_filename,
            dtype=np.float32,
            driver=driver,
            options=opts,
        )

        # Regrow connected components after phase modification
        # TODO decide whether we want to have the
        # 'min_conncomp_frac' option in the config
        conncomp_path = grow_conncomp_snaphu(
            unw_filename=unw_filename,
            corr_filename=corr_filename,
            nlooks=nlooks,
            mask_filename=combined_mask_file,
            ccl_nodata=ccl_nodata,
            cost=unwrap_options.snaphu_options.cost,
            scratchdir=scratchdir,
        )

        # Move the intermediate ".interp" or ".goldstein" into the scratch directory
        if scratchdir is not None:
            shutil.move(unwrapper_unw_filename, scratchdir)

    # Reset the input nodata values to be nodata in the unwrapped and CCL
    logger.info(f"Setting nodata values of {unw_path} file")
    set_nodata_values(
        filename=unw_filename, output_nodata=unw_nodata, like_filename=ifg_filename
    )
    logger.info(f"Setting nodata values of {conncomp_path} file")
    set_nodata_values(
        filename=conncomp_path, output_nodata=ccl_nodata, like_filename=ifg_filename
    )

    if delete_scratch:
        assert scratchdir is not None
        shutil.rmtree(scratchdir, ignore_errors=True)

    return Path(unw_filename), conncomp_path

unwrap_snaphu_py(ifg_filename, corr_filename, unw_filename, nlooks, ntiles=(1, 1), tile_overlap=(0, 0), nproc=1, mask_file=None, zero_where_masked=False, unw_nodata=DEFAULT_UNW_NODATA, ccl_nodata=DEFAULT_CCL_NODATA, init_method='mst', single_tile_reoptimize=False, min_conncomp_frac=0.001, cost='smooth', scratchdir=None)

Unwrap an interferogram using SNAPHU.

Parameters:

Name Type Description Default
ifg_filename Filename

Path to input interferogram.

required
corr_filename Filename

Path to input correlation file.

required
unw_filename Filename

Path to output unwrapped phase file.

required
nlooks float

Effective number of looks used to form the input correlation data.

required
ntiles tuple[int, int]

Number of (row, column) tiles to split for full image into. If ntiles is an int, will use (ntiles, ntiles)

(1, 1)
tile_overlap tuple[int, int]

Number of pixels to overlap in the (row, col) direction. Default = (0, 0)

(0, 0)
nproc int

If specifying ntiles, number of processes to spawn to unwrap the tiles in parallel. Default = 1, which unwraps each tile in serial.

1
mask_file Filename

Path to binary byte mask file, by default None. Assumes that 1s are valid pixels and 0s are invalid.

None
zero_where_masked bool

Set wrapped phase/correlation to 0 where mask is 0 before unwrapping. If not mask is provided, this is ignored. By default False.

False
unw_nodata float

If providing unwrap_callback, provide the nodata value for your unwrapping function.

DEFAULT_UNW_NODATA
ccl_nodata float

Nodata value for the connected component labels.

DEFAULT_CCL_NODATA
init_method str, choices = {"mcf", "mst"}

initialization method, by default "mst"

'mst'
single_tile_reoptimize bool

If True, after unwrapping with multiple tiles, an additional post-processing unwrapping step is performed to re-optimize the unwrapped phase using a single tile. This option is disregarded when ntiles is (1, 1). It supersedes the regrow_conncomps option -- if both are enabled, only the single-tile re-optimization step will be performed in order to avoid redundant computation. Defaults to False.

False
min_conncomp_frac float

Minimum size of a single connected component, as a fraction of the total number of pixels in the tile. Defaults to 1e-3

0.001
cost str

Statistical cost mode. Default = "smooth"

'smooth'
scratchdir Filename

If provided, uses a scratch directory to save the intermediate files during unwrapping.

None

Returns:

Name Type Description
unw_path Path

Path to output unwrapped phase file.

conncomp_path Path

Path to output connected component label file.

Source code in src/dolphin/unwrap/_snaphu_py.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def unwrap_snaphu_py(
    ifg_filename: Filename,
    corr_filename: Filename,
    unw_filename: Filename,
    nlooks: float,
    ntiles: tuple[int, int] = (1, 1),
    tile_overlap: tuple[int, int] = (0, 0),
    nproc: int = 1,
    mask_file: Optional[Filename] = None,
    zero_where_masked: bool = False,
    unw_nodata: Optional[float] = DEFAULT_UNW_NODATA,
    ccl_nodata: Optional[int] = DEFAULT_CCL_NODATA,
    init_method: str = "mst",
    single_tile_reoptimize: bool = False,
    min_conncomp_frac: float = 0.001,
    cost: str = "smooth",
    scratchdir: Optional[Filename] = None,
) -> tuple[Path, Path]:
    """Unwrap an interferogram using `SNAPHU`.

    Parameters
    ----------
    ifg_filename : Filename
        Path to input interferogram.
    corr_filename : Filename
        Path to input correlation file.
    unw_filename : Filename
        Path to output unwrapped phase file.
    nlooks : float
        Effective number of looks used to form the input correlation data.
    ntiles : tuple[int, int], optional
        Number of (row, column) tiles to split for full image into.
        If `ntiles` is an int, will use `(ntiles, ntiles)`
    tile_overlap : tuple[int, int], optional
        Number of pixels to overlap in the (row, col) direction.
        Default = (0, 0)
    nproc : int, optional
        If specifying `ntiles`, number of processes to spawn to unwrap the
        tiles in parallel.
        Default = 1, which unwraps each tile in serial.
    mask_file : Filename, optional
        Path to binary byte mask file, by default None.
        Assumes that 1s are valid pixels and 0s are invalid.
    zero_where_masked : bool, optional
        Set wrapped phase/correlation to 0 where mask is 0 before unwrapping.
        If not mask is provided, this is ignored.
        By default False.
    unw_nodata : float, optional
        If providing `unwrap_callback`, provide the nodata value for your
        unwrapping function.
    ccl_nodata : float, optional
        Nodata value for the connected component labels.
    init_method : str, choices = {"mcf", "mst"}
        initialization method, by default "mst"
    single_tile_reoptimize : bool
        If True, after unwrapping with multiple tiles, an additional post-processing
        unwrapping step is performed to re-optimize the unwrapped phase using a single
        tile. This option is disregarded when `ntiles` is (1, 1). It supersedes the
        `regrow_conncomps` option -- if both are enabled, only the single-tile
        re-optimization step will be performed in order to avoid redundant computation.
        Defaults to False.
    min_conncomp_frac : float, optional
        Minimum size of a single connected component, as a fraction of the total number
        of pixels in the tile. Defaults to 1e-3
    cost : str
        Statistical cost mode.
        Default = "smooth"
    scratchdir : Filename, optional
        If provided, uses a scratch directory to save the intermediate files
        during unwrapping.

    Returns
    -------
    unw_path : Path
        Path to output unwrapped phase file.
    conncomp_path : Path
        Path to output connected component label file.

    """
    import snaphu

    unw_suffix = full_suffix(unw_filename)
    cc_filename = str(unw_filename).replace(unw_suffix, CONNCOMP_SUFFIX)
    with ExitStack() as stack:
        if zero_where_masked and (mask_file is not None):
            logger.info(f"Zeroing phase/corr of pixels masked in {mask_file}")
            zeroed_ifg_file, zeroed_corr_file = _zero_from_mask(
                ifg_filename, corr_filename, mask_file
            )
            igram = stack.enter_context(snaphu.io.Raster(zeroed_ifg_file))
            corr = stack.enter_context(snaphu.io.Raster(zeroed_corr_file))
        else:
            igram = stack.enter_context(snaphu.io.Raster(ifg_filename))
            corr = stack.enter_context(snaphu.io.Raster(corr_filename))

        if mask_file is None:
            mask = None
        else:
            mask = stack.enter_context(snaphu.io.Raster(mask_file))

        unw, conncomp = snaphu.unwrap(
            igram,
            corr,
            nlooks=nlooks,
            init=init_method,
            cost=cost,
            mask=mask,
            ntiles=ntiles,
            tile_overlap=tile_overlap,
            nproc=nproc,
            scratchdir=scratchdir,
            single_tile_reoptimize=single_tile_reoptimize,
            min_conncomp_frac=min_conncomp_frac,
            # https://github.com/isce-framework/snaphu-py/commit/a77cbe1ff115d96164985523987b1db3278970ed
            # On frame-sized ifgs, especially with decorrelation, defaults of
            # (500, 100) for (tile_cost_thresh, min_region_size) lead to
            # "Exceeded maximum number of secondary arcs"
            # "Decrease TILECOSTTHRESH and/or increase MINREGIONSIZE"
            tile_cost_thresh=500,
            # ... "and/or increase MINREGIONSIZE"
            min_region_size=300,
        )

        # Save the numpy results
        with snaphu.io.Raster.create(
            unw_filename,
            like=igram,
            nodata=unw_nodata,
            dtype="f4",
            **DEFAULT_TIFF_OPTIONS_RIO,
        ) as unw_raster:
            unw_raster[:, :] = unw
        with snaphu.io.Raster.create(
            cc_filename,
            like=igram,
            nodata=ccl_nodata,
            dtype="u2",
            **DEFAULT_TIFF_OPTIONS_RIO,
        ) as conncomp_raster:
            conncomp_raster[:, :] = conncomp

    if zero_where_masked and mask_file is not None:
        logger.info(f"Zeroing unw/conncomp of pixels masked in {mask_file}")

        return _zero_from_mask(unw_filename, cc_filename, mask_file)

    return Path(unw_filename), Path(cc_filename)

unwrap_whirlwind(ifg_filename, corr_filename, unw_filename, nlooks, mask_file=None, zero_where_masked=False, unw_nodata=DEFAULT_UNW_NODATA, ccl_nodata=DEFAULT_CCL_NODATA, interpolate=False, interp_cutoff=0.5, interp_num_neighbors=20, interp_max_radius=51, interp_min_radius=0, interp_alpha=0.75, cost_threshold=50, conncomp_sigma=None, conncomp_cycle_prob=None, min_size_px=100, max_ncomps=1024)

Unwrap an interferogram and grow conncomps using whirlwind.

Uses whirlwind.unwrap, which emits both the unwrapped phase and SNAPHU-style connected component labels from a single MCF solve.

Parameters:

Name Type Description Default
ifg_filename Filename

Path to input interferogram.

required
corr_filename Filename

Path to input correlation file.

required
unw_filename Filename

Path to output unwrapped phase file.

required
nlooks float

Effective number of looks used to form the input correlation data.

required
mask_file Filename

Path to binary byte mask file. Assumes 1 = valid, 0 = invalid.

None
zero_where_masked bool

Set wrapped phase/correlation to 0 where mask is 0 before unwrapping. Ignored if no mask is provided. Default False.

False
unw_nodata float

Nodata value for the output unwrapped phase raster.

DEFAULT_UNW_NODATA
ccl_nodata int

Nodata value for the connected component labels.

DEFAULT_CCL_NODATA
interpolate bool

Enable whirlwind's spiral PS interpolation pre-pass (fill valid pixels with coherence below interp_cutoff from nearby high-coherence phasors before unwrapping). Default False.

False
interp_cutoff float

Spiral interpolation parameters; see whirlwind.unwrap. Only used when interpolate is True.

0.5
interp_num_neighbors float

Spiral interpolation parameters; see whirlwind.unwrap. Only used when interpolate is True.

0.5
interp_max_radius float

Spiral interpolation parameters; see whirlwind.unwrap. Only used when interpolate is True.

0.5
interp_min_radius float

Spiral interpolation parameters; see whirlwind.unwrap. Only used when interpolate is True.

0.5
interp_alpha float

Spiral interpolation parameters; see whirlwind.unwrap. Only used when interpolate is True.

0.5
cost_threshold int

Connected-component boundary threshold in raw cost units. Default 50.

50
conncomp_sigma float

Set cost_threshold from a Gaussian-equivalent noise level or a target per-edge one-cycle probability; see whirlwind.unwrap for precedence. Default None.

None
conncomp_cycle_prob float

Set cost_threshold from a Gaussian-equivalent noise level or a target per-edge one-cycle probability; see whirlwind.unwrap for precedence. Default None.

None
min_size_px int

Discard connected components smaller than this many pixels. Default 100.

100
max_ncomps int

Maximum number of connected components to keep. Default 1024.

1024

Returns:

Name Type Description
unw_path Path

Path to output unwrapped phase file.

conncomp_path Path

Path to output connected component label file.

Source code in src/dolphin/unwrap/_whirlwind.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def unwrap_whirlwind(
    ifg_filename: Filename,
    corr_filename: Filename,
    unw_filename: Filename,
    nlooks: float,
    mask_file: Optional[Filename] = None,
    zero_where_masked: bool = False,
    unw_nodata: Optional[float] = DEFAULT_UNW_NODATA,
    ccl_nodata: Optional[int] = DEFAULT_CCL_NODATA,
    interpolate: bool = False,
    interp_cutoff: float = 0.5,
    interp_num_neighbors: int = 20,
    interp_max_radius: int = 51,
    interp_min_radius: int = 0,
    interp_alpha: float = 0.75,
    cost_threshold: int = 50,
    conncomp_sigma: Optional[float] = None,
    conncomp_cycle_prob: Optional[float] = None,
    min_size_px: int = 100,
    max_ncomps: int = 1024,
) -> tuple[Path, Path]:
    """Unwrap an interferogram and grow conncomps using whirlwind.

    Uses ``whirlwind.unwrap``, which emits both the
    unwrapped phase and SNAPHU-style connected component labels from a
    single MCF solve.

    Parameters
    ----------
    ifg_filename : Filename
        Path to input interferogram.
    corr_filename : Filename
        Path to input correlation file.
    unw_filename : Filename
        Path to output unwrapped phase file.
    nlooks : float
        Effective number of looks used to form the input correlation data.
    mask_file : Filename, optional
        Path to binary byte mask file. Assumes 1 = valid, 0 = invalid.
    zero_where_masked : bool, optional
        Set wrapped phase/correlation to 0 where mask is 0 before unwrapping.
        Ignored if no mask is provided. Default False.
    unw_nodata : float, optional
        Nodata value for the output unwrapped phase raster.
    ccl_nodata : int, optional
        Nodata value for the connected component labels.
    interpolate : bool, optional
        Enable whirlwind's spiral PS interpolation pre-pass (fill valid pixels
        with coherence below ``interp_cutoff`` from nearby high-coherence
        phasors before unwrapping). Default False.
    interp_cutoff, interp_num_neighbors, interp_max_radius, interp_min_radius, \
interp_alpha
        Spiral interpolation parameters; see ``whirlwind.unwrap``. Only used
        when ``interpolate`` is True.
    cost_threshold : int, optional
        Connected-component boundary threshold in raw cost units. Default 50.
    conncomp_sigma, conncomp_cycle_prob : float, optional
        Set ``cost_threshold`` from a Gaussian-equivalent noise level or a
        target per-edge one-cycle probability; see ``whirlwind.unwrap`` for
        precedence. Default None.
    min_size_px : int, optional
        Discard connected components smaller than this many pixels. Default 100.
    max_ncomps : int, optional
        Maximum number of connected components to keep. Default 1024.

    Returns
    -------
    unw_path : Path
        Path to output unwrapped phase file.
    conncomp_path : Path
        Path to output connected component label file.

    """
    import snaphu  # used here only for raster I/O
    import whirlwind as ww

    # Create a context manager that combines other context managers -- one for each
    # input raster file. Upon exiting the context block, each context manager in the
    # stack will be closed in LIFO order.
    with ExitStack() as stack:
        if zero_where_masked and (mask_file is not None):
            logger.info(f"Zeroing phase/corr of pixels masked in {mask_file}")
            zeroed_ifg_file, zeroed_corr_file = _zero_from_mask(
                ifg_filename, corr_filename, mask_file
            )
            igram = stack.enter_context(snaphu.io.Raster(zeroed_ifg_file))
            corr = stack.enter_context(snaphu.io.Raster(zeroed_corr_file))
        else:
            igram = stack.enter_context(snaphu.io.Raster(ifg_filename))
            corr = stack.enter_context(snaphu.io.Raster(corr_filename))

        if mask_file is None:
            mask_arr = None
        else:
            mask = stack.enter_context(snaphu.io.Raster(mask_file))
            mask_arr = np.ascontiguousarray(mask[:, :], dtype=bool)

        logger.info("Unwrapping using whirlwind")
        igram_arr = np.ascontiguousarray(igram[:, :], dtype=np.complex64)
        corr_arr = np.ascontiguousarray(corr[:, :], dtype=np.float32)
        # ww.unwrap returns (phase, conncomp). As of whirlwind 2026-06-03 the
        # default phase solver is the verified single-tile linear MCF (ww-orig
        # parity + adaptive PD/SSP fallback);  Goldstein is off by default
        # (pass goldstein_alpha>0 to enable; under evaluation upstream).
        unw, conncomp_arr = ww.unwrap(
            igram_arr,
            corr_arr,
            float(nlooks),
            mask=mask_arr,
            interpolate=interpolate,
            interp_cutoff=interp_cutoff,
            interp_num_neighbors=interp_num_neighbors,
            interp_max_radius=interp_max_radius,
            interp_min_radius=interp_min_radius,
            interp_alpha=interp_alpha,
            cost_threshold=cost_threshold,
            conncomp_sigma=conncomp_sigma,
            conncomp_cycle_prob=conncomp_cycle_prob,
            min_size_px=min_size_px,
            max_ncomps=max_ncomps,
        )

        logger.info("Writing unwrapped phase to raster file")
        with snaphu.io.Raster.create(
            unw_filename,
            like=igram,
            nodata=unw_nodata,
            dtype=np.float32,
            **DEFAULT_TIFF_OPTIONS_RIO,
        ) as unw_raster:
            unw_raster[:, :] = unw

        unw_suffix = full_suffix(unw_filename)
        cc_filename = str(unw_filename).replace(unw_suffix, CONNCOMP_SUFFIX)

        logger.info("Writing whirlwind connected component labels")
        with snaphu.io.Raster.create(
            cc_filename,
            like=igram,
            nodata=ccl_nodata,
            dtype=np.uint16,
            **DEFAULT_TIFF_OPTIONS_RIO,
        ) as conncomp_raster:
            conncomp_raster[:, :] = conncomp_arr.astype(np.uint16)

    if zero_where_masked and (mask_file is not None):
        logger.info(f"Zeroing unw/conncomp of pixels masked in {mask_file}")
        return _zero_from_mask(unw_filename, cc_filename, mask_file)

    return Path(unw_filename), Path(cc_filename)