Diese Seite wurde maschinell aus dem Englischen übersetzt und kann Fehler enthalten. Englisches Original

Workflow, Studien und Kalibrierung

Die Module auf der obersten Ebene des Pakets führen die Schritte aus, die unabhängig vom Simulationscode sind oder beide Codes beinhalten.

Gelände

Stage 1 - DEM ingest, reprojection and clipping to the region of interest.

Takes the user’s initial (and optional target) DEM, reprojects to the project CRS if needed, and clips it to the ROI boundary polygon. The clipped initial DEM feeds bathymetry interpolation onto the mesh; the (initial, target) pair feeds an optional DEM-of-Difference (DoD) - the dem_target - dem_initial bed-change raster, thresholded by a minimum level of detection (see dem_of_difference() / resolve_lod()) - used as topographic-change calibration data for the morphodynamic (GAIA) calibration. The DoD is produced whenever dem_of_difference.enabled or morphodynamics.enabled.

class axqua.dem.ClippedDEM(path: 'Path', nodata: 'float')[Quellcode]

Bases: object

Parameter:
path: Path
nodata: float
axqua.dem.clip_to_roi(dem_path, boundary_path, out_path, *, target_epsg=None, all_touched=False, compress='lzw')[Quellcode]

Clip a raster to a boundary polygon and write it to out_path.

By default the raster is cropped in its own grid and CRS (resolution preserved). If the raster has no embedded CRS it is assumed to be in the boundary’s CRS, which is then stamped onto the output. Pass target_epsg to reproject the raster before clipping.

