Lavaca Bay SFINCS Tutorial¶
This notebook demonstrates how to build and run a
SFINCS coastal flood model for
Lavaca Bay, Texas using the coastal_calibration Python API.
The workflow has three phases:
- Create — build a SFINCS model from an Area of Interest (AOI) polygon using HydroMT-SFINCS.
- Run — execute the full simulation pipeline: download forcing data, write SFINCS input files, run the model, produce a downscaled flood depth map, and compare results against NOAA tide-gauge observations.
- Visualize — plot the flood depth map and station comparisons.
Setup¶
from __future__ import annotations
import os
from pathlib import Path
notebook_dir = Path.cwd() # assumes notebook is run from docs/examples/notebooks/
os.chdir(notebook_dir.parent / "lavaca-tx")
from coastal_calibration import SfincsCreateConfig, SfincsCreator, configure_logger
configure_logger(level="INFO")
create_config = SfincsCreateConfig.from_dict(
{
"aoi": "./aoi.geojson",
"output_dir": "./output",
"download_dir": "../downloads/lavaca_grid",
"grid": {
"resolution": 512,
"crs": "utm",
"rotated": False,
"refinement": [
{"polygon": "./refine.geojson", "level": 3},
],
},
"elevation": {
"datasets": [
{"name": "noaa_3m", "zmin": -20000, "source": "noaa_3m"},
{"name": "gebco_15arcs", "zmin": -20000, "source": "gebco_15arcs"},
],
"buffer_cells": 1,
},
"mask": {"zmin": -50.0, "boundary_zmax": -1.0, "reset_bounds": True},
"subgrid": {
"nr_subgrid_pixels": 4,
"lulc_dataset": "esa_worldcover",
"manning_land": 0.04,
"manning_sea": 0.02,
},
"river_discharge": {
"flowlines": "./discharge_nwm.geojson",
"nwm_id_column": "flowpath_id",
},
"add_noaa_gages": True,
}
)
Run the create workflow¶
creator = SfincsCreator(create_config)
result = creator.run()
if not result.success:
raise RuntimeError(f"Model creation failed at stage '{result.stages_failed}': {result.errors}")
print(result)
========================================
Coastal Calibration Workflow
Start Time: 2026-03-25 11:57:27
========================================
----------------------------------------
Stage: create_grid
Start Time: 2026-03-25 11:57:27
Create SFINCS grid from AOI
AOI: docs/examples/lavaca-tx/aoi.geojson
Resolution: 512 m, CRS: utm
Refinement: 1 polygon(s), max level 3
Grid created successfully
[✓] COMPLETED (1s)
----------------------------------------
Stage: create_fetch_data
Start Time: 2026-03-25 11:57:28
Fetch elevation and land cover data for AOI
Reusing existing noaa_3m.tif
Reusing existing gebco_15arcs.tif
Reusing existing esa_worldcover.tif
Fetched 3 dataset(s): ['noaa_3m', 'gebco_15arcs', 'esa_worldcover']
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_elevation
Start Time: 2026-03-25 11:57:28
Add elevation and bathymetry data
Elevation datasets: ['noaa_3m', 'gebco_15arcs']
Elevation created successfully
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_mask
Start Time: 2026-03-25 11:57:29
Create active cell mask
zmin=-50.0
Active mask created successfully
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_boundary
Start Time: 2026-03-25 11:57:29
Create water level boundary cells
boundary_zmax=-1.0
Boundary cells created successfully
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_discharge
Start Time: 2026-03-25 11:57:29
Add river discharge source points
Read 4 flowpath(s) from discharge_nwm.geojson
9356656: snapped to active cell (192 m away)
9356634: snapped to active cell (155 m away)
9349399: snapped to active cell (327 m away)
7843181: snapped to active cell (291 m away)
Wrote 4 discharge source location(s) to sfincs_nwm.src
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_subgrid
Start Time: 2026-03-25 11:57:29
Create subgrid tables
nr_subgrid_pixels=4
Subgrid tables created successfully
[✓] COMPLETED (30s)
----------------------------------------
Stage: create_obs
Start Time: 2026-03-25 11:58:00
Add observation points
Loading cached station metadata from cache/coops_stations_metadata.json
Fetching datum information for 4 station(s)
Fetching data from 4 station(s)
Added 4 NOAA CO-OPS observation point(s)
noaa_8773037: no wet cell within 1000 m (nearest 9976 m away)
noaa_8773259: placed at face center z=-1.002 m (314 m from original)
noaa_8773701: placed at face center z=-3.194 m (32 m from original)
noaa_8773767: placed at face center z=-10.451 m (33 m from original)
Snapped 3 observation point(s) to nearest wet cell
[✓] COMPLETED (0s)
----------------------------------------
Stage: create_write
Start Time: 2026-03-25 11:58:00
Write SFINCS model to disk
Output directory:
docs/examples/lavaca-tx/output
Model written successfully
[✓] COMPLETED (0s)
----------------------------------------
========================================
Workflow COMPLETED | Total Duration: 0:00:33
========================================
Timing Summary:
----------------------------------------
[✓] create_grid: 1s
[✓] create_fetch_data: 0s
[✓] create_elevation: 0s
[✓] create_mask: 0s
[✓] create_boundary: 0s
[✓] create_discharge: 0s
[✓] create_subgrid: 30s
[✓] create_obs: 0s
[✓] create_write: 0s
WorkflowResult: SUCCESS Start: 2026-03-25 11:57:27 End: 2026-03-25 11:58:00 Duration: 33s Completed: create_grid, create_fetch_data, create_elevation, create_mask, create_boundary, create_discharge, create_subgrid, create_obs, create_write
Inspect the created model¶
output = Path("output")
assert output.exists(), (
f"Output directory not found: {output.resolve()} — run the create step first."
)
for f in sorted(output.iterdir()):
if f.name.startswith(".") or f.suffix == ".log":
continue
size = f.stat().st_size
label = f"{size / 1e6:.1f} MB" if size > 1e6 else f"{size / 1e3:.1f} KB"
print(f" {f.name:<30s} {label}")
create_progress.json 2.2 KB create_result.json 1.3 KB gis 0.1 KB obs_station_map.json 0.5 KB sfincs.inp 0.9 KB sfincs.nc 15.2 MB sfincs.obs 0.2 KB sfincs_nwm.src 0.1 KB sfincs_subgrid.nc 23.0 MB subgrid 0.2 KB
from coastal_calibration import CoastalCalibConfig, CoastalCalibRunner
run_config = CoastalCalibConfig.from_dict(
{
"model": "sfincs",
"simulation": {
"start_date": "2025-06-01",
"duration_hours": 100,
"coastal_domain": "atlgulf",
"meteo_source": "nwm_ana",
},
"boundary": {"source": "stofs"},
"paths": {
"work_dir": "./run",
"raw_download_dir": "../downloads",
},
"download": {"enabled": True},
"model_config": {
"prebuilt_dir": "./output",
"discharge_locations_file": "./output/sfincs_nwm.src",
"merge_discharge": True,
"forcing_to_mesh_offset_m": 0.0, # STOFS already in NAVD88
"vdatum_mesh_to_msl_m": 0.17, # NAVD88 mesh -> MSL
"include_precip": True,
"include_wind": True,
"include_pressure": True,
"inp_overrides": {
"tspinup": 10800,
"advection": 0,
"viscosity": 0,
"nuvisc": 0.01,
"cdnrb": 3,
"cdwnd": [0.0, 28.0, 50.0],
"cdval": [0.001, 0.0025, 0.0025],
},
# Flood depth map — path to a high-resolution DEM.
# Here we reuse the NOAA 3m DEM fetched during model creation.
"floodmap_dem": "../downloads/lavaca_grid/noaa_3m.tif",
},
}
)
Note on the SFINCS executable¶
The sfincs_exe field overrides the default PATH lookup for the SFINCS binary.
When running inside a pixi environment with the sfincs feature, the binary
is compiled automatically and available on PATH — no sfincs_exe needed.
If you compiled SFINCS manually, set sfincs_exe to the path of the binary.
If neither is available, the pipeline will complete all stages up to
sfincs_run and then fail at model execution.
Run the pipeline¶
runner = CoastalCalibRunner(run_config)
result = runner.run()
if not result.success:
raise RuntimeError(f"Model run failed at stage '{result.stages_failed}': {result.errors}")
print(result)
Cleaned generated files from
docs/examples/lavaca-tx/run
========================================
Coastal Calibration Workflow
Start Time: 2026-03-25 11:58:00
========================================
----------------------------------------
Stage: download
Start Time: 2026-03-25 11:58:00
Download input data (NWM, STOFS)
meteo/nwm_ana: 101/101 [OK]
hydro/nwm: 101/101 [OK]
coastal/stofs: 1/1 [OK]
Total: 203/203 (failed: 0)
Download complete — raw files stored in
docs/examples/downloads
[✓] COMPLETED (10s)
----------------------------------------
Stage: sfincs_symlinks
Start Time: 2026-03-25 11:58:11
Create .nc symlinks for NWM data
Skipped 213 meteo + 0 streamflow symlinks (already exist)
[✓] COMPLETED (0s)
----------------------------------------
Stage: sfincs_data_catalog
Start Time: 2026-03-25 11:58:11
Generate HydroMT data catalog for SFINCS
Data catalog written to
docs/examples/lavaca-tx/run/data_catalog.y
ml
[✓] COMPLETED (0s)
----------------------------------------
Stage: sfincs_init
Start Time: 2026-03-25 11:58:11
Initialize SFINCS model (pre-built)
Copied pre-built model from
docs/examples/lavaca-tx/output to
docs/examples/lavaca-tx/run/sfincs_model
Removed stale output files: sfincs_netbndbzsbzifile.nc, sfincs_netamuv.nc,
sfincs_netamp.nc, sfincs_netampr.nc, sfincs_map.nc, sfincs_his.nc
SFINCS model initialized (grid_type=quadtree) at
docs/examples/lavaca-tx/run/sfincs_model
[✓] COMPLETED (0s)
----------------------------------------
Stage: sfincs_timing
Start Time: 2026-03-25 11:58:12
Set SFINCS timing
Spinup: 3600 s
Timing set: 2025-06-01 00:00:00 to 2025-06-05 04:00:00
[✓] COMPLETED (0s)
----------------------------------------
Stage: sfincs_forcing
Start Time: 2026-03-25 11:58:12
Add water level forcing
Read 143 boundary point(s) from
docs/examples/lavaca-tx/run/sfincs_model/s
fincs.bnd
Loaded stofs_waterlevel: time=101, node=98922
Interpolated stofs_waterlevel to 143 boundary points (101 time steps)
Wrote boundary forcing to sfincs_netbndbzsbzifile.nc
Water level forcing added from stofs_waterlevel
[✓] COMPLETED (19s)
----------------------------------------
Stage: sfincs_discharge
Start Time: 2026-03-25 11:58:31
Add discharge sources
Added 4 discharge source point(s) from
docs/examples/lavaca-tx/output/sfincs_nwm.
src
Assigned discharge timeseries to 4 point(s) (4 unique feature_id(s))
[✓] COMPLETED (7s)
----------------------------------------
Stage: sfincs_precip
Start Time: 2026-03-25 11:58:38
Add precipitation forcing
Precipitation forcing added from nwm_ana_meteo (res=512 m)
[✓] COMPLETED (2s)
----------------------------------------
Stage: sfincs_wind
Start Time: 2026-03-25 11:58:41
Add wind forcing
Wind forcing added from nwm_ana_meteo (res=512 m)
[✓] COMPLETED (3s)
----------------------------------------
Stage: sfincs_pressure
Start Time: 2026-03-25 11:58:45
Add atmospheric pressure forcing
Atmospheric pressure forcing added from nwm_ana_meteo (baro=1, res=512 m)
[✓] COMPLETED (2s)
----------------------------------------
Stage: sfincs_write
Start Time: 2026-03-25 11:58:48
Write SFINCS model
Applied 7 sfincs.inp override(s): {'tspinup': 10800, 'advection': 0,
'viscosity': 0, 'nuvisc': 0.01, 'cdnrb': 3, 'cdwnd': [0.0, 28.0, 50.0], 'cdval':
[0.001, 0.0025, 0.0025]}
SFINCS model written to
docs/examples/lavaca-tx/run/sfincs_model
[✓] COMPLETED (0s)
----------------------------------------
Stage: sfincs_run
Start Time: 2026-03-25 11:58:48
Run SFINCS model
Running SFINCS via native executable:
.pixi/envs/dev/bin/sfincs
SFINCS run completed
[✓] COMPLETED (7m 49s)
----------------------------------------
Stage: sfincs_floodmap
Start Time: 2026-03-25 12:06:37
Downscale flood depth map
Loaded zsmax from
docs/examples/lavaca-tx/run/sfincs_model/s
fincs_map.nc
Creating index COG:
docs/examples/lavaca-tx/run/sfincs_model/f
loodmap_index.tif
Index COG created (29.7 MB)
Downscaling flood depth map
GeoTIFF has no overviews, building them: floodmap_hmax.tif
Building 6 overview levels: [2, 4, 8, 16, 32, 64]
Flood depth map written:
docs/examples/lavaca-tx/run/sfincs_model/f
loodmap_hmax.tif (543.5 MB)
Flood depth map:
docs/examples/lavaca-tx/run/sfincs_model/f
loodmap_hmax.tif
[✓] COMPLETED (4m 7s)
----------------------------------------
Stage: sfincs_plot
Start Time: 2026-03-25 12:10:45
Plot simulated vs observed water levels
Loading cached station metadata from cache/coops_stations_metadata.json
Fetching datum information for 4 station(s)
Fetching data from 4 station(s)
Matched 4 observation point(s) to NOAA station(s): 8773037, 8773259, 8773701,
8773767
Applied mesh→MSL vdatum offset: +0.1700 m
Loading cached station metadata from cache/coops_stations_metadata.json
Requesting water_level data for 4 station(s) from 20250601 00:00 to 20250605
04:00
Fetching data from 4 station(s)
Successfully retrieved data for 4 station(s)
Loading cached station metadata from cache/coops_stations_metadata.json
Fetching datum information for 4 station(s)
Fetching data from 4 station(s)
Saved 1 comparison figure(s) to
docs/examples/lavaca-tx/run/sfincs_model/f
igs
[✓] COMPLETED (2s)
----------------------------------------
========================================
Workflow COMPLETED | Total Duration: 0:12:46
========================================
Timing Summary:
----------------------------------------
[✓] download: 10s
[✓] sfincs_symlinks: 0s
[✓] sfincs_data_catalog: 0s
[✓] sfincs_init: 0s
[✓] sfincs_timing: 0s
[✓] sfincs_forcing: 19s
[✓] sfincs_discharge: 7s
[✓] sfincs_precip: 2s
[✓] sfincs_wind: 3s
[✓] sfincs_pressure: 2s
[✓] sfincs_write: 0s
[✓] sfincs_run: 7m 49s
[✓] sfincs_floodmap: 4m 7s
[✓] sfincs_plot: 2s
WorkflowResult: SUCCESS Start: 2026-03-25 11:58:00 End: 2026-03-25 12:10:47 Duration: 12m 47s Completed: download, sfincs_symlinks, sfincs_data_catalog, sfincs_init, sfincs_timing, sfincs_forcing, sfincs_discharge, sfincs_precip, sfincs_wind, sfincs_pressure, sfincs_write, sfincs_run, sfincs_floodmap, sfincs_plot
3. View results¶
The pipeline generates station comparison plots (modeled vs. observed water levels at NOAA CO-OPS tide gauges).
from IPython.display import Image, display
figs_dir = Path("run/sfincs_model/figs")
assert figs_dir.exists(), f"Results not found: {figs_dir.resolve()} — run the pipeline first."
for png in sorted(figs_dir.glob("stations_comparison_*.png")):
display(Image(filename=str(png), width=800))
4. SFINCS mesh¶
The SFINCS model uses a quadtree grid with local refinement. Coarser cells (512 m) cover the offshore domain while regions near the coastline and inside the bay are refined to smaller cell sizes (down to 64 m).
from coastal_calibration.plotting import SfincsGridInfo, plot_floodmap, plot_mesh
info = SfincsGridInfo.from_model_root("run/sfincs_model")
print(info)
SfincsGridInfo(quadtree, EPSG:32614) Faces: 70,723 Edges: 142,599 Level 1: 23,471 cells (512 m) Level 2: 545 cells (256 m) Level 3: 1,087 cells (128 m) Level 4: 45,620 cells (64 m)
fig, ax = plot_mesh(info, title="Lavaca Bay SFINCS mesh")
5. Flood depth map¶
The pipeline automatically produces a downscaled flood depth map when
floodmap_dem is configured. The sfincs_floodmap stage reads the
maximum water surface elevation (zsmax) from the SFINCS map output,
builds an index COG mapping DEM pixels to SFINCS grid cells, and
writes a Cloud Optimized GeoTIFF of flood depth at the DEM resolution.
fig, ax = plot_floodmap(
"run/sfincs_model/floodmap_hmax.tif",
title="Max water depth, Lavaca Bay, TX",
)
fig.savefig("../images/lavaca_thumb.png", dpi=150, bbox_inches="tight")
The flood depth COG can be opened in QGIS or any GIS viewer. You can also generate a flood depth map outside the pipeline using the standalone function:
from coastal_calibration.utils.floodmap import create_flood_depth_map
create_flood_depth_map(
model_root="run/sfincs_model",
dem_path="../downloads/lavaca_grid/noaa_3m.tif",
)
Summary¶
This notebook demonstrated the full Lavaca Bay SFINCS workflow via the Python API:
SfincsCreateConfig.from_dict({...})+SfincsCreator(config).run()— built the model from an AOICoastalCalibConfig.from_dict({...})+CoastalCalibRunner(config).run()— downloaded data, ran SFINCS, and compared results against NOAA observations- Inspected the quadtree mesh and its refinement levels
- Visualized the downscaled flood depth map (
floodmap_hmax.tif)