Skip to content

Commit 1bf10dc

Browse files
authored
Merge pull request #45 from bopen/abstract-product
Abstract away the definition of the input product
2 parents f88cc0e + b1c8779 commit 1bf10dc

10 files changed

+397
-233
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,12 @@ The following code applies the geometric terrain correction to the VV polarizati
105105

106106
```python
107107
>>> import sarsen
108-
>>> gtc = sarsen.terrain_correction(
108+
>>> product = sarsen.Sentinel1SarProduct(
109109
... "tests/data/S1B_IW_GRDH_1SDV_20211223T051122_20211223T051147_030148_039993_5371.SAFE",
110110
... measurement_group="IW/VV",
111+
... )
112+
>>> gtc = sarsen.terrain_correction(
113+
... product,
111114
... dem_urlpath="tests/data/Rome-30m-DEM.tif",
112115
... )
113116

@@ -117,8 +120,7 @@ The radiometric correction can be activated using the key `correct_radiometry`:
117120

118121
```python
119122
>>> rtc = sarsen.terrain_correction(
120-
... "tests/data/S1B_IW_GRDH_1SDV_20211223T051122_20211223T051147_030148_039993_5371.SAFE",
121-
... measurement_group="IW/VV",
123+
... product,
122124
... dem_urlpath="tests/data/Rome-30m-DEM.tif",
123125
... correct_radiometry="gamma_nearest"
124126
... )

sarsen/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@
2121
__version__ = "999"
2222

2323
from .apps import terrain_correction
24+
from .datamodel import SarProduct
25+
from .sentinel1 import Sentinel1SarProduct
2426

25-
__all__ = ["__version__", "terrain_correction"]
27+
__all__ = ["__version__", "terrain_correction", "SarProduct", "Sentinel1SarProduct"]

sarsen/__main__.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import typer
66

7-
from . import apps
7+
from . import apps, sentinel1
88

99
app = typer.Typer()
1010

@@ -14,7 +14,7 @@ def info(
1414
product_urlpath: str,
1515
) -> None:
1616
"""Print information about the Sentinel-1 product."""
17-
product_info = apps.product_info(product_urlpath)
17+
product_info = sentinel1.product_info(product_urlpath)
1818
for key, value in product_info.items():
1919
print(f"{key}: {value}")
2020

@@ -33,9 +33,12 @@ def gtc(
3333
client_kwargs = json.loads(client_kwargs_json)
3434
real_chunks = chunks if chunks > 0 else None
3535
logging.basicConfig(level=logging.INFO)
36-
apps.terrain_correction(
36+
product = sentinel1.Sentinel1SarProduct(
3737
product_urlpath,
3838
measurement_group,
39+
)
40+
apps.terrain_correction(
41+
product,
3942
dem_urlpath,
4043
output_urlpath=output_urlpath,
4144
enable_dask_distributed=enable_dask_distributed,
@@ -59,9 +62,12 @@ def stc(
5962
client_kwargs = json.loads(client_kwargs_json)
6063
real_chunks = chunks if chunks > 0 else None
6164
logging.basicConfig(level=logging.INFO)
62-
apps.terrain_correction(
65+
product = sentinel1.Sentinel1SarProduct(
6366
product_urlpath,
6467
measurement_group,
68+
)
69+
apps.terrain_correction(
70+
product,
6571
dem_urlpath,
6672
correct_radiometry="gamma_bilinear",
6773
simulated_urlpath=simulated_urlpath,
@@ -87,9 +93,12 @@ def rtc(
8793
client_kwargs = json.loads(client_kwargs_json)
8894
real_chunks = chunks if chunks > 0 else None
8995
logging.basicConfig(level=logging.INFO)
90-
apps.terrain_correction(
96+
product = sentinel1.Sentinel1SarProduct(
9197
product_urlpath,
9298
measurement_group,
99+
)
100+
apps.terrain_correction(
101+
product,
93102
dem_urlpath,
94103
correct_radiometry="gamma_bilinear",
95104
output_urlpath=output_urlpath,

sarsen/apps.py

+26-162
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,22 @@
1-
from __future__ import annotations
2-
3-
import functools
41
import logging
5-
from typing import Any, Dict, Optional, Tuple, TypeVar, Union
2+
from typing import Any, Callable, Dict, Optional, Tuple
63
from unittest import mock
74

8-
import attrs
95
import numpy as np
106
import rioxarray
117
import xarray as xr
12-
import xarray_sentinel
138

14-
from . import chunking, geocoding, orbit, radiometry, scene
9+
from . import chunking, datamodel, geocoding, orbit, radiometry, scene
1510

1611
logger = logging.getLogger(__name__)
1712

1813

19-
T_SarProduct = TypeVar("T_SarProduct", bound="SarProduct")
20-
21-
22-
@attrs.define(slots=False)
23-
class SarProduct:
24-
product_urlpath: str
25-
measurement_group: str
26-
measurement_chunks: int = 2048
27-
kwargs: Dict[str, Any] = {}
28-
29-
@functools.cached_property
30-
def measurement(self) -> xr.Dataset:
31-
ds, self.kwargs = open_dataset_autodetect(
32-
self.product_urlpath,
33-
group=self.measurement_group,
34-
chunks=self.measurement_chunks,
35-
**self.kwargs,
36-
)
37-
return ds
38-
39-
@functools.cached_property
40-
def orbit(self) -> xr.Dataset:
41-
ds, self.kwargs = open_dataset_autodetect(
42-
self.product_urlpath, group=f"{self.measurement_group}/orbit", **self.kwargs
43-
)
44-
return ds
45-
46-
@functools.cached_property
47-
def calibration(self) -> xr.Dataset:
48-
ds, self.kwargs = open_dataset_autodetect(
49-
self.product_urlpath,
50-
group=f"{self.measurement_group}/calibration",
51-
**self.kwargs,
52-
)
53-
return ds
54-
55-
@functools.cached_property
56-
def coordinate_conversion(self) -> xr.Dataset:
57-
ds, self.kwargs = open_dataset_autodetect(
58-
self.product_urlpath,
59-
group=f"{self.measurement_group}/coordinate_conversion",
60-
**self.kwargs,
61-
)
62-
return ds
63-
64-
65-
def open_dataset_autodetect(
66-
product_urlpath: str,
67-
group: Optional[str] = None,
68-
chunks: Optional[Union[int, Dict[str, int]]] = None,
69-
**kwargs: Any,
70-
) -> Tuple[xr.Dataset, Dict[str, Any]]:
71-
kwargs.setdefault("engine", "sentinel-1")
72-
try:
73-
ds = xr.open_dataset(product_urlpath, group=group, chunks=chunks, **kwargs)
74-
except FileNotFoundError:
75-
# re-try with Planetary Computer option
76-
kwargs[
77-
"override_product_files"
78-
] = "{dirname}/{prefix}{swath}-{polarization}{ext}"
79-
ds = xr.open_dataset(product_urlpath, group=group, chunks=chunks, **kwargs)
80-
return ds, kwargs
81-
82-
83-
def product_info(
84-
product_urlpath: str,
85-
**kwargs: Any,
86-
) -> Dict[str, Any]:
87-
"""Get information about the Sentinel-1 product."""
88-
root_ds = xr.open_dataset(
89-
product_urlpath, engine="sentinel-1", check_files_exist=True, **kwargs
90-
)
91-
92-
measurement_groups = [g for g in root_ds.attrs["subgroups"] if g.count("/") == 1]
93-
94-
gcp_group = measurement_groups[0] + "/gcp"
95-
96-
gcp, kwargs = open_dataset_autodetect(product_urlpath, group=gcp_group, **kwargs)
97-
98-
bbox = [
99-
gcp.attrs["geospatial_lon_min"],
100-
gcp.attrs["geospatial_lat_min"],
101-
gcp.attrs["geospatial_lon_max"],
102-
gcp.attrs["geospatial_lat_max"],
103-
]
104-
105-
product_attrs = [
106-
"product_type",
107-
"mode",
108-
"swaths",
109-
"transmitter_receiver_polarisations",
110-
]
111-
product_info = {attr_name: root_ds.attrs[attr_name] for attr_name in product_attrs}
112-
product_info.update(
113-
{
114-
"measurement_groups": measurement_groups,
115-
"geospatial_bounds": gcp.attrs["geospatial_bounds"],
116-
"geospatial_bbox": bbox,
117-
}
118-
)
119-
120-
return product_info
121-
122-
12314
def simulate_acquisition(
12415
dem_ecef: xr.DataArray,
12516
position_ecef: xr.DataArray,
126-
coordinate_conversion: Optional[xr.Dataset] = None,
17+
slant_range_time_to_ground_range: Callable[
18+
[xr.DataArray, xr.DataArray], xr.DataArray
19+
],
12720
correct_radiometry: Optional[str] = None,
12821
) -> xr.Dataset:
12922
"""Compute the image coordinates of the DEM given the satellite orbit."""
@@ -139,13 +32,12 @@ def simulate_acquisition(
13932

14033
acquisition["slant_range_time"] = slant_range_time
14134

142-
if coordinate_conversion is not None:
143-
ground_range = xarray_sentinel.slant_range_time_to_ground_range(
144-
acquisition.azimuth_time,
145-
slant_range_time,
146-
coordinate_conversion,
147-
)
148-
acquisition["ground_range"] = ground_range.drop_vars("azimuth_time")
35+
maybe_ground_range = slant_range_time_to_ground_range(
36+
acquisition.azimuth_time,
37+
slant_range_time,
38+
)
39+
if maybe_ground_range is not None:
40+
acquisition["ground_range"] = maybe_ground_range.drop_vars("azimuth_time")
14941
if correct_radiometry is not None:
15042
gamma_area = radiometry.compute_gamma_area(
15143
dem_ecef, acquisition.dem_distance / slant_range
@@ -157,22 +49,8 @@ def simulate_acquisition(
15749
return acquisition
15850

15951

160-
def calibrate_measurement(
161-
measurement_ds: xr.Dataset, beta_nought_lut: xr.DataArray
162-
) -> xr.DataArray:
163-
measurement = measurement_ds.measurement
164-
if measurement.attrs["product_type"] == "SLC" and measurement.attrs["mode"] == "IW":
165-
measurement = xarray_sentinel.mosaic_slc_iw(measurement)
166-
167-
beta_nought = xarray_sentinel.calibrate_intensity(measurement, beta_nought_lut)
168-
beta_nought = beta_nought.drop_vars(["pixel", "line"])
169-
170-
return beta_nought
171-
172-
17352
def terrain_correction(
174-
product_urlpath: str,
175-
measurement_group: str,
53+
product: datamodel.SarProduct,
17654
dem_urlpath: str,
17755
output_urlpath: Optional[str] = "GTC.tif",
17856
simulated_urlpath: Optional[str] = None,
@@ -181,17 +59,14 @@ def terrain_correction(
18159
grouping_area_factor: Tuple[float, float] = (3.0, 3.0),
18260
open_dem_raster_kwargs: Dict[str, Any] = {},
18361
chunks: Optional[int] = 1024,
184-
measurement_chunks: int = 1024,
18562
radiometry_chunks: int = 2048,
18663
radiometry_bound: int = 128,
18764
enable_dask_distributed: bool = False,
18865
client_kwargs: Dict[str, Any] = {"processes": False},
189-
**kwargs: Any,
19066
) -> xr.DataArray:
19167
"""Apply the terrain-correction to sentinel-1 SLC and GRD products.
19268
193-
:param product_urlpath: input product path or url
194-
:param measurement_group: group of the measurement to be used, for example: "IW/VV"
69+
:param product: SarProduct instance representing the input data
19570
:param dem_urlpath: dem path or url
19671
:param orbit_group: overrides the orbit group name
19772
:param calibration_group: overrides the calibration group name
@@ -214,7 +89,6 @@ def terrain_correction(
21489
Be aware that `grouping_area_factor` too high may degrade the final result
21590
:param open_dem_raster_kwargs: additional keyword arguments passed on to ``xarray.open_dataset``
21691
to open the `dem_urlpath`
217-
:param kwargs: additional keyword arguments passed on to ``xarray.open_dataset`` to open the `product_urlpath`
21892
"""
21993
# rioxarray must be imported explicitly or accesses to `.rio` may fail in dask
22094
assert rioxarray.__version__
@@ -244,15 +118,11 @@ def terrain_correction(
244118
dem_urlpath, chunks=chunks, **open_dem_raster_kwargs
245119
)
246120

247-
logger.info(f"open data product {product_urlpath!r}")
248-
249-
product = SarProduct(
250-
product_urlpath, measurement_group, measurement_chunks, **kwargs
251-
)
252-
product_type = product.measurement.attrs["product_type"]
253121
allowed_product_types = ["GRD", "SLC"]
254-
if product_type not in allowed_product_types:
255-
raise ValueError(f"{product_type=}. Must be one of: {allowed_product_types}")
122+
if product.product_type not in allowed_product_types:
123+
raise ValueError(
124+
f"{product.product_type=}. Must be one of: {allowed_product_types}"
125+
)
256126

257127
logger.info("pre-process DEM")
258128

@@ -270,30 +140,26 @@ def terrain_correction(
270140
"azimuth_time": template_raster.astype("datetime64[ns]"),
271141
}
272142
)
273-
acquisition_kwargs = {
274-
"position_ecef": product.orbit.position,
275-
"correct_radiometry": correct_radiometry,
276-
}
277-
if product_type == "GRD":
143+
if product.product_type == "GRD":
278144
acquisition_template["ground_range"] = template_raster
279-
acquisition_kwargs["coordinate_conversion"] = product.coordinate_conversion
280145
if correct_radiometry is not None:
281146
acquisition_template["gamma_area"] = template_raster
282147

283148
acquisition = xr.map_blocks(
284149
simulate_acquisition,
285150
dem_ecef,
286-
kwargs=acquisition_kwargs,
151+
kwargs={
152+
"position_ecef": product.state_vectors(),
153+
"slant_range_time_to_ground_range": product.slant_range_time_to_ground_range,
154+
"correct_radiometry": correct_radiometry,
155+
},
287156
template=acquisition_template,
288157
)
289158

290159
if correct_radiometry is not None:
291160
logger.info("simulate radiometry")
292161

293-
grid_parameters = radiometry.azimuth_slant_range_grid(
294-
product.measurement.attrs,
295-
grouping_area_factor,
296-
)
162+
grid_parameters = product.grid_parameters(grouping_area_factor)
297163

298164
if correct_radiometry == "gamma_bilinear":
299165
gamma_weights = radiometry.gamma_weights_bilinear
@@ -339,13 +205,11 @@ def terrain_correction(
339205

340206
logger.info("calibrate image")
341207

342-
beta_nought = calibrate_measurement(
343-
product.measurement, product.calibration.betaNought
344-
)
208+
beta_nought = product.beta_nought()
345209

346210
logger.info("terrain-correct image")
347211

348-
if product_type == "GRD":
212+
if product.product_type == "GRD":
349213
interp_kwargs = {"ground_range": acquisition.ground_range}
350214
else:
351215
interp_kwargs = {"slant_range_time": acquisition.slant_range_time}

0 commit comments

Comments
 (0)