Parameter:
  • dem_path (input raster, ROI polygon, output raster.)

  • boundary_path (input raster, ROI polygon, output raster.)

  • out_path (input raster, ROI polygon, output raster.)

  • target_epsg (optional EPSG to reproject the raster to before clipping.)

  • all_touched (include pixels touched by the polygon edge (default: center-in).)

  • compress (GeoTIFF compression for the output (default ````“lzw“:py:class:`).`)

Rückgabetyp:

Path

axqua.dem.clip_dem_to_roi(cfg, dem_path, out_path=None)[Quellcode]

Clip dem_path to the configuration’s ROI boundary, in the project CRS.

Convenience wrapper around clip_to_roi() that takes the boundary and CRS from cfg. By default the clipped raster is written next to the source as <stem>-roi-clip.tif; pass out_path to choose another location. The output path is returned.

Parameter:

cfg (Config)

Rückgabetyp:

Path

axqua.dem.clip_dem(cfg, dem_path, out_name)[Quellcode]

Pipeline wrapper: clip dem_path to the ROI, reprojected to the project CRS.

Parameter:
Rückgabetyp:

ClippedDEM

axqua.dem.propagated_lod(sigma_initial, sigma_target, confidence_level=0.95)[Quellcode]

Minimum level of detection [m] from propagated survey uncertainty.

LoD = t * sqrt(sigma_initial^2 + sigma_target^2) (Brasington/Wheaton), the spatially-uniform propagated-error threshold below which a bed-elevation change is indistinguishable from survey noise at confidence_level.

Parameter:
Rückgabetyp:

float

axqua.dem.resolve_lod(cfg)[Quellcode]

Resolve the minimum level of detection [m] and a human-readable rationale.

Priority: an explicit dem_of_difference.min_lod > the propagated survey uncertainty (when both per-DEM uncertainties are given) > 0 (no thresholding).

Parameter:

cfg (Config)

Rückgabetyp:

tuple[float, str]

axqua.dem.dem_of_difference(cfg, initial, target)[Quellcode]

Compute target - initial on the initial ROI grid -> bed-change raster (m).

Applies the minimum level of detection (see resolve_lod()): cells whose absolute change is below the LoD are masked to nodata (dem_of_difference.mask_below_lod, the default) or set to 0. The net / erosion / deposition volumes over the significant cells are logged.

Parameter:
Rückgabetyp:

Path

axqua.dem.run(cfg)[Quellcode]

Execute stage 1; returns the produced raster paths.

Parameter:

cfg (Config)

Rückgabetyp:

dict[str, Path]

Abflussdaten und Abflusskurven

Readers for hydraulic inputs: inflow, stage-discharge, and measurements.

Handles the Bavarian LfU (gkd.bayern.de) CSV export format (UTF-8 BOM, ;-separated, comma decimals, ~10 metadata lines, then a Datum;... header) as well as plain 2-column CSVs, so the workflow ingests both the raw gauge files in stage-discharge/ and tidy user-prepared tables.

class axqua.hydraulics.Inflow(times_s: 'np.ndarray | None', discharge: 'np.ndarray', steady_value: 'float')[Quellcode]

Bases: object

Parameter:
  • times_s (np.ndarray | None)

  • discharge (np.ndarray)

  • steady_value (float)

times_s: np.ndarray | None
discharge: np.ndarray
steady_value: float
axqua.hydraulics.read_inflow(path, steady=True)[Quellcode]

Read an inflow discharge series (LfU or generic) into an Inflow.

Parameter:
Rückgabetyp:

Inflow

axqua.hydraulics.read_rating_pairs(path)[Quellcode]

The pairs of a rating curve: (Q, WSE) as two arrays sorted by Q.

Parameter:

path (Path)

axqua.hydraulics.read_stage_discharge(path)[Quellcode]

Read a rating curve -> callable mapping discharge (m3/s) to WSE (m).

Accepts a Q,WSE (or Q,stage) CSV with one or more rows. A single Q-h pair is enough for a steady run (the WSE is then constant); with several rows the WSE is linearly interpolated in Q. Querying a Q outside the tabulated range clamps to the nearest endpoint and logs a warning (the curve does not extrapolate), so for steady runs supply the pair at the simulated discharge.

Parameter:

path (Path)

axqua.hydraulics.read_measurements(path, crs_epsg)[Quellcode]

Read hydraulic measurement points into a tidy DataFrame.

Returns columns: id, x, y, z (vertical offset, default 0), and any of water_depth / scalar_velocity that are present. Accepts a point shapefile/gpkg or a CSV with x/y columns.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.hydraulics.resolve_outflow_wse(cfg, inflow_q)[Quellcode]

Downstream water-surface elevation for a prescribed-elevation outflow.

Shared by both backends: TELEMAC writes it as PRESCRIBED ELEVATIONS and OpenFOAM as a prghPressure profile on the outlet patch, and they must agree or the two models are not driven the same way.

stage_discharge (default) reads the rating curve and interpolates the WSE at the simulated discharge; elevation uses the fixed prescribed value.

Parameter:

inflow_q (float)

Rückgabetyp:

float

Generate an outflow stage-discharge rating curve from normal-flow hydraulics.

For a steady simulation the downstream (outflow) boundary needs the water-surface elevation at the simulated discharge. When no measured rating curve is available, this module synthesizes one from a roughness coefficient and a prismatic channel cross-section by inverting Manning’s uniform- (normal-) flow equation:

Q = (1 / n) * A(h) * R(h) ** (2/3) * sqrt(S0)

for a trapezoidal section with bottom width b and side slope m (horizontal:vertical, m = 0 is rectangular):

A(h) = (b + m * h) * h                      # flow area
P(h) = b + 2 * h * sqrt(1 + m ** 2)         # wetted perimeter
R(h) = A(h) / P(h)                           # hydraulic radius

Q is strictly increasing in depth, so the normal depth h_n conveying a given Q is found by bisection; the stage is WSE = bed_elevation + h_n.

Roughness is given either as a Manning n or a Strickler Kst (n = 1/Kst). The result is written as a Q,WSE,depth CSV that axqua.hydraulics. read_stage_discharge() consumes as boundaries.stage_discharge.

axqua.rating.normal_depth(discharge, *, manning=None, strickler=None, slope, bottom_width, side_slope=0.0, tol=1e-06)[Quellcode]

Normal (uniform-flow) depth [m] conveying discharge [m3/s].

Roughness via manning (n) or strickler (Kst). slope is the longitudinal bed slope S0 (m/m, > 0); bottom_width and side_slope (H:V) define the trapezoidal section. Solved by bisection on the (monotonic) Manning equation.

Parameter:
Rückgabetyp:

float

axqua.rating.generate_stage_discharge(out, discharges, *, manning=None, strickler=None, slope, bottom_width, side_slope=0.0, bed_elevation=0.0)[Quellcode]

Write a normal-flow Q,WSE,depth rating CSV for discharges to out.

For a steady run a single discharge (the simulated Q) is enough; pass several to tabulate a curve. WSE = bed_elevation + normal_depth(Q).

Parameter:
Rückgabetyp:

Path

axqua.rating.synthesize_outflow_rating(cfg, discharge, *, side_slope=0.0, slope=None, bed_elevation=None, width=None, manning=None, strickler=None, out=None)[Quellcode]

Derive an outflow stage-discharge curve from the case geodata + config.

Unless given explicitly, the trapezoidal-channel parameters are taken from the geodata: width = total length of the outflow liquid-boundary line(s) (boundaries.liquid_boundaries, field tagged outflow); bed_elevation = thalweg (minimum DEM elevation) sampled along that line; slope = reach bed slope from a linear fit of the DEM along geodata.channel_centerline. The roughness defaults to the lateral-boundary friction in the config (friction.boundary_law/boundary_coefficient; Strickler or Manning). discharge is one discharge or several; writes a Q,WSE,depth CSV with one row per discharge to out (default boundaries.stage_discharge).

Parameter:
Rückgabetyp:

Path

axqua.rating.stage_for_discharge(discharge, *, station, bed, ks, slope, freeboard=10.0)[Quellcode]

Water level [m] at which a surveyed cross-section conveys discharge.

Inverts the section conveyance (_section_conveyance()) by bisection between the section thalweg and its highest point plus freeboard. This is the normal (uniform-flow) stage of the section: the level at which the friction slope equals the bed slope.

Used both by section_rating() (one stage per requested discharge, written to a rating CSV) and by the pre-wet initial condition, which needs the normal stage of a whole sequence of cross-sections along the reach - see axqua.steering._normal_depth_prewet_surface().

axqua.rating.section_rating(out, discharges, *, station, bed, ks, slope, log_label='')[Quellcode]

Stage-discharge curve from a surveyed cross-section rather than a trapezoid.

station/bed are the along-section distance (m) and bed elevation (m) of the real section; ks is the Nikuradse roughness (m), a scalar or one value per station. Each requested discharge is turned into a stage by stage_for_discharge().

A trapezoidal idealisation of the same section (generate_stage_discharge()) puts the full width at the full depth, so it over-estimates the flow area at a given stage and returns a stage that is too low. On a V- or U-shaped natural section the error is large enough to force a supercritical drawdown at a prescribed-elevation outflow - the boundary pulls the level below the reach’s own normal depth and the flow accelerates through the outlet to satisfy continuity. Integrating the real section removes that artifact.

axqua.rating.synthesize_outflow_rating_from_section(cfg, discharge, *, slope=None, out=None, n_samples=300)[Quellcode]

Outflow rating from the DEM cross-section along the outflow boundary line.

Samples the bed along the outflow liquid-boundary line straight out of geodata.dem_initial, picks up the Nikuradse ks of the roughness zone each sample falls in (geodata.roughness_zones + roughness_table; the lateral friction.boundary_* roughness otherwise), takes the reach slope from a linear fit of the DEM along geodata.channel_centerline, and inverts the section conveyance with section_rating().

Parameter:

n_samples (int)

Feldmessungen

Ground-truth ingestion: field measurements -> tidy calibration tables.

Calibration ground truth comes in two halves that live in separate places and are joined here:

  • positions - a point layer (shapefile/GeoPackage) giving where each measurement was taken, in some CRS (often not the project CRS); reprojected to the project CRS on ingest.

  • values - the measured quantities, in a source-specific export (e.g. a SonTek FlowTracker2 .ft.sum workbook).

Every source is normalized to the same tidy schema: the first three columns are x, y, z (project CRS, meters), followed by quantity columns. For FlowTracker hydraulics these are u, v, w (velocity components, m/s), u_err, v_err, w_err (per-component error, m/s) and h (water depth, m).

FlowTracker is only one possible source; the tidy schema is generic so other ground-truth (e.g. ADCP, hand-held probes, sediment samples) can be added as further adapters without touching the calibration stage that consumes the tidy tables.

axqua.ground_truth.read_xlsx_sheet(path, sheet=0)[Quellcode]

Read one worksheet into a raw, header-less DataFrame.

Cells keep their string form except plain numbers, which are coerced to float. Robust to the styling that defeats pandas.read_excel/openpyxl.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.ground_truth.read_flowtracker_values(xlsx, sheet=None)[Quellcode]

Read a SonTek FlowTracker2 .ft.sum workbook -> per-vertical values.

Returns columns ID, u, v, w, u_err, v_err, w_err, h (one row per measurement vertical). Coordinates are not here - they come from the paired DGPS position layer (see read_flowtracker()).

sheet names a worksheet to read instead of the first. A profile sheet (several readings per vertical, headers suffixed raw) is detected by its own column names and returns one row per reading, keyed by the same ID - so a vertical appears several times and the caller must apply select_profile_rows().

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.ground_truth.read_flowtracker(xlsx, positions, crs_epsg, join_key='ID', *, sheet=None, profile='single', group_key=None)[Quellcode]

Join FlowTracker values to their DGPS positions -> tidy hydraulics table.

Parameter:
  • xlsx (Path) – the .ft.sum export with the measured velocities/depths.

  • positions (Path) – point layer (shp/gpkg) of the survey points, with a matching join_key column; reprojected to crs_epsg for the x/y output.

  • crs_epsg (int) – project CRS the output coordinates are expressed in.

  • join_key (str)

  • sheet (str | int | None)

  • profile (str)

  • group_key (str | None)

Rückgabe:

the tidy schema x, y, z then u, v, w, u_err, v_err, w_err, h.

Rückgabetyp:

pandas.DataFrame

axqua.ground_truth.scalar_velocity(df)[Quellcode]

Depth-averaged scalar velocity magnitude from the components present.

Parameter:

df (pandas.DataFrame)

Rückgabetyp:

pandas.Series

axqua.ground_truth.read_points(path, crs_epsg)[Quellcode]

Read a point layer/CSV that already carries coords + quantities -> tidy.

A spatial layer (shp/gpkg/geojson) is reprojected to crs_epsg and its geometry becomes x, y; a CSV must provide x/y columns (or aliases). All other columns pass through (canonicalised where recognized).

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.ground_truth.VERTICAL_TOLERANCE = 0.01

Readings closer together than this in plan belong to one vertical [m]. A FlowTracker profile is taken with the rod standing in one place, so its readings share a position exactly; the tolerance only absorbs coordinate rounding.

axqua.ground_truth.select_point_profiles(df, rule='single', *, group_key=None)[Quellcode]

Apply a profile rule to a point layer that holds several readings per vertical.

A layer exported from a FlowTracker campaign has one point per reading: three points on top of each other where a vertical was sampled at 0.2, 0.6 and 0.8 of the depth. A depth-averaged model has one value there, so the readings of a vertical are reduced by the same rules as a FlowTracker workbook (select_profile_rows()) - by default the reading nearest 0.6 of the depth.

The vertical is named by group_key where the layer has such a column, and otherwise by the position itself. A layer with one reading per position comes back unchanged, which is every layer this function did not exist for.

Parameter:
  • df (pandas.DataFrame)

  • rule (str)

  • group_key (str | None)

Rückgabetyp:

pandas.DataFrame

axqua.ground_truth.compile_ground_truth(cfg)[Quellcode]

Compile the configured raw sources into the tidy multi-tab table.

Sources sharing a category are concatenated into one sheet; a filled calibration-target template (ground_truth.targets) contributes its hydraulics / morphodynamics tabs the same way. Returns the written path (cfg.ground_truth_path), or None when neither is configured (the user supplies the tidy table directly).

Rückgabetyp:

Path | None

axqua.ground_truth.read_tidy(path)[Quellcode]

Read the tidy multi-tab ground-truth table -> {category: DataFrame}.

Each sheet/file becomes one category; columns are canonicalised and any quantity columns absent from a given dataset are simply not present (callers must tolerate missing quantities).

Parameter:

path (Path)

Rückgabetyp:

dict[str, pandas.DataFrame]

axqua.ground_truth.write_tidy(tables, path)[Quellcode]

Write {category: DataFrame} to a multi-tab xlsx (one sheet per category).

Parameter:
Rückgabetyp:

Path

axqua.ground_truth.DEFAULT_RELATIVE_HEIGHT = 0.4

Height above the bed, as a fraction of the column, of the conventional „0.6 depth“ point. 0.6 is measured DOWN FROM THE SURFACE (that is the FlowTracker/USGS convention), so the probe sits at 0.4 of the depth ABOVE the bed. Getting this backwards misplaces every target by 0.2*h.

axqua.ground_truth.relative_height(df, *, default=0.4)[Quellcode]

Height above the bed as a fraction of the column, per measurement row.

Returns (f, source); source names which rule fired, so a placement audit can say where every target’s position came from rather than leaving it implicit. First hit wins:

  1. an explicit f column;

  2. h_meas and h -> 1 - h_meas/h (FlowTracker MeasD/FinalD);

  3. pct_depth -> 1 - pct_depth (% Depth, also from the surface);

  4. a label ending -<p>h (the q47-3-1501-0.93h convention);

  5. default, with one warning naming how many rows took it.

Parameter:
  • df (pandas.DataFrame)

  • default (float)

Rückgabetyp:

tuple[pandas.Series, str]

axqua.ground_truth.select_profile_rows(df, rule='single', *, group_key='ID')[Quellcode]

Reduce a multi-depth profile table to the rows that become targets.

A FlowTracker vertical may carry several readings at different depths. Which of them a calibration should see is a modeling decision, not a data property, so it is named explicitly:

single

one row per vertical - the reading nearest the conventional 0.6 depth. The default, and a no-op when there is already one row per vertical, so every existing config keeps its present behavior.

all

every reading, one target each.

drop-lowest

every reading except the one nearest the bed, and only where there is more than one - a lone reading is kept. The near-bed reading sits inside the grain roughness on a coarse bed (KB15: 0.055 m above a bed with ks = 0.089 m), where a wall function returns a boundary condition rather than a result.

depth-average

one row per vertical carrying the USGS three-point depth-averaged velocity (u_0.2 + 2*u_0.6 + u_0.8) / 4 where three readings allow it, else the plain mean. This is the quantity a depth-averaged 2D model actually predicts.

The lowest reading is found by height above the bed, never by row order: the readings of one vertical arrive in whatever order they were entered (KB15 vertical 1501 reads 0.926, 0.540, 0.292 of the depth from the surface), and the source workbook has a column literally called entry order fixed.

Parameter:
  • df (pandas.DataFrame)

  • rule (str)

  • group_key (str)

Rückgabetyp:

pandas.DataFrame

Is the compiled ground truth’s elevation plausible against the model?

This module exists because of one defect that cost a calibration campaign. Inn KB15’s ground truth was joined to a DGPS layer whose z is the GNSS antenna elevation rather than the bed, so every target sat 2.26-2.70 m above the water. Nothing complained: HydroBayesCal’s OpenFOAM binding finds the nearest extraction point with no distance cutoff, so it returned a plausible-looking 0.003 m/s and the surrogate trained on it. The error was only visible as a failed calibration.

The discriminator is the whole point. A reach can legitimately have a biased DEM - KB15’s is 0.3 m high in the wetted channel from bathymetric-LiDAR attenuation - so „the residual is large“ cannot be the test. What separates the two is shape:

  • an un-subtracted instrument height is a constant. Every point in the group is offset by the same rover-pole length, so the residual is large, tight, and flat in depth;

  • a physical bed bias grows with the water column (attenuation integrates along the light path), so the residual has a significant slope against depth.

On KB15 the two signatures are ~30 sigma apart, so the thresholds below are not delicately tuned. The depth-proportional case is reported and named, never silenced, because it is real and the reader needs to know it is there.

axqua.ground_truth_qa.OFFSET_THRESHOLD = 0.3

A residual must exceed this to be worth reporting at all [m].

axqua.ground_truth_qa.SIGMA_RATIO = 4.0

…and be at least this tight, relative to its own scatter, to be called constant.

axqua.ground_truth_qa.CONSTANT_TOLERANCE = 0.06

…with scatter no larger than this [m].

axqua.ground_truth_qa.REFUSE_THRESHOLD = 1.0

Above this, a constant residual is an error even under warn - a meter of „bed“ error in a wadeable reach is never data.

axqua.ground_truth_qa.MIN_GROUP = 5

Fewer points than this cannot support the statistics.

class axqua.ground_truth_qa.ElevationFinding(severity: "Literal['error', 'warning', 'info']", code: "Literal['instrument-height', 'depth-proportional-bias', 'above-water-surface', 'outside-domain', 'mixed']", group: 'str', n: 'int', offset: 'float', slope: 'float', slope_p: 'float', scatter: 'float', message: 'str')[Quellcode]

Bases: object

Parameter:
  • severity (Literal['error', 'warning', 'info'])

  • code (Literal['instrument-height', 'depth-proportional-bias', 'above-water-surface', 'outside-domain', 'mixed'])

  • group (str)

  • n (int)

  • offset (float)

  • slope (float)

  • slope_p (float)

  • scatter (float)

  • message (str)

severity: Literal['error', 'warning', 'info']
code: Literal['instrument-height', 'depth-proportional-bias', 'above-water-surface', 'outside-domain', 'mixed']
group: str
n: int
offset: float
slope: float
slope_p: float
scatter: float
message: str
line()[Quellcode]
Rückgabetyp:

str

axqua.ground_truth_qa.reference_bed(cfg, x, y)[Quellcode]

The model bed, else the DEM, else nothing. Never raises.

Rückgabetyp:

tuple[numpy.ndarray, str] | tuple[None, str]

axqua.ground_truth_qa.check_ground_truth_elevations(cfg, *, tables=None)[Quellcode]

Compare the compiled ground truth’s z with the model bed or the DEM.

Rückgabetyp:

list[ElevationFinding]

axqua.ground_truth_qa.report(findings, *, strict=False, source='')[Quellcode]

Log the findings; raise under strict if any is an error.

Parameter:
Rückgabetyp:

None

Calibration-target template: a user-friendly calibration-target-data.xlsx.

Ground-truth data for the Bayesian calibration needs a consistent structure: measured values keyed by unique IDs that link to point layers (GeoPackage / shapefile) in user-sources/geodata/, so positions (surveyed once, in some CRS) and values (typed into a spreadsheet) stay cleanly separated but joinable.

This module writes and reads that structure:

  • write_target_template() generates the template workbook (default user-sources/ground-truth/calibration-target-data.xlsx) with four tabs:

    • hydraulics - one row per measurement vertical: ID (or explicit x/y), velocity components u_x, u_y, u_z, the horizontal magnitude U_h (auto = sqrt(u_x^2 + u_y^2), live Excel formula), the RMS turbulent fluctuations u_x', u_y', u_z', U_h', TKE (auto proxy 0.5*(u_x‘^2 + u_y‘^2 + u_z‘^2) when not measured directly), water depth and bottom elevation.

    • morphodynamics - grain-size characteristics per sample point (d16..d90 [mm], fine fraction < 1 mm) plus a dz DoD column that is auto-filled on ingest from the DEM-of-Difference raster whenever the case provides one (geodata.dem_target / geodata.dem_of_difference).

    • parameters - the calibration parameters: a drop-down over the parameter catalog, a zone/class column, the current value, min/max test range, and a tips column (live lookup of the catalog’s typical ranges). Rows for the case’s friction zones (from geodata.roughness_zones + roughness_table) are prefilled with their current ks.

    • parameter-catalog - the reference table of calibration-relevant TELEMAC-2D / TELEMAC-3D / GAIA keywords with default ranges and tips; feeds the drop-down and the tips lookup.

  • read_targets() ingests the filled template into the tidy ground-truth tables (joining IDs to the configured position layers, reprojected to the project CRS; grain sizes converted mm -> m; DoD sampled at the sample points).

  • read_target_parameters() turns the parameters tab into CalibrationParameter rows with HydroBayesCal names (zone<N>, gaia<KEYWORD> <class>, or the literal TELEMAC keyword).

The template stays robust to Excel quirks: ingest never trusts the cached formula results for derived quantities (U_h, U_h', TKE are recomputed from the components when blank), and the workbook is read with the zip-level reader from axqua.ground_truth.

class axqua.targets.ParameterSpec(label: 'str', module: 'str', keyword: 'str', per_class: 'str', vmin: 'float', vmax: 'float', tips: 'str')[Quellcode]

Bases: object

Parameter:
label: str
module: str
keyword: str
per_class: str
vmin: float
vmax: float
tips: str
hbc_name(klass)[Quellcode]

HydroBayesCal parameter name for this spec (and zone/class number).

Parameter:

klass (int | None)

Rückgabetyp:

str

axqua.targets.catalog_by_label()[Quellcode]
Rückgabetyp:

dict[str, ParameterSpec]

axqua.targets.write_target_template(cfg, path=None, force=False)[Quellcode]

Write the calibration-target-data.xlsx template for this case.

Prefills the parameters tab with the case’s friction zones (current ks from geodata.roughness_table) and, when morphodynamics is on, a Shields row per configured sediment class; wires the parameter drop-down and the live tips lookup to the parameter-catalog sheet; declares the DoD status on the morphodynamics tab. Refuses to overwrite an existing (possibly user-filled) file unless force.

Parameter:
Rückgabetyp:

Path

axqua.targets.read_targets(cfg)[Quellcode]

Ingest the filled template -> tidy tables {category: DataFrame}.

  • hydraulics: x, y, z then u, v, w, u_mag, u_std, v_std, w_std, u_mag_std, tke, h - blank derived cells (u_mag, u_mag_std, tke) are recomputed from the components.

  • morphodynamics: x, y, z then d16..d90 (converted mm -> m), fine_fraction and dz (auto-sampled from the DoD raster).

Parameter:

cfg (Config)

Rückgabetyp:

dict[str, pandas.DataFrame]

axqua.targets.read_target_parameters(cfg)[Quellcode]

Read the parameters tab into HydroBayesCal calibration parameters.

Rows need a parameter (drop-down label, else a literal TELEMAC keyword) and a numeric Min < Max; incomplete rows are skipped with a warning. Names follow HydroBayesCal’s convention (zone<N> for friction zones, gaia<KEYWORD> <class> for GAIA, the literal keyword otherwise).

Parameter:

cfg (Config)

Rückgabetyp:

list[CalibrationParameter]

FlowTracker2 -> calibration-target hydraulics tab.

Reads SonTek FlowTracker2 exports and writes one row per measurement point into the hydraulics tab of a calibration-target-data.xlsx template, keyed by each point’s own ID (which then joins the DGPS point layer in user-sources/geodata/, see axqua.targets).

Per point it extracts the three velocity components and their turbulent RMS fluctuations:

  • u_x, u_y, u_z <- the reported mean components VelX/VelY/VelZ (aka v(x)/v(y)/v(z)).

  • u_x', u_y', u_z' (RMS fluctuations) <- the sample standard deviation u std / Std Dev v'(x) / sigma_x. These are NOT the ``VxErr`` columns: VxErr is the FlowTracker standard error of the mean (the velocity measurement uncertainty), i.e. ~``std / sqrt(Npts)``. When only a plain .ft.sum summary is available (VxErr + Npts but no std-dev column), the RMS is reconstructed as u' ~ VxErr * sqrt(Npts).

TKE is taken from a measured column when present, else left to the template’s live formula 0.5*(u_x'^2 + u_y'^2 + u_z'^2); the water depth is the local total/final depth. U_h and U_h' recompute from the components.

Three export layouts are auto-detected by their headers:

  1. SonTek ``.ft.sum`` (ID, MeasD, Npts, VelX/VelY/VelZ, VxErr/VyErr/VzErr, FinalD): only stderr -> RMS reconstructed.

  2. TKE-stats export (VelX... plus u std/v std/w std): real RMS.

  3. FT_TKE_Summary (Point, v(x)..., Std Dev v'(x)..., TKE [m2/s2]): real RMS + measured TKE; multiple worksheets scanned.

axqua.flowtracker.read_flowtracker(path, *, sheet=None, reconstruct_rms=True)[Quellcode]

Read one FlowTracker2 export -> per-point velocity/fluctuation table.

Returns columns ID, u, v, w, u_std, v_std, w_std, tke, depth (one row per measurement point). All worksheets that look like a FlowTracker table are scanned and concatenated unless sheet restricts to one (name or 0-based index). With reconstruct_rms the RMS fluctuations are filled from VxErr * sqrt(Npts) where a direct std-dev column is absent.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.flowtracker.read_flowtrackers(paths, *, reconstruct_rms=True)[Quellcode]

Read and concatenate several FlowTracker2 exports (e.g. day1 + day2).

Parameter:

reconstruct_rms (bool)

Rückgabetyp:

pandas.DataFrame

axqua.flowtracker.fill_template_hydraulics(template, sources, *, sheet_name='hydraulics', start_row=2, reconstruct_rms=True)[Quellcode]

Write FlowTracker points into the template’s hydraulics tab, keyed by ID.

sources is one path or a list of FlowTracker2 exports. Existing rows in the tab are cleared first (so re-running is idempotent). u_x/u_y/u_z and u_x'/u_y'/u_z' are written; U_h, U_h' and (unless a measured TKE is present) TKE keep their live formulas; the water depth is filled. Returns the number of points written.

Parameter:
  • template (Path)

  • sheet_name (str)

  • start_row (int)

  • reconstruct_rms (bool)

Rückgabetyp:

int

axqua.flowtracker.write_driver_script(path, force=False)[Quellcode]

Drop the extract_flowtracker.py driver next to the template.

Parameter:
Rückgabetyp:

Path | None

Physical-flume measurements as cross-section reference data.

A laboratory campaign on a scale model does not produce what axqua.ground_truth expects. There are no coordinates and no point cloud: there are a handful of cross-sections, each with a mean water depth and a maximum velocity, at a few steady discharges. Comparing those against a model means comparing aggregates along a section with aggregates along the same section - not a modeled value at a point against a measured value at a point, which is what the calibration CSV would otherwise imply.

This module reads such a campaign into a tidy per-section table. The model side of the comparison is axqua.solvers.telemac.sections.section_statistics(), which reduces a result file over the same section lines, and compare() puts the two side by side.

Scale

A flume is usually a scale model, and Froude similarity scales the two quantities differently: at a length scale L (prototype:model), lengths go as L and velocities as sqrt(L). Which of the two conventions a published table is already in is a property of that table, not something to infer, so both scales are explicit arguments here and neither is guessed. read_depths() and read_velocities() report the convention they were given, and compare() records it in its output, so a comparison can never quietly be between a prototype model and a model-scale measurement.

axqua.labdata.DISCHARGE_LABELS = {'HQ': 1.0, 'HQ100': 1.0, 'MQ': 0.135, 'Q30': 0.06}

discharge labels used in the flume campaign, and what they mean in the prototype

axqua.labdata.LABEL_ALIASES = {'HQ': 'HQ100', 'HQ_100': 'HQ100', 'Q100': 'HQ100'}

The same discharge is spelled differently on different sheets of one campaign - the published depth summary says HQ100 where the velocity summary says HQ. Left alone, the two tables simply fail to join and half the reference data disappears into unmatched rows, which is the quiet kind of wrong.

axqua.labdata.canonical_label(label)[Quellcode]

One spelling per discharge, so sheets of the same campaign join.

Parameter:

label (str)

Rückgabetyp:

str

axqua.labdata.froude_velocity_scale(length_scale)[Quellcode]

Velocity scale under Froude similarity for a given length scale.

Parameter:

length_scale (float)

Rückgabetyp:

float

axqua.labdata.read_depths(path, *, sheet=0, length_scale=1.0)[Quellcode]

Read the flume water-depth summary into one row per discharge and section.

The sheet is a stack of blocks, one per discharge: a title row naming the discharge (Q30 - 60 L/s) whose remaining cells are the section labels (XS1 - US2), then a MEAN row and a standard-deviation row, both in millimeters. Depths are returned in meters, multiplied by length_scale.

Returns a list of dicts with discharge_label, discharge, section, station, h_mean, h_std.

Parameter:
Rückgabetyp:

list[dict]

axqua.labdata.read_velocities(path, *, sheet=0, velocity_scale=1.0)[Quellcode]

Read the flume velocity summary into one row per discharge and section.

The sheet is one table with a Discharge column filled only on the first row of each block, a section column, and the time-averaged maxima with their standard deviations. Velocities are returned in m/s, multiplied by velocity_scale.

Returns dicts with discharge_label, discharge, section, u_max, u_std, u3d_max, u3d_std and u_section_mean. A section where the ADV could not measure - too shallow, in this campaign - yields None rather than a row that looks measured.

Parameter:
Rückgabetyp:

list[dict]

axqua.labdata.reference_table(depths, velocities=None)[Quellcode]

Merge the depth and velocity readings into one row per discharge and section.

Parameter:
axqua.labdata.compare(reference, modelled, *, length_scale=1.0, velocity_scale=1.0)[Quellcode]

Put measured section aggregates next to modeled ones.

modeled is the output of section_statistics() for the same sections, carrying a name that matches the reference section. The scales are recorded in the result so a table can never be read without knowing which convention the measurements were in.

Parameter:
axqua.labdata.write_comparison(table, path)[Quellcode]

Write a comparison table, with the scale convention in the header comment.

Parameter:

path (Path)

Rückgabetyp:

Path

How much of a model-data mismatch is just not knowing where the point is.

A calibration compares a measured value against the model sampled at the measurement’s coordinates. That comparison silently assumes the coordinates are exact. When they are not, the model is read at the wrong place and the difference is charged to the model - or, worse, to whichever parameter happens to move it.

This is not a hypothetical. Inn KB15’s September-2025 survey was positioned with an RTK float solution rather than a fixed one, which is decimeter-to-metre horizontal rather than centimeter. On that reach the modeled water depth at the survey verticals is 0.374 m while the deepest node within 3 m is 0.648 m, so a position error of a few meters moves the modeled depth by more than the entire model-data discrepancy being calibrated against.

The fix is not to relocate the points. Their true positions are unknown, and snapping them to wherever the model happens to agree would manufacture agreement and calibrate nothing. The fix is to state the uncertainty honestly: if the point is known only to within sigma, then the model value at that point is uncertain by however much the model varies over sigma. That uncertainty is added in quadrature to the measurement error, so a target sitting on a steep gradient carries wide error bars and a target in a uniform patch carries narrow ones - which is exactly the weighting a likelihood should give them.

The spread is measured by sampling, not by a gradient. A gradient is a local linearisation, and the fields that make this matter - depth near a bank, velocity at a shear layer - are precisely the ones where a linearisation is worst over a meter-scale radius. Sampling a ring at the uncertainty radius costs nothing here (the sampler is a KD-tree lookup) and stays honest on a step.

axqua.positional_error.GNSS_SIGMA = {'dgps': 1.0, 'rtk-fixed': 0.03, 'rtk-float': 1.0, 'standalone': 3.0}

Horizontal sigma [m] for common GNSS solution qualities. These are working figures for judging a survey, not a specification of anyone’s receiver: a float solution in particular degrades with baseline, sky view and occupation time, and is the one quality where quoting a single number is least defensible.

axqua.positional_error.RING_POINTS = 8

Points on the sampling ring(s). Eight is enough to catch a one-sided step without making the sampler the cost of the calibration.

axqua.positional_error.sample_spread(sample, x, y, sigma, *, radii=(0.5, 1.0), ring_points=8)[Quellcode]

Spread of sample over the positional uncertainty, per point.

sample maps (xs, ys) to model values. The field is read at each point and on rings at radii multiples of sigma; the returned value is the standard deviation over those samples - an estimate of how much the model value could differ purely because the coordinates are uncertain.

Non-finite samples (a point that falls dry, or outside the mesh) are dropped rather than propagated, so a vertical near the waterline still gets a spread from the samples that did land in water.

Parameter:
Rückgabetyp:

numpy.ndarray

axqua.positional_error.telemac_sampler(geometry, results, variable)[Quellcode]

A nearest-node sampler for one SELAFIN variable.

Nearest-node rather than P1: a result merged from a parallel run is not a clean triangulation (it carries coincident nodes and zero-area triangles at the subdomain interfaces), and the question here is how much the field varies over meters - which nearest-node answers perfectly well on a sub-metre mesh.

Parameter:

variable (str)

Rückgabetyp:

Callable[[numpy.ndarray, numpy.ndarray], numpy.ndarray]

axqua.positional_error.describe(sigma, quality=None)[Quellcode]

One line naming the uncertainty and where it came from.

Parameter:
Rückgabetyp:

str

axqua.positional_error.resolve_sigma(cfg)[Quellcode]

(sigma, quality) from ground_truth.position_quality / position_sigma.

An explicit sigma wins over a named quality, so a survey whose real accuracy is known does not have to be described by a lookup table.

Rückgabetyp:

tuple[float, str | None]

Deriving the transform that puts CAD-local geometry on the map.

An STL carries no coordinate system - the format has no field for one. The drawing the STL was exported from usually does, because a construction drawing is set out in the national grid. So the transform is recoverable by matching the two: the same structure, drawn twice, once in meters of the map and once in whatever the CAD file used.

What can honestly be recovered, and what cannot

Matching two axis-aligned bounding boxes gives scale and translation reliably, and rotation only up to the ambiguity of a rectangle - a footprint rotated by 90 degrees has the same bounding box with its sides swapped, and one rotated by 180 has exactly the same box. This module therefore reports candidates and asks for a decision rather than picking one: it prints both boxes, the size ratio, the aspect agreement and the rotations that would align them, and leaves the choice to someone who can look at the result in QGIS. Silently choosing would produce a model that is plausible, wrong, and hard to catch later.

The honest fallback is the identity: run the case in the drawing’s own coordinates. The model is physically identical and only its position on a map is missing, which matters for a fish pass far less than it would for a river reach.

axqua.georef.COMMON_SCALES = ((1.0, 'metres'), (0.001, 'millimetres'), (0.01, 'centimetres'), (0.3048, 'feet'))

scales a CAD drawing plausibly uses, as (factor, what it means)

class axqua.georef.Match(transform, cad_bounds, map_bounds, scale_ratio, aspect_cad, aspect_map, rotation_candidates=<factory>, verdict='unavailable', message='', recommendation='')[Quellcode]

Bases: object

A proposed georeferencing, with everything needed to judge it.

Parameter:
transform: Transform
cad_bounds: tuple
map_bounds: tuple
scale_ratio: float
aspect_cad: float
aspect_map: float
rotation_candidates: list[float]
verdict: str = 'unavailable'
message: str = ''
recommendation: str = ''
as_yaml()[Quellcode]

The surfaces keys this match implies, ready to paste into a config.

Rückgabetyp:

str

axqua.georef.read_dxf_bounds(path, *, layers=None)[Quellcode]

Bounding box of a DXF drawing, in its own (map) coordinates.

GDAL’s DXF driver reads the entities as ordinary features, so this needs no CAD library. layers filters by the Layer attribute, which is how a drawing full of dimension lines, hatches and title blocks is reduced to the structure itself - without it the box is the box of the sheet, not of the building.

Parameter:
axqua.georef.dxf_layers(path)[Quellcode]

Every layer of a DXF with its feature count, for choosing what to match on.

Parameter:

path (Path)

Rückgabetyp:

list[tuple[str, int]]

axqua.georef.match(stl, dxf, *, layers=None, rotation_deg=None, dz=0.0)[Quellcode]

Propose a transform placing stl where dxf says the structure is.

The scale comes from the ratio of the two footprints‘ diagonals, snapped to a common CAD unit when it is within 2 % of one - a drawing is in millimeters or it is not, and 0.00098 is the former measured through a bounding box that includes a kerb. The rotation is not solved for: the candidates that could align the boxes are reported, and rotation_deg selects one. The translation then follows from the box centers, which is exact once scale and rotation are fixed.

Parameter:
Rückgabetyp:

Match

axqua.georef.parse_control_point(text)[Quellcode]

"12.5,40.0:4473100.2,5332090.7" -> the CAD pair and the map pair.

Parameter:

text (str)

Rückgabetyp:

tuple[tuple[float, float], tuple[float, float]]

axqua.georef.match_control_points(stl, points, *, dz=0.0)[Quellcode]

Fit a transform from control points, with the residuals that judge it.

The residuals are the whole point of reporting rather than applying: two points always fit exactly, so a third is what tells you whether the pairs were identified correctly. A meter of residual on a fish pass means a point was mis-picked.

Parameter:
Rückgabetyp:

Match

axqua.georef.log_control_points(stl, result)[Quellcode]

Narrate a control-point fit, and say where the geometry lands under it.

Parameter:
Rückgabetyp:

Match

axqua.georef.report_control_points(stl, points, *, dz=0.0)[Quellcode]

The control-point path as a CLI return code. Kept for direct callers.

Parameter:
Rückgabetyp:

int

axqua.georef.log_match(result, logger_obj=None)[Quellcode]

Log a match() for a human to accept or reject; returns it unchanged.

Parameter:

result (Match)

Rückgabetyp:

Match

Gemeinsame Schritte der Fall-Skripte

Shared workflow helpers for the per-case scripts.

The per-case preprocessing.py / initial_run.py / mesh_convergence_study.py are deliberately thin: the logic they have in common lives here so it is written once and stays identical across every case (template, example-Inn, ering-revised, …).

The central rule is that the steady discharge and the outflow stage prescription come from the case’s own ``case-config.yml``, never from a constant hard-coded in a script. prepare_steady_inputs() reads boundaries.prescribed_flowrate (or the inflow series) so each case uses its own configured boundary conditions; an explicit discharge= only overrides it when a script genuinely needs to.

axqua.workflow.resolve_discharge(cfg, discharge=None)[Quellcode]

Steady discharge [m3/s] the case is built for.

Resolution order: an explicit discharge argument, then the case config’s boundaries.prescribed_flowrate (the inflow Q set in case-config.yml), then the mean of the inflow series in boundaries.inflow. Raises when none of these is available - so a case never silently inherits another case’s discharge.

Parameter:
Rückgabetyp:

float

axqua.workflow.synthesize_constant_inflow(cfg, discharge)[Quellcode]

Write a tiny constant-Q inflow series when boundaries.inflow is missing, point the config at it, and return its path (the existing inflow otherwise).

Parameter:
Rückgabetyp:

Path

axqua.workflow.synthesize_rating_if_missing(cfg, discharge, side_slope=1.0)[Quellcode]

Synthesize the outflow stage-discharge rating from the geodata when the stage_discharge outflow condition is active and no rating CSV is given.

Width comes from the outflow boundary line, bed + reach slope from the DEM and roughness from friction.boundary_* (see synthesize_outflow_rating()). Returns the rating path (existing or generated), or None for the other outflow conditions (elevation / free), which need no rating curve.

Parameter:
Rückgabetyp:

Path | None

axqua.workflow.prepare_steady_inputs(cfg, discharge=None, *, side_slope=1.0, prewet_depth=<object object>)[Quellcode]

Configure the steady boundary conditions for a build and return the discharge.

Prescribes the steady discharge (from the config, see resolve_discharge()), synthesizes a constant inflow series and the outflow stage-discharge rating when those inputs are missing, so the build draws the inflow Q (m3/s) and the outflow stage (H) straight from case-config.yml. Used by preprocessing.py and the mesh-convergence study so they share one source of truth for the steady setup.

prewet_depth defaults to the sentinel „leave it as the config has it“; pass a float (or None) to set initialization.prewet_depth - the mesh-convergence study hotstarts the channel this way, the dry-start initial run leaves it untouched.

Parameter:
Rückgabetyp:

float

axqua.workflow.format_flux_convergence(fc)[Quellcode]

Render a FluxConvergence result as user-facing report lines (whether the boundary fluxes reached mass balance, the hotstart time-step recommendation, and the produced csv/png paths).

Kept here so every case’s initial_run.py prints the convergence summary the same way; the analysis itself lives in axqua.flux_convergence.

Rückgabetyp:

list[str]

axqua.workflow.format_3d_cases(setups)[Quellcode]

Render axqua.threed.build_3d_cases() results as user-facing report lines (one block per steering file; a None setup reports why it was skipped).

Kept here so every case’s add3d.py prints the same summary; the builders live in axqua.threed / axqua.unsteady.

Parameter:

setups (dict)

Rückgabetyp:

list[str]

axqua.workflow.expected_duration(cfg)[Quellcode]

Total simulated time [s] the run marches to when no stop criterion fires.

Mirrors axqua.steering’s DURATION logic: the explicit hydrodynamics.duration when set, else the n_time_steps * time_step fallback. Used to scale the solver progress bar (the variable time step makes the number of steps unknown a priori, so progress is measured against the simulated-time cap instead).

Parameter:

cfg (Config)

Rückgabetyp:

float

axqua.workflow.run_solver_streaming(runtime, cfg, *, cas_file=None, solver=None, ncsize=None, cwd=None, duration=None, show_progress=True, sink=None, should_stop=None)[Quellcode]

Run the TELEMAC solver, echoing its listing live with a progress bar.

Unlike a plain TelemacRuntime.run_solver(), this streams the solver’s stdout/stderr to the console as the run marches (so it is not silent) and, when show_progress, overlays a simulated-time progress bar parsed from TELEMAC’s listing header. The bar’s end time is duration when given (pass it whenever the .cas does not march to the config’s own time cap - e.g. a per-layer 3D run or a hydrograph-spanning unsteady run), else expected_duration(). cwd overrides the run folder (default cfg.model_dir - the convergence studies run each level in its own subfolder). Returns the CompletedProcess (its returncode reports success/failure; a non-zero exit does not raise). A run that TELEMAC-2D ended itself because a value left its limits comes back as a failure too, although TELEMAC reports it as a correct end: see _fail_on_limit_stop().

sink is an event sink (axqua.jobs.events): the same parsed iteration and simulated time that drive the terminal bar are also emitted as structured progress, so a detached job never asks anyone to parse a console log. should_stop is polled between lines and terminates the whole solver process group when it returns true.

Parameter:
axqua.workflow.LIMIT_STOP_CODE = 2

The return code of a run that TELEMAC-2D stopped on its limits (axqua.solvers.telemac.sortie.LIMIT_STOP). TELEMAC’s own plante.F uses 2 for a „controlled“ stop on an error, which is what this is.

axqua.workflow.LIMIT_STOP_REMEDY = 'A value of that size is a numerical instability, not a flow. Look at the mesh, the bed and the initial water depth at that position. Common causes are a sink or a liquid boundary on nearly dry cells, an initial water surface far from the steady one, and a time step that is too large (hydrodynamics.desired_courant).'

What to do about it, in the words every caller can pass on.

axqua.workflow.mesh_from_geometry(cfg)[Quellcode]

Rebuild a Mesh from the built geometry.slf.

Enough of one for the geometry-only helpers (water table, node masks, areas): coordinates, element table, bed and the boundary numbering. Saves re-meshing just to ask a question about a finished run.

Parameter:

cfg (Config)

axqua.workflow.water_table_mask(cfg)[Quellcode]

Depth the porous body’s water table holds in place, per node (else None).

Rebuilt from the same config and the same seeded surface the build used, so the report and the model agree on which water is groundwater-fed rather than stray. Returns None - never raises - when the case has no water table or it cannot be reconstructed: this only decorates a report.

Parameter:

cfg (Config)

axqua.workflow.report_wetting(cfg)[Quellcode]

Wetted-extent + outlet-profile report for a finished run.

A balanced flux budget says nothing about where the water is, which is the question this answers: how much of the wetted area actually carries flow, how much is stagnant film, how much the initial condition put there, and whether the film is still draining or has plateaued (a 2D model has neither infiltration nor evaporation, so water perched above the converged surface never leaves). The outlet profile then says whether the prescribed downstream stage is holding water up over ground that should be dry.

Writes wetting-report.csv and outlet-profile.csv into model_dir and returns report lines. Never raises: a diagnostic must not fail a successful run.

Parameter:

cfg (Config)

Rückgabetyp:

list[str]

axqua.workflow.report_sections(cfg, out_name='baffle-XS-q.csv')[Quellcode]

Discharge across the geodata.control_sections lines of a finished run.

Integrates Q = int (H*U) . n ds from the result, so the sections can be drawn and changed in GIS without re-running the solver - which is how the split of the total discharge between the threads of a braided reach gets read off and checked against field transects. Empty list when the case defines no control sections.

Parameter:
Rückgabetyp:

list[str]

Run the cheap solver first, so the expensive one starts from an informed guess.

An OpenFOAM free-surface run of a water body is expensive in a way a 2D depth-averaged run is not - and almost everything it needs to know at t = 0 is something TELEMAC already answers in minutes: where the water is, how deep it is, how fast it moves, and roughly where its surface sits. Starting interFoam without that means meshing air that will never be wet, paying for the whole filling transient, and doing both in the slowest model in the chain.

This module is the orchestration that makes the seed automatic rather than a prerequisite the user has to remember. ensure_seed() returns a path to a TELEMAC result for axqua.solvers.openfoam.hotstart.load_hotstart() to read, having either found one, produced one, or been told not to.

Three ways it can get there, in order of preference

  1. An existing converged result. simulation/r2d.slf from the real 2D study is a better seed than anything this module would generate, and it is already paid for.

  2. A dedicated coarse pre-run. A copy of the case at pre_run.size_scale times the cell size, pre-wetted and marched at a higher Courant number, into its own folder. Minutes, not hours, and it exists so that a case which has never been through the 2D study can still go straight to OpenFOAM.

  3. Nothing, when the user unchecks pre_run.enabled. The caller then builds cold under a flat lid, exactly as before this module existed.

Why it lives here and not in the OpenFOAM backend

tests/test_capabilities.py::test_openfoam_does_not_import_the_telemac_backend enforces that the two backends are siblings: OpenFOAM is hotstarted from a TELEMAC result, but it reads that through the shared SERAFIN codec in axqua.core.selafin, never by reaching into the other backend. Orchestration that must drive both therefore belongs above both, next to axqua.workflow - which is where it is.

The seed is an approximation, and is treated as one

TELEMAC is a model too, and it can be wrong. Nothing here freezes the 3D free surface onto the 2D answer: the surface is still solved, the lid stands a freeboard above it with room to rise, and the footprint is dilated past the wetted edge. axqua.solvers.openfoam.report.surface_freedom() checks afterwards whether the run ever needed more room than it was given. (The one exception is openfoam.mode: rigid-lid, where the surface really is prescribed - which is the trade that mode exists to make, and it says so.)

class axqua.prerun.SeedResult(path=None, source='none', converged=None, imbalance=None, runtime_s=0.0, notes=<factory>)[Quellcode]

Bases: object

The 2D (or 3D) state an OpenFOAM case will be seeded from.

Parameter:
path: Path | None = None
source: str = 'none'
converged: bool | None = None
imbalance: float | None = None
runtime_s: float = 0.0
notes: list[str]
property ok: bool
summary()[Quellcode]

Printable account of what happened, for a build report.

Rückgabetyp:

list[str]

axqua.prerun.ensure_seed(cfg, *, enabled=None, validate_env=True)[Quellcode]

Find or produce the TELEMAC result an OpenFOAM build should be seeded from.

Never raises for an absent or unconverged seed - a cold build is a legitimate outcome and the caller reports it - except under pre_run.require: error, where an unconverged pre-run is exactly what the user asked to be stopped for.

Parameter:
  • enabled (bool, optional) – Per-invocation override of openfoam.pre_run.enabled. None follows the config; False is the „uncheck the box for this run“ path used by --no-pre-run and the case scripts‘ module-level switch.

  • validate_env (bool) – Passed to axqua.pipeline.run() for the pre-run.

Rückgabetyp:

SeedResult

Netzkonvergenzstudie

Mesh-convergence (grid-independence) study for the 2D hydraulic case.

Runs the same steady simulation on a sequence of progressively finer meshes and monitors quantities of interest phi (water depth h, scalar velocity U) at a fixed set of probe points. Convergence is judged on the Grid Convergence Index (GCI) of the finest grid triplet - with an asymptotic-range check to decide whether the GCI is trustworthy, else on the finest successive relative change - against a tolerance (default 5%; Celik et al. 2008 / Roache: GCI ~5-10% is acceptable engineering accuracy, ~1-2% is high-precision work). This is the formal way to quantify the spatial-discretization error and demonstrate grid independence.

Notes following standard practice (Celik et al. 2008, ASME J. Fluids Eng. 130):

  • Mesh convergence is distinct from iterative (solver) convergence - the latter (residual reduction within one run) must be reached first or the comparison is meaningless. The runs here use TELEMAC’s variable time step so each mesh marches at its own CFL-admissible dt (refining Delta x forces a smaller dt).

  • Use at least three meshes with a refinement factor r >= 1.3 between grids (ratio_levels()). Closer-spaced grids push the grid-to-grid solution differences into iterative/sampling noise and degenerate the GCI denominator r**p - 1, making the observed order p and the GCI unstable.

  • Every level (and every candidate extension) gets the physical validity checks of axqua.mesh_validity (y+, roughness regime, dx vs. ks, turbulence model consistency) - refinement is only meaningful while the closures hold.

The study can resume from completed per-level results (each level folder carries a level.json) and can extend the ladder one finer mesh at a time after showing a runtime estimate + validity check (interactive prompt; in a non-interactive session it stops and reports instead).

The TELEMAC execution is injected (the simulate callable), so the report maths are unit-testable without the solver.

axqua.convergence.extract_at_points(slf_path, quantities, points)[Quellcode]

Read the last frame of slf_path and sample quantities at points.

Rückgabetyp:

dict[str, numpy.ndarray]

class axqua.convergence.LevelResult(label, cell_size, n_elem, n_nodes, min_edge, dt, runtime_s, qoi, scale=1.0, mean_depth=None, mean_velocity=None, validity=None)[Quellcode]

Bases: object

One mesh in the sequence: its resolution, size and sampled QoI.

Parameter:
label: str
cell_size: float
n_elem: int
n_nodes: int
min_edge: float
dt: float | None
runtime_s: float | None
qoi: dict[str, numpy.ndarray]
scale: float = 1.0
mean_depth: float | None = None
mean_velocity: float | None = None
validity: object | None = None
repr_value(qty)[Quellcode]

Representative scalar: mean magnitude over the probes.

Parameter:

qty (str)

Rückgabetyp:

float

class axqua.convergence.ConvergenceReport(levels: 'list[LevelResult]', quantities: 'list[str]', tolerance: 'float', probes: 'np.ndarray', rel_change: 'dict[str, list[float]]'=<factory>, observed_order: 'dict[str, float | None]'=<factory>, gci_fine: 'dict[str, float | None]'=<factory>, converged: 'bool' = False, case: 'str' = '', order_used: 'dict[str, float | None]'=<factory>, gci_med: 'dict[str, float | None]'=<factory>, asymptotic_ratio: 'dict[str, float | None]'=<factory>, verdict_basis: 'str' = 'rel_change')[Quellcode]

Bases: object

Parameter:
levels: list[LevelResult]
quantities: list[str]
tolerance: float
probes: numpy.ndarray
rel_change: dict[str, list[float]]
observed_order: dict[str, float | None]
gci_fine: dict[str, float | None]
converged: bool = False
case: str = ''
order_used: dict[str, float | None]
gci_med: dict[str, float | None]
asymptotic_ratio: dict[str, float | None]
verdict_basis: str = 'rel_change'
format()[Quellcode]
Rückgabetyp:

str

save(path)[Quellcode]
Rückgabetyp:

Path

recommendation()[Quellcode]

Recommend the coarsest grid-independent cell size (convergence vs. cost).

A mesh is adequate when refining it further (to the next finer mesh) changes every quantity of interest by less than the tolerance; the coarsest such mesh is the cheapest converged choice. Returns a dict with the recommended LevelResult, its cell size, whether it converged, a one-line reason, and the runtime speed-up versus the finest mesh.

Rückgabetyp:

dict

to_xlsx(path)[Quellcode]

Write a styled .xlsx convergence report with the cell-size recommendation.

Rückgabetyp:

Path

axqua.convergence.write_readme(base_dir, *, report=None, case='', tolerance=None, quantities=None, n_probes=None, turbulence=None, extra_entries=None)[Quellcode]

Write a README.md documenting the study folder for external readers.

Explains that mesh-convergence.xlsx is the central summary file and what it contains, the governing equations and quantities examined, and the purpose of every file in each per-level model folder. The text is deliberately software-agnostic (it names TELEMAC-2D, not the build tooling).

Normally called with the finished report at the end of a study; with report=None the per-level folders are scanned instead (level.json), so a README can be regenerated for an already-completed study. extra_entries maps additional file/folder names to one-line descriptions appended to the directory-contents section.

Parameter:
Rückgabetyp:

Path

axqua.convergence.build_report(levels, quantities, tolerance, probes)[Quellcode]

Assemble a ConvergenceReport (relative change, order, GCI, verdict).

The verdict is GCI-based when, for every quantity, the finest grid triplet yields a GCI whose asymptotic-range check passes (GCI(med) / (r**p GCI(fine)) within ASYMPTOTIC_BAND, ~1 in the asymptotic range): converged iff GCI(fine) < tolerance. Otherwise it falls back to the finest successive relative change vs. the tolerance (recorded in verdict_basis).

Parameter:
Rückgabetyp:

ConvergenceReport

axqua.convergence.default_levels(cfg)[Quellcode]

Three meshes spanning one doubling of cell size around the config sizes.

Returns (label, channel_size, floodplain_size) coarse -> fine. The finest matches the config; coarser ones double the cell size (so coarse/fine = 2).

Parameter:

cfg (Config)

Rückgabetyp:

list[tuple[str, float, float]]

axqua.convergence.percent_levels(cfg, percents=(0.4, 0.2, 0.0, -0.2, -0.4))[Quellcode]

Cell-size levels at fractional offsets from the config size, coarse -> fine.

Positive percents coarsen (larger cells), negative refine (smaller cells); the default gives two 40%/20%-coarser, the baseline, and two 20%/40%-finer meshes. Returns (label, channel_size, floodplain_size).

Bemerkung

prefer ratio_levels() - the +-20% steps give successive grid ratios of only ~1.17-1.33, below the r >= 1.3 that Celik et al. (2008) recommend for a stable observed order / GCI.

Parameter:

cfg (Config)

Rückgabetyp:

list[tuple[str, float, float]]

axqua.convergence.ratio_levels(cfg, ratio=1.3, n_coarser=1, n_finer=2)[Quellcode]

Cell-size ladder with a constant refinement factor ratio, coarse -> fine.

The default gives four meshes: one coarser (x ratio), the config baseline, and two finer (/ ratio, / ratio**2) - the finest three form the GCI triplet. Celik et al. (2008) recommend ratio >= 1.3: with closer-spaced grids the solution differences drop into iterative/sampling noise and the GCI denominator r**p - 1 degenerates, so p and the GCI become unstable. Returns (label, channel_size, floodplain_size) tuples; extend the ladder one finer level at a time via run_mesh_convergence(extend_ratio=...).

Parameter:
Rückgabetyp:

list[tuple[str, float, float]]

axqua.convergence.default_probes(cfg, n=40)[Quellcode]

Probe points for the QoI: the ground-truth points if available, else samples along the channel centerline (falling back to the ROI bbox diagonal).

Parameter:
Rückgabetyp:

numpy.ndarray

axqua.convergence.make_telemac_simulator(cfg, discharge, *, base_dir, runtime=None, n_processors=None)[Quellcode]

Build a simulate(label, ch, fp, probes, quantities) -> LevelResult that builds the case at the given resolution, runs TELEMAC once at constant discharge (variable time step for per-mesh CFL), and samples the QoI.

The level’s (ch, fp) sizes are interpreted relative to the config baseline: the level copy is built with mesh.size_scale = ch / cfg.mesh.channel_size so the whole sizing field scales - including any per-zone Max Edge Length (m) values from the mesh-zones gpkg, which would otherwise silently override the per-level sizes and leave every „level“ with the same mesh (the bug behind the invalid first ering study).

Parameter:
axqua.convergence.estimate_next_runtime(levels, next_cell_size)[Quellcode]

Estimated wall-clock [s] of a run at next_cell_size, with its basis.

Fits log(runtime) = a + b log(cell_size) over the completed levels; when fewer than two distinct sizes carry runtimes, or the fitted exponent is implausible, falls back to the theoretical t ~ dx**-3 scaling from the finest level (elements ~ dx**-2, CFL-bound steps ~ dx**-1). (None, ...) when no runtimes were recorded (e.g. an injected test simulator).

Parameter:
Rückgabetyp:

tuple[float | None, str]

axqua.convergence.run_mesh_convergence(cfg, *, discharge, levels=None, quantities=('WATER DEPTH', 'SCALAR VELOCITY'), probes=None, tolerance=0.05, simulate=None, base_dir=None, runtime=None, n_processors=None, extend_ratio=None, max_extra_levels=3, auto_extend=False, ask=None, on_level=None, result_name=None, check_validity=True)[Quellcode]

Run the convergence study and return the ConvergenceReport.

on_level(done, total, label) is called before the first mesh and after every mesh that has been run or reused, so that a job can say „mesh 2 of 4“ while the study runs. A study took twenty minutes and reported nothing until it had ended.

levels is a list of (label, channel_size, floodplain_size) coarse->fine (default: one doubling around the config sizes; prefer ratio_levels()). The sizes are interpreted relative to the config baseline - each level is built with mesh.size_scale = channel_size / cfg.mesh.channel_size so gpkg per-zone sizes scale along (see make_telemac_simulator()). simulate overrides the TELEMAC runner (used in tests); by default each mesh is built and run once at constant discharge. probes default to the ground-truth / centerline points. tolerance is the GCI / relative-change goal (default 5%, per Celik et al. 2008).

Resume: completed levels found under base_dir (result SELAFIN + level.json with matching sizes) are offered for reuse - QoI re-extracted, only missing levels run. Extension: with extend_ratio set, a not-converged study offers up to max_extra_levels additional refinements, each preceded by a runtime estimate and the mesh-validity check of the candidate size. Both go through ask(prompt, default=…) -> bool (default: stdin prompt; non-interactive sessions take the stated default: resume = yes, extend = no). An interactive prompt left unanswered for ``PROMPT_TIMEOUT_S`` seconds assumes ‚yes‘, so an attended-then-abandoned run keeps refining rather than stalling. A candidate whose turbulence/roughness model no longer matches the pinned closure is still run (the caveat is warned, not enforced as a stop). Inject a callable for scripted runs/tests.

Auto-refinement: with auto_extend set, resume and every extension are auto-approved (no prompt, interactive or not), so the study keeps refining one grid at a time until it reaches the GCI/relative-change goal, exhausts max_extra_levels, or a level fails to mesh/run (the practical refinement limit) - then it writes the report from the completed levels.

result_name and check_validity serve a study of another code (the OpenFOAM free-surface model, axqua.solvers.openfoam.convergence): the result file of a level, and whether the checks of the TELEMAC turbulence and roughness closure (axqua.mesh_validity) apply to its levels at all.

Parameter:
  • cfg (Config)

  • discharge (float)

  • tolerance (float)

  • base_dir (Path | None)

  • n_processors (int | None)

  • extend_ratio (float | None)

  • max_extra_levels (int)

  • auto_extend (bool)

  • result_name (str | None)

  • check_validity (bool)

Rückgabetyp:

ConvergenceReport

axqua.convergence.level_progress(ctx)[Quellcode]

What a convergence study calls after each of its runs; None without a job.

axqua.convergence.write_study_report(report, folder, stem, ctx, *, readme=False)[Quellcode]

Write the report of a convergence study next to its levels, and record it.

The case scripts have always done this after the study returned. The job did not: it ran the study for twenty minutes and left the verdict in its log, so the plugin had nothing to show for it and „the report“ the documentation points at did not exist. A workbook that cannot be written must not fail a study that ran. Shared by the backends: a study of each code ends the same way.

Parameter:
Rückgabetyp:

None

Physical validity checks for a mesh resolution (y+, roughness, turbulence).

Standard companion checks for the mesh-convergence study: grid refinement is only meaningful while the turbulence closure and the bed-roughness parameterization remain applicable at the finer cell size, so every level (and every candidate refinement offered to the user) gets:

  • the shear velocity from the fully-rough log law, u* = kappa * U / ln(11 h / ks), built from that level’s mean probe depth h, mean probe velocity U and the channel Nikuradse roughness ks;

  • the dimensionless wall distance y+ = u* dx / nu of the lateral wall functions (first grid point ~ one channel cell off the bank); the log-law window needs y+ >= ~30. In a river model y+ is O(1e4), so this passes for any realistic dx - it is reported as standard practice, not as the binding constraint;

  • the roughness Reynolds number ks+ = u* ks / nu: >= 70 fully rough (the Nikuradse friction law’s regime), 5..70 transitional (law marginal), < 5 hydraulically smooth;

  • the cell size vs. ks ratio: once dx < ks the mesh resolves bed scales the roughness closure already parameterizes (double-counting risk) - usually the first check to fire under refinement;

  • the turbulence-model consistency: what axqua.steering.turbulence_pick_for_dx() would auto-select at this dx versus the model the study is pinned to - a regime change means the pinned closure’s validity assumptions no longer hold at this resolution.

Pure maths - no solver, no gmsh; unit-testable standalone.

axqua.mesh_validity.shear_velocity_loglaw(depth, velocity, ks, kappa=0.41)[Quellcode]

Shear velocity u* [m/s] from the depth-averaged fully-rough log law.

Inverts U/u* = ln(11 h / ks) / kappa (log law integrated over the depth, zero-velocity height z0 = ks/30 -> the classic 11 h / ks argument). NaN when the inputs are unusable (h, U or ks non-positive, or 11h/ks <= 1, i.e. roughness height of the order of the flow depth).

Parameter:
Rückgabetyp:

float

axqua.mesh_validity.wall_y_plus(u_star, dx, nu=1e-06)[Quellcode]

Dimensionless wall distance y+ = u* * dx / nu of the first grid point (one channel cell off a lateral wall).

Parameter:
Rückgabetyp:

float

axqua.mesh_validity.roughness_reynolds(u_star, ks, nu=1e-06)[Quellcode]

Roughness Reynolds number ks+ = u* * ks / nu.

Parameter:
Rückgabetyp:

float

axqua.mesh_validity.manning_to_ks(n)[Quellcode]

Nikuradse ks [m] from a Manning n via the Strickler relation Kst = 1/n = 26 / ks^(1/6), i.e. ks = (26 n)**6.

Parameter:

n (float)

Rückgabetyp:

float

axqua.mesh_validity.channel_ks(cfg)[Quellcode]

The channel-bed Nikuradse roughness ks [m] from the case config.

Resolution order mirrors axqua.steering._friction_rows(): explicit friction.zones (a zone named *channel*/*riverbed*) > geodata.roughness_zones + roughness_table (the Zone ID overlapping the channel mesh-zone union the most) > None (checks needing ks are skipped). Manning/Strickler coefficients are converted via the Strickler relation.

Parameter:

cfg (Config)

Rückgabetyp:

float | None

class axqua.mesh_validity.MeshValidity(dx, depth, velocity, ks, u_star=nan, y_plus=nan, ks_plus=nan, dx_over_ks=None, y_plus_ok=True, ks_regime='unknown', dx_below_ks=False, turbulence_pick=None, turbulence_pick_name='', pinned_model=None, regime_change=False, estimated=False, warnings=<factory>)[Quellcode]

Bases: object

Validity of the turbulence + roughness modeling at one cell size.

Parameter:
dx: float
depth: float
velocity: float
ks: float | None
u_star: float = nan
y_plus: float = nan
ks_plus: float = nan
dx_over_ks: float | None = None
y_plus_ok: bool = True
ks_regime: str = 'unknown'
dx_below_ks: bool = False
turbulence_pick: int | None = None
turbulence_pick_name: str = ''
pinned_model: int | None = None
regime_change: bool = False
estimated: bool = False
warnings: list[str]
lines()[Quellcode]

Human-readable summary lines (report block / extension prompt).

Rückgabetyp:

list[str]

axqua.mesh_validity.check_level(cfg, *, dx, depth, velocity, ks=None, pinned_model=None, estimated=False)[Quellcode]

Run the validity checks for one cell size dx (see the module docstring).

depth/velocity are the level’s mean probe values (or the finest completed level’s, for a candidate refinement; then set estimated=True when they are placeholders). ks defaults to channel_ks(); pinned_model is the turbulence model the study is pinned to (None skips the consistency check). The turbulence auto-pick deliberately ignores any explicit hydrodynamics.turbulence_model (the study pins that on each level copy).

Parameter:
Rückgabetyp:

MeshValidity

Studie der vertikalen Layer

Vertical-layer convergence study for the TELEMAC-3D case.

The 2D study (axqua.convergence) varies the horizontal cell size; this one varies the number of sigma layers (NUMBER OF HORIZONTAL LEVELS) on the same horizontal mesh, to demonstrate that the depth-resolved solution is independent of the vertical discretization. For a sequence of layer counts (coarse = few layers -> fine = many) it runs the hotstarted non-hydrostatic 3D case (axqua.threed.build_3d_cas()), samples the depth-averaged quantities of interest (water depth, scalar velocity, from the 3D run’s 2D RESULT FILE) at the ground-truth probe points, and reports the relative change between successive layer counts plus an observed order of convergence and a Grid Convergence Index.

The „grid size“ for the GCI is the layer thickness dz (which halves as the layer count roughly doubles), so the same Richardson/GCI maths as the 2D study apply - the report helpers are reused from axqua.convergence. The TELEMAC execution is injected (the simulate callable), so the report maths are unit-testable without the solver.

class axqua.vertical_convergence.VerticalLevel(n_levels, dz, n_nodes3d, n_elem3d, time_step, runtime_s, qoi)[Quellcode]

Bases: object

One vertical resolution: its layer count, thickness and sampled QoI.

Parameter:
n_levels: int
dz: float
n_nodes3d: int
n_elem3d: int
time_step: float | None
runtime_s: float | None
qoi: dict[str, numpy.ndarray]
property cell_size: float

the layer thickness.

Type:

The grid size used for the GCI maths

property label: str
repr_value(qty)[Quellcode]
Parameter:

qty (str)

Rückgabetyp:

float

class axqua.vertical_convergence.VerticalConvergenceReport(levels: 'list[VerticalLevel]', quantities: 'list[str]', tolerance: 'float', probes: 'np.ndarray', rel_change: 'dict[str, list[float]]'=<factory>, observed_order: 'dict[str, float | None]'=<factory>, gci_fine: 'dict[str, float | None]'=<factory>, converged: 'bool' = False, case: 'str' = '')[Quellcode]

Bases: object

Parameter:
levels: list[VerticalLevel]
quantities: list[str]
tolerance: float
probes: numpy.ndarray
rel_change: dict[str, list[float]]
observed_order: dict[str, float | None]
gci_fine: dict[str, float | None]
converged: bool = False
case: str = ''
format()[Quellcode]
Rückgabetyp:

str

save(path)[Quellcode]
Rückgabetyp:

Path

recommendation()[Quellcode]

Recommend the fewest layers that are grid-independent (cheapest 3D run).

A vertical resolution is adequate when adding more layers changes every quantity of interest by less than the tolerance; the coarsest such level is the cheapest converged choice.

Rückgabetyp:

dict

to_xlsx(path)[Quellcode]

Write a styled .xlsx vertical-convergence report with the recommendation.

Rückgabetyp:

Path

axqua.vertical_convergence.layer_levels(cfg, data, counts=None)[Quellcode]

Layer counts to test, ascending (few -> many).

With explicit counts they are used as-is; otherwise a ladder is derived from the inferred baseline so the layer thickness roughly halves each step (a refinement ratio ~2 for the GCI): ~half, baseline, ~double and ~triple the baseline number of intervals.

Parameter:
Rückgabetyp:

list[int]

axqua.vertical_convergence.make_telemac3d_simulator(cfg, *, results_2d, base_dir, runtime=None, n_processors=None, total_time_factor=4.0, hotstart='constant_depth', n_time_steps=None, target_time=None, min_depth=None)[Quellcode]

Build a simulate(n_levels, probes, quantities) -> VerticalLevel.

Each layer count runs in its own base_dir/L<n>/ folder (the fixed geometry and boundary files are symlinked in, so the horizontal mesh is not rebuilt), launches telemac3d.py once and samples the depth-averaged QoI from the run’s 2D result.

target_time (s), when set, holds the simulated physical time constant across all layer counts: each level’s NUMBER OF TIME STEPS is scaled to round(target_time / dt_level) (dt shrinks with the layer count for a fixed Courant, so a finer grid takes proportionally more steps). This removes the fixed-step-count confound where finer levels reach a much earlier transient state (and so appear „not converged“ for reasons of unequal spin-up, not resolution). It overrides n_time_steps.

Parameter:
  • cfg (Config)

  • results_2d (Path)

  • base_dir (Path)

  • n_processors (int | None)

  • total_time_factor (float)

  • hotstart (str)

  • n_time_steps (int | None)

  • target_time (float | None)

  • min_depth (float | None)

axqua.vertical_convergence.run_vertical_convergence(cfg, *, results_2d=None, counts=None, quantities=('WATER DEPTH', 'SCALAR VELOCITY'), probes=None, tolerance=0.02, simulate=None, base_dir=None, runtime=None, n_processors=None, total_time_factor=4.0, hotstart='constant_depth', n_time_steps=None, target_time=None, min_depth=None, on_level=None)[Quellcode]

Run the vertical-layer convergence study and return the report.

on_level(done, total, label) is called before the first run and after each, so that a job can report which layer count is running.

Needs the converged 2D result (initial_run output) for the hotstart, the horizontal mesh and the probe points. counts overrides the auto-derived layer ladder; simulate overrides the TELEMAC runner (used in tests).

target_time (s) holds the simulated physical time constant across layer counts (steps scaled per level’s dt), removing the fixed-step-count spin-up confound; it overrides n_time_steps.

Parameter:
  • cfg (Config)

  • results_2d (str | Path | None)

  • tolerance (float)

  • base_dir (Path | None)

  • n_processors (int | None)

  • total_time_factor (float)

  • hotstart (str)

  • n_time_steps (int | None)

  • target_time (float | None)

  • min_depth (float | None)

Rückgabetyp:

VerticalConvergenceReport

Kalibrierung

Stage 5 - calibration points CSV and HydroBayesCal config emit.

Turns the hydraulic measurements into the calibration-points CSV that HydroBayesCal reads (id, x, y, z, <QTY>_DATA, <QTY>_ERROR columns) and emits a config_Telemac.py wiring the produced case to the calibration parameters and BAL settings declared in the axqua config.

axqua.calibration.build_calibration_csv(cfg, *, floors=None, path=None, z_resolver=None, category=None)[Quellcode]

Write the HydroBayesCal calibration-points CSV from the tidy ground truth.

The tidy table is compiled from ground_truth.sources when given, otherwise read from a user-authored ground_truth.measurements. Returns None if neither is configured.

floors clamps the per-quantity _ERROR column from below. This is a correctness guard, not a nicety: HydroBayesCal builds each observation’s variance from this column, so a measured error that is zero (or a value near zero multiplied by the relative fraction) gives that point a near-zero variance and lets the least trustworthy measurement dominate the whole likelihood. The default None leaves the existing behavior exactly as it was.

path overrides the output location, for a second CSV alongside the default one (the OpenFOAM calibration targets the same points with different columns).

z_resolver replaces the z column. The default (None) writes the tidy table’s own z, which is the surveyed bed - fine for a 2D calibration, where z is ignored, and wrong for a 3D one, where it places the extraction point on the bed and couples it to the survey’s vertical accuracy. A solver adapter supplies a resolver that places targets in the model’s own column instead (see axqua.model_column). It is a callback rather than a flag because the two HydroBayesCal bindings disagree on what z means - OpenFOAM wants an absolute elevation, TELEMAC-3D a height above the model bed - and that knowledge belongs to the adapter, not here.

category selects one tidy tab by name instead of letting the quantities decide. This is what lets a 2D and a 3D calibration read the same survey under different profile rules - the 2D one depth-averaged, the 3D one per-reading - by compiling them as separate categories (hydraulics and hydraulics-3d). They must not share a category: compile_ground_truth concatenates sources that do, which would silently hand both calibrations the union of the two row sets.

Parameter:
Rückgabetyp:

Path | None

axqua.calibration.compile_ground_truth(cfg)[Quellcode]

Compile configured raw ground-truth sources into the tidy table (if any).

Parameter:

cfg (Config)

Rückgabetyp:

Path | None

axqua.calibration.roughness_zone_parameters(cfg)[Quellcode]

zone<N> calibration parameters declared by the roughness table.

A zone is a calibration parameter when it has a non-degenerate [ks_min, ks_max] prior and is not flagged calibration = False. Writing a genuine range is itself the declaration, so a table with bounds but no calibration column calibrates all of them rather than silently none; the column is how you opt a zone out. Excluded zones are held at their nominal ks - they still get a .tbl row and still influence the flow, they are just not inferred.

Returns an empty list for the legacy single-ks schema, which carries no bounds; there the config’s calibration.parameters remains the only source.

Parameter:

cfg (Config)

Rückgabetyp:

list

axqua.calibration.merged_parameters(cfg)[Quellcode]

Calibration parameters, merged from the three places they can be declared.

Precedence, later wins on a name collision: config calibration.parameters < the roughness table’s zone<N> rows (geodata.roughness_table with the bounds schema) < the parameters tab of the filled calibration-target template. The roughness table outranks the config because it is where the zonation itself is defined - adding a zone there must not require editing the parameter list in two files - while the spreadsheet stays the live override.

Parameter:

cfg (Config)

Rückgabetyp:

list

axqua.calibration.telemac_parameters(cfg)[Quellcode]

The declared parameters minus the ones that belong to another solver.

A case that calibrates both codes declares both sets in one calibration.parameters list, so this filter is the mirror image of axqua.solvers.openfoam.calibration.openfoam_parameters(). Without it an OpenFOAM ks / Cmu would be emitted into config_Telemac.py as though it were a TELEMAC keyword, and HydroBayesCal would spend the whole campaign rewriting a .cas line that does not exist.

Parameter:

cfg (Config)

Rückgabetyp:

list

axqua.calibration.emit_hbc_config(cfg, calibration_csv)[Quellcode]

Emit a HydroBayesCal config_Telemac.py for the produced case.

Parameter:
Rückgabetyp:

Path

HydroBayesCal integration: build inputs, emit config, launch calibration.

Keeps the per-case run_Bayes_cal.py / run_Bayes_cal_multiflow.py scripts thin - they only declare case-specific choices (calibration target, and for the multi-flow case the list of FlowSpec) and call run_single_flow_calibration() / run_multiflow_calibration().

Two calibration modes:

  • single-flow - one steady discharge, ground truth compiled from the configured ground_truth.sources (see axqua.ground_truth); the case’s built .cas is calibrated directly with HydroBayesCal’s stock bal_telemac.py.

  • multi-flow - several steady discharges, each with its own field campaign (axqua.campaigns) and its own steering file derived from the built base case; run with the additive bal_telemac_multiflow.py + MultiflowTelemacModel. One shared roughness is calibrated against all campaigns jointly.

HydroBayesCal is a pip dependency (pip install 'axqua[calibration]'), not an external checkout: the drivers ship with the package from 1.4.2 on (hydroBayesCal.copy_driver), so there is no directory to point at and no second conda environment to name. It is an optional extra because its surrogate stack - bayesvalidrox, gpytorch, scikit-learn - is heavy and irrelevant to building or running a TELEMAC case, so require_hbc() imports it lazily and explains the install if it is missing.

A staged driver is still run as a subprocess, but in this interpreter’s own environment: the driver is a script with an argparse main(), and it must run with TELEMAC sourced (every surrogate iteration launches the solver), which mutates the environment in ways not to inflict on the caller’s process. Pass checkout= to develop against an unreleased driver from a source tree.

Lessons baked in (see the module functions):

  • the built .cas must output SCALAR VELOCITY (graphic printout M);

  • the staged driver extracts the last (converged) SELAFIN frame, not the mean_last window that would average a dry-start transient;

  • HydroBayesCal’s prior-sample count drives a per-location dense covariance in gpytorch, so it is kept modest (calibration.prior_samples, default 5000) - ample for a low-dimensional roughness posterior and safe on a 16 GB box;

  • per-flow steering files differ only in Q, the outflow stage (from the synthesized rating at that Q), DURATION and the results name.

axqua.bayescal.ensure_graphic_output(cas_path, code='M')[Quellcode]

Ensure the .cas writes a graphic-printout variable (default M = SCALAR VELOCITY). Returns True if the file was patched. HydroBayesCal reads the quantity straight from the results SELAFIN, so it must be printed.

Parameter:
Rückgabetyp:

bool

axqua.bayescal.other_calibration_running(model_dir)[Quellcode]

A TELEMAC run from this model dir already active (would fight over the shared friction.tbl).

Parameter:

model_dir (Path)

Rückgabetyp:

bool

axqua.bayescal.launch(cfg, driver, config_path, *, env=None, note='HydroBayesCal calibration')[Quellcode]

Run a staged TELEMAC driver with TELEMAC sourced. See axqua.hbc.launch_driver().

env is accepted and ignored; it named the separate conda environment this used to need, before HydroBayesCal became a dependency of axqua itself.

Parameter:
Rückgabetyp:

int

axqua.bayescal.build_velocity_csv(cfg, *, vel_err_floor=0.01, depth_err_floor=0.02)[Quellcode]

Compile the configured ground truth and write the calibration-points CSV.

The velocity target is the horizontal FlowTracker speed (0.6-depth proxy for the depth-averaged velocity) with propagated, floored error; water depth is carried alongside. Reads ground_truth.sources via axqua.ground_truth.compile_ground_truth().

Parameter:
Rückgabetyp:

tuple[Path, pandas.DataFrame]

axqua.bayescal.run_single_flow_calibration(cfg, *, calibration_quantities=None, extraction_quantities=None, vel_err_floor=0.01, depth_err_floor=0.02, prepare_only=False, checkout=None, env=None)[Quellcode]

End-to-end single-discharge calibration of the built case.

Compiles the calibration CSV, ensures the .cas prints the target, emits the HydroBayesCal config, stages the driver and (unless prepare_only) launches it. Returns the driver exit code (0 when prepare_only).

Parameter:
Rückgabetyp:

int

axqua.bayescal.AS_BUILT = 'case-as-built'

Folder in the calibration directory that holds the files of the built case while a calibration rewrites them.

axqua.bayescal.case_as_built(cfg)[Quellcode]

Hand the built case back the way the calibration found it.

HydroBayesCal runs TELEMAC in the folder of the built case and writes every parameter set it tests into the files there: the roughness values into the friction table, the name of the results file into the steering file. When it has finished, the case holds the last set it tried, which is neither the case that was built nor the calibrated one, and the next steady run uses it without a word. On the Isar example the friction table read ks = 0.195 m in zone 4 after a calibration of a case built with 0.05 m.

The files are copied into the calibration folder before the driver starts and copied back when it returns, whether it succeeded or not. A calibration that is killed restores nothing; see restore_as_built() for that case.

Parameter:

cfg (Config)

axqua.bayescal.restore_as_built(cfg)[Quellcode]

Copy the files a calibration set aside back into the built case.

Returns whether there was anything to restore. A copy is only ever found here outside a running calibration when that calibration was killed, by a cancelled job or a power cut, before it could restore the case itself. The copy is then the case as built, and what is in the model folder is the last parameter set the driver had reached. Every TELEMAC run of the case calls this first, so a cancelled calibration cannot leak into the next simulation.

Parameter:

cfg (Config)

Rückgabetyp:

bool

class axqua.bayescal.FlowSpec(name, discharge, kind, values, positions=None, sheet='KB15', duration=None, vel_err_floor=0.01, hotstart_from=None)[Quellcode]

Bases: object

One steady discharge in a multi-flow calibration.

kind + values (+ positions) feed axqua.campaigns(); the steering file is derived from the built base case, differing only in Q, the outflow stage, DURATION and the results name. vel_err_floor raises the per-point velocity error for a campaign that should inform but not dominate the joint likelihood (e.g. a single cross-section).

Parameter:
name: str
discharge: float
kind: str
values: Path
positions: Path | None = None
sheet: str = 'KB15'
duration: float | None = None
vel_err_floor: float = 0.01
hotstart_from: str | None = None
compile(crs_epsg)[Quellcode]
Parameter:

crs_epsg (int)

Rückgabetyp:

pandas.DataFrame

axqua.bayescal.outflow_stage(cfg, discharge, out_dir)[Quellcode]

Outflow water-surface elevation at discharge from the synthetic normal-flow rating (extrapolation warning: unreliable far above the geometry calibration discharge / overbank).

Parameter:
Rückgabetyp:

float

axqua.bayescal.write_flow_cas(cfg, base_cas, flow, rating_dir, *, duration=None, dest_dir=None, listing=None, graphic=None)[Quellcode]

Derive one flow’s steering file from the built base .cas.

With flow.hotstart_from the run continues from that result file instead of the base case’s pre-wet/dry seed. DURATION then measures re-equilibration after the roughness change, not the fill - but it must still be long enough for the field to forget the seed, or every run is pulled toward the roughness the seed was converged at and the calibration under-reports sensitivity. Verify it at the prior extremes before trusting a short hotstart duration.

Parameter:
Rückgabetyp:

Path

axqua.bayescal.emit_multiflow_config(cfg, out_dir, model_dir, flows, csv_paths, *, n_cpus, init_runs, max_runs, complete_bal_mode=True, only_bal_mode=False, prior_samples=None, adaptive_init_runs=None)[Quellcode]

Write config_Telemac_multiflow.py (standard blocks + a multiflow block of per-flow steering/CSV entries).

Parameter:
Rückgabetyp:

Path

axqua.bayescal.compile_flow_csvs(cfg, flows, multiflow_dir)[Quellcode]

Compile each flow’s calibration-points CSV; returns {flow name: path}.

Parameter:
Rückgabetyp:

dict[str, Path]

axqua.bayescal.run_multiflow_smoke(cfg, flows, multiflow_dir, csv_paths, *, checkout=None, env=None, init_runs=3)[Quellcode]

Isolated end-to-end plumbing test: tiny 30 s runs in a copied model dir.

Verifies steering/friction updates, extraction at every point, per-run JSON accumulation (rows must differ across init_runs distinct ks levels) and the combined matrix shape - without the full solver cost.

Parameter:
Rückgabetyp:

int

axqua.bayescal.run_multiflow_calibration(cfg, flows, *, launch_mode='prepare', checkout=None, env=None, force=False)[Quellcode]

Compile CSVs + per-flow steering, emit config, stage drivers, then act by launch_mode:

  • prepare - write everything, do not launch (default);

  • smoke - run the isolated plumbing test;

  • run - launch the full calibration (initial design + BAL);

  • resume - reuse completed per-flow initial designs (only_bal_mode) and go straight to surrogate + BAL.

Parameter:
Rückgabetyp:

int

Multi-campaign FlowTracker -> per-flow HydroBayesCal calibration-point CSVs.

A multi-discharge calibration compares one model against velocity ground truth from several field campaigns, each measured at a different steady discharge. This module turns each campaign’s FlowTracker2 export into a calibration-points CSV in the HydroBayesCal schema (first four columns id, x, y, z read by position, then <QTY>_DATA / <QTY>_ERROR read by name).

Per vertical it keeps the measurement nearest 0.6 x depth (the standard depth-averaged-velocity proxy that TELEMAC-2D SCALAR VELOCITY = sqrt(u^2+v^2) is compared to) and forms the target from the horizontal components only. WATER DEPTH (the vertical’s total depth) is carried alongside.

Three source layouts are handled (kind on config-level flow specs, or the compile_* helpers directly):

  • adapter - a revised summary workbook whose sheet already has one row per vertical, joined to a DGPS point layer by ID (via axqua.ground_truth.read_flowtracker()); the standard axqua path.

  • transect - a taped cross-section whose Station is <vertical>-<sub> with 0.2/0.6/0.8 sub-verticals and coordinates only on bank-anchor rows; positions come from a companion point GeoPackage keyed by vertical number.

  • inline - verticals with EPSG coordinates repeated inline on every row and 0.2/0.6/0.8 profiles.

  • points - a point layer with one point per reading, as the corrected campaign layers of 2026-10 are (via axqua.ground_truth.read_points()); the readings of a vertical are reduced by a profile rule, the depth average by default.

The per-point velocity error propagates the FlowTracker component errors and is floored (vel_err_floor). Raise the floor for a campaign that is not an independent representative sample of the 2D flow field (e.g. one dense cross-section), so it informs but does not dominate the joint likelihood (HydroBayesCal weights each point by 1/variance).

axqua.campaigns.velocity_and_error(u, v, u_err, v_err, vel_err_floor=0.01)[Quellcode]

Horizontal speed sqrt(u^2+v^2) and propagated per-point error (floored).

sigma_V = sqrt((u*su)^2 + (v*sv)^2) / V, then clipped up to vel_err_floor.

axqua.campaigns.assemble_velocity_csv(x, y, z, u, v, u_err, v_err, h, labels, vel_err_floor=0.01, depth_err_floor=0.02)[Quellcode]

Build the HydroBayesCal calibration-points frame from per-vertical arrays.

Rückgabetyp:

pandas.DataFrame

axqua.campaigns.compile_adapter(values_xlsx, positions, crs_epsg=25832, vel_err_floor=0.01, label='pt')[Quellcode]

Revised summary workbook joined to a DGPS layer by ID (standard path).

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.campaigns.compile_transect(values_xlsx, positions_gpkg, sheet='KB15', vel_err_floor=0.01, target_depth=0.6, label='tr')[Quellcode]

Taped cross-section: values from the xlsx, positions from a point gpkg.

Station is <vertical>-<sub>; positions come from positions_gpkg whose Point attribute is the (integer) vertical number.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.campaigns.compile_inline(values_xlsx, sheet='KB15', vel_err_floor=0.01, target_depth=0.6, label='in')[Quellcode]

Verticals with inline EPSG coordinates on every row and 0.2/0.6/0.8 profiles.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.campaigns.compile_points(layer, crs_epsg, vel_err_floor=0.01, label='pt', profile='depth-average')[Quellcode]

A point layer with one point per reading -> one calibration point per vertical.

The layer carries no component errors, so every point gets vel_err_floor. It carries no elevation either, so z is 0, which the 2D calibration ignores.

Parameter:
Rückgabetyp:

pandas.DataFrame

axqua.campaigns.compile_campaign(kind, **kwargs)[Quellcode]

Dispatch to the adapter / transect / inline / points layout compiler.

Parameter:

kind (str)

Rückgabetyp:

pandas.DataFrame

Solver-agnostic HydroBayesCal plumbing: find it, stage a driver, launch it.

The part of the calibration path that is the same whichever solver is being calibrated. The solver-specific halves live beside their solvers:

They are genuinely separate rather than one parameterised emitter: the two HydroBayesCal config schemas differ in block names (simulation vs hydrodynamic_simulation), in block membership (n_cpus lives under sampling for OpenFOAM and at top level for TELEMAC), in carrying a whole extra interfoam block, and in what case_template_dir means - referenced in place for TELEMAC, copied per run for OpenFOAM. A single emitter would be an if solver == ladder with two disjoint bodies. What they really share is this module: locating HydroBayesCal, staging a driver out of it with the patches axqua needs, and running that driver inside the right solver environment.

axqua.hbc.require_hbc()[Quellcode]

Import HydroBayesCal, or explain how to install it.

It is an optional dependency (pip install axqua[calibration]): the surrogate stack it pulls in - bayesvalidrox, gpytorch, scikit-learn - is heavy and irrelevant to building or running a solver case, which is what most of axqua does. So it is imported where it is used rather than at module import.

axqua.hbc.hbc_version()[Quellcode]

Installed HydroBayesCal version, for the staging log.

Rückgabetyp:

str

axqua.hbc.EXTRACTION_PATCH = {'output_extraction_time="mean_last"': 'output_extraction_time="last"'}

The extraction-window patch every staged driver gets. The stock drivers average the last window of result frames (mean_last); on a run marching to steady state from a dry or pre-wetted start that averages the residual transient into the calibration values. last takes the converged frame.

axqua.hbc.stage_driver(out_dir, template_name, *, checkout=None, also=(), replacements=None, required=())[Quellcode]

Copy a HydroBayesCal driver into out_dir, with axqua’s patches applied.

The driver comes from the installed HydroBayesCal package (hydroBayesCal.copy_driver, which also brings any sibling a driver imports), so there is no checkout path to configure. checkout overrides that with a source tree, for developing against an unreleased driver.

replacements are further literal-to-literal rewrites, applied on top of EXTRACTION_PATCH. required names literals that must have matched somewhere; if one did not, this raises rather than proceeding. That guard is the point of the argument: a patch rewrites a string literal in someone else’s file, so without it a HydroBayesCal release that changes the literal turns the patch into a silent no-op - and the failure then shows up as a wrong result hours later rather than as an error at staging time.

Every staged file is patched, not just the primary one. That is not caution: bal_telemac_multiflow.py imports run_complex_model / run_bal_model from its bal_telemac.py sibling and calls them without passing output_extraction_time, so a multi-flow run takes the sibling’s default. With only the primary patched, multi-flow calibrations silently averaged the transient - the exact failure this patch exists to prevent - while single-flow ones did not.

Parameter:
Rückgabetyp:

Path

axqua.hbc.solver_running(pattern, model_dir, *, extra=())[Quellcode]

Whether a solver process from model_dir is already active.

Two calibrations sharing one model directory fight over the files each rewrites per run (the friction table, the steering file, the field dictionaries), so this is checked before launching rather than diagnosed afterwards from a confusing result.

Parameter:
Rückgabetyp:

bool

axqua.hbc.launch_driver(environment, driver, config_path, *, note='HydroBayesCal calibration', extra_args=(), env=None)[Quellcode]

Run a staged driver inside environment, in this interpreter’s Python.

HydroBayesCal is a dependency of axqua, so it is importable here - there is no second conda environment to name and no interpreter to guess. The driver is still run as a subprocess rather than imported, for two reasons that matter over a calibration lasting hours: it is a script with module-level state and an argparse main(), and it has to run with the solver sourced, since every surrogate iteration launches the solver. Sourcing mutates the environment, which is exactly the sort of thing not to do to the caller’s own process.

sys.executable is used explicitly so the subshell cannot pick up a different python from the sourced solver environment.

Returns the driver’s exit code.

Parameter:
Rückgabetyp:

int

axqua.hbc.DRIVER_LOG = 'logfile.log'

The name of the log a HydroBayesCal driver keeps in its results folder, and the line it writes there each time it starts a run of the full model. The driver announces a run twice. Running  full complexity model # 3 counts within the initial design and ... model after BAL # 1 within the active learning, so both start again at 1; Running full complexity model 9 counts through both, and is the one read here.

class axqua.hbc.RunCounter(logfile, report, *, interval=5.0)[Quellcode]

Bases: object

Report which model run a HydroBayesCal driver has reached, from its own log.

A calibration is the longest job aXqua starts and was the only one that said nothing while it ran: the driver is a separate process whose output is not parsed, so the job stood at „iteration 0“ from the first minute to the last. The driver does write one line per model run to logfile.log in its results folder, and that is enough to say „run 7 of 12“.

Used as a context manager around the launch. A thread reads what the log has gained every interval seconds and calls report with the highest run number seen, once per change. Only lines written after entry count, so the log of an earlier calibration in the same folder is not replayed. Nothing here can fail a calibration: an unreadable log and a failing report are both ignored, and a driver that words the line differently simply reports no progress.

Parameter:
  • logfile (Path | str)

  • report (Callable[[int], None])

  • interval (float)

poll()[Quellcode]

Read what the log has gained and report a new run number, if there is one.

Rückgabetyp:

None

axqua.hbc.solver_environment(block, legacy_script)[Quellcode]

A SolverEnvironment for a calibration launch.

assume_entered is forced off, and that is not defensive noise: it otherwise defaults to reading the AXQUA_ENV_CAPTURED marker a detached launcher exports. A calibration started from inside a job that had already entered another solver’s environment would then skip sourcing this one entirely and run against whatever binaries that environment put on $PATH - a failure that is cheap to prevent here and expensive to diagnose later.

Rückgabetyp:

SolverEnvironment

Where a measurement sits in the model’s water column.

A calibration target has to be placed at a coordinate the solver can be sampled at. Doing that from the surveyed elevation couples every target to the survey’s vertical accuracy - and on a real reach that accuracy is the weakest link. On Inn KB15 it failed three ways at once: a GNSS rover-pole height that was never subtracted (+2.26/2.51/2.70 m), the measurement height above the bed never added (-0.34 m), and a genuine bathymetric-LiDAR bias in the DEM (+0.3 m, growing with depth). The result was targets ~2.6 m from where they belonged, and because HydroBayesCal’s OpenFOAM binding searches for the nearest point with no distance cutoff, it returned plausible-looking near-zero velocities instead of failing.

Placing the target relative to the model’s own column removes all three at a stroke:

z_target = bed_model(x, y) + f * depth_model(x, y)

with f the measurement’s height above the bed as a fraction of the column (axqua.ground_truth.relative_height()). Nothing but x, y and f comes from the survey, and f is a ratio, so a vertical error cannot enter. It is also what a 3D profile calibration actually asks: at 40% of the depth, how fast is the water?

The approximation this makes, stated plainly. HydroBayesCal fixes the extraction coordinates for the whole experimental design, so the column has to come from one reference solution rather than from each perturbed run. Every ModelColumn therefore carries a source string naming what it was built from, and it is written into the placement audit rather than left implicit.

axqua.model_column.DEFAULT_MARGIN = 0.05

Keep a target this far (as a fraction of the column) off the bed and the surface. A target on the bed sits in the wall-function layer, where the modeled velocity is a boundary condition rather than a result; one on the surface sits on the lid.

class axqua.model_column.ModelColumn(x, y, bed, depth, inside, source)[Quellcode]

Bases: object

The model’s bed and depth at a set of query points.

Parameter:
x: numpy.ndarray
y: numpy.ndarray
bed: numpy.ndarray
depth: numpy.ndarray
inside: numpy.ndarray
source: str
property wse: numpy.ndarray

Modeled water-surface elevation.

usable(*, min_depth=0.05)[Quellcode]

Points inside the domain with a column deep enough to place a target in.

Parameter:

min_depth (float)

Rückgabetyp:

numpy.ndarray

height_at(f, *, margin=0.05)[Quellcode]

Height above the model bed. TELEMAC-3D’s CSV z convention.

Returns (height, clamped); clamped marks the points whose requested fraction was pulled inside [margin, 1-margin], so a caller can report them instead of silently moving a target.

Parameter:

margin (float)

z_at(f, *, margin=0.05)[Quellcode]

Absolute elevation. OpenFOAM’s CSV z convention.

Parameter:

margin (float)

axqua.model_column.triangulation_defects(x, y, ikle)[Quellcode]

Degenerate triangles and coincident nodes, without building anything.

Rückgabetyp:

dict

axqua.model_column.safe_triangulation(x, y, ikle, *, source='the mesh')[Quellcode]

A matplotlib.tri.Triangulation, or an error naming the real cause.

matplotlib’s own message (Triangulation is invalid) says nothing about why, and the why here is specific and recurring: SERAFIN stores x/y as float32. At an easting of ~759,500 m the representable spacing is about 0.06 m, which is coarser than the short cross-stream edges of a 0.5 m anisotropic channel mesh - so a result file collapses thousands of triangles to zero area even though the mesh that produced it is fine. geometry.slf is written double-precision (SERAFIND) and does not suffer this, which is why it, not r2d.slf, is the file to triangulate.

Parameter:

source (str)

axqua.model_column.telemac_column(x, y, *, geometry, fields=None, bed_var='BOTTOM', depth_var='WATER DEPTH', method='linear')[Quellcode]

The model column at (x, y) from a TELEMAC geometry + result.

Coordinates and connectivity always come from geometry (SERAFIND, double precision); fields supplies the depth. That split is deliberate - see safe_triangulation().

Parameter:
  • geometry (Path)

  • fields (Path | None)

  • bed_var (str)

  • depth_var (str)

  • method (Literal['linear', 'nearest'])

Rückgabetyp:

ModelColumn

axqua.model_column.openfoam_column(x, y, *, cfg, cache=None, state=None)[Quellcode]

The model column at (x, y) from the OpenFOAM campaign lattice.

Not from geometry.slf: the campaign mesh is coarsened by openfoam.cell_size_factor (0.5 m -> 2.0 m on KB15) and its bed is resampled from the DEM onto that lattice, so on a rough bed the two disagree by decimeters - enough to place a near-bed target below the OpenFOAM bed. The column has to come from the mesh the solver will actually run on.

Under mode: rigid-lid the lid is the prescribed free surface, so the column height is exactly the modeled depth.

cache is an .npz sidecar written at template-build time; without it the lattice is rebuilt, which is deterministic given the same config, DEM and seed.

Parameter:

cache (Path | None)

Rückgabetyp:

ModelColumn

axqua.model_column.columns_from_openfoam_mesh(mesh)[Quellcode]

(cell_xy, bed, top, dx) per plan column of a built OpenFOAM mesh.

Reads the mesh’s own per-column arrays rather than re-deriving anything, so the column a target is placed in is the column the solver will run on.

Rückgabetyp:

tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, float]

axqua.model_column.write_column_cache(cfg, path, *, mesh=None, state=None)[Quellcode]

Persist the campaign lattice’s columns beside the calibration inputs.

Pass mesh when one has just been built - then nothing is recomputed and the cache is exact by construction. Otherwise the lattice is rebuilt from cfg and state, which is deterministic given the same config, DEM and seed.

Deliberately not written inside the case template: _clean_template would not strip it and every per-run copy would carry it.

Parameter:

path (Path)

Rückgabetyp:

Path

Validierung

Validation: the calibrated model against measurements of another flow situation.

A calibration finds the parameters with which the model reproduces one set of measurements. Whether those parameters describe the reach, or only that one day, shows when the model is run for another situation - another discharge, measured in another survey - and compared with what was measured then. That is what this module does, and it is the only kind of validation aXqua offers: the measurements of one survey are taken within hours in one flow field, so holding some of them back (leave-one-out, k-fold, a random share) tests how well the model interpolates within that flow field and says nothing about another discharge.

The validation run is the calibrated model with other boundary values. Its steering file is derived from the built steady case and differs in exactly these lines: the prescribed discharges and water levels of the situation, the names of its own result and friction files, and the calibrated values. Mesh, bed, numerical settings and initial state are untouched, so a difference between model and measurement cannot come from a model that was built differently.

The calibrated values are the joint posterior maximum of the newest calibration (calibrated_parameters()), read from what HydroBayesCal saved. They are written into copies of the files HydroBayesCal changed during the calibration, by the same rules: zone<N> is the coefficient of friction zone N in the friction table, f.<NAME> a constant in the user Fortran file, and any other name a keyword of the steering file.

The model is read at the measurement points on the geometry file of the model (see axqua.postproc.selafin_vtk for why not on the result), with the mesh’s own triangles. A point outside the mesh has no model value; it is counted and left out of the statistics.

class axqua.validation.CalibratedSet(names, values, source, estimate='joint posterior maximum')[Quellcode]

Bases: object

The parameter values a calibration ended with.

Parameter:
names: list[str]
values: list[float]
source: Path
estimate: str = 'joint posterior maximum'
as_dict()[Quellcode]
Rückgabetyp:

dict[str, float]

class axqua.validation.ValidationReport(name, table, summary, n_points=0, n_outside=0, time=0.0, notes=<factory>)[Quellcode]

Bases: object

Model against measurement, point by point and in summary.

Parameter:
name: str
table: Any
summary: dict[str, dict[str, float]]
n_points: int = 0
n_outside: int = 0
time: float = 0.0
notes: list[str]
as_dict()[Quellcode]
Rückgabetyp:

dict[str, Any]

class axqua.validation.ValidationSetup(name, cas, results, parameters, flowrate, flowrates, elevations, friction=None, notes=<factory>)[Quellcode]

Bases: object

The files of one validation run.

Parameter:
name: str
cas: Path
results: Path
parameters: dict[str, float]
flowrate: float
flowrates: str
elevations: str
friction: Path | None = None
notes: list[str]
as_dict()[Quellcode]
Rückgabetyp:

dict[str, Any]

axqua.validation.boundaries_of(cfg)[Quellcode]

The liquid boundaries of the built case, as a person needs them to state the discharges of another situation: number, inflow or outflow, calibrated discharge.

Rückgabetyp:

list[dict[str, Any]]

axqua.validation.build(cfg, situation, parameters)[Quellcode]

Write the steering file of the validation run into the model folder.

Parameter:

parameters (dict[str, float])

Rückgabetyp:

ValidationSetup

axqua.validation.calibrated_parameters(cfg)[Quellcode]

The joint posterior maximum of the newest calibration of this case.

HydroBayesCal saves one dictionary per calibration quantity; the newest is read. An older HydroBayesCal that did not save the joint maximum gives the mean of the posterior sample instead, and CalibratedSet.estimate says so.

Rückgabetyp:

CalibratedSet

axqua.validation.compare(points, geometry, result, *, name='validation', measurement_error=0.1, wet_depth=0.01)[Quellcode]

Read the model at points and compare it with what was measured there.

points is a table in the form of the calibration points. The error of a measurement is its own (<QUANTITY>_ERROR) combined with measurement_error times the measured value, which is how the calibration weighs a measurement.

Parameter:
Rückgabetyp:

ValidationReport

axqua.validation.evaluate(cfg, situation, setup=None)[Quellcode]

Compare the validation result of situation with its measurements and write the report: a table per point, a summary, and a figure of model against measurement. Everything goes into calibration-validation/validation/.

Parameter:

setup (ValidationSetup | None)

Rückgabetyp:

ValidationReport

axqua.validation.inside_model(cfg, x, y)[Quellcode]

Whether each point lies on the mesh of the built model.

Rückgabetyp:

numpy.ndarray

axqua.validation.situation_named(cfg, name=None)[Quellcode]

The validation situation called name; the only one when no name is given.

Parameter:

name (str | None)

axqua.validation.slug(name)[Quellcode]

A name that can be part of a file name.

Parameter:

name (str)

Rückgabetyp:

str

axqua.validation.statistics(measured, modelled, error=None)[Quellcode]

The numbers a validation is judged by.

bias is the mean of model minus measurement, so a negative bias is a model that is too slow or too shallow. within_error is the share of points at which the two differ by no more than the error of the measurement, and within_two_errors by no more than twice that; for a model without systematic deviation and a correctly stated error they are about 0.68 and 0.95.

Rückgabetyp:

dict[str, float]

axqua validation: what a case needs for a validation, and what it has.

axqua validation <case-file> lists the validation situations of the case (calibration.validation), the inflows of the built model with the discharge each carries in the calibrated case, whether a finished calibration exists, and the result of each validation that was run. The form on the Calibration & validation tab of the plugin is filled from this.

The validation itself is a job: axqua submit <case-file> --kind validation.

axqua.validationcli.describe(cfg)[Quellcode]

Everything about the validation of cfg as one document.

Rückgabetyp:

dict

axqua.validationcli.run_validation(argv)[Quellcode]
Parameter:

argv (list[str])

Rückgabetyp:

int

Fortschrittsanzeige und -protokollierung

Live progress reporting for a streaming TELEMAC solver run.

TELEMAC’s launcher (telemac2d.py / telemac3d.py) prints its listing to stdout as the run marches. axqua normally captures that silently, so a solver run looks like total silence in the terminal. SolverProgress instead echoes every line as it arrives (so the run is not a black box) and, by parsing the per-listing header, overlays a single-line progress bar of simulated time against the run’s DURATION - the point the variable-time-step march reaches when no steady-state stop criterion fires.

The header is written by TELEMAC’s entete.f (2D) / mittit.f (3D) as, e.g.:

ITERATION      500    TIME:      0 H  2 MIN  5.0000 S   (       125.0000 S)

so the iteration counter is the integer after ITERATION and the absolute simulated time is the value in the trailing ( ... S) group (with a day/hour/minute/second fallback for the first minute, where TELEMAC omits that group).

class axqua.progress.ProgressBar(total, *, width=40, out=None)[Quellcode]

Bases: object

Terminal rendering for a streamed solver run - the part no solver owns.

On a TTY the bar sticks to the bottom line (rewritten in place with \r) while the raw listing scrolls above it; off a TTY (piped/logged) the raw lines are echoed and a bar line is emitted only when the whole-percent advances, so the log is not flooded.

Split out of SolverProgress so the OpenFOAM renderer (axqua.solvers.openfoam.runtime.OpenFoamProgress), which parses a completely different listing, presents an identical bar rather than a second look-alike implementation. Callers echo() each raw line and update() the position; SolverProgress keeps doing both from one feed().

Parameter:
  • total (float)

  • width (int)

  • out (TextIO | None)

text()[Quellcode]
Rückgabetyp:

str

echo(line)[Quellcode]

Print one raw output line, keeping the bar at the bottom on a TTY.

Parameter:

line (str)

Rückgabetyp:

None

update(position, suffix='', *, force=False)[Quellcode]

Move the bar to position (in the same units as total).

Parameter:
Rückgabetyp:

None

close()[Quellcode]

Finish the bar so later output does not overwrite it.

Rückgabetyp:

None

class axqua.progress.SolverProgress(total_time, *, width=40, out=None, sink=None, echo=True)[Quellcode]

Bases: object

Echo streamed TELEMAC output and render a simulated-time progress bar.

Feed each stdout line to feed(); call close() when the run ends. The terminal rendering is ProgressBar; this class owns only the TELEMAC listing parsing.

Parameter:
property width: int
property out: TextIO
property tty: bool
feed(line)[Quellcode]

Echo line and, if it is a listing header, refresh the progress bar.

Parameter:

line (str)

Rückgabetyp:

None

close()[Quellcode]

Finish the bar so later output does not overwrite it.

Rückgabetyp:

None

Compound logging: one timestamped logfile capturing every action.

setup_logging(logfile) attaches a file handler (and, by default, a console handler) to the axqua logger and to the py.warnings logger, so a single compound logfile records all actions, the per-step timings emitted by log_step(), and every warning and error. The file handler logs at DEBUG (nothing is dropped); the console mirrors it at the chosen level. Python warnings (warnings.warn) are routed in via logging.captureWarnings.

Each script/phase logs into its own output folder: preprocessing.py -> preprocessing/axqua.log, the build (pipeline.run) -> the simulation (model) folder, and the mesh-convergence study -> postprocessing/ (scoped via logging_to()). Append mode, so successive steps accumulate.

axqua.logsetup.setup_logging(logfile=None, *, level=20, console=True, capture_warnings=True)[Quellcode]

Route axqua logs (+ warnings/errors) to a compound logfile and console.

Idempotent: the console handler is added once per process and each logfile is attached once, so repeated calls (CLI, pipeline, scripts) don’t duplicate output. Returns the resolved logfile path (or None when none was given).

Parameter:
Rückgabetyp:

Path | None

axqua.logsetup.reset_logging()[Quellcode]

Remove every handler this module added (used by tests / re-init).

Rückgabetyp:

None

axqua.logsetup.logging_to(logfile, *, level=20, console=True)[Quellcode]

Scope a compound logfile to a block (for a phase with its own output dir).

Ensures the console handler, then attaches a file handler for logfile for the duration of the with block only, so a later phase logging elsewhere is not written here. Used for the mesh-convergence study (logs to postprocessing/).

Parameter:
axqua.logsetup.log_step(name, *, logger=<Logger axqua (WARNING)>, level=20)[Quellcode]

Time a calculation step and log its start, duration, or failure.

Logs START <name>, then DONE <name> in N.NNs on success or FAILED <name> after N.NNs: ... (and re-raises) on error, so every step’s elapsed time lands in the compound logfile.

Parameter: