Cette page a été traduite automatiquement de l’anglais et peut contenir des erreurs. Original en anglais

Post-traitement

axqua.postproc rend les chiffres à partir des résultats de chaque moteur. Un moteur décrit ce qu’il a produit (fichiers, champs, temps), et ce paquet le dessine. Il n’importe pas de moteur.

Description du résultat

What a visualizer can open, described without naming a solver.

Standard library only, deliberately: a solver backend has to build these to answer axqua.core.registry.SolverBackend.describe_results(), and that call must stay cheap enough to make from a capability listing. Anything heavier belongs in the renderer, not in the description of what there is to render.

class axqua.postproc.dataset.Dataset(solver, kind, path, times=(), fields=frozenset({}), patches=frozenset({}), bounds=None, vector_fields=<factory>, notes=(), decomposed=False)[source]

Bases : object

One openable result.

kind is how a reader should open it, not what produced it:

openfoam

a case.foam handle - VisIt and ParaView both read the case directory natively, so nothing is converted.

vtu / pvd

a VTK XML file. This is the TELEMAC route, because no visualizer reads SERAFIN and there is no foamToVTK analogue for it (see axqua.postproc.selafin_vtk).

Paramètres:
solver: str
kind: str
path: Path
times: tuple[float, ...] = ()
fields: frozenset[str] = frozenset({})
patches: frozenset[str] = frozenset({})
bounds: tuple[float, ...] | None = None
vector_fields: Mapping[str, tuple[str, ...]]
notes: tuple[str, ...] = ()
decomposed: bool = False

a reader has to open it as a decomposed case, or it sees t = 0 alone.

Type:

An OpenFOAM case whose result only the processor folders of a parallel run hold

has(capability)[source]

Whether this dataset satisfies one scene requirement.

The vocabulary is deliberately tiny and string-based so a scene can declare what it needs without importing anything: field:U, patch:lid, times>1.

Paramètres:

capability (str)

Type renvoyé:

bool

property last_time: float | None

Chiffres

What to draw: the scene registry, and why a scene is or is not available.

Standard library only. A Scene declares its requirements in the small vocabulary axqua.postproc.dataset.Dataset.has() understands, so plan() can say why a figure cannot be made instead of failing somewhere inside a renderer - which, for a tool that shells out to VisIt, is the difference between a useful message and a silent empty PNG.

Scene.render names a renderer rather than holding a function, so the same scene definition serves any backend that implements that name.

class axqua.postproc.scenes.Scene(name: 'str', title: 'str', solvers: 'frozenset[str]', requires: 'tuple[str, ...]', render: 'str', options: 'Mapping[str, Any]' = <factory>, fallbacks: 'tuple[tuple[str, ...], ...]' = ())[source]

Bases : object

Paramètres:
name: str
title: str
solvers: frozenset[str]
requires: tuple[str, ...]
render: str
options: Mapping[str, Any]
fallbacks: tuple[tuple[str, ...], ...] = ()

Alternative requirement sets, tried in order when requires is unmet. This is how one scene covers both the two-phase and the rigid-lid case: under a rigid lid there is no alpha.water to contour, but there IS a lid patch carrying the (prescribed) surface.

axqua.postproc.scenes.available_names()[source]
Type renvoyé:

tuple[str, …]

axqua.postproc.scenes.resolve(names)[source]

Scenes by name, in the order given. None means every scene.

Paramètres:

names (Sequence[str] | None)

Type renvoyé:

list[Scene]

axqua.postproc.scenes.check(scene, dataset, *, extras=())[source]

(available, reason) for one scene against one dataset.

extras are requirements satisfied by something other than the dataset itself - groundtruth:hydraulics comes from the case, not from the result file.

Paramètres:
Type renvoyé:

tuple[bool, str]

axqua.postproc.scenes.resolved_requirements(scene, dataset, *, extras=())[source]

Which requirement set this dataset actually satisfies.

The renderer needs to know: a rigid-lid case reaches free-surface through the lid-patch fallback, and must be drawn - and captioned - differently from a two-phase case that has a real interface to contour.

