Skip to content

Field integration ​

data_integrate(mesh, arrays=…) computes a cell-measure-weighted total and mean of one or more cell_data arrays — the natural companion to gradient: gradient differentiates a field, this integrates one. It is a mesh operation, not a file format, uses only standard C++/numpy, and runs under every mesh backend.

meshio++ could already differentiate (gradient), estimate error from a field (error estimation) and summarize a data array's raw statistics (data_info) — but none of those gives a physically meaningful total. A density field's total mass, a heat-flux field's total power, a material tag's occupied volume: all three need the value weighted by the cell it lives on, which data_info's plain unweighted mean of raw values cannot give.

python
import meshioplusplus as mp

mesh = mp.read("solution.vtu")

report = mp.data_integrate(mesh, arrays=["density"])
arr = report[0]
arr["domain"]["total_per_component"]   # sum(value * |measure|) -- total mass
arr["domain"]["mean_per_component"]    # the measure-weighted mean density
arr["regions"]                         # the same, independently, per named Cell region

Where it lives, and why ​

The five data operations are defined by never touching geometry. This one consumes data arrays but reads geometry (each cell's own length/area/volume via detail::cell_measure), so it lives in the mesh-operations layer (operations/data_integrate.hpp) alongside gradient and estimate_error, not in the data_* family.

It is still reachable as meshioplusplus data integrate in both CLIs, because that is where a user looks for it — the same deliberate inconsistency gradient and estimate_error already accept in the data group.

What it computes ​

Per-region totals of a cell field, weighted by cell measure

For each requested array and each component k:

domain_measure[k] = Σ |measure(cell)|              over cells with a computable
                                                     measure AND a finite value[k]
total[k]          = Σ value[k](cell) * |measure(cell)|   over the same cells
mean[k]           = total[k] / domain_measure[k]          (NaN if the denominator is 0)

measure is the cell's own length (1-D), area (2-D) or volume (3-D), the same primitive gradient's Green-Gauss method and compute_quality use.

Exclusion is symmetric between geometry and value, and deliberately does not fall back to a unit weight. A cell whose measure cannot be computed (a ragged or unsupported-type block, or a degenerate one) is excluded from both the numerator and the denominator — never given a fallback weight of 1, unlike cell_data_to_point_data's own Measure weighting, because a silent unit-weight substitution would corrupt a physical total in a way it only softens an average. Excluded cells are counted in num_skipped.

A non-finite value in one component excludes that cell from that component's numerator and denominator too — not just zeroed into the numerator, or the mean would be silently biased. Counted per component in num_nan_per_component. This mirrors the geometric exclusion rule one level down, component by component: a NaN in component 1 of a 3-component array does not disturb components 0 and 2.

There is deliberately no whole-array (cross-component) total or mean, unlike data_info's collapsed statistics over a flattened numeric stream. Summing a vector field's x/y/z totals into one number has no general physical meaning, unlike a flattened mean, which needs only "these are numbers".

There is deliberately no nan_policy, like gradient and data_info: this is a reduction with nothing to exclude a value from — non-finite values are always excluded and always counted, with no configurable policy.

cell_data only ​

A point_data-only name fails, naming point_data_to_cell_data (CLI data to-cell) as the fix — the mirror image of gradient's own contract, which throws on a cell_data name pointing at data to-point. Proper point_data integration needs shape-function quadrature, a separate and larger job than this one.

arrays empty (or omitted) means every cell_data array, in sorted name order.

Regions ​

Every named Cell region present on the mesh gets its own independent entry in regions, alongside the whole-mesh domain entry — mirroring split's own "split by region" contract: regions are not a partition. A cell belonging to two regions contributes fully to both; a cell in none contributes to neither.

Point/Side regions are skipped, exactly as split skips them for the same reason — there is no cell-measure meaning for a group of points or facets.

python
mesh.regions.append(mp.Region(name="inlet", kind="cell", entries=[0, 1, 4]))
report = mp.data_integrate(mesh, arrays=["density"])
for region in report[0]["regions"]:
    print(region["name"], region["total_per_component"])

The mesh is never modified ​

Like data_info and compute_stats, this returns a read-only report; the input mesh comes back untouched. There is no counterpart to gradient's mesh-with-a-new-array-attached shape.

Determinism ​

Output is byte-identical across the three mesh backends, across thread counts, and across the C++-core / numpy-fallback boundary (pinned by tests/python/test_data_integrate.py::test_cpp_matches_python). Per-cell measures are computed once, shared across every array and every region, via parallel_for; every weighted-sum reduction is chunked-parallel_for-then-serially-merged (detail::accumulate_weighted), the same idiom data_info's detail::accumulate_stats already uses.

CLI ​

sh
meshioplusplus data integrate IN --array NAME [--array NAME2 ...] [--json]

Report-only, like data info — there is no OUT, since the mesh is never modified. Both CLIs produce the same report shape. See the CLI reference.

Other languages ​

c
mio_data_integrate* result = mio_data_integrate_create(mesh, names, count);
mio_field_integral_info entry;
mio_data_integrate_entry(result, 0, &entry);   // whole-mesh: num_cells, num_skipped, ...
double total, mean, domain_measure; int64_t num_nan;
mio_data_integrate_component(result, 0, 0, &total, &mean, &domain_measure, &num_nan);
mio_data_integrate_free(result);
fortran
type(mio_field_integral_info), allocatable :: fi(:)
real(real64), allocatable :: totals(:), means(:), domain_measures(:)
fi = m%data_integrate(arrays=['density '], totals=totals, means=means, &
                      domain_measures=domain_measures)
! Per-region breakdown of one named array is a separate call:
fi = m%data_integrate_region('density', totals=totals)
julia
report = data_integrate(mesh, ["density"])
report[1].domain.components[1].total
report[1].regions   # one entry per named Cell region
r
report <- mio_data_integrate(mesh, "density")
report[[1]]$domain$components["total"]
report[[1]]$regions   # a list, one per named Cell region
js
const report = m.dataIntegrate(mesh, ['density']);
report[0].domain.totalPerComponent;
report[0].regions;   // one entry per named Cell region

Two conventions differ across these surfaces and are worth pinning down:

  • On the flat ABIs (C, Fortran, Julia, R, WASM) this is a read-only report handle/value, never an opaque mesh-carrying result — there is no mesh to hand back, so data_info's shape is the right one, extended with a region axis.
  • Fortran alone splits domain and region access into two calls (data_integrate for the whole mesh, data_integrate_region for one named array's per-region breakdown) rather than nesting three axes (array × region × component) into one call's optional out-arguments.

Released under the MIT License.