Skip to content

Commit be3c400

Browse files
authored
Merge pull request #25 from DHI/feature/swot_pixel
Add SWOT Pixel Cloud (PIXC) flow as fourth project direction; refactor: restructure README to consolidate duplications and document SWOT Pixel Cloud.
2 parents 6d5416f + 663412e commit be3c400

15 files changed

Lines changed: 1429 additions & 125 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,7 @@ run.py
154154
config.yaml
155155
config_river.yaml
156156
config_raster.yaml
157+
config_pixel_cloud.yaml
157158
notebooks/SWOT_Download.ipynb
158-
notebooks/swot_examples/*
159+
notebooks/swot_examples/*
160+
notebooks/swot_pixel_examples/*

HydroEO/cli/__init__.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def fetch_swot_raster(
258258
end: str = typer.Option(..., "--end", help="End date (YYYY-MM-DD)."),
259259
aoi_name: str = typer.Option("aoi", "--aoi-name", help="Label for the AOI."),
260260
product: str = typer.Option(
261-
"SWOT_L2_HR_PIXC_D", "--product", help="SWOT raster product short name."
261+
"SWOT_L2_HR_PIXC_2.0", "--product", help="SWOT raster product short name."
262262
),
263263
output: str = typer.Option(
264264
"hydroeo_output", "--output", "-o", help="Output directory (created if absent)."
@@ -300,6 +300,99 @@ def fetch_swot_raster(
300300
typer.echo(f"SWOT raster download complete. Output: {output}")
301301

302302

303+
# ---------------------------------------------------------------------------
304+
# fetch swot-pixc
305+
# ---------------------------------------------------------------------------
306+
307+
308+
@fetch_app.command("swot-pixc")
309+
def fetch_swot_pixc(
310+
bbox: str = typer.Option(
311+
...,
312+
"--bbox",
313+
help="Bounding box as a quoted string: 'MINLON MINLAT MAXLON MAXLAT'.",
314+
),
315+
start: str = typer.Option(..., "--start", help="Start date (YYYY-MM-DD)."),
316+
end: str = typer.Option(..., "--end", help="End date (YYYY-MM-DD)."),
317+
aoi_name: str = typer.Option("aoi", "--aoi-name", help="Label for the AOI."),
318+
product: str = typer.Option(
319+
"SWOT_L2_HR_PIXC_D", "--product", help="SWOT pixel cloud product short name."
320+
),
321+
output: str = typer.Option(
322+
"hydroeo_output", "--output", "-o", help="Output directory (created if absent)."
323+
),
324+
classes: Optional[str] = typer.Option(
325+
None,
326+
"--classes",
327+
help="Comma-separated water classes to include (e.g., 'open_water,water_near_land'). "
328+
"Default: open_water,water_near_land.",
329+
),
330+
fields: Optional[str] = typer.Option(
331+
None,
332+
"--fields",
333+
help="Comma-separated fields to grid (e.g., 'heightEGM,height'). Default: heightEGM.",
334+
),
335+
grid_resolution: int = typer.Option(
336+
100, "--grid-resolution", help="Grid resolution in meters. Default: 100."
337+
),
338+
stat_method: str = typer.Option(
339+
"median",
340+
"--stat-method",
341+
help="Binning statistic method (median, mean, min, max, count). Default: median.",
342+
),
343+
username: Optional[str] = typer.Option(
344+
None, "--username", help="EarthAccess username (or set EARTHACCESS_USERNAME)."
345+
),
346+
password: Optional[str] = typer.Option(
347+
None,
348+
"--password",
349+
help="EarthAccess password (or set EARTHACCESS_PASSWORD).",
350+
hide_input=True,
351+
),
352+
verbose: bool = typer.Option(
353+
False, "--verbose", "-v", help="Enable debug logging."
354+
),
355+
):
356+
"""Download SWOT pixel cloud data, preprocess, and grid to rasters.
357+
358+
Point cloud data is filtered by water class, extracted with heightEGM computation,
359+
and gridded to regular GeoTIFF rasters using specified statistics.
360+
"""
361+
_configure_logging(verbose)
362+
parsed_bbox = _parse_bbox(bbox)
363+
startdate = _parse_date(start, "--start")
364+
enddate = _parse_date(end, "--end")
365+
u, p = _resolve_earthaccess_creds(username, password)
366+
367+
# Parse optional classes and fields
368+
classes_list = (
369+
[c.strip() for c in classes.split(",")]
370+
if classes
371+
else ["open_water", "water_near_land"]
372+
)
373+
fields_list = [f.strip() for f in fields.split(",")] if fields else ["heightEGM"]
374+
375+
config = {
376+
"aoi": {
377+
"name": aoi_name,
378+
"type": "bbox",
379+
"bbox": parsed_bbox,
380+
},
381+
"product": product,
382+
"startdate": [startdate.year, startdate.month, startdate.day],
383+
"enddate": [enddate.year, enddate.month, enddate.day],
384+
"classes": classes_list,
385+
"fields": fields_list,
386+
"grid_resolution": grid_resolution,
387+
"stat_method": stat_method,
388+
}
389+
390+
from HydroEO.satellites.swot.pixc import download_pixc
391+
392+
download_pixc(config=config, project_dir=output, credentials=(u, p))
393+
typer.echo(f"SWOT pixel cloud download complete. Output: {output}")
394+
395+
303396
# ---------------------------------------------------------------------------
304397
# fetch swot-lake
305398
# ---------------------------------------------------------------------------

HydroEO/flows.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ def generate_reservoirs_summaries(
974974
reservoir_type=prj.reservoirs.type,
975975
show=show,
976976
save=save,
977+
products=getattr(prj, "to_process", None),
977978
)
978979

979980
logger.info("Summarizing merged results")

HydroEO/plotting.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def plot_cleaning(
145145
reservoir_type="reservoirs",
146146
show=True,
147147
save=False,
148+
products=None,
148149
):
149150
"""Plot cleaning progression for a reservoir (unfiltered, cleaned, merged).
150151
@@ -166,6 +167,8 @@ def plot_cleaning(
166167
Whether to display the plot.
167168
save : bool
168169
Whether to save the plot to PNG.
170+
products : list, optional
171+
List of products to filter on. Passed to get_*_fn functions.
169172
"""
170173
sns.set()
171174
cmap = cm.batlow.resampled(5)
@@ -181,7 +184,7 @@ def plot_cleaning(
181184
fig.suptitle(f"{reservoir_type}: {reservoir_id}")
182185

183186
# plot unfiltered timeseries
184-
df = get_unfiltered_fn(reservoir_id)
187+
df = get_unfiltered_fn(reservoir_id, products)
185188
if df is not None:
186189
df = df[["date", "height", "platform", "product"]]
187190

@@ -207,7 +210,7 @@ def plot_cleaning(
207210
ax.tick_params(axis="x", rotation=45)
208211

209212
# now plot cleaned timeseries
210-
df = get_cleaned_fn(reservoir_id)
213+
df = get_cleaned_fn(reservoir_id, products)
211214
df = df[["date", "height", "platform", "product"]]
212215

213216
ax = main_ax[1]

HydroEO/project.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from HydroEO.waterbody import Reservoirs, Rivers
1515
from HydroEO import flows
1616
from HydroEO.satellites.swot.raster import download_raster
17+
from HydroEO.satellites.swot.pixc import download_pixc
1718
from HydroEO.utils import general
1819
from HydroEO.constants import MISSION_DEFAULTS
1920
from HydroEO.validation import validate_config
@@ -217,6 +218,12 @@ def __post_init__(self):
217218
# Will be instantiated in download() when needed
218219
self.swot_raster_config = self.config["swot_raster"]
219220

221+
if "swot_pixc" in self.config.keys() and self.config["swot_pixc"].get(
222+
"enabled", True
223+
):
224+
# Store the SWOT Pixel Cloud config for later use in download/preprocess
225+
self.swot_pixc_config = self.config["swot_pixc"]
226+
220227
### make sure we have a local crs (If we were not able to set it up from the config, grab it from one of the elements)
221228
if self.local_crs is None:
222229
if hasattr(self, "rivers"):
@@ -242,6 +249,9 @@ def __post_init__(self):
242249
elif hasattr(self, "swot_raster_config"):
243250
# For swot_raster, use global CRS as local if no local CRS specified
244251
self.local_crs = self.global_crs
252+
elif hasattr(self, "swot_pixc_config"):
253+
# For swot_pixc, use global CRS as local if no local CRS specified
254+
self.local_crs = self.global_crs
245255
else:
246256
raise UserWarning(
247257
"Must provide a local crs or a river or reservoir shapefile to determine local crs"
@@ -371,6 +381,12 @@ def download(self):
371381
credentials=(self.earthdata_user, self.earthdata_pass),
372382
global_crs=self.global_crs,
373383
)
384+
if hasattr(self, "swot_pixc_config"):
385+
download_pixc(
386+
config=self.swot_pixc_config,
387+
project_dir=self.dirs["main"],
388+
credentials=(self.earthdata_user, self.earthdata_pass),
389+
)
374390

375391
def update(self):
376392
# get the current date of the system

0 commit comments

Comments
 (0)