Generate the moment coefficient (Cm)

The moment coefficient is the pressure-induced moment about a chosen lever origin, normalized by a reference area and length. The v3 pipeline reuses the per-triangle force contribution and adds a moment_contribution op about lever_origin, then sums each direction over the body.

Set up a working directory

A pipeline template declares its inputs, ops and outputs with paths that are resolved relative to the template file. The shipped example templates live under fixtures/tests/pressure/templates/ and reference sibling ../data/ and ../galpao/ folders, so we copy the templates and the fixture data into a single scratch directory and run from there. In a real project you would instead point the template at your own data and run cfdmod run <template>.yaml directly.

[1]:
%matplotlib inline
import pathlib
import shutil
import tempfile


def find_fixtures() -> pathlib.Path:
    """Walk up from the current directory to locate the repo fixtures."""
    for base in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:
        candidate = base / "fixtures" / "tests" / "pressure"
        if candidate.is_dir():
            return candidate
    raise FileNotFoundError("could not locate fixtures/tests/pressure")


fixtures = find_fixtures()
workdir = pathlib.Path(tempfile.mkdtemp(prefix="cfdmod_pressure_"))
for name in ("data", "galpao", "templates"):
    shutil.copytree(fixtures / name, workdir / name)
(workdir / "out").mkdir(exist_ok=True)

print("working directory:", workdir)
working directory: /tmp/cfdmod_pressure_qtxri708

Compute Cp first

The force, moment and shape coefficients all consume a Cp time series. We run the cp.yaml template first; it writes ./out/cp.time_series inside the working directory, which the template below reads back in.

[2]:
from cfdmod import load_template, run_template
from cfdmod.adapters.xdmf_h5 import XdmfH5Storage

storage = XdmfH5Storage(pathlib.Path("/"))
run_template(load_template(workdir / "templates" / "cp.yaml"), storage=storage)
print("Cp time series written to", workdir / "out")
Cp time series written to /tmp/cfdmod_pressure_qtxri708/out

The Cm pipeline template

The chain is mesh_attach -> body_grouping -> force_contribution -> moment_contribution (about lever_origin) -> field_series_for_groups. The moment op reuses the cf_<dir> fields produced upstream.

[3]:
print((workdir / "templates" / "cm.yaml").read_text())
# Cm pipeline template (v3 schema).
#
# Reads a Cp time series and produces per-body moment coefficients.
# Composes:
#   1. mesh_attach -> areas, normals, centroids on the cp source.
#   2. body_grouping -> body assignment per triangle.
#   3. force_contribution -> per-triangle cf_x/cf_y/cf_z.
#   4. moment_contribution -> per-triangle cm_x/cm_y/cm_z about a chosen
#      lever_origin. Reuses cf_<dir> from step 3.
#   5. field_series_for_groups -> sum the cm_<dir> over each body.
name: cm_default

inputs:
  cp:
    kind: surface
    path: ./out/cp.time_series

pipeline:
  - id: cp_with_mesh
    kind: mesh_attach
    source: cp
    mesh: ../galpao/galpao.normalized.lnas

  - id: cp_grouped
    kind: body_grouping
    source: cp_with_mesh
    mesh: ../galpao/galpao.normalized.lnas
    bodies:
      building: []

  - id: with_forces
    kind: force_contribution
    source: cp_grouped
    field: cp
    nominal_area: 100.0
    directions: [x, y, z]

  - id: with_moments
    kind: moment_contribution
    source: with_forces
    lever_origin: [0.0, 10.0, 10.0]
    nominal_area: 100.0
    nominal_volume: 10.0
    directions: [x, y, z]

  - id: cm_x
    kind: field_series_for_groups
    source: with_moments
    grouping: body
    field: cm_x
    agg: sum

  - id: cm_y
    kind: field_series_for_groups
    source: with_moments
    grouping: body
    field: cm_y
    agg: sum

  - id: cm_z
    kind: field_series_for_groups
    source: with_moments
    grouping: body
    field: cm_z
    agg: sum

outputs:
  cm_x: {source: cm_x, path: ./out/cm_x.time_series}
  cm_y: {source: cm_y, path: ./out/cm_y.time_series}
  cm_z: {source: cm_z, path: ./out/cm_z.time_series}

Run the pipeline

[4]:
bindings = run_template(load_template(workdir / "templates" / "cm.yaml"), storage=storage)
sorted(bindings)
[2026-07-06 01:02:57,695] [INFO] - cfdmod - apply_groupings: 1 grouping(s) on mesh with 2915 triangle(s) and 20 surface(s)
[2026-07-06 01:02:57,697] [INFO] - cfdmod -   [0] BySurfaceGrouping produced 1 group(s): ['building']
[4]:
['cm_x',
 'cm_y',
 'cm_z',
 'cp',
 'cp_grouped',
 'cp_with_mesh',
 'with_forces',
 'with_moments']

Per-body moment coefficients

Each cm_<dir> output is a GroupsDataSource with one row per body.

[5]:
import pandas as pd

from cfdmod.core.grouping import groups_in


def group_labels(ds):
    pg = ds.parent_grouping
    return [pg.label(g) for g in groups_in(pg)]


rows = {}
for direction in ("x", "y", "z"):
    ds = bindings[f"cm_{direction}"]
    rows[f"cm_{direction}"] = ds.fields.read(f"cm_{direction}").mean(axis=1)
df = pd.DataFrame(rows, index=group_labels(bindings["cm_x"]))
df.index.name = "body"
df
[5]:
cm_x cm_y cm_z
body
building 87554.298519 -135575.443228 26091.187522

Time history of the moment coefficient

[6]:
import matplotlib.pyplot as plt

cm_z = bindings["cm_z"]
times = cm_z.time.times()
series = cm_z.fields.read("cm_z")

fig, ax = plt.subplots(figsize=(7, 3.5))
for row, label in enumerate(group_labels(cm_z)):
    ax.plot(times, series[row], label=label)
ax.set_xlabel("time")
ax.set_ylabel("cm_z")
ax.set_title("Moment coefficient (z) per body")
ax.legend(loc="best", fontsize=8)
plt.tight_layout()
plt.show()
../../../_images/use_cases_pressure_coefficients_calculate_Cm_12_0.png