Flux de travail, études et calage
Les modules au niveau supérieur du paquet effectuent les étapes qui sont indépendantes du code de simulation ou impliquent les deux codes.
Terrain
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.
- axqua.dem.clip_to_roi(dem_path, boundary_path, out_path, *, target_epsg=None, all_touched=False, compress='lzw')[source]
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.
- Paramètres:
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 EPSGtoreproject the rastertobefore clipping.)all_touched (
include pixels touched by the polygon edge (default:center-in).)compress (
GeoTIFF compression for the output (default ````»lzw »:py:class:`).`)
- Type renvoyé:
- axqua.dem.clip_dem_to_roi(cfg, dem_path, out_path=None)[source]
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.
- axqua.dem.clip_dem(cfg, dem_path, out_name)[source]
Pipeline wrapper: clip dem_path to the ROI, reprojected to the project CRS.
- Paramètres:
- Type renvoyé:
- axqua.dem.propagated_lod(sigma_initial, sigma_target, confidence_level=0.95)[source]
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 atconfidence_level.
- axqua.dem.resolve_lod(cfg)[source]
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).
- axqua.dem.dem_of_difference(cfg, initial, target)[source]
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.- Paramètres:
cfg (Config)
initial (ClippedDEM)
target (ClippedDEM)
- Type renvoyé:
Débit données et courbes de tarage
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')[source]
Bases :
object- Paramètres:
times_s (np.ndarray | None)
discharge (np.ndarray)
steady_value (float)
- discharge: np.ndarray
- axqua.hydraulics.read_inflow(path, steady=True)[source]
Read an inflow discharge series (LfU or generic) into an
Inflow.
- axqua.hydraulics.read_rating_pairs(path)[source]
The pairs of a rating curve:
(Q, WSE)as two arrays sorted by Q.- Paramètres:
path (Path)
- axqua.hydraulics.read_stage_discharge(path)[source]
Read a rating curve -> callable mapping discharge (m3/s) to WSE (m).
Accepts a
Q,WSE(orQ,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.- Paramètres:
path (Path)
- axqua.hydraulics.read_measurements(path, crs_epsg)[source]
Read hydraulic measurement points into a tidy DataFrame.
Returns columns: id, x, y, z (vertical offset, default 0), and any of
water_depth/scalar_velocitythat are present. Accepts a point shapefile/gpkg or a CSV with x/y columns.
- axqua.hydraulics.resolve_outflow_wse(cfg, inflow_q)[source]
Downstream water-surface elevation for a prescribed-elevation outflow.
Shared by both backends: TELEMAC writes it as
PRESCRIBED ELEVATIONSand OpenFOAM as aprghPressureprofile 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;elevationuses the fixed prescribed value.
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)[source]
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.
- axqua.rating.generate_stage_discharge(out, discharges, *, manning=None, strickler=None, slope, bottom_width, side_slope=0.0, bed_elevation=0.0)[source]
Write a normal-flow
Q,WSE,depthrating 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).
- axqua.rating.synthesize_outflow_rating(cfg, discharge, *, side_slope=0.0, slope=None, bed_elevation=None, width=None, manning=None, strickler=None, out=None)[source]
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 taggedoutflow); bed_elevation = thalweg (minimum DEM elevation) sampled along that line; slope = reach bed slope from a linear fit of the DEM alonggeodata.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 aQ,WSE,depthCSV with one row per discharge to out (defaultboundaries.stage_discharge).
- axqua.rating.stage_for_discharge(discharge, *, station, bed, ks, slope, freeboard=10.0)[source]
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 - seeaxqua.steering._normal_depth_prewet_surface().
- axqua.rating.section_rating(out, discharges, *, station, bed, ks, slope, log_label='')[source]
Stage-discharge curve from a surveyed cross-section rather than a trapezoid.
station/bedare the along-section distance (m) and bed elevation (m) of the real section;ksis the Nikuradse roughness (m), a scalar or one value per station. Each requested discharge is turned into a stage bystage_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)[source]
Outflow rating from the DEM cross-section along the outflow boundary line.
Samples the bed along the
outflowliquid-boundary line straight out ofgeodata.dem_initial, picks up the Nikuradseksof the roughness zone each sample falls in (geodata.roughness_zones+roughness_table; the lateralfriction.boundary_*roughness otherwise), takes the reach slope from a linear fit of the DEM alonggeodata.channel_centerline, and inverts the section conveyance withsection_rating().- Paramètres:
n_samples (int)
Mesures de terrain
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.sumworkbook).
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)[source]
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.
- axqua.ground_truth.read_flowtracker_values(xlsx, sheet=None)[source]
Read a SonTek FlowTracker2
.ft.sumworkbook -> 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 (seeread_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 sameID- so a vertical appears several times and the caller must applyselect_profile_rows().
- axqua.ground_truth.read_flowtracker(xlsx, positions, crs_epsg, join_key='ID', *, sheet=None, profile='single', group_key=None)[source]
Join FlowTracker values to their DGPS positions -> tidy hydraulics table.
- Paramètres:
xlsx (
Path) – the.ft.sumexport with the measured velocities/depths.positions (
Path) – point layer (shp/gpkg) of the survey points, with a matchingjoin_keycolumn; reprojected tocrs_epsgfor the x/y output.crs_epsg (
int) – project CRS the output coordinates are expressed in.join_key (str)
profile (str)
group_key (str | None)
- Renvoie:
the tidy schema
x, y, zthenu, v, w, u_err, v_err, w_err, h.- Type renvoyé:
pandas.DataFrame
- axqua.ground_truth.scalar_velocity(df)[source]
Depth-averaged scalar velocity magnitude from the components present.
- Paramètres:
df (pandas.DataFrame)
- Type renvoyé:
pandas.Series
- axqua.ground_truth.read_points(path, crs_epsg)[source]
Read a point layer/CSV that already carries coords + quantities -> tidy.
A spatial layer (shp/gpkg/geojson) is reprojected to
crs_epsgand its geometry becomesx, y; a CSV must providex/ycolumns (or aliases). All other columns pass through (canonicalised where recognized).
- 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)[source]
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.
- axqua.ground_truth.compile_ground_truth(cfg)[source]
Compile the configured raw sources into the tidy multi-tab table.
Sources sharing a
categoryare concatenated into one sheet; a filled calibration-target template (ground_truth.targets) contributes itshydraulics/morphodynamicstabs the same way. Returns the written path (cfg.ground_truth_path), orNonewhen neither is configured (the user supplies the tidy table directly).- Type renvoyé:
Path | None
- axqua.ground_truth.read_tidy(path)[source]
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).
- axqua.ground_truth.write_tidy(tables, path)[source]
Write
{category: DataFrame}to a multi-tab xlsx (one sheet per category).
- 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)[source]
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:an explicit
fcolumn;h_measandh->1 - h_meas/h(FlowTrackerMeasD/FinalD);pct_depth->1 - pct_depth(% Depth, also from the surface);a
labelending-<p>h(theq47-3-1501-0.93hconvention);default, with one warning naming how many rows took it.
- axqua.ground_truth.select_profile_rows(df, rule='single', *, group_key='ID')[source]
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:
singleone 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.
allevery reading, one target each.
drop-lowestevery 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-averageone row per vertical carrying the USGS three-point depth-averaged velocity
(u_0.2 + 2*u_0.6 + u_0.8) / 4where 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.
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')[source]
Bases :
object- Paramètres:
- axqua.ground_truth_qa.reference_bed(cfg, x, y)[source]
The model bed, else the DEM, else nothing. Never raises.
- Type renvoyé:
tuple[numpy.ndarray, str] | tuple[None, str]
- axqua.ground_truth_qa.check_ground_truth_elevations(cfg, *, tables=None)[source]
Compare the compiled ground truth’s
zwith the model bed or the DEM.- Type renvoyé:
- axqua.ground_truth_qa.report(findings, *, strict=False, source='')[source]
Log the findings; raise under strict if any is an error.
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 (defaultuser-sources/ground-truth/calibration-target-data.xlsx) with four tabs:hydraulics- one row per measurement vertical:ID(or explicitx/y), velocity componentsu_x, u_y, u_z, the horizontal magnitudeU_h(auto = sqrt(u_x^2 + u_y^2), live Excel formula), the RMS turbulent fluctuationsu_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 adz DoDcolumn 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 (fromgeodata.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 theparameterstab intoCalibrationParameterrows 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')[source]
Bases :
object- Paramètres:
- axqua.targets.write_target_template(cfg, path=None, force=False)[source]
Write the
calibration-target-data.xlsxtemplate for this case.Prefills the
parameterstab with the case’s friction zones (current ks fromgeodata.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 theparameter-catalogsheet; declares the DoD status on themorphodynamicstab. Refuses to overwrite an existing (possibly user-filled) file unlessforce.
- axqua.targets.read_targets(cfg)[source]
Ingest the filled template -> tidy tables
{category: DataFrame}.hydraulics:x, y, zthenu, 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, zthend16..d90(converted mm -> m),fine_fractionanddz(auto-sampled from the DoD raster).
- axqua.targets.read_target_parameters(cfg)[source]
Read the
parameterstab 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).- Paramètres:
cfg (Config)
- Type renvoyé:
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 componentsVelX/VelY/VelZ(akav(x)/v(y)/v(z)).u_x', u_y', u_z'(RMS fluctuations) <- the sample standard deviationu std/Std Dev v'(x)/sigma_x. These are NOT the ``VxErr`` columns:VxErris the FlowTracker standard error of the mean (the velocity measurement uncertainty), i.e. ~``std / sqrt(Npts)``. When only a plain.ft.sumsummary is available (VxErr+Nptsbut no std-dev column), the RMS is reconstructed asu' ~ 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:
SonTek ``.ft.sum`` (
ID, MeasD, Npts, VelX/VelY/VelZ, VxErr/VyErr/VzErr, FinalD): only stderr -> RMS reconstructed.TKE-stats export (
VelX...plusu std/v std/w std): real RMS.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)[source]
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 unlesssheetrestricts to one (name or 0-based index). Withreconstruct_rmsthe RMS fluctuations are filled fromVxErr * sqrt(Npts)where a direct std-dev column is absent.
- axqua.flowtracker.read_flowtrackers(paths, *, reconstruct_rms=True)[source]
Read and concatenate several FlowTracker2 exports (e.g. day1 + day2).
- Paramètres:
reconstruct_rms (bool)
- Type renvoyé:
pandas.DataFrame
- axqua.flowtracker.fill_template_hydraulics(template, sources, *, sheet_name='hydraulics', start_row=2, reconstruct_rms=True)[source]
Write FlowTracker points into the template’s hydraulics tab, keyed by ID.
sourcesis 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_zandu_x'/u_y'/u_z'are written;U_h,U_h'and (unless a measured TKE is present)TKEkeep their live formulas; the water depth is filled. Returns the number of points written.
- axqua.flowtracker.write_driver_script(path, force=False)[source]
Drop the
extract_flowtracker.pydriver next to the template.
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
HQ100where the velocity summary saysHQ. 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)[source]
One spelling per discharge, so sheets of the same campaign join.
- axqua.labdata.froude_velocity_scale(length_scale)[source]
Velocity scale under Froude similarity for a given length scale.
- axqua.labdata.read_depths(path, *, sheet=0, length_scale=1.0)[source]
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 aMEANrow 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.
- axqua.labdata.read_velocities(path, *, sheet=0, velocity_scale=1.0)[source]
Read the flume velocity summary into one row per discharge and section.
The sheet is one table with a
Dischargecolumn 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_stdandu_section_mean. A section where the ADV could not measure - too shallow, in this campaign - yieldsNonerather than a row that looks measured.
- axqua.labdata.reference_table(depths, velocities=None)[source]
Merge the depth and velocity readings into one row per discharge and section.
- axqua.labdata.compare(reference, modelled, *, length_scale=1.0, velocity_scale=1.0)[source]
Put measured section aggregates next to modeled ones.
modeled is the output of
section_statistics()for the same sections, carrying anamethat matches the referencesection. The scales are recorded in the result so a table can never be read without knowing which convention the measurements were in.
- axqua.labdata.write_comparison(table, path)[source]
Write a comparison table, with the scale convention in the header comment.
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)[source]
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 atradiimultiples 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.
- Paramètres:
sample (Callable[[numpy.ndarray, numpy.ndarray], numpy.ndarray])
sigma (float)
ring_points (int)
- Type renvoyé:
- axqua.positional_error.telemac_sampler(geometry, results, variable)[source]
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.
- Paramètres:
variable (str)
- Type renvoyé:
- axqua.positional_error.describe(sigma, quality=None)[source]
One line naming the uncertainty and where it came from.
- axqua.positional_error.resolve_sigma(cfg)[source]
(sigma, quality)fromground_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.
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='')[source]
Bases :
objectA proposed georeferencing, with everything needed to judge it.
- Paramètres:
- axqua.georef.read_dxf_bounds(path, *, layers=None)[source]
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.
layersfilters by theLayerattribute, 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.
- axqua.georef.dxf_layers(path)[source]
Every layer of a DXF with its feature count, for choosing what to match on.
- axqua.georef.match(stl, dxf, *, layers=None, rotation_deg=None, dz=0.0)[source]
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.
- axqua.georef.parse_control_point(text)[source]
"12.5,40.0:4473100.2,5332090.7"-> the CAD pair and the map pair.
- axqua.georef.match_control_points(stl, points, *, dz=0.0)[source]
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.
- axqua.georef.log_control_points(stl, result)[source]
Narrate a control-point fit, and say where the geometry lands under it.
Étapes partagées des scripts cas
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)[source]
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 incase-config.yml), then the mean of the inflow series inboundaries.inflow. Raises when none of these is available - so a case never silently inherits another case’s discharge.
- axqua.workflow.synthesize_constant_inflow(cfg, discharge)[source]
Write a tiny constant-Q inflow series when
boundaries.inflowis missing, point the config at it, and return its path (the existing inflow otherwise).
- axqua.workflow.synthesize_rating_if_missing(cfg, discharge, side_slope=1.0)[source]
Synthesize the outflow stage-discharge rating from the geodata when the
stage_dischargeoutflow 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_*(seesynthesize_outflow_rating()). Returns the rating path (existing or generated), or None for the other outflow conditions (elevation/free), which need no rating curve.
- axqua.workflow.prepare_steady_inputs(cfg, discharge=None, *, side_slope=1.0, prewet_depth=<object object>)[source]
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 fromcase-config.yml. Used bypreprocessing.pyand 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.
- axqua.workflow.format_flux_convergence(fc)[source]
Render a
FluxConvergenceresult 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.pyprints the convergence summary the same way; the analysis itself lives inaxqua.flux_convergence.
- axqua.workflow.format_3d_cases(setups)[source]
Render
axqua.threed.build_3d_cases()results as user-facing report lines (one block per steering file; aNonesetup reports why it was skipped).Kept here so every case’s
add3d.pyprints the same summary; the builders live inaxqua.threed/axqua.unsteady.
- axqua.workflow.expected_duration(cfg)[source]
Total simulated time [s] the run marches to when no stop criterion fires.
Mirrors
axqua.steering’sDURATIONlogic: the explicithydrodynamics.durationwhen set, else then_time_steps * time_stepfallback. 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).
- 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)[source]
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.casdoes not march to the config’s own time cap - e.g. a per-layer 3D run or a hydrograph-spanning unsteady run), elseexpected_duration(). cwd overrides the run folder (defaultcfg.model_dir- the convergence studies run each level in its own subfolder). Returns theCompletedProcess(itsreturncodereports 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.
- 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 ownplante.Fuses 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)[source]
Rebuild a
Meshfrom the builtgeometry.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.
- Paramètres:
cfg (Config)
- axqua.workflow.water_table_mask(cfg)[source]
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.- Paramètres:
cfg (Config)
- axqua.workflow.report_wetting(cfg)[source]
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.csvandoutlet-profile.csvintomodel_dirand returns report lines. Never raises: a diagnostic must not fail a successful run.
- axqua.workflow.report_sections(cfg, out_name='baffle-XS-q.csv')[source]
Discharge across the
geodata.control_sectionslines of a finished run.Integrates
Q = int (H*U) . n dsfrom 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.
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
An existing converged result.
simulation/r2d.slffrom the real 2D study is a better seed than anything this module would generate, and it is already paid for.A dedicated coarse pre-run. A copy of the case at
pre_run.size_scaletimes 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.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>)[source]
Bases :
objectThe 2D (or 3D) state an OpenFOAM case will be seeded from.
- Paramètres:
- axqua.prerun.ensure_seed(cfg, *, enabled=None, validate_env=True)[source]
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.- Paramètres:
- Type renvoyé:
Étude de convergence en maillage
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 denominatorr**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)[source]
Read the last frame of slf_path and sample quantities at points.
- Type renvoyé:
- 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)[source]
Bases :
objectOne mesh in the sequence: its resolution, size and sampled QoI.
- Paramètres:
- qoi: dict[str, numpy.ndarray]
- 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')[source]
Bases :
object- Paramètres:
levels (list[LevelResult])
tolerance (float)
probes (numpy.ndarray)
converged (bool)
case (str)
verdict_basis (str)
- levels: list[LevelResult]
- probes: numpy.ndarray
- recommendation()[source]
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.- Type renvoyé:
- axqua.convergence.write_readme(base_dir, *, report=None, case='', tolerance=None, quantities=None, n_probes=None, turbulence=None, extra_entries=None)[source]
Write a
README.mddocumenting the study folder for external readers.Explains that
mesh-convergence.xlsxis 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=Nonethe 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.
- axqua.convergence.build_report(levels, quantities, tolerance, probes)[source]
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))withinASYMPTOTIC_BAND, ~1 in the asymptotic range): converged iffGCI(fine) < tolerance. Otherwise it falls back to the finest successive relative change vs. the tolerance (recorded inverdict_basis).- Paramètres:
levels (list[LevelResult])
tolerance (float)
probes (numpy.ndarray)
- Type renvoyé:
- axqua.convergence.default_levels(cfg)[source]
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).
- axqua.convergence.percent_levels(cfg, percents=(0.4, 0.2, 0.0, -0.2, -0.4))[source]
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).Note
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.
- axqua.convergence.ratio_levels(cfg, ratio=1.3, n_coarser=1, n_finer=2)[source]
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 denominatorr**p - 1degenerates, so p and the GCI become unstable. Returns(label, channel_size, floodplain_size)tuples; extend the ladder one finer level at a time viarun_mesh_convergence(extend_ratio=...).
- axqua.convergence.default_probes(cfg, n=40)[source]
Probe points for the QoI: the ground-truth points if available, else samples along the channel centerline (falling back to the ROI bbox diagonal).
- Paramètres:
- Type renvoyé:
- axqua.convergence.make_telemac_simulator(cfg, discharge, *, base_dir, runtime=None, n_processors=None)[source]
Build a
simulate(label, ch, fp, probes, quantities) -> LevelResultthat 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 withmesh.size_scale = ch / cfg.mesh.channel_sizeso the whole sizing field scales - including any per-zoneMax 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).
- axqua.convergence.estimate_next_runtime(levels, next_cell_size)[source]
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 theoreticalt ~ dx**-3scaling from the finest level (elements ~ dx**-2, CFL-bound steps ~ dx**-1).(None, ...)when no runtimes were recorded (e.g. an injected test simulator).
- 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)[source]
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; preferratio_levels()). The sizes are interpreted relative to the config baseline - each level is built withmesh.size_scale = channel_size / cfg.mesh.channel_sizeso gpkg per-zone sizes scale along (seemake_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.jsonwith 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.
- axqua.convergence.level_progress(ctx)[source]
What a convergence study calls after each of its runs;
Nonewithout a job.
- axqua.convergence.write_study_report(report, folder, stem, ctx, *, readme=False)[source]
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.
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 depthh, mean probe velocityUand the channel Nikuradse roughnessks;the dimensionless wall distance
y+ = u* dx / nuof the lateral wall functions (first grid point ~ one channel cell off the bank); the log-law window needsy+ >= ~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:>= 70fully rough (the Nikuradse friction law’s regime),5..70transitional (law marginal),< 5hydraulically smooth;the cell size vs. ks ratio: once
dx < ksthe 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 thisdxversus 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)[source]
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 heightz0 = ks/30-> the classic11 h / ksargument). NaN when the inputs are unusable (h, U or ks non-positive, or11h/ks <= 1, i.e. roughness height of the order of the flow depth).
- axqua.mesh_validity.wall_y_plus(u_star, dx, nu=1e-06)[source]
Dimensionless wall distance y+ = u* * dx / nu of the first grid point (one channel cell off a lateral wall).
- axqua.mesh_validity.roughness_reynolds(u_star, ks, nu=1e-06)[source]
Roughness Reynolds number ks+ = u* * ks / nu.
- axqua.mesh_validity.manning_to_ks(n)[source]
Nikuradse ks [m] from a Manning n via the Strickler relation
Kst = 1/n = 26 / ks^(1/6), i.e.ks = (26 n)**6.
- axqua.mesh_validity.channel_ks(cfg)[source]
The channel-bed Nikuradse roughness ks [m] from the case config.
Resolution order mirrors
axqua.steering._friction_rows(): explicitfriction.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.
- 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>)[source]
Bases :
objectValidity of the turbulence + roughness modeling at one cell size.
- Paramètres:
dx (float)
depth (float)
velocity (float)
ks (float | None)
u_star (float)
y_plus (float)
ks_plus (float)
dx_over_ks (float | None)
y_plus_ok (bool)
ks_regime (str)
dx_below_ks (bool)
turbulence_pick (int | None)
turbulence_pick_name (str)
pinned_model (int | None)
regime_change (bool)
estimated (bool)
- axqua.mesh_validity.check_level(cfg, *, dx, depth, velocity, ks=None, pinned_model=None, estimated=False)[source]
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=Truewhen they are placeholders). ks defaults tochannel_ks(); pinned_model is the turbulence model the study is pinned to (None skips the consistency check). The turbulence auto-pick deliberately ignores any explicithydrodynamics.turbulence_model(the study pins that on each level copy).
Etude de la verticale couches
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)[source]
Bases :
objectOne vertical resolution: its layer count, thickness and sampled QoI.
- Paramètres:
- qoi: dict[str, numpy.ndarray]
- 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' = '')[source]
Bases :
object- Paramètres:
- levels: list[VerticalLevel]
- probes: numpy.ndarray
- axqua.vertical_convergence.layer_levels(cfg, data, counts=None)[source]
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.
- 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)[source]
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), launchestelemac3d.pyonce 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 STEPSis scaled toround(target_time / dt_level)(dtshrinks 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.
- 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)[source]
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_runoutput) 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.
Calage
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)[source]
Write the HydroBayesCal calibration-points CSV from the tidy ground truth.
The tidy table is compiled from
ground_truth.sourceswhen given, otherwise read from a user-authoredground_truth.measurements. ReturnsNoneif neither is configured.floors clamps the per-quantity
_ERRORcolumn 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 defaultNoneleaves 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
zcolumn. The default (None) writes the tidy table’s ownz, which is the surveyed bed - fine for a 2D calibration, wherezis 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 (seeaxqua.model_column). It is a callback rather than a flag because the two HydroBayesCal bindings disagree on whatzmeans - 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 (
hydraulicsandhydraulics-3d). They must not share a category:compile_ground_truthconcatenates sources that do, which would silently hand both calibrations the union of the two row sets.
- axqua.calibration.compile_ground_truth(cfg)[source]
Compile configured raw ground-truth sources into the tidy table (if any).
- axqua.calibration.roughness_zone_parameters(cfg)[source]
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 flaggedcalibration = False. Writing a genuine range is itself the declaration, so a table with bounds but nocalibrationcolumn 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.tblrow and still influence the flow, they are just not inferred.Returns an empty list for the legacy single-
ksschema, which carries no bounds; there the config’scalibration.parametersremains the only source.
- axqua.calibration.merged_parameters(cfg)[source]
Calibration parameters, merged from the three places they can be declared.
Precedence, later wins on a name collision: config
calibration.parameters< the roughness table’szone<N>rows (geodata.roughness_tablewith the bounds schema) < theparameterstab 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.
- axqua.calibration.telemac_parameters(cfg)[source]
The declared parameters minus the ones that belong to another solver.
A case that calibrates both codes declares both sets in one
calibration.parameterslist, so this filter is the mirror image ofaxqua.solvers.openfoam.calibration.openfoam_parameters(). Without it an OpenFOAMks/Cmuwould be emitted intoconfig_Telemac.pyas though it were a TELEMAC keyword, and HydroBayesCal would spend the whole campaign rewriting a.casline that does not exist.
- axqua.calibration.emit_hbc_config(cfg, calibration_csv)[source]
Emit a HydroBayesCal
config_Telemac.pyfor the produced case.
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(seeaxqua.ground_truth); the case’s built.casis calibrated directly with HydroBayesCal’s stockbal_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 additivebal_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
.casmust outputSCALAR VELOCITY(graphic printoutM);the staged driver extracts the last (converged) SELAFIN frame, not the
mean_lastwindow 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),
DURATIONand the results name.
- axqua.bayescal.ensure_graphic_output(cas_path, code='M')[source]
Ensure the
.caswrites a graphic-printout variable (defaultM= SCALAR VELOCITY). Returns True if the file was patched. HydroBayesCal reads the quantity straight from the results SELAFIN, so it must be printed.
- axqua.bayescal.other_calibration_running(model_dir)[source]
A TELEMAC run from this model dir already active (would fight over the shared friction.tbl).
- axqua.bayescal.launch(cfg, driver, config_path, *, env=None, note='HydroBayesCal calibration')[source]
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.
- axqua.bayescal.build_velocity_csv(cfg, *, vel_err_floor=0.01, depth_err_floor=0.02)[source]
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.sourcesviaaxqua.ground_truth.compile_ground_truth().
- 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)[source]
End-to-end single-discharge calibration of the built case.
Compiles the calibration CSV, ensures the
.casprints the target, emits the HydroBayesCal config, stages the driver and (unlessprepare_only) launches it. Returns the driver exit code (0 whenprepare_only).
- 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)[source]
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.- Paramètres:
cfg (Config)
- axqua.bayescal.restore_as_built(cfg)[source]
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.
- class axqua.bayescal.FlowSpec(name, discharge, kind, values, positions=None, sheet='KB15', duration=None, vel_err_floor=0.01, hotstart_from=None)[source]
Bases :
objectOne steady discharge in a multi-flow calibration.
kind+values(+positions) feedaxqua.campaigns(); the steering file is derived from the built base case, differing only in Q, the outflow stage,DURATIONand the results name.vel_err_floorraises the per-point velocity error for a campaign that should inform but not dominate the joint likelihood (e.g. a single cross-section).- Paramètres:
- axqua.bayescal.outflow_stage(cfg, discharge, out_dir)[source]
Outflow water-surface elevation at
dischargefrom the synthetic normal-flow rating (extrapolation warning: unreliable far above the geometry calibration discharge / overbank).
- axqua.bayescal.write_flow_cas(cfg, base_cas, flow, rating_dir, *, duration=None, dest_dir=None, listing=None, graphic=None)[source]
Derive one flow’s steering file from the built base
.cas.With
flow.hotstart_fromthe run continues from that result file instead of the base case’s pre-wet/dry seed.DURATIONthen 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.
- 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)[source]
Write
config_Telemac_multiflow.py(standard blocks + amultiflowblock of per-flow steering/CSV entries).
- axqua.bayescal.compile_flow_csvs(cfg, flows, multiflow_dir)[source]
Compile each flow’s calibration-points CSV; returns {flow name: path}.
- axqua.bayescal.run_multiflow_smoke(cfg, flows, multiflow_dir, csv_paths, *, checkout=None, env=None, init_runs=3)[source]
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_runsdistinct ks levels) and the combined matrix shape - without the full solver cost.
- axqua.bayescal.run_multiflow_calibration(cfg, flows, *, launch_mode='prepare', checkout=None, env=None, force=False)[source]
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.
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 (viaaxqua.ground_truth.read_flowtracker()); the standard axqua path.transect- a taped cross-section whoseStationis<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 (viaaxqua.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)[source]
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 tovel_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)[source]
Build the HydroBayesCal calibration-points frame from per-vertical arrays.
- Type renvoyé:
pandas.DataFrame
- axqua.campaigns.compile_adapter(values_xlsx, positions, crs_epsg=25832, vel_err_floor=0.01, label='pt')[source]
Revised summary workbook joined to a DGPS layer by ID (standard path).
- axqua.campaigns.compile_transect(values_xlsx, positions_gpkg, sheet='KB15', vel_err_floor=0.01, target_depth=0.6, label='tr')[source]
Taped cross-section: values from the xlsx, positions from a point gpkg.
Stationis<vertical>-<sub>; positions come frompositions_gpkgwhosePointattribute is the (integer) vertical number.
- axqua.campaigns.compile_inline(values_xlsx, sheet='KB15', vel_err_floor=0.01, target_depth=0.6, label='in')[source]
Verticals with inline EPSG coordinates on every row and 0.2/0.6/0.8 profiles.
- axqua.campaigns.compile_points(layer, crs_epsg, vel_err_floor=0.01, label='pt', profile='depth-average')[source]
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, sozis 0, which the 2D calibration ignores.
- axqua.campaigns.compile_campaign(kind, **kwargs)[source]
Dispatch to the
adapter/transect/inline/pointslayout compiler.- Paramètres:
kind (str)
- Type renvoyé:
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:
axqua.bayescal- TELEMAC (.casrewriting,config_Telemac.py, the single- and multi-flow entry points);axqua.solvers.openfoam.calibration- OpenFOAM (theinterFoamcase template,config_OpenFOAM.py).
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()[source]
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()[source]
Installed HydroBayesCal version, for the staging log.
- Type renvoyé:
- 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.lasttakes the converged frame.
- axqua.hbc.stage_driver(out_dir, template_name, *, checkout=None, also=(), replacements=None, required=())[source]
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.pyimportsrun_complex_model/run_bal_modelfrom itsbal_telemac.pysibling and calls them without passingoutput_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.
- axqua.hbc.solver_running(pattern, model_dir, *, extra=())[source]
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.
- axqua.hbc.launch_driver(environment, driver, config_path, *, note='HydroBayesCal calibration', extra_args=(), env=None)[source]
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
argparsemain(), 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.executableis used explicitly so the subshell cannot pick up a differentpythonfrom the sourced solver environment.Returns the driver’s exit code.
- 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 # 3counts within the initial design and... model after BAL # 1within the active learning, so both start again at 1;Running full complexity model 9counts through both, and is the one read here.
- class axqua.hbc.RunCounter(logfile, report, *, interval=5.0)[source]
Bases :
objectReport 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.login 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.
- axqua.hbc.solver_environment(block, legacy_script)[source]
A
SolverEnvironmentfor a calibration launch.assume_enteredis forced off, and that is not defensive noise: it otherwise defaults to reading theAXQUA_ENV_CAPTUREDmarker 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.- Type renvoyé:
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)[source]
Bases :
objectThe model’s bed and depth at a set of query points.
- Paramètres:
x (numpy.ndarray)
y (numpy.ndarray)
bed (numpy.ndarray)
depth (numpy.ndarray)
inside (numpy.ndarray)
source (str)
- bed: numpy.ndarray
- depth: numpy.ndarray
- inside: numpy.ndarray
- property wse: numpy.ndarray
Modeled water-surface elevation.
- usable(*, min_depth=0.05)[source]
Points inside the domain with a column deep enough to place a target in.
- Paramètres:
min_depth (float)
- Type renvoyé:
- height_at(f, *, margin=0.05)[source]
Height above the model bed. TELEMAC-3D’s CSV
zconvention.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.- Paramètres:
margin (float)
- axqua.model_column.triangulation_defects(x, y, ikle)[source]
Degenerate triangles and coincident nodes, without building anything.
- Type renvoyé:
- axqua.model_column.safe_triangulation(x, y, ikle, *, source='the mesh')[source]
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.slfis written double-precision (SERAFIND) and does not suffer this, which is why it, notr2d.slf, is the file to triangulate.- Paramètres:
source (str)
- axqua.model_column.telemac_column(x, y, *, geometry, fields=None, bed_var='BOTTOM', depth_var='WATER DEPTH', method='linear')[source]
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().
- axqua.model_column.openfoam_column(x, y, *, cfg, cache=None, state=None)[source]
The model column at
(x, y)from the OpenFOAM campaign lattice.Not from
geometry.slf: the campaign mesh is coarsened byopenfoam.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-lidthe lid is the prescribed free surface, so the column height is exactly the modeled depth.cache is an
.npzsidecar written at template-build time; without it the lattice is rebuilt, which is deterministic given the same config, DEM and seed.- Paramètres:
cache (Path | None)
- Type renvoyé:
- axqua.model_column.columns_from_openfoam_mesh(mesh)[source]
(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.
- Type renvoyé:
- axqua.model_column.write_column_cache(cfg, path, *, mesh=None, state=None)[source]
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_templatewould not strip it and every per-run copy would carry it.
Validation
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')[source]
Bases :
objectThe parameter values a calibration ended with.
- class axqua.validation.ValidationReport(name, table, summary, n_points=0, n_outside=0, time=0.0, notes=<factory>)[source]
Bases :
objectModel against measurement, point by point and in summary.
- Paramètres:
- class axqua.validation.ValidationSetup(name, cas, results, parameters, flowrate, flowrates, elevations, friction=None, notes=<factory>)[source]
Bases :
objectThe files of one validation run.
- Paramètres:
- axqua.validation.boundaries_of(cfg)[source]
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.
- axqua.validation.build(cfg, situation, parameters)[source]
Write the steering file of the validation run into the model folder.
- Paramètres:
- Type renvoyé:
- axqua.validation.calibrated_parameters(cfg)[source]
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.estimatesays so.- Type renvoyé:
- axqua.validation.compare(points, geometry, result, *, name='validation', measurement_error=0.1, wet_depth=0.01)[source]
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.
- axqua.validation.evaluate(cfg, situation, setup=None)[source]
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/.- Paramètres:
setup (ValidationSetup | None)
- Type renvoyé:
- axqua.validation.inside_model(cfg, x, y)[source]
Whether each point lies on the mesh of the built model.
- Type renvoyé:
- axqua.validation.situation_named(cfg, name=None)[source]
The validation situation called name; the only one when no name is given.
- Paramètres:
name (str | None)
- axqua.validation.statistics(measured, modelled, error=None)[source]
The numbers a validation is judged by.
biasis the mean of model minus measurement, so a negative bias is a model that is too slow or too shallow.within_erroris the share of points at which the two differ by no more than the error of the measurement, andwithin_two_errorsby no more than twice that; for a model without systematic deviation and a correctly stated error they are about 0.68 and 0.95.
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.
Affichage et enregistrement des progrès
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)[source]
Bases :
objectTerminal 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
SolverProgressso 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. Callersecho()each raw line andupdate()the position;SolverProgresskeeps doing both from onefeed().- echo(line)[source]
Print one raw output line, keeping the bar at the bottom on a TTY.
- Paramètres:
line (str)
- Type renvoyé:
None
- class axqua.progress.SolverProgress(total_time, *, width=40, out=None, sink=None, echo=True)[source]
Bases :
objectEcho streamed TELEMAC output and render a simulated-time progress bar.
Feed each stdout line to
feed(); callclose()when the run ends. The terminal rendering isProgressBar; this class owns only the TELEMAC listing parsing.
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)[source]
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
Nonewhen none was given).
- axqua.logsetup.reset_logging()[source]
Remove every handler this module added (used by tests / re-init).
- Type renvoyé:
None
- axqua.logsetup.logging_to(logfile, *, level=20, console=True)[source]
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
withblock only, so a later phase logging elsewhere is not written here. Used for the mesh-convergence study (logs to postprocessing/).
- axqua.logsetup.log_step(name, *, logger=<Logger axqua (WARNING)>, level=20)[source]
Time a calculation step and log its start, duration, or failure.
Logs
START <name>, thenDONE <name> in N.NNson success orFAILED <name> after N.NNs: ...(and re-raises) on error, so every step’s elapsed time lands in the compound logfile.