Calculate Harmonic Parameters#
Besides using static harmonic parameters to derive the non-flood likelihood \(P(\sigma^0|nonflood)\), we can also dynamically calculate the parameters form past backscattering information. In this notebook we show how we can extract these coefficients that describe seasonal patterns in Sentinel 1 radar backscatter variability.
import hvplot.xarray # noqa
import numpy as np
import xarray as xr
from dask.distributed import Client
from dask_flood_mapper import flood
from dask_flood_mapper.catalog import (format_datetime_for_xarray_selection,
initialize_catalog, initialize_search)
from dask_flood_mapper.harmonic_params import (
create_harmonic_parameters, process_harmonic_parameters_datacube)
from dask_flood_mapper.processing import prepare_dc, process_sig0_dc
client = Client(processes=False, threads_per_worker=2, n_workers=1, memory_limit="12GB")
client
Client
Client-dc695bb4-6e14-11f0-8985-000d3a969aea
Connection method: Cluster object | Cluster type: distributed.LocalCluster |
Dashboard: http://10.1.0.199:8787/status |
Cluster Info
LocalCluster
060fce8e
Dashboard: http://10.1.0.199:8787/status | Workers: 1 |
Total threads: 2 | Total memory: 11.18 GiB |
Status: running | Using processes: False |
Scheduler Info
Scheduler
Scheduler-bb101933-db7d-4ef7-8d80-c128274b2fa1
Comm: inproc://10.1.0.199/2437/1 | Workers: 1 |
Dashboard: http://10.1.0.199:8787/status | Total threads: 2 |
Started: Just now | Total memory: 11.18 GiB |
Workers
Worker: 0
Comm: inproc://10.1.0.199/2437/4 | Total threads: 2 |
Dashboard: http://10.1.0.199:43707/status | Memory: 11.18 GiB |
Nanny: None | |
Local directory: /tmp/dask-scratch-space/worker-8sf6tatg |
As an example we will select only a small region of interest contained in the Zingst case study.
time_range = "2022-10-11T05:25:26"
minlon, maxlon = 12.999, 13
minlat, maxlat = 53.999, 54
bounding_box = [minlon, minlat, maxlon, maxlat]
We will now load the sigma nought datacube from EODC. In order to fit the harmonic functions to the timeseries, the temporal range is extended to include the three years before the provided time range. This is done by using the dynamic
parameter of initialize_search
.
eodc_catalog = initialize_catalog()
search = initialize_search(eodc_catalog, bounding_box, time_range, dynamic=True)
items_sig0 = search.item_collection()
sig0_dc = prepare_dc(items_sig0, bounding_box, bands="VV")
sig0_dc, orbit_sig0 = process_sig0_dc(sig0_dc, items_sig0, bands="VV")
sig0_dc
---------------------------------------------------------------------------
CPLE_HttpResponseError Traceback (most recent call last)
File rasterio/_base.pyx:310, in rasterio._base.DatasetBase.__init__()
File rasterio/_base.pyx:221, in rasterio._base.open_dataset()
File rasterio/_err.pyx:359, in rasterio._err.exc_wrap_pointer()
CPLE_HttpResponseError: CURL error: Failed to connect to data.eodc.eu port 443 after 159 ms: Couldn't connect to server
During handling of the above exception, another exception occurred:
RasterioIOError Traceback (most recent call last)
Cell In[4], line 5
3 items_sig0 = search.item_collection()
4 sig0_dc = prepare_dc(items_sig0, bounding_box, bands="VV")
----> 5 sig0_dc, orbit_sig0 = process_sig0_dc(sig0_dc, items_sig0, bands="VV")
6 sig0_dc
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/dask_flood_mapper/processing.py:55, in process_sig0_dc(sig0_dc, items_sig0, bands)
48 def process_sig0_dc(
49 sig0_dc: xr.Dataset,
50 items_sig0: ItemCollection,
51 bands: str | list[str],
52 ) -> tuple[xr.Dataset, np.ndarray]:
53 """Process the sig0 datacube."""
54 sig0_dc: xr.Dataset = (
---> 55 post_process_eodc_cube(sig0_dc, items_sig0, bands)
56 .rename_vars({"VV": "sig0"})
57 .assign_coords(orbit=("time", extract_orbit_names(items_sig0)))
58 .dropna(dim="time", how="all")
59 .sortby("time")
60 )
61 orbit_sig0: np.ndarray = order_orbits(sig0_dc)
62 sig0_dc: xr.Dataset = sig0_dc.groupby("time").mean(skipna=True)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/xarray/util/deprecation_helpers.py:115, in _deprecate_positional_args.<locals>._decorator.<locals>.inner(*args, **kwargs)
111 kwargs.update({name: arg for name, arg in zip_args})
113 return func(*args[:-n_extra_args], **kwargs)
--> 115 return func(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/xarray/core/dataset.py:6356, in Dataset.dropna(self, dim, how, thresh, subset)
6354 if dim in array.dims:
6355 dims = [d for d in array.dims if d != dim]
-> 6356 count += np.asarray(array.count(dims))
6357 size += math.prod([self.sizes[d] for d in dims])
6359 if thresh is not None:
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/xarray/core/common.py:165, in AbstractArray.__array__(self, dtype)
164 def __array__(self: Any, dtype: DTypeLike | None = None) -> np.ndarray:
--> 165 return np.asarray(self.values, dtype=dtype)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/xarray/core/variable.py:525, in Variable.values(self)
522 @property
523 def values(self):
524 """The variable's data as a numpy.ndarray"""
--> 525 return _as_array_or_item(self._data)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/xarray/core/variable.py:323, in _as_array_or_item(data)
309 def _as_array_or_item(data):
310 """Return the given values as a numpy array, or as an individual item if
311 it's a 0d datetime64 or timedelta64 array.
312
(...)
321 TODO: remove this (replace with np.asarray) once these issues are fixed
322 """
--> 323 data = np.asarray(data)
324 if data.ndim == 0:
325 if data.dtype.kind == "M":
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/dask/array/core.py:1693, in Array.__array__(self, dtype, **kwargs)
1692 def __array__(self, dtype=None, **kwargs):
-> 1693 x = self.compute()
1694 if dtype and x.dtype != dtype:
1695 x = x.astype(dtype)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/dask/base.py:375, in DaskMethodsMixin.compute(self, **kwargs)
351 def compute(self, **kwargs):
352 """Compute this dask collection
353
354 This turns a lazy Dask collection into its in-memory equivalent.
(...)
373 dask.compute
374 """
--> 375 (result,) = compute(self, traverse=False, **kwargs)
376 return result
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/dask/base.py:661, in compute(traverse, optimize_graph, scheduler, get, *args, **kwargs)
658 postcomputes.append(x.__dask_postcompute__())
660 with shorten_traceback():
--> 661 results = schedule(dsk, keys, **kwargs)
663 return repack([f(r, *a) for r, (f, a) in zip(results, postcomputes)])
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_builder.py:436, in _dask_loader_tyx()
434 with rdr.restore_env(env, load_state):
435 for ti, ti_srcs in enumerate(srcs):
--> 436 _fill_nd_slice(
437 ti_srcs, gbox, cfg, chunk[ti], ydim=ydim, selection=selection
438 )
439 return chunk
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_builder.py:513, in _fill_nd_slice()
510 return dst
512 src, *rest = srcs
--> 513 yx_roi, pix = src.read(cfg, dst_gbox, dst=dst, selection=selection)
514 assert len(yx_roi) == 2
515 assert pix.ndim == dst.ndim
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_rio.py:115, in read()
107 def read(
108 self,
109 cfg: RasterLoadParams,
(...)
113 selection: Optional[ReaderSubsetSelection] = None,
114 ) -> tuple[tuple[slice, slice], np.ndarray]:
--> 115 return rio_read(self._src, cfg, dst_geobox, dst=dst, selection=selection)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_rio.py:526, in rio_read()
520 if cfg.fail_on_error:
521 log.error(
522 "Aborting load due to failure while reading: %s:%d",
523 src.uri,
524 src.band,
525 )
--> 526 raise e
527 except rasterio.errors.RasterioError as e:
528 if cfg.fail_on_error:
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_rio.py:512, in rio_read()
508 return roi, out.transpose([1, 2, 0])
510 try:
511 return fixup_out(
--> 512 _rio_read(src, cfg, dst_geobox, prep_dst(dst), selection=selection)
513 )
514 except (
515 rasterio.errors.RasterioIOError,
516 rasterio.errors.RasterBlockError,
517 rasterio.errors.WarpOperationError,
518 rasterio.errors.WindowEvaluationError,
519 ) as e:
520 if cfg.fail_on_error:
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/odc/loader/_rio.py:560, in _rio_read()
549 def _rio_read(
550 src: RasterSource,
551 cfg: RasterLoadParams,
(...)
556 # if resampling is `nearest` then ignore sub-pixel translation when deciding
557 # whether we can just paste source into destination
558 ttol = 0.9 if cfg.nearest else 0.05
--> 560 with rasterio.open(src.uri, "r", sharing=False) as rdr:
561 assert isinstance(rdr, rasterio.DatasetReader)
562 ovr_idx: Optional[int] = None
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/rasterio/env.py:463, in wrapper()
460 session = DummySession()
462 with env_ctor(session=session):
--> 463 return f(*args, **kwds)
File /opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/rasterio/__init__.py:356, in open()
353 path = _parse_path(raw_dataset_path)
355 if mode == "r":
--> 356 dataset = DatasetReader(path, driver=driver, sharing=sharing, **kwargs)
357 elif mode == "r+":
358 dataset = get_writer_for_path(path, driver=driver)(
359 path, mode, driver=driver, sharing=sharing, **kwargs
360 )
File rasterio/_base.pyx:312, in rasterio._base.DatasetBase.__init__()
RasterioIOError: CURL error: Failed to connect to data.eodc.eu port 443 after 159 ms: Couldn't connect to server
Calculate Harmonic Parameters#
This function fits sine and cosine functions known as harmonic oscillators to each pixel of the Sentinel 1 \(\sigma^0\) datacube. These seasonally varying curves can then be extracted from time series. What is left is the noise or transient events, for example flood events, superimposed on the seasonal trend.
# Function to reformat time range for selecting from the Xarray
datetime_xr = format_datetime_for_xarray_selection(search, time_range)
hpar_list = create_harmonic_parameters(sig0_dc)
__, hpar_dc, __ = process_harmonic_parameters_datacube(sig0_dc, datetime_xr, hpar_list)
hpar_dc
t = sig0_dc.time.dt.dayofyear
n = 365
y = (
hpar_dc.M0
+ hpar_dc.C1 * np.cos(2 * np.pi * t / n)
+ hpar_dc.S1 * np.sin(2 * np.pi * t / n)
+ hpar_dc.C2 * np.cos(2 * np.pi * t / n)
+ hpar_dc.S2 * np.sin(2 * np.pi * t / n)
+ hpar_dc.C3 * np.cos(2 * np.pi * t / n)
+ hpar_dc.S3 * np.sin(2 * np.pi * t / n)
)
Fit Harmonic Function to Original Data#
Finally, we merge the two datasets and superimpose the fitted harmonic function on the raw sigma nought data.
xr.merge([y.rename("pred"), sig0_dc]).squeeze().assign_coords(
x=range(len(sig0_dc.x)), y=range(len(sig0_dc.y))
).hvplot(x="time")
Calculating of harmonic parameters can be selected during flood mapping by setting the parameter
to True
.
flood.decision(bbox=bounding_box, datetime=time_range, dynamic=True)
This setting theoretically frees the user from the need for a precomputed harmonic parameters dataset, thereby only requiring sentinel-1 sigma nought and PLIA datasets as static inputs. Do mind, however, that calculating harmonic parameters can be a computationally expensive operation.