Postprocessing
axqua.postproc Rendert Zahlen aus den Ergebnissen eines Backends. Ein Backend beschreibt, was es produziert hat (Dateien, Felder, Zeiten), und dieses Paket zeichnet es. Es importiert kein Backend.
Beschreibung des Ergebnisses
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)[Quellcode]
Bases:
objectOne openable result.
kind is how a reader should open it, not what produced it:
openfoama
case.foamhandle - VisIt and ParaView both read the case directory natively, so nothing is converted.vtu/pvda VTK XML file. This is the TELEMAC route, because no visualizer reads SERAFIN and there is no
foamToVTKanalogue for it (seeaxqua.postproc.selafin_vtk).
- Parameter:
- 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)[Quellcode]
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.
Zahlen
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, ...], ...]' = ())[Quellcode]
Bases:
object- Parameter:
- axqua.postproc.scenes.available_names()[Quellcode]
- axqua.postproc.scenes.resolve(names)[Quellcode]
Scenes by name, in the order given.
Nonemeans every scene.
- axqua.postproc.scenes.check(scene, dataset, *, extras=())[Quellcode]
(available, reason)for one scene against one dataset.extras are requirements satisfied by something other than the dataset itself -
groundtruth:hydraulicscomes from the case, not from the result file.
- axqua.postproc.scenes.resolved_requirements(scene, dataset, *, extras=())[Quellcode]
Which requirement set this dataset actually satisfies.
The renderer needs to know: a rigid-lid case reaches
free-surfacethrough the lid-patch fallback, and must be drawn - and captioned - differently from a two-phase case that has a real interface to contour.
- axqua.postproc.scenes.plan(datasets, *, names=None, extras=())[Quellcode]
For every requested scene: the dataset that can draw it, or the reason none can.
Reported rather than raised, so
axqua postproc --listcan show a whole case at once - the same idea asaxqua case-statusfor capabilities.
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)[Quellcode]
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.
- axqua.postproc.render.case_extras(cfg)[Quellcode]
Requirements satisfied by the case rather than by a result file.
Currently just the ground truth, which is what the
profilesscene compares against - it is a property of the case, not of the SELAFIN or the OpenFOAM directory, so a dataset could never report it.
- axqua.postproc.render.measurement_points(cfg)[Quellcode]
The verticals the
profilesscene samples along.Each carries the bed elevation and the measured depth, so the lineout spans the actual water column rather than a guessed range.
- axqua.postproc.render.plan(cfg, *, names=None, solver=None)[Quellcode]
(scene, dataset|None, reason)for every requested scene.
- axqua.postproc.render.plan_lines(cfg, *, names=None, solver=None)[Quellcode]
The plan as text, for
axqua postproc --list.
- axqua.postproc.render.render(cfg, *, names=None, solver=None, out_dir=None, script_only=False)[Quellcode]
Render the requested scenes. Returns a process exit code.
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)[Quellcode]
One VisIt Curve2D export as
(distance_along_line, value).- Parameter:
path (Path)
- Rückgabetyp:
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 itvisit -cli -nowindrops into an interactive loop and the subprocess hangs forever - the single most likely silent failure in this whole path.outputToCurrentDirectory = 0plus an explicitoutputDirectory, 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)[Quellcode]
Bases:
ShellRuntimeRun 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.environmentcovers the installs that do need a setup script, and is what makes this work through WSL on Windows.- check_available()[Quellcode]
- Rückgabetyp:
- execute(script, *, on_line=None)[Quellcode]
Run one generated script. Never raises on a non-zero exit - see
failed(), which reads the output rather than the status.- Parameter:
script (Path)
- Rückgabetyp:
- axqua.postproc.visit.failed(proc)[Quellcode]
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.
- Parameter:
proc (CompletedProcess)
- Rückgabetyp:
str | None
- axqua.postproc.visit.available(visit)[Quellcode]
The VisIt version, or
None. Never raises - this is a probe.
- axqua.postproc.visit.visit_target(dataset)[Quellcode]
The path VisIt’s own readers open, which is not always the canonical one.
For an OpenFOAM case that is
system/controlDict, not thecase.foammarker: unlike ParaView, VisIt’s OpenFOAM reader cannot open a zero-byte.foamhandle and reports the file as invalid. Measured against VisIt 3.5.0 -case.foamyields 0 meshes and 0 variables,system/controlDictyields the full case. TheDatasetkeepscase.foambecause that is what ParaView and the QGIS manifest want; knowing how VisIt opens a case is this module’s business.
- axqua.postproc.visit.render_script(dataset, scene, out_dir, *, width=1600, height=1000, time='last', extras=(), **kwargs)[Quellcode]
The complete VisIt script for one scene against one dataset.
- axqua.postproc.visit.write_script(text, path)[Quellcode]
Export von TELEMAC-Ergebnissen
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>)[Quellcode]
Bases:
objectWhat a conversion wrote.
- Parameter:
- as_dict()[Quellcode]
- 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)[Quellcode]
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.
- Parameter:
slf (SelafinFile)
- Rückgabetyp:
- axqua.postproc.selafin_vtk.export_selafin(source, out_dir, *, name=None, geometry=None, frames='all', zsource='flat', compress=True, on_frame=None)[Quellcode]
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
.pvdand the folder never disagree.
- axqua.postproc.selafin_vtk.select_frames(n_frames, frames='all')[Quellcode]
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.
- axqua.postproc.selafin_vtk.vector_groups(names)[Quellcode]
Variables that are the components of one vector:
{vector: [components]}.VELOCITY UandVELOCITY V(andVELOCITY W) becomeVELOCITY. A name is only grouped when its first two components both exist, soFRICTION VELOCITYor a tracer calledTRACER Xalone stays what it is.