Paramètres:
Type renvoyé:

tuple[str, …]

axqua.postproc.scenes.plan(datasets, *, names=None, extras=())[source]

For every requested scene: the dataset that can draw it, or the reason none can.

Reported rather than raised, so axqua postproc --list can show a whole case at once - the same idea as axqua case-status for capabilities.

Paramètres:
Type renvoyé:

list[tuple[Scene, object | None, str]]

Orchestration: config -> datasets -> scenes -> files.

The only module here that knows about both halves. It asks each enabled backend what it has produced (axqua.core.registry.SolverBackend.describe_results()), matches that against the requested scenes, and hands the pairs to a rendering backend.

It does not import a solver: the datasets arrive through the registry as plain data, which is the whole point of the describe_results seam.

axqua.postproc.render.datasets(cfg, *, solver=None)[source]

Every result this case can show, across the enabled backends.

Never raises for one misbehaving backend: a listing that dies because a solver it does not care about has a broken case directory is useless.

Paramètres:

solver (str | None)

Type renvoyé:

list[Dataset]

axqua.postproc.render.case_extras(cfg)[source]

Requirements satisfied by the case rather than by a result file.

Currently just the ground truth, which is what the profiles scene compares against - it is a property of the case, not of the SELAFIN or the OpenFOAM directory, so a dataset could never report it.

Type renvoyé:

list[str]

axqua.postproc.render.measurement_points(cfg)[source]

The verticals the profiles scene samples along.

Each carries the bed elevation and the measured depth, so the lineout spans the actual water column rather than a guessed range.

Type renvoyé:

list[dict]

axqua.postproc.render.plan(cfg, *, names=None, solver=None)[source]

(scene, dataset|None, reason) for every requested scene.

Paramètres:
Type renvoyé:

list[tuple]

axqua.postproc.render.plan_lines(cfg, *, names=None, solver=None)[source]

The plan as text, for axqua postproc --list.

Paramètres:
Type renvoyé:

list[str]

axqua.postproc.render.render(cfg, *, names=None, solver=None, out_dir=None, script_only=False)[source]

Render the requested scenes. Returns a process exit code.

Paramètres:
Type renvoyé:

int

Modeled vertical velocity profiles against the measured verticals.

The scientific payoff of the 3D model, and the figure that closes the loop with the OpenFOAM calibration: run it once at the prior center and once at the posterior best, and the two panels are the calibration result.

VisIt samples, matplotlib draws. VisIt’s Lineout produces one curve per vertical (axqua.postproc.visit._render_profiles()); this module reads those curves and plots them. The split is deliberate - the measured overlay needs error bars, the figure has to match aXqua’s other figures (report._write_plot, flux_convergence), and once the curves exist the comparison must stay reproducible on a machine with no VisIt at all.

axqua.postproc.profiles.MEASUREMENT_FRACTION = 0.4

Fallback height above the BED, as a fraction of the depth, when the ground truth does not say where the probe sat. The « 0.6 depth » convention measures DOWN FROM THE SURFACE (verified on this dataset: MeasD/FinalD has median 0.59), so the probe is at 0.4 of the depth ABOVE the bed. This constant was 0.6 and documented as « from the BED », which placed every comparison 0.2*h too high. Prefer the per-row value from ground_truth.relative_height, which reads MeasD/FinalD.

axqua.postproc.profiles.read_curve(path)[source]

One VisIt Curve2D export as (distance_along_line, value).

Paramètres:

path (Path)

Type renvoyé:

tuple[numpy.ndarray, numpy.ndarray]

axqua.postproc.profiles.compare(cfg, curves_dir, out_dir)[source]

Plot modeled profiles against the measurements; write the figure and a CSV.

Paramètres:
Type renvoyé:

list[Path]

VisIt

The VisIt backend: generate a script, run it in VisIt’s own Python.

VisIt ships its own interpreter, so import visit from aXqua’s environment is not possible - and unlike ParaView there is no importable module to fall back on. The backend therefore writes a script and executes it with visit -cli -nowin -s, which is the same shape as axqua.solvers.openfoam.runtime.OpenFoamRuntime sourcing etc/bashrc: aXqua never imports the tool’s Python, it invokes it.

Everything the script needs is baked in as a literal. No sys.argv, no environment lookups: argument forwarding after -s differs between VisIt versions, and a self-contained script is also the reproducible record of how a figure was made. The generated scripts are kept under <postprocessing_dir>/visit/ for exactly that reason.

Three details are load-bearing and each is pinned by a test:

  • the script ends with exit(0). Without it visit -cli -nowin drops into an interactive loop and the subprocess hangs forever - the single most likely silent failure in this whole path.

  • outputToCurrentDirectory = 0 plus an explicit outputDirectory, or the PNG lands wherever VisIt happened to start.

  • errors inside the script print AXQUA-VISIT-ERROR: and exit non-zero, because VisIt’s CLI exit status alone is not reliable enough to judge a run by.

class axqua.postproc.visit.VisitRuntime(postproc_config)[source]

Bases : ShellRuntime

Run a generated script in VisIt’s own Python.

Normally there is nothing to source - VisIt ships a self-contained launcher - so the environment degrades to a plain bash -lc. postproc.environment covers the installs that do need a setup script, and is what makes this work through WSL on Windows.

check_available()[source]
Type renvoyé:

str

execute(script, *, on_line=None)[source]

Run one generated script. Never raises on a non-zero exit - see failed(), which reads the output rather than the status.

Paramètres:

script (Path)

Type renvoyé:

CompletedProcess

axqua.postproc.visit.failed(proc)[source]

The error a VisIt run reported, or None.

VisIt’s CLI exit status is unreliable, so the marker the generated script prints is what is trusted; the status is only a fallback.

Paramètres:

proc (CompletedProcess)

Type renvoyé:

str | None

axqua.postproc.visit.available(visit)[source]

The VisIt version, or None. Never raises - this is a probe.

Paramètres:

visit (str | Path)

Type renvoyé:

str | None

axqua.postproc.visit.visit_target(dataset)[source]

The path VisIt’s own readers open, which is not always the canonical one.

For an OpenFOAM case that is system/controlDict, not the case.foam marker: unlike ParaView, VisIt’s OpenFOAM reader cannot open a zero-byte .foam handle and reports the file as invalid. Measured against VisIt 3.5.0 - case.foam yields 0 meshes and 0 variables, system/controlDict yields the full case. The Dataset keeps case.foam because that is what ParaView and the QGIS manifest want; knowing how VisIt opens a case is this module’s business.

Paramètres:

dataset (Dataset)

Type renvoyé:

Path

axqua.postproc.visit.render_script(dataset, scene, out_dir, *, width=1600, height=1000, time='last', extras=(), **kwargs)[source]

The complete VisIt script for one scene against one dataset.

Paramètres:
Type renvoyé:

str

axqua.postproc.visit.write_script(text, path)[source]
Paramètres:
Type renvoyé:

Path

Exportation des résultats de TELEMAC

TELEMAC results for ParaView and VisIt.

Neither program reads SERAFIN, the result format of TELEMAC (checked with ParaView 6.1 and VisIt 3.5), and both read VTK. This module converts a result file into VTK without TELEMAC and without the vtk package: the unstructured-grid format is a small XML header followed by the arrays, which numpy writes directly.

One result becomes one folder:

<name>.pvd what ParaView opens: the list of time steps with their times <name>.visit what VisIt opens: the same list in VisIt’s own form <name>/<name>_0000.vtu … one file per printout

A 2D result becomes a surface of triangles in the plane, as the model has it. The fields are the variables of the result under their TELEMAC names (WATER DEPTH, FREE SURFACE). The nodes stand at elevation zero by default, and that is deliberate: an area or a volume integrated over the surface is then the one of the model, and a streamline stays in the plane the depth-averaged velocity lives in. With the nodes at the bed elevation the same integral runs over the sloping bed, which on the Isar example is 1.4 % more area and 1.7 % more water than the model holds. The relief is one filter away where it is wanted for a picture (Warp By Scalar with BOTTOM in ParaView, Elevate in VisIt), and zsource="bed" or "surface" writes it into the files for those who prefer that.

