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()