Compressed active-cell solver¶
GPU-only. See Compressed active-cell mesh for the concepts.
geoswe.CompressedSolver is resolved lazily, because geoswe.compressed_solver
imports CuPy at module load: the name exists on a CPU-only install and raises
ImportError naming the gpu extra only when you touch it. For the same
reason it is deliberately left out of geoswe.__all__, so that
from geoswe import * keeps working without a GPU.
- class geoswe.CompressedSolver[source]¶
Bases:
objectShallow-water solver on the compressed active-cell mesh (GPU, float32).
Build the problem as a dense
Solver2D(mesh, bed, initial state, Manning roughness), mark the cells to keep withset_inside_mask, and convert:cs = CompressedSolver.from_dense(s) # packs the active cells, frees ``s`` cs.set_rain(rain) # a RainfallForcing; optional cs.run(t_end=3600.0) h = cs.depth() # NumPy (nx, ny), zero outside the mask
Only the active cells and a two-cell halo around them are stored and updated. The halo keeps the state it had in
s(dry, for a dry start), so water that reaches the edge of the active set leaves it, as with the dense"fall"boundary. The otherset_*methods attach the forcings of the coastal applications (stage ring, sponge, infiltration, drains, cross-sections); they take the bundles thatgeoswe.runlib.driverbuilds. For a domain too large to build densely, seesave_cacheandrun_cached.- classmethod from_dense(s, *, ngh=None, dx=None, cfl=None, h_min=None, g=None, m_cls_xp=None, m_tab_xp=None, x0=None, y0=None, crs_wkt='', nx_glob=None, ny_glob=None, comm=None, dims=None, i0_glob=0, j0_glob=0, Nx_loc=None, Ny_loc=None, cfl_no_sigma=False, cfl_linf=False, nx_orig=None, ny_orig=None, gauge_every_s=360.0, say=<built-in function print>)[source]¶
Pack the dense Solver2D
sinto the flat layout, free its fields, and build the MPI halo.On one GPU every argument but
scan be left out: the grid, CFL number, depth floor, gravity and Manning roughness are read froms, and a solver with no inside mask keeps every cell. Under MPI the global grid size and the rank’s placement must be passed, as the run driver does.cfl_linf=Trueuses the dense solver’s velocity norm,max(|u|, |v|), in the time step, so that a compressed run takes the dense run’s steps. Rainfall and the other forcings are not copied froms; attach them with theset_*methods.m_cls_xp(the Manning class of every cell) is on the padded grid,(nx + 2*ngh, ny + 2*ngh), the shape ofs.q[0], not the interior(nx, ny);m_tab_xpis the class table it indexes. The packing gather is a CuPy fancy index, which wraps an out-of-range index instead of raising, so an interior-shaped field would be gathered from the wrong cells; the shape is therefore checked (geoswe.compressed_mesh.CompressedMesh2D.pack()).
- set_sponge(sponge)[source]¶
Attach the open-boundary sponge the run driver builds:
{"keep": k, "amb": a}, two fields on the padded grid (the shape ofs.q[0], not the interior(nx, ny)). Each step relaxes the band toward the open-ocean state,h <- keep*h + ambwithhu, hv <- keep*hu, keep*hv, sokeep = 1andamb = 0leave a cell alone. The driver setskeep = 1 - alphaover the band andamb = alphatimes the ambient still-water depth, which pulls the band toward that depth and absorbs the outgoing wave.Nonefor no sponge.
- set_rain(rain)[source]¶
Attach rainfall: a
RainfallForcing(uniform, or one(nx, ny)frame per time), or the bundle the run driver builds for a gridded product on its native grid (native_rate_dev,lookup_dev,t_s).
- add_inflow(ghost_idx, nbr_idx, normal, ds, t_series, q_series)[source]¶
Add one discharge (hydrograph) inlet; call once per river.
ghost_idxare flat indices of the inlet’s GHOST cells andnbr_idxtheir active neighbours (the pairing _build_ghost_bc produces – pass the subset that is the river mouth). Each inlet keeps its own normal, width and hydrograph.
- property n_active¶
Number of active cells, the ones that are updated.
- property n_stored¶
Number of stored cells, the active cells plus their two-cell halo.
- depth()[source]¶
Water depth as a NumPy array on the unpadded grid, zero outside the active set.
Under MPI this is the calling rank’s block.
- save_cache(cache_dir)[source]¶
Write the flat mesh, state, bed, roughness and forcing bundles to
cache_dir(per-rankr##subdirectories under MPI) for laterrun_cachedloads.The ring, sponge, rain and karst-drain bundles are persisted. Green-Ampt infiltration, the stage clamp and the cross-section gauges are not, and
run_cachedcannot apply them: a replay of a run that used them omits those terms, which changes the trajectory. This warns for the two that do (Green-Ampt, the clamp); the cross sections only sample. Forcings set after this call, and theset_drain/set_infilbundles read atrun()time, are not in the file either.
- run(t_end=None, *, out_dir=None, frame_every_s=0.0, checkpoint_every_s=0.0, ckpt_dir=None, resume=False, max_wall_s=0.0, stop_at_epoch=0.0, say=<built-in function print>, dt_max=None, dt_min=None)[source]¶
Time-step the compressed mesh from
t = 0tot_end.Writes depth frames every
frame_every_sof simulated time intoout_dir(plus gauges, cross-sections and the running depth maximum when enabled), checkpoints everycheckpoint_every_sand atmax_wall_s/stop_at_epochdeadlines, and resumes bit-identically fromckpt_dirwhenresumeis set. With noout_dirnothing is written; read the result withdepth().dt_maxcaps the time step. On one rank, rain also keeps the step below the CFL step of the film it lays down, as ingeoswe.Solver2D.run(), so rain on a dry bed is not deposited minutes at a time.dt_minraises when the step collapses below it rather than grinding on to the job’s wall clock (defaultGEOSWE_DT_MIN, 0 = off).say=Nonegives a silent run.The frames are not GeoTIFFs. Each is one compressed
.npzper rank,<out_dir>/frames_parallel/depth_<index:05d>_t<seconds:07d>_r<rank:02d>.npz, holding a single arrayh: that rank’s interior subdomain of the depth field, float16 by default (a 0.008 m step at a depth of 10 m), or float32 withSWE_FRAME_FP32=1.frames_parallel/manifest.json, written beside them, carries the grid, the CRS and each rank’s(i0, j0, nx, ny)offsets, which is the only way to place the shards back on the global grid; no reader for the format ships with the library.enable_max_depthwrites GeoTIFFs instead.
Cache replay¶
run_cached runs a cache written by CompressedSolver.save_cache() without
rebuilding the mesh. It is not re-exported at the top level: import it as
from geoswe.compressed_solver import run_cached.
- geoswe.compressed_solver.run_cached(cache_dir, *, inflows=None, t_end, frame_every_s, out_dir, cfl=0.5, h_min=1e-06, g=9.81, comm=None, checkpoint_every_s=0.0, ckpt_dir=None, resume=False, max_wall_s=0.0, stop_at_epoch=0.0, say=<built-in function print>, bench=False, dt_max=None, dt_min=None)[source]¶
Load the flat cache straight to GPU (no dense domain) and run the loop. Under MPI (comm.size>1) each rank loads cache_dir/r<rank>/ and rebuilds its halo.
dt_maxcaps the time step, in seconds. UnlikeCompressedSolver.runthis path applies no rain cap of its own: the published cached benchmark replays with rain, and the cap would change its step schedule. Without one, rain on a dry bed takes the dry-partition CFL step, 478.9 s at dx = 3 m, and lays that whole interval of rain down in one go. Passdt_max="rain"for the same film boundCompressedSolver.runapplies (27.4 s on that grid at 40 mm/h), or a number for a cap of your own. That bound is read from the rain table in the cache, so theGEOSWE_RAIN_NPZandGEOSWE_RAIN_UNIFORM_MMHRoverrides, which the loop applies afterwards, do not move it; pass the cap as a number when you replay another deck through them.dt_minraises when the step collapses below it, instead of grinding on to the job’s wall clock; it defaults toGEOSWE_DT_MIN(0 = off).say=Nonegives a silent run, as inCompressedSolver.run.
The compressed mesh¶
The flat active-cell layout the solver is packed into. Reading it is a
measurement, not part of running a case: memory_summary sizes the
indirection tables, and pack/unpack move a padded (nx + 2*ngh, ny +
2*ngh) array in and out of the flat (N_active,) one.
- class geoswe.compressed_mesh.CompressedMesh2D(mesh, inside_mask)[source]¶
Bases:
objectWraps a regular Cartesian Mesh2D + an inside_mask into a flat compressed-cell layout with neighbor indirection.
- pack(arr_2d)[source]¶
Gather the active subset of a padded (nxp, nyp) or (…, nxp, nyp) array into a flat (N_active,) or (…, N_active) array.
Uses fancy indexing.
ij_activelives on the ACTIVE backend (a CuPy device array under the GPU backend), soarr_2dmust be on the same backend – pack(numpy_array) raises under CuPy;cp.asarraythe input first.The last two axes must be the PADDED extent. CuPy’s fancy indexing wraps an out-of-range index per axis rather than raising (NumPy raises IndexError, and the compressed solver is CuPy-only), so a field on another extent is gathered from the wrong cells and comes back plausible and finite: measured on a 24x20 mesh handed a (22, 18) field, 61 of 328 entries came from a wrapped index, the worst 2218 off. Hence the shape check, the same hazard set_manning_table names.
- unpack(arr_flat, fill=0.0, out=None)[source]¶
Scatter a flat (N_active,) array back to (nxp, nyp) with
fillat outside/ghost cells. Ifoutis given, write into it in place.The mirror of pack’s check, for the same reason: a CuPy scatter wraps an out-of-range index too (measured: index 5 into a 4-long axis writes row 1), so a wrongly shaped
outwould be filled at the wrong cells with no error at all.
- property memory_summary¶
Approximate memory cost of the compressed-mesh tables in MB.