Refactor and extend NetCDF input support for topography and storm surge (met) forcing#701
Refactor and extend NetCDF input support for topography and storm surge (met) forcing#701mandli wants to merge 58 commits into
Conversation
|
@kbarnhart This may be useful for some of the work you are doing to read in data for precipitation into GeoClaw. While not currently listed, precipitation is I think the most likely next variable field that I would add, may be a good test. |
|
@mandli Thanks for working on this! I'm tied up this week but will try to go through it soon. |
I have a bunch more to explain in this PR on why, what, and how it was implemented coming so no rush. I also am trying to get a docs PR to accompany this. UPDATE - Added what changed and is now addressed. Still working on the doc PR and will link it here when issued. |
Includes better support for discovery of variables, coordinate mapping, cropping of data files, and more robust reading overall.
Adds 3 tests to bowl-slosh-netcdf to test dimension ordering, CF compliance, and cropping. Lat-long coordinate utilities will be added to Chile 2010. Also includes another bug fix for mixing up dimension and variable meta-data inquiries in topo_module.f90.
Includes a basic test and more NetCDF tests for writing NetCDF files from topotools, flipping latitude writing, and wrapping longitude.
Adds additional tests to the isaac test for the new NetCDF capabilities.
This allows to use dask if it is available, but then uses the generic NetCDF backend if it is not and does not do lazy loading. This is probably not generally an issue for any use cases I have seen.
CI testing revealed that of course dtopo was not being generated for this test. Written to allow future testing of dtopo variants as well.
This allows for more ways to specify variables and remove the duplicative aspects of the met forcing that was present. Much more robust time handling as well without assuming epochs.
Replaced with simply checking for minimal dims and vars instead
Added a dict of common bathy variable names to fallback on if not provided, but still allows for override.
This allows wrapping topo files in longitudinal direction
Needed to remove the isaac.storm file being written directly during the run of setrun.py and instead only do so when called from `__main__`.
This was not finishing in a reasonable amount of time in the config tested. Lowered this to max levels = 4 for now.
Adds parameter to storm objects that allow for the time scale of the storm to be changed. This is useful for testing and for running idealized cases of storms that may be slower or faster than provided in the input data. This also adds basic tests in the Python to support this functionality.
This brings the met forcing input up to par with the functionality added for the topography input.
Created a type-4 writer, changed to NaN sentinels.
Uses matplotlib functionality to do so
Will give a warning if these are provided and do not match.
Coordinate-agnostic refactor (Phases 1-2): rename lat/lon-specific identifiers and descriptor keys to neutral x/y across Python and Fortran so non-geographic grids (e.g. projected/rectilinear WRF output in meters) are first-class, not assumed to be lat/lon. Python (netcdf_utils + consumers): - FileMetadata.lon_name/lat_name -> x_name/y_name; lon_convention -> lon_wrap; lat_order (str 'N_to_S'/'S_to_N') -> y_increasing (bool); lon_offset -> lon_wrap_offset. - _find_lon/lat_name -> _find_x/y_name; _detect_lon_convention -> _detect_lon_wrap; _detect_lat_order -> _detect_y_increasing. - dim_order role tokens 'lon'/'lat' -> 'x'/'y'; all 3 descriptor writers updated; topotools/dtopotools/storm/util/data consumers. Fortran (topo_module, data_storm_module): - nc_/dnc_ coordinate arrays and descriptor parser keys renamed to match; nc_y_increasing is a logged-only logical; lon_wrap drives the storm 0-360 longitude normalization. Only the genuinely-geographic 0-360 longitude wrap keeps lon naming. CF discovery strings (standard_name, fallback names, units) preserved. Tests updated; type-4 topo/dtopo/met end-to-end all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0-360 longitude wrap (lon_wrap / lon_wrap_offset) only makes sense for a geographic longitude axis. _detect_lon_wrap now returns None for a projected/rectilinear x axis, detected by positive evidence: length units (m/km), a projection standard_name, or coordinate values outside the plausible degree range [-360, 360]. Unit-less lon/lat axes still default to geographic, so existing files are unaffected. When lon_wrap is None: - topo_entries() restricts _compute_lon_entries to the identity offset (allow_wrap=False): a crop is taken straight from the file extent with no +/-360 wrap attempt. - write_met_descriptor omits the lon_wrap line; Fortran's default (180) is the correct no-wrap behavior. Adds projected-axis inspector tests (units-based, value-range-based) and a regression test that an unit-less [0,360] axis still wraps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parallel to the existing x_shift, add a y-direction registration shift for both topography and dtopography, and enable x_shift for dtopography (it previously raised NotImplementedError). These are constant coordinate offsets (domain = file + shift) applied to all file types. The per-file preprocessing block grows from 7 to 8 lines: y_shift is inserted immediately after x_shift (order: crop_extent, coarsen, buffer, align, x_shift, y_shift, z_shift, negate_z), consistently in the Python writer, DTopoData.read parser (10-line file block), and both Fortran readers (read_topo_settings, read_dtopo_settings). Python: Topography/DTopography gain y_shift; read() applies it to the y coordinate; dtopo now applies x_shift/y_shift to x/X and y/Y; guards and the topo_type=1 check updated. Fortran: tp_y_shift applied to ylowtopo/yhitopo, the types-2/3 and type-4 crop y-bound conversions, the align y component, and type-4 ylocs. New dtp_x_shift/dtp_y_shift applied to xlow/xhi/ylow/yhidtopo. Tests: unit round-trips updated; bowl-slosh y_shift identity e2e and a dtopo x/y-shift identity e2e (Fortran apply path) added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parallel to topo/dtopo, the data-storm forcing grid can now be registered onto the domain with x_shift/y_shift (domain = file + shift). This lets a projected or mis-registered met grid be placed correctly. - Storm gains x_shift/y_shift; write_data emits two descriptor lines after storm_time_scale; Storm.read parses them. - data_storm_module: met_x_shift/met_y_shift read from the descriptor and applied to the full coordinate arrays (after the geographic lon_wrap normalization) before cropping, for both OWI/ASCII and NetCDF paths. - Regenerated the committed owi_ascii/isaac.storm fixture for the new 2-line-longer descriptor. Tests: .storm round-trip covers x_shift/y_shift; new e2e test_isaac_netcdf_shift_identity offsets the ERA5 grid by (+1.5,+0.75) and cancels it via the descriptor, matching the unshifted owi_ascii baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Old functionality inadvertently worked and new functionality revealed that stricter rules broke the reading of met data. This is now fixed and better gaurds put into place.
DTopoInspector._compute_time_axis only special-cased datetime64 time coordinates; a bare-duration units="seconds" coordinate (as written by DTopography._write_netcdf) can decode via xarray into timedelta64, and casting that straight to float reinterpreted the raw nanosecond tick count as seconds -- a billion-fold inflation of t0dtopo. For a single-frame dtopo file (e.g. chile2010's t=1s deformation), that meant the simulated time never reached t0dtopo, so the coseismic deformation was never applied, and since topo_finalized never flips true, dt_max_dtopo pinned the timestep for the entire run. Fix by opening with decode_timedelta=False and adding a defensive timedelta64 conversion branch. Also clear a stray _FillValue=NaN that xarray's default encoding was writing onto coordinate variables (lat/ lon/time) in both the dtopo and topo netCDF writers -- invalid for a monotonic axis. Separately, default dz/elevation to float32 on disk in both writers: validated numerically that float32 gives sub-millimeter precision across the full elevation range, including Mariana-trench depths, roughly halving netCDF file sizes. In-memory arrays stay float64. Add regression coverage: a unit test reproducing the timedelta64 cast bug directly, dtype assertions for the float32 default, and a remote chile2010 test that writes the real dtopo as netCDF and runs the full Fortran read path end to end (fails without the fix -- flat gauge, dt=0.2 pinned for the whole run -- passes with it).
a831b75 to
3522890
Compare
NetCDF4-python's internal data.shape = ... reshape, deprecated by numpy 2.5. Wired around it to catch this in the netCDF4 code that was throwing this warning.
- Now requires declared units for netCDF input files. - Also adds time reference for dtopo files in netCDF. - Adds control over dtypes for topo and dtopo netCDF files. Note: This may currently break backwards compatibility due to the unit requirements. This needs to be addressed as to what the best approach is to handle units and when there are inconsistencies, either implied or explicit.
| # Prefer netcdf4, fall back to h5netcdf, else leave as None so xarray | ||
| # guesses (and raises its own clear error if no engine is installed). | ||
| engine = next( | ||
| (name for name in ("netcdf4", "h5netcdf") |
There was a problem hiding this comment.
I had to change line 260 to
(name.lower() for name in ("netCDF4", "h5netcdf")
to get this to work, since netCDF4 is the name of the module I have installed but xarray expects engine="netcdf4" lowercase.
There was a problem hiding this comment.
Good catch, strange it was working for me and I have netCDF4 installed.
|
Is it possible to include compression when writing netCDF files? I tried an example taking an dtopo file in |
Yes, there is, although there are some pitfalls with using some of the libraries that may be required. Let me look into it and at least expose an interface that will do it and we can decide whether we want to default to that or not. |
Supersede the strict rejection of non-contract units with automatic conversion, on both read paths. Python computes a multiplicative scale_factor (topo/dtopo elevation, met wind/pressure) and, for the met time axis, a time_scale (seconds per file time unit) via units.convert(); the in-memory readers (Topography.read / DTopography.read, *_type=4) apply it directly, and Fortran applies it on the descriptor path used by a running simulation. All unit logic stays in Python; Fortran is a dumb multiplier. Defaults of 1.0 leave every contract-unit file bit-identical (regression suite is the guardrail). Missing units still raise (with the assume_units override) and unrecognized or dimensionally-incompatible units still raise -- only recognized units convert. A real ERA5 file (hPa pressure, "hours since ..." time) now runs directly instead of being rejected. Fortran: nc_scale_factor (topo_module.f90), dnc_scale_factor (dtopo), var_scale + nc_time_scale (data_storm_module.f90); the met time_scale is distinct from the existing physical storm_time_scale (they compose). Tests: reject-tests flipped to convert-tests; descriptor-writer asserts the new keys; end-to-end regression variants prove the converted run matches the contract baseline bit-for-bit -- bowl-slosh km topo, chile2010 km dtopo, and isaac met in hPa (variable scale) and hours-since (time scale). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a met NetCDF variable has no CF 'units' attribute, use the storm
format's documented unit instead of raising: MetInspector gains a
format_units {role: unit} option, consulted before assume_units, then
converted via the existing scale_factor path (e.g. NWS13/OWI pressure
'mbar' -> Pa, scale 100). Storm._met_format_units maps nws13/owi to
{wind m/s, pressure mbar} and Storm.write_data passes it. CF/ERA5 files,
which carry their own units, are unaffected. Python-only.
Warn+auto-correct hPa-mislabeled-as-Pa; raise on implausible elevation/ wind/pressure (skip_sanity_check opts out). Remove the dead allow_conversion param so inspectors always convert, fix stale reject-and-pre-convert docs, and add a met hPa + "hours since" regression.
Case matters as it turns out, and xarray was being picky about what we were handing it. Thanks to @rjleveque for finding this one.
Add a compression= option to Topography/DTopography NetCDF writers (None default = uncompressed/bit-identical; True = zlib level 1 + shuffle) via netcdf_utils.compression_encoding. Reads are transparent.
If only float32 is used when writing netCDF files, the resulting may not have enough precision on a lat/lon grid that is on the order of the finest topo files now. Dtopo was already forcing this, topo now matches.
This PR replaces the partial, ad-hoc NetCDF support that existed for
topo_type=4with a complete, robust NetCDF input pipeline covering both topography and gridded met forcing for storm surge including at least partially what is described in #699. The new infrastructure handles CF convention discovery, coordinate normalization, unit enforcement, and descriptor-based dispatch in a way that is consistent across both input types and extensible to future fields (precipitation, friction). New documentation can be found at clawpack/doc#240.New Python Module
netcdf_utils.pyAdded a new python module
src/python/geoclaw/netcdf_utils.pythat provides:NetCDFInterrogator-- base class for CF-aware file inspection: coordinate discovery, lon convention detection ([0,360] vs [-180,180]), lat order detection (N-to-S vs S-to-N), dimension order discovery, fill value resolution (_FillValuevsmissing_valuewith correct CF precedence), and crop bound validation. Operates lazily via xarray -- no data arrays are loaded during interrogation.TopoInterrogator(NetCDFInterrogator)-- adds fill value detection within the crop region (hard error -- silent NaN in bathymetry is a correctness hazard), unit verification and conversion to the GeoClaw contract (meters, positive up).MetInterrogator(NetCDFInterrogator)-- adds multi-variable synchronization checks (wind_u, wind_v, pressure on the same grid and time axis), unit validation and conversion (pressure to Pa, wind to m/s), CF time decoding to seconds from a user-defined offset, and ensemble dimension detection.DescriptorWriter-- serializes interrogator output to the descriptor format consumed by Fortran: inline key=value lines intopo.datafor topography, and the body of*.stormfiles for met forcing.CFNormalizer-- standalone utility for repairing CF metadata (standard_name, axis, units, _FillValue) on existing NetCDF files without resampling or reprojecting. Idempotent.Modifications
units.pythat has aGEOCLAW_NETCDF_UNITSdictionary defining the units that Fortran expects.Python enforces this contract during interrogation. Fortran assumes it without checking. The contract is documented centrally here and referenced from
netcdf_utils.py. May want in the future to leverage a specific library for this. We could also extend this to be THE units contract for GeoClaw.Extension of
topo_type = 4. This required 2 changes:a. Extended
data.pyto understand thattopo_type=4may have additional descriptors after the usual line of topo type and path. These lines carry information fromNetCDFInterrogatorincluding variable and coordinate names, coordinate conventions (e.g. order, wrapping), fill value, and cropping. This allows Fortran to simply read these values in and assume they are cogent. This also allows topography files to remain as is without pre-processing.b. Modification of
topo_module.f90had two sets of changes:Bug fixes in existing NetCDF implementation:
New capabilities: Primarily the Fortran now reads the descriptor lines for
topo_type=4that come fromDescriptorWrite/data.py.start/countarguments fornf90_get_varfrom crop bounds, enabling domain subsetting without loading the full filenf90_*call that fails, suitable for SLURM batch job logsFortran reader still assumes the unit contract from
units.pyand no unit conversion is applied in Fortran.Modified
utils.pyso thatget_netcdf_namesis consistent with newNetCDFInterrogator. This is still a parallel implementation and will be deprecated (see known issues).Tests
Addded tests:
All these are marked as
netcdftests and are run with the existing NetCDFbased tests. Open question whether a couple of the regression tests take too
long. WRF variant is skipped pending
MetPreprocessorimplementation.Full pytest suite for
netcdf_utils.py:conftest.py-- in-memory NetCDF fixtures covering all coordinate variants (lon convention, lat order, dim order), fill value variants (_FillValue only, missing_value only, both present/agreeing, both present/conflicting, neither), unit variants for met forcing, and bad/edge casestest_base_interrogator.py-- coordinate detection, fill value resolution, crop validation, laziness assertiontest_topo_interrogator.py-- fill-in-crop detection, unit paths, descriptor output correctness across coordinate variantstest_met_interrogator.py-- multi-variable sync, unit conversion, time decoding, ensemble dimension handling, descriptor outputtest_descriptor_writer.py-- round-trip correctness for both topo and met descriptor formatstest_cf_normalizer.py-- attribute repair, idempotency, unknown variable passthroughAll fixtures build NetCDF files in memory via
netCDF4-- no binary files are checked in.Modified:
test_storm.pyExtended to cover ERA5-style and NWS13-style NetCDF storm inputs:
test_netcdf_var_mapping_era5-- verifies dimension/variable discovery for ERA5 layout (valid_timedim,u10/v10/mslvariable names, lon [0,360])test_netcdf_var_mapping_nws13-- same for NWS13 variable schematest_data_storm_roundtrip-- extended to parametrize overnetcdf_era5andnetcdf_nws13variantstest_netcdf_wrf_stub-- skipped with explanation; documents that WRF support requiresMetPreprocessorfor string-time axis and curvilinear grid handling, deferred to a future PRModified:
test_bowl_slosh_netcdf.pyExtended
_make_bowl_netcdf_topographyto support coordinate variants (dim_order,cf_compliant,cropped). Addedtest_bowl_slosh_netcdf_variantsparametrized over all non-standard variants, comparing against the existing reference gauge data. Results must match to 1e-4 relative/absolute since the bathymetry is identical.New test in
examples/tsunami/chile2010/Added
test_chile2010.pywith a full regression test parametrized over topo input variants:standard- existing ASCII topo, baseline behaviornetcdf_topotools- writes topo viatopotools.Topography.write()to NetCDF, then verifies CF compliance viaCFNormalizer; exercises thetopotoolswrite path that was previously untestedlat_flip- manually constructed NetCDF with N-to-S latitudelon_360- manually constructed NetCDF with [0,360] longitudeAll variants compare against committed reference gauge data.
Numerical behavior
Topography changes: the bug fixes in
topo_module.f90for coordinate handling mean that NetCDF topo files with N-to-S latitude or [0,360] longitude will now produce correct results where they previously produced silently wrong bathymetry. Files with the previously assumed layout (S-to-N, [-180,180]) are unaffected.All other simulation parameters and numerical methods are unchanged.
Known issues and deferred work
util.get_netcdf_namesand the discovery logic inNetCDFInterrogatorare parallel implementations. Consolidation is deferred to the surge module refactor to avoid scope creep here.Storm.writedoes not yet callDescriptorWriterdirectly; the.stormdescriptor for NetCDF met forcing is currently written by the test setup code. Full integration is part of the surge refactor.Timescharacter array) and curvilinear grid (XLAT/XLONG2D coordinate variables). Stub test documents the gap.GEOCLAW_NETCDF_UNITSbut not yet implemented.CFNormalizer-based tool for making arbitrary NetCDF files GeoClaw-ready is available innetcdf_utils.pybut not yet exposed as a command-line utility.