A 3D result becomes a volume of prisms between the planes of the TELEMAC-3D mesh, at the elevations of each printout (ELEVATION Z): the mesh moves with the water surface from frame to frame, as it does in the simulation.

Components that belong together are also written as a vector (VELOCITY from VELOCITY U, V and, in 3D, W), because arrows and streamlines need one.

Coordinates come from the geometry file of the model, not from the result. TELEMAC writes its results in single precision, coordinates included, and a number near five million has a resolution of half a meter there: in a result file of a reach in UTM every northing is a multiple of 0.5 m. The simulation is not affected, since TELEMAC computes with the double-precision geometry, but the mesh stored in the result is. On the full-resolution Isar model (0.5 m channel cells) 14 % of its cells have no area left and 70 % have changed their area by more than a quarter. The fields are stored per node, in the numbering of the geometry, so they are simply put on the nodes of the geometry file (geometry=; axqua.model_column.safe_triangulation() states the same rule for sampling). Without a geometry file the coordinates of the result are used, and Export.notes says how coarse they are. The files are written in double precision either way.

Each file carries its time twice, as TimeValue (which ParaView reads) and as TIME and CYCLE (which VisIt reads), so that a single .vtu opened by itself still knows when it is.

class axqua.postproc.selafin_vtk.Export(source, folder, pvd, visit, files=<factory>, times=<factory>, fields=<factory>, vectors=<factory>, kind='2d', n_points=0, n_cells=0, coordinates='result', notes=<factory>)[source]

Bases : object

What a conversion wrote.

Paramètres:
source: Path
folder: Path
pvd: Path
visit: Path
files: list[Path]
times: list[float]
fields: list[str]
vectors: list[str]
kind: str = '2d'
n_points: int = 0
n_cells: int = 0
coordinates: str = 'result'
notes: list[str]
as_dict()[source]
Type renvoyé:

dict[str, Any]

axqua.postproc.selafin_vtk.ZSOURCES = ('flat', 'bed', 'surface')

in the plane, at the bed, at the water surface.

Type:

Where the nodes of a 2D result stand

axqua.postproc.selafin_vtk.coordinate_resolution(slf)[source]

The spacing [m] of the numbers a result stores its coordinates with.

Zero for a double-precision file. For single precision it is the distance between two neighboring representable numbers at the largest coordinate.

Paramètres:

slf (SelafinFile)

Type renvoyé:

float

axqua.postproc.selafin_vtk.export_selafin(source, out_dir, *, name=None, geometry=None, frames='all', zsource='flat', compress=True, on_frame=None)[source]

Convert a TELEMAC result into VTK files for ParaView and VisIt.

geometry is the geometry file of the model, whose coordinates are exact where those of a result are rounded (see the module text); frames selects the printouts (select_frames()); zsource is where the nodes of a 2D result stand (ZSOURCES); on_frame is called with (done, total, time) after each file, for a progress display.

The folder of a result is rewritten whole: files of an earlier export of the same result that the new one does not write are removed, so that the .pvd and the folder never disagree.

Paramètres:
Type renvoyé:

Export

axqua.postproc.selafin_vtk.select_frames(n_frames, frames='all')[source]

The indices a caller asked for: "all", "last", one number, or a list.

A number counts from the end when negative. "every N" takes each N-th frame and always the last one, which is the state a steady run was run for.

Paramètres:
Type renvoyé:

list[int]

axqua.postproc.selafin_vtk.vector_groups(names)[source]

Variables that are the components of one vector: {vector: [components]}.

VELOCITY U and VELOCITY V (and VELOCITY W) become VELOCITY. A name is only grouped when its first two components both exist, so FRICTION VELOCITY or a tracer called TRACER X alone stays what it is.

Paramètres:

names (Sequence[str])

Type renvoyé:

dict[str, list[str]]