Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
446cf65
Bump ruff from 0.11.11 to 0.11.12
dependabot[bot] Jun 2, 2025
186c090
Merge pull request #63 from ASFHyP3/dependabot/pip/ruff-0.11.12
jtherrmann Jun 2, 2025
877109b
Bump mypy from 1.15.0 to 1.16.0
dependabot[bot] Jun 2, 2025
226dbdd
Merge pull request #64 from ASFHyP3/dependabot/pip/mypy-1.16.0
jtherrmann Jun 2, 2025
c7ea71c
Bump ruff from 0.11.12 to 0.11.13
dependabot[bot] Jun 9, 2025
65fb7d7
Merge pull request #65 from ASFHyP3/dependabot/pip/ruff-0.11.13
jtherrmann Jun 12, 2025
83c1fe5
update dem strategy to match opera
forrestfwilliams Jun 16, 2025
734c0bd
update usage and changelog
forrestfwilliams Jun 16, 2025
b0d94e1
fix mypy
forrestfwilliams Jun 16, 2025
d2380de
fix ruff
forrestfwilliams Jun 16, 2025
89fd698
add new dem tests
forrestfwilliams Jun 16, 2025
31a7a7c
add backoff decorator
forrestfwilliams Jun 16, 2025
a881e3e
update based on review feedback
forrestfwilliams Jun 16, 2025
df33ef8
Update src/hyp3_opera_rtc/dem.py
forrestfwilliams Jun 16, 2025
da5f967
Merge branch 'develop' into validation
forrestfwilliams Jun 16, 2025
f1f2c91
fix spelling of meridian
asjohnston-asf Jun 16, 2025
dc92dd4
Merge branch 'validation' of github.com:ASFHyP2/hyp3-OPERA-RTC into v…
asjohnston-asf Jun 16, 2025
ddf4730
Merge pull request #66 from ASFHyP3/validation
forrestfwilliams Jun 16, 2025
86f467b
correctly use buffered geometry when subsetting dem
asjohnston-asf Jun 17, 2025
846dc2b
Merge branch 'develop' into buffer
asjohnston-asf Jun 17, 2025
2f39fa5
Merge pull request #67 from ASFHyP3/buffer
asjohnston-asf Jun 17, 2025
d343f1c
increase DEM buffer distance
forrestfwilliams Jun 18, 2025
8c3de37
update changelog
forrestfwilliams Jun 18, 2025
6d9bf15
Merge branch 'develop' into validation
forrestfwilliams Jun 18, 2025
0786b48
update tests for new bounds
forrestfwilliams Jun 18, 2025
6fd6e7d
group changelog entry under 0.1.4
asjohnston-asf Jun 18, 2025
bca80b2
restore whitespace
asjohnston-asf Jun 18, 2025
ecd8007
Merge pull request #68 from ASFHyP3/validation
asjohnston-asf Jun 18, 2025
c4fdb40
Bump ruff from 0.11.13 to 0.12.0
dependabot[bot] Jun 23, 2025
f81dac5
update dem margin to match PCM
forrestfwilliams Jun 23, 2025
02c1b64
update changelog
forrestfwilliams Jun 23, 2025
3f6bcc7
Merge branch 'develop' into validation
forrestfwilliams Jun 23, 2025
fd3ea22
fix tests
forrestfwilliams Jun 23, 2025
0700b8c
Merge pull request #71 from ASFHyP3/validation
forrestfwilliams Jun 23, 2025
e7e7b5d
Merge pull request #70 from ASFHyP3/dependabot/pip/ruff-0.12.0
jtherrmann Jun 23, 2025
8f2068a
Bump mypy from 1.16.0 to 1.16.1
dependabot[bot] Jun 23, 2025
b3c13ed
Merge pull request #69 from ASFHyP3/dependabot/pip/mypy-1.16.1
jtherrmann Jun 23, 2025
50d151b
update bounding box logic and add CLI entrypoint for DEM debugging
forrestfwilliams Jun 25, 2025
1ea2d35
update changelog
forrestfwilliams Jun 25, 2025
0178837
Merge branch 'develop' into validation
forrestfwilliams Jun 25, 2025
7ba5f1e
update based on review
forrestfwilliams Jun 25, 2025
7fa08b6
update global names
forrestfwilliams Jun 25, 2025
5872289
fix ruff
forrestfwilliams Jun 25, 2025
3c3279d
Merge pull request #72 from ASFHyP3/validation
forrestfwilliams Jun 25, 2025
c0ca76a
fix mypy-required assert
forrestfwilliams Jun 26, 2025
1e7156d
Merge pull request #73 from ASFHyP3/validation
forrestfwilliams Jun 26, 2025
4cd5f3a
switch to v1.2 dem
forrestfwilliams Jun 26, 2025
78c1204
Merge pull request #74 from ASFHyP3/validation
forrestfwilliams Jun 26, 2025
ed46bf5
dependabot groups
jtherrmann Jun 26, 2025
ecb5e40
Merge pull request #75 from ASFHyP3/dependabot-groups
jhkennedy Jun 27, 2025
ec2a01a
Bump ruff from 0.12.0 to 0.12.1 in the pip-deps group
dependabot[bot] Jun 27, 2025
515ac66
revert dem version, update burst db
forrestfwilliams Jun 30, 2025
b1c14be
Merge pull request #77 from ASFHyP3/validation
forrestfwilliams Jun 30, 2025
0791571
update PGE RunConfig to more closely match upstream version
jtherrmann Jun 30, 2025
f9ff72f
tweak todo
jtherrmann Jun 30, 2025
b1aa0e8
remove extra pge template comments
jtherrmann Jul 1, 2025
0a5b3af
reindent pge template to match upstream
jtherrmann Jul 1, 2025
94407f9
Merge pull request #79 from ASFHyP3/reformat-pge-template
jtherrmann Jul 1, 2025
00bb976
Merge branch 'develop' into update-pge-template
jtherrmann Jul 1, 2025
ada92ba
whitespace
jtherrmann Jul 1, 2025
82538d2
revise comments
jtherrmann Jul 1, 2025
18bea25
add some todos
jtherrmann Jul 1, 2025
0caacf7
Apply suggestions from code review
jtherrmann Jul 1, 2025
4570ff7
remove todo
jtherrmann Jul 1, 2025
2ad60a2
Merge branch 'update-pge-template' of github.com:ASFHyP3/hyp3-OPERA-R…
jtherrmann Jul 1, 2025
e0b92fa
remove some todos
jtherrmann Jul 1, 2025
11d3b21
parameterize data validity start date
jtherrmann Jul 1, 2025
05c34f5
changelog
jtherrmann Jul 1, 2025
42cf977
remove some more comments
jtherrmann Jul 1, 2025
fca8be1
Merge pull request #78 from ASFHyP3/update-pge-template
forrestfwilliams Jul 1, 2025
af16f4a
Merge pull request #76 from ASFHyP3/dependabot/pip/pip-deps-e6b17d0ad6
jtherrmann Jul 1, 2025
5d81a50
Bump ruff from 0.12.1 to 0.12.2 in the pip-deps group
dependabot[bot] Jul 7, 2025
c9e8173
add in first changes
Jul 8, 2025
c010a56
roll back some changes
Jul 8, 2025
0e17d45
remove extra newline
Jul 8, 2025
cb867f4
fix some missing fun names
Jul 8, 2025
abf9a1f
rename parsing fun
Jul 8, 2025
b002f0a
fix typos
Jul 8, 2025
5042b9f
straighten out new functions
Jul 8, 2025
659d0d2
update changelog
Jul 8, 2025
179b8c2
fix granule exists parameter
Jul 8, 2025
bf27c3e
get pytests to pass
Jul 8, 2025
00c538a
fix old name
Jul 8, 2025
4a5e2bf
Refactor prep_rtc.py
williamh890 Jul 10, 2025
df56d8e
Remove unused import
williamh890 Jul 10, 2025
ec25c58
Changes so opera processing can start for SLC granules
williamh890 Jul 10, 2025
6111177
Upload groups and zips all bursts by the h5 file name
williamh890 Jul 11, 2025
92a88c7
Ruff format
williamh890 Jul 11, 2025
9e6ce96
Remove global short_name variables
williamh890 Jul 11, 2025
44b412f
Merge pull request #82 from ASFHyP3/accept-slc-refactor
williamh890 Jul 11, 2025
5870c07
Add improve test for whole slc upload_rtc
williamh890 Jul 14, 2025
825a7d4
add reference to SLC capability
Jul 14, 2025
9175dd5
Merge pull request #80 from ASFHyP3/dependabot/pip/pip-deps-2ffa01b6f8
jtherrmann Jul 15, 2025
5cf297d
Merge pull request #81 from ASFHyP3/accept-slc
williamh890 Jul 21, 2025
c7820cb
Only zip products for burst processing
williamh890 Jul 22, 2025
9796235
Update test for not zipping products
williamh890 Jul 22, 2025
46f69b5
Add omp-num-threads cli param
williamh890 Jul 22, 2025
0832b44
Fix import order
williamh890 Jul 22, 2025
77d3290
Update changelog
williamh890 Jul 22, 2025
e47fe7c
Switch to -- instead of ++
williamh890 Jul 22, 2025
17d9ec9
Change param to num workers instead of omp-num-threads
williamh890 Jul 22, 2025
8e1152f
refactor product zipping
asjohnston-asf Jul 22, 2025
9b499d6
Update changelog
williamh890 Jul 22, 2025
91bb489
Merge branch 'no-zip-slc' of github.com:ASFHyP3/hyp3-OPERA-RTC into n…
williamh890 Jul 22, 2025
f409430
Update changelog
williamh890 Jul 22, 2025
7e1dc37
Remove int cast from res and workers params
williamh890 Jul 22, 2025
366cdcc
Merge pull request #84 from ASFHyP3/no-zip-slc
williamh890 Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ updates:
interval: weekly
labels:
- bumpless
groups:
pip-deps:
patterns:
- "*"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels:
- bumpless
groups:
github-actions-deps:
patterns:
- "*"
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/)
and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.4]

### Added
- Added functionality to `prep_rtc.py` and `upload_rtc.py` to accept SLCs or co-pol bursts.
- Add `--num-workers` as `prep_rtc` cli param

### Changed
- Updated DEM fetching/tiling strategy to match OPERA's.
- Updated DEM bounds buffer to 100 km from 0.025 degrees
- Updated burst database to the OPERA-provided burst_db_0.2.0_230831-bbox-only.sqlite file
- Updated our [PGE RunConfig template](./src/hyp3_opera_rtc/templates/pge.yml.j2) to more closely align with the [upstream version](https://github.com/nasa/opera-sds-pcm/blob/9bd74458957197b0c6680540c8d09c26ffab81df/conf/RunConfig.yaml.L2_RTC_S1.jinja2.tmpl).

## [0.1.3]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ USER root
RUN chown rtc_user:rtc_user /home/rtc_user/scratch
USER rtc_user

RUN curl https://asf-dem-west.s3.amazonaws.com/AUX/opera-burst-bbox-only.sqlite3 -o /home/rtc_user/opera-burst-bbox-only.sqlite3
RUN curl https://asf-dem-west.s3.amazonaws.com/AUX/burst_db_0.2.0_230831-bbox-only.sqlite -o /home/rtc_user/burst_db_0.2.0_230831-bbox-only.sqlite

COPY --chown=rtc_user:rtc_user . /home/rtc_user/hyp3-opera-rtc/
RUN conda env create -f /home/rtc_user/hyp3-opera-rtc/environment.yml && \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ docker run -it --rm \
[CO_POL_GRANULE]
```

Where you replace `[CO_POL_GRANULE]` with the name of the Sentinel-1 co-pol burst SLC scene
Where you replace `[CO_POL_GRANULE]` with the name of the Sentinel-1 co-pol burst or SLC scene
for which to generate the OPERA RTC product.
Here are some useful examples:

Expand All @@ -49,7 +49,7 @@ Here are some useful examples:

## Architecture

The plugin is composed of three nested docker environments that depend on eachother. They are laid out as below:
The plugin is composed of three nested docker environments that depend on each other. They are laid out as below:

```
+-------------------------+
Expand Down
4 changes: 2 additions & 2 deletions requirements-static.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ruff==0.11.11
mypy==1.15.0
ruff==0.12.2
mypy==1.16.1
lxml-stubs
types-shapely
types-requests
177 changes: 133 additions & 44 deletions src/hyp3_opera_rtc/dem.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
from concurrent.futures import ThreadPoolExecutor
from itertools import product
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
from tempfile import NamedTemporaryFile

import numpy as np
import shapely
import shapely.ops
import shapely.wkt
from hyp3lib.fetch import download_file
from osgeo import gdal
from shapely.geometry import LinearRing, Polygon
from hyp3lib.util import GDALConfigManager
from osgeo import gdal, osr
from shapely.geometry import LinearRing, Polygon, box


gdal.UseExceptions()
URL = 'https://nisar.asf.earthdatacloud.nasa.gov/STATIC/DEM/v1.1/EPSG4326'

EARTH_APPROX_CIRCUMFERENCE_KM = 40075017.0
EARTH_RADIUS_KM = EARTH_APPROX_CIRCUMFERENCE_KM / (2 * np.pi)
DEM_MARGIN_KM = 200

def check_antimeridean(poly: Polygon) -> list[Polygon]:

def margin_km_to_deg(margin_in_km: float) -> float:
"""Converts a margin value from kilometers to degrees."""
km_to_deg_at_equator = 1000.0 / (EARTH_APPROX_CIRCUMFERENCE_KM / 360.0)
margin_in_deg = margin_in_km * km_to_deg_at_equator
return margin_in_deg


def margin_km_to_longitude_deg(margin_in_km: float, lat: float) -> float:
"""Converts a margin value from kilometers to degrees as a function of latitude."""
delta_lon = 180 * 1000 * margin_in_km / (np.pi * EARTH_RADIUS_KM * np.cos(np.pi * lat / 180))
return delta_lon


def polygon_from_bounds(bounds: tuple[float, float, float, float]) -> Polygon:
"""Create a polygon (EPSG:4326) from the lat/lon coordinates corresponding to a provided bounding box."""
lon_min, lat_min, lon_max, lat_max = bounds
# note we can also use the center lat here
lat_worst_case = max([lat_min, lat_max])
lat_margin = margin_km_to_deg(DEM_MARGIN_KM)
lon_margin = margin_km_to_longitude_deg(DEM_MARGIN_KM, lat=lat_worst_case)
# Check if the bbox crosses the antimeridian and apply the margin accordingly
# so that any resultant DEM is split properly by check_dateline
if lon_max - lon_min > 180:
lon_min, lon_max = lon_max, lon_min

poly = box(
lon_min - lon_margin, max([lat_min - lat_margin, -90]), lon_max + lon_margin, min([lat_max + lat_margin, 90])
)
return poly


def split_antimeridian(poly: Polygon) -> list[Polygon]:
"""Check if the provided polygon crosses the antimeridian and split it if it does."""
x_min, _, x_max, _ = poly.bounds

# Check anitmeridean crossing
# Check anitmeridian crossing
if (x_max - x_min > 180.0) or (x_min <= 180.0 <= x_max):
dateline = shapely.wkt.loads('LINESTRING( 180.0 -90.0, 180.0 90.0)')

Expand Down Expand Up @@ -54,38 +88,93 @@ def check_antimeridean(poly: Polygon) -> list[Polygon]:
return polys


def get_dem_granule_url(lat: int, lon: int) -> str:
lat_tens = np.floor_divide(lat, 10) * 10
lat_cardinal = 'S' if lat_tens < 0 else 'N'

lon_tens = np.floor_divide(lon, 20) * 20
lon_cardinal = 'W' if lon_tens < 0 else 'E'

prefix = f'{lat_cardinal}{np.abs(lat_tens):02d}_{lon_cardinal}{np.abs(lon_tens):03d}'
filename = f'DEM_{lat_cardinal}{np.abs(lat):02d}_00_{lon_cardinal}{np.abs(lon):03d}_00.tif'
file_url = f'{URL}/{prefix}/{filename}'
return file_url


def get_latlon_pairs(polygon: Polygon) -> list:
minx, miny, maxx, maxy = polygon.bounds
lats = np.arange(np.floor(miny), np.floor(maxy) + 1).astype(int)
lons = np.arange(np.floor(minx), np.floor(maxx) + 1).astype(int)
return list(product(lats, lons))


def download_opera_dem_for_footprint(output_path: Path, footprint: Polygon) -> None:
footprints = check_antimeridean(footprint)
latlon_pairs = []
for footprint in footprints:
latlon_pairs += get_latlon_pairs(footprint)
urls = [get_dem_granule_url(lat, lon) for lat, lon in latlon_pairs]

with TemporaryDirectory() as tmpdir_str:
tmpdir = Path(tmpdir_str)
with ThreadPoolExecutor(max_workers=4) as executor:
executor.map(lambda url: download_file(url, str(tmpdir)), urls)
vrt_filepath = str(tmpdir / 'dem.vrt')
input_files = [str(file) for file in tmpdir.glob('*.tif')]
gdal.BuildVRT(vrt_filepath, input_files)
gdal.Translate(str(output_path), vrt_filepath, format='GTiff')
def snap_coord(val: float, snap: float, offset: float, round_func: Callable) -> float:
return round_func(float(val - offset) / snap) * snap + offset


def translate_dem(vrt_filename: str, output_path: str, bounds: tuple[float, float, float, float]) -> None:
"""Write a local subset of the OPERA DEM for a region matching the provided bounds.

Params:
vrt_filename: Path to the input VRT file
output_path: Path to the translated output GTiff file
bounds: Bounding box in the form of (lon_min, lat_min, lon_max, lat_max)
"""
ds = gdal.Open(vrt_filename, gdal.GA_ReadOnly)

# update cropping coordinates to not exceed the input DEM bounding box
input_x_min, xres, _, input_y_max, _, yres = ds.GetGeoTransform()
length = ds.GetRasterBand(1).YSize
width = ds.GetRasterBand(1).XSize

# Snap edge coordinates using the DEM pixel spacing
# (xres and yres) and starting coordinates (input_x_min and
# input_x_max). Maximum values are rounded using np.ceil
# and minimum values are rounded using np.floor
x_min, y_min, x_max, y_max = bounds
snapped_x_min = snap_coord(x_min, xres, input_x_min, np.floor)
snapped_x_max = snap_coord(x_max, xres, input_x_min, np.ceil)
snapped_y_min = snap_coord(y_min, yres, input_y_max, np.floor)
snapped_y_max = snap_coord(y_max, yres, input_y_max, np.ceil)

input_y_min = input_y_max + length * yres
input_x_max = input_x_min + width * xres

adjusted_x_min = max(snapped_x_min, input_x_min)
adjusted_x_max = min(snapped_x_max, input_x_max)
adjusted_y_min = max(snapped_y_min, input_y_min)
adjusted_y_max = min(snapped_y_max, input_y_max)

try:
gdal.Translate(
output_path, ds, format='GTiff', projWin=[adjusted_x_min, adjusted_y_max, adjusted_x_max, adjusted_y_min]
)
except RuntimeError as err:
if 'negative width and/or height' in str(err):
gdal.Translate(output_path, ds, format='GTiff', projWin=[x_min, y_max, x_max, y_min])
else:
raise

# stage_dem.py takes a bbox as an input. The longitude coordinates
# of this bbox are unwrapped i.e., range in [0, 360] deg. If the
# bbox crosses the anti-meridian, the script divides it in two
# bboxes neighboring the anti-meridian. Here, x_min and x_max
# represent the min and max longitude coordinates of one of these
# bboxes. We Add 360 deg if the min longitude of the downloaded DEM
# tile is < 180 deg i.e., there is a dateline crossing.
# This ensures that the mosaicked DEM VRT will span a min
# range of longitudes rather than the full [-180, 180] deg
sr = osr.SpatialReference(ds.GetProjection())
epsg_str = sr.GetAttrValue('AUTHORITY', 1)

if x_min <= -180.0 and epsg_str == '4326':
ds = gdal.Open(output_path, gdal.GA_Update)
geotransform = list(ds.GetGeoTransform())
geotransform[0] += 360.0
ds.SetGeoTransform(tuple(geotransform))


def download_opera_dem_for_footprint(outfile: Path, bounds: tuple[float, float, float, float]) -> None:
"""Download a DEM from the specified S3 bucket.

Params:
outfile: Path to the where the output DEM file is to be staged.
bounds: Bounding box in the form of (lon_min, lat_min, lon_max, lat_max).
"""
poly = polygon_from_bounds(bounds)
polys = split_antimeridian(poly)
dem_list = []

with NamedTemporaryFile(suffix='.txt') as cookie_file:
with GDALConfigManager(
GDAL_HTTP_COOKIEJAR=cookie_file.name,
GDAL_HTTP_COOKIEFILE=cookie_file.name,
GDAL_DISABLE_READDIR_ON_OPEN='EMPTY_DIR',
):
vrt_filename = '/vsicurl/https://nisar.asf.earthdatacloud.nasa.gov/STATIC/DEM/v1.1/EPSG4326/EPSG4326.vrt'
for idx, poly in enumerate(polys):
output_path = str(outfile.parent / f'{outfile.stem}_{idx}.tif')
dem_list.append(output_path)
translate_dem(vrt_filename, output_path, poly.bounds)

gdal.BuildVRT(str(outfile), dem_list)
Loading