Core data structures ==================== pyOFTools moves data through a pipeline. Every stage has a concrete Python type, and every type has a small, well-defined shape. This page lists them and shows how they connect. Knowing these types is optional for writing a post-processor — the ``builders`` module hides most of them — but it is essential for writing custom nodes, reading tracebacks, or debugging why an aggregator rejected a dataset. The pipeline in one picture --------------------------- .. mermaid:: graph LR subgraph Source Mesh["fvMesh / sampledSurface / sampledSet"] end subgraph "Geometry adapter" Adapter["InternalMesh
SurfaceMesh
SetGeometry
BoundaryMesh"] end subgraph "Dataset (field + geometry + mask/groups)" DS["InternalDataSet
SurfaceDataSet
PointDataSet
PatchDataSet"] end subgraph "Nodes" Filter["Filter node
(Box, Sphere, Threshold)"] Binner["Binner
(Directional)"] Agg["Aggregator
(Sum, Mean, VolIntegrate)"] end Result["AggregatedDataSet"] Writer["TableWriter → CSV"] Mesh --> Adapter Adapter --> DS DS -->|masked| Filter Filter --> DS DS -->|grouped| Binner Binner --> DS DS --> Agg Agg --> Result Result --> Writer Read the arrows as "produces". The dataset types in the middle are the stable currency of the pipeline: filters and binners return a dataset of the same kind they received, aggregators always return an ``AggregatedDataSet``. The four input datasets ----------------------- All four share the same outer shape — ``name``, ``field``, ``geometry``, optional ``mask``, optional ``groups`` — and differ only in the geometry they carry. Pydantic ``BaseModel`` is used so nodes can declare dataset fields by type and have them validated at construction. .. list-table:: :header-rows: 1 :widths: 20 24 30 26 * - Dataset - Geometry protocol - What the geometry carries - Produced by * - ``InternalDataSet`` - ``InternalMesh`` - ``positions`` (cell centres), ``volumes`` - ``field(mesh, name)`` * - ``SurfaceDataSet`` - ``SurfaceMesh`` - ``positions`` (face centres), ``face_areas``, ``face_area_magnitudes``, ``total_area`` - ``create_plane``, ``create_iso_surface``, ``create_patch_surface`` * - ``PointDataSet`` - ``SetGeometry`` - ``positions`` (sample points), ``distance`` (cumulative arc length) - ``create_uniform_set``, ``create_polyline_set``, ``create_circle_set``, ``create_cloud_set`` * - ``PatchDataSet`` - ``BoundaryMesh`` - ``positions`` (patch face centres) - Boundary-field builders ``field`` is a pybFoam ``scalarField`` / ``vectorField`` / ``tensorField`` / ``symmTensorField``. Size matches the geometry (one entry per cell, face, or sample point). ``mask`` is an optional boolean array selecting which elements participate in subsequent steps. ``Box`` and ``Sphere`` write to it; aggregators honour it. ``groups`` is an optional integer array assigning each element to a bin. ``Directional`` writes to it; aggregators reduce per-group, producing one ``AggregatedData`` row per group. The geometry protocols ---------------------- Geometry is a Python ``Protocol``, not a class hierarchy. Anything with the right attributes satisfies it, so OpenFOAM objects wrap into the pipeline via thin adapters (pure Python, no copies): - ``FvMeshInternalAdapter(mesh)`` — exposes cell centres and cell volumes from an ``fvMesh``. - ``SampledSurfaceAdapter(surface)`` — exposes face centres and face areas from a ``sampledSurface`` (what ``create_plane`` and ``create_iso_surface`` return underneath). - ``SampledSetAdapter(set)`` — exposes point positions and distances from a ``sampledSet`` (underneath the ``create_*_set`` builders). Once wrapped, nothing in the pipeline knows it is talking to an OpenFOAM object — only to an ``InternalMesh``, ``SurfaceMesh``, etc. This is what lets the same ``Mean`` node operate on volumes, surfaces, or lines. The output dataset ------------------ ``AggregatedDataSet`` is the pipeline's exit type. It has no geometry and no mask — just a name and a list of ``AggregatedData`` rows. Each row carries: - ``value`` — a scalar, vector, or tensor result. - ``group`` — the bin label(s) the value belongs to (``None`` if ungrouped). - ``group_name`` — the names of the grouping columns. ``AggregatedDataSet.headers`` and ``AggregatedDataSet.grouped_values`` expose the data in a CSV-ready shape; ``TableWriter`` consumes those directly. Which node consumes which dataset --------------------------------- Most nodes are geometry-agnostic: ``Sum`` and ``Mean`` work on any dataset because they only touch ``field``, ``mask``, and ``groups``. A few need specific geometry: - ``VolIntegrate`` requires ``InternalMesh`` (needs cell volumes). - ``SurfIntegrate`` requires ``SurfaceMesh`` or ``BoundaryMesh`` (needs face areas). - ``Directional`` reads ``positions`` from any geometry, so it works on all four input dataset types. - ``Box`` and ``Sphere`` read ``positions`` and write to ``mask``; they also work on all four. A type mismatch surfaces as a Pydantic validation error at node construction or a ``TypeError`` at ``compute()`` time, not as silent wrong results. Lifetime and ownership ---------------------- - A ``WorkFlow`` is rebuilt every time step inside :class:`~pyOFTools.tables.table.TableWriter`. The field data is read fresh from the running solver, so you see current values — not a stale snapshot. - Geometry adapters hold references to the underlying OpenFOAM objects. Those live for the lifetime of the ``fvMesh``. Don't cache an adapter past a mesh motion / topology change. - Aggregation results (the ``AggregatedDataSet``) are plain Python numbers and lists. They are safe to pickle, compare in tests, or return from a function. See also -------- - :doc:`workflow_internals` — how ``WorkFlow`` chains nodes and what ``TableWriter`` does in parallel. - :doc:`custom_nodes` — the ``compute(dataset) -> dataset`` contract you implement when you add a new node.