Skip to content

Commit 1c11487

Browse files
author
Talib Oliver Cabrera
committed
adding wavelength reader
1 parent 432bcf0 commit 1c11487

2 files changed

Lines changed: 72 additions & 33 deletions

File tree

src/cal_disp/workflow.py

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ def _build_event_mask(
3535
event_db: Path | None,
3636
mask_dir: Path,
3737
) -> Path | None:
38-
"""Generate and combine event/deformation masks from GeoJSON databases.
38+
"""Generate and combine event/deformation masks from GeoJSON database.
3939
40-
Runs ``generate_event_mask`` for each available GeoJSON, selects only
41-
features whose ``frame_id`` and ``event_date`` overlap the DISP product
42-
epoch, and ANDs the results into a single combined mask GeoTIFF
43-
(1 = valid, 0 = masked). Returns ``None`` when no database is provided.
40+
For each available GeoJSON, selects only features whose ``frame_id``
41+
and ``event_date`` overlap the DISP product epoch, and ANDs the results
42+
into a single combined mask GeoTIFF (1 = valid, 0 = masked).
43+
Returns ``None`` when no database is provided.
4444
"""
4545
from cal_disp.prep.generate_event_mask import generate_event_mask
4646

@@ -65,7 +65,6 @@ def _build_event_mask(
6565
if len(mask_paths) == 1:
6666
return mask_paths[0]
6767

68-
# AND all individual masks: a pixel is valid only if valid in every mask
6968
combined = mask_dir / "combined_event_mask.tif"
7069
with rasterio.open(mask_paths[0]) as src:
7170
combined_mask = src.read(1).astype(bool)
@@ -78,6 +77,30 @@ def _build_event_mask(
7877
return combined
7978

8079

80+
def _read_wavelength_m(disp_file: Path) -> float:
81+
"""Read the radar wavelength in meters from a DISP product file.
82+
83+
Returns ~0.05546 for Sentinel-1 C-band.
84+
"""
85+
from netCDF4 import Dataset # type: ignore[import-untyped]
86+
87+
try:
88+
with Dataset(disp_file, "r") as nc:
89+
ident_group = nc.groups["identification"]
90+
wl_m = float(ident_group.variables["radar_wavelength"][:])
91+
wl_units = getattr(ident_group.variables["radar_wavelength"], "units", "m")
92+
logger.debug(
93+
"Read radar_wavelength %.6f %s from %s", wl_m, wl_units, disp_file.name
94+
)
95+
return wl_m
96+
except Exception as e:
97+
msg = (
98+
f"Could not read /identification/radar_wavelength from {disp_file.name}. "
99+
"Ensure the file is a valid DISP product with an /identification group."
100+
)
101+
raise RuntimeError(msg) from e
102+
103+
81104
def _date_to_decimal_year(dt: datetime) -> float:
82105
"""Convert a datetime to a decimal year (e.g. 2022.55)."""
83106
year = dt.year
@@ -162,11 +185,7 @@ def _compute_gnss_los(
162185
cache_dir: Path,
163186
reference_frame: str,
164187
) -> np.ndarray:
165-
"""Return GNSS displacement projected into LOS (mm).
166-
167-
For the ``constant`` grid type the velocity field is computed once and
168-
cached; subsequent calls load from the cache.
169-
"""
188+
"""Return GNSS displacement projected into LOS in meters."""
170189
if grid_type == "constant":
171190
cache = cache_dir / f"gnss_los_velocity_{reference_frame}.npy"
172191
if cache.exists():
@@ -185,20 +204,24 @@ def _compute_gnss_los(
185204
)
186205
np.save(cache, gnss_velocity)
187206
logger.info("Cached GNSS LOS velocity to %s", cache)
188-
# Scale velocity (mm/yr) to the acquisition interval (yr)
189-
return gnss_velocity * (sec_decimal - ref_decimal)
207+
# Scale velocity (mm/yr) by acquisition interval (yr), then mm to m
208+
return gnss_velocity * (sec_decimal - ref_decimal) / 1000.0
190209

191210
# variable: epoch-specific displacement
192211
logger.info(
193212
"Computing GNSS LOS displacement (%.4f → %.4f)...", ref_decimal, sec_decimal
194213
)
195-
return gnss_ref.compute_displacement_los(
196-
ref_date=ref_decimal,
197-
sec_date=sec_decimal,
198-
los_east=los_east,
199-
los_north=los_north,
200-
los_up=los_up,
201-
netcdf_file=disp_file,
214+
# mm to m
215+
return (
216+
gnss_ref.compute_displacement_los(
217+
ref_date=ref_decimal,
218+
sec_date=sec_decimal,
219+
los_east=los_east,
220+
los_north=los_north,
221+
los_up=los_up,
222+
netcdf_file=disp_file,
223+
)
224+
/ 1000.0
202225
)
203226

204227

@@ -404,10 +427,10 @@ def run_calibration(
404427
reference_frame=cal.reference_frame,
405428
)
406429

407-
# Prepare displacement array (mm, 2-D)
430+
# Prepare displacement array.
408431
# A single DISP file encodes one (ref_date, sec_date) pair.
409432
# displacement is 2-D (y, x); time is a length-1 coordinate.
410-
disp_2d = ds_disp.displacement.values * 1000.0 # m → mm
433+
disp_2d = ds_disp.displacement.values.astype(np.float32)
411434

412435
# Build valid-pixel mask
413436
mask = ~np.isnan(disp_2d)
@@ -438,19 +461,20 @@ def run_calibration(
438461
if cal.unwrap_error_correction:
439462
from venti.unwrap import correct_region_offset
440463

441-
_WAVELENGTH_MM = 0.0555 / 2 * 1000 # S1 C-band half-wavelength
442-
logger.info("Applying unwrap-error correction...")
464+
wavelength_m = _read_wavelength_m(disp_file)
465+
logger.info(
466+
"Applying unwrap-error correction (wavelength=%.5f m)...",
467+
wavelength_m,
468+
)
443469
disp_masked = correct_region_offset(
444470
input_disp=disp_masked,
445471
mask=mask,
446-
wavelength=_WAVELENGTH_MM,
472+
wavelength=wavelength_m,
447473
)
448474
if isinstance(disp_masked, np.ma.MaskedArray):
449475
disp_masked = disp_masked.filled(np.nan)
450476

451477
# Optional tropospheric correction
452-
# Form the differential (secondary − reference) on the fly, matching the
453-
# DISP-S1 sign convention used in venti's calibration workflow.
454478
_tropo_applied = False
455479
if reference_tropo_files and secondary_tropo_files:
456480
if dem_file is None:
@@ -470,11 +494,11 @@ def run_calibration(
470494
output_dir=tropo_dir,
471495
)
472496
with rasterio.open(ref_tropo_path) as _src:
473-
ref_tropo_m = _src.read(1).astype(np.float32)
497+
ref_tropo = _src.read(1).astype(np.float32)
474498
with rasterio.open(sec_tropo_path) as _src:
475-
sec_tropo_m = _src.read(1).astype(np.float32)
476-
tropo_corr_mm = (sec_tropo_m - ref_tropo_m) * 1000.0 # m → mm
477-
disp_masked = np.where(mask, disp_masked - tropo_corr_mm, np.nan)
499+
sec_tropo = _src.read(1).astype(np.float32)
500+
tropo_corr = sec_tropo - ref_tropo # differential LOS delay (meters)
501+
disp_masked = np.where(mask, disp_masked - tropo_corr, np.nan)
478502
_tropo_applied = True
479503
logger.info("Tropospheric correction applied.")
480504

@@ -537,8 +561,8 @@ def run_calibration(
537561
if ds_factor > 1:
538562
cal_surface = upsample_array(cal_surface, original_shape)
539563

540-
# Convert back to metres and insert time dimension
541-
cal_surface_m = (cal_surface / 1000.0).astype(np.float32)
564+
# Insert time dimension
565+
cal_surface_m = cal_surface.astype(np.float32)
542566

543567
coords = {"time": time, "y": y, "x": x}
544568
calibration = xr.DataArray(

tests/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ def sample_disp_product(tmp_path: Path) -> Path:
110110
)
111111
filepath = tmp_path / filename
112112
ds.to_netcdf(filepath, engine="h5netcdf")
113+
114+
import h5py
115+
116+
with h5py.File(filepath, "a") as f:
117+
ident = f.create_group("identification")
118+
ident.create_dataset("radar_wavelength", data=np.float32(0.05546))
119+
ident["radar_wavelength"].attrs["units"] = "m"
120+
113121
return filepath
114122

115123

@@ -187,6 +195,13 @@ def sample_disp_product_with_corrections(tmp_path: Path) -> Path:
187195
ds.to_netcdf(filepath, engine="h5netcdf")
188196
ds_corr.to_netcdf(filepath, group="corrections", mode="a", engine="h5netcdf")
189197

198+
import h5py
199+
200+
with h5py.File(filepath, "a") as f:
201+
ident = f.create_group("identification")
202+
ident.create_dataset("radar_wavelength", data=np.float32(0.05546))
203+
ident["radar_wavelength"].attrs["units"] = "m"
204+
190205
return filepath
191206

192207

0 commit comments

Comments
 (0)