Generate the force coefficient (Cf)¶
The force coefficient integrates the pressure coefficient over a body’s triangles, weighted by area and projected onto each axis:
As a v3 pipeline this is: attach the mesh geometry, group triangles into bodies, compute a per-triangle force contribution, then sum each direction over each 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_wsn9h4q5
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_wsn9h4q5/out
The Cf pipeline template¶
mesh_attach pulls areas / normals / centroids from the .lnas mesh; body_grouping assigns each triangle to a body; force_contribution produces per-triangle cf_x / cf_y / cf_z; and field_series_for_groups sums each direction over the body.
[3]:
print((workdir / "templates" / "cf.yaml").read_text())
# Cf pipeline template (v3 schema).
#
# Reads a Cp time series (produced by the cp.yaml template above) and
# returns the per-body force coefficients per direction.
#
# Steps
# 1. mesh_attach: pull areas + normals + centroids from the lnas mesh.
# 2. body_grouping: assign each triangle to a body from the bodies dict.
# 3. force_contribution: per-triangle cf_x / cf_y / cf_z from cp.
# 4. field_series_for_groups: sum each cf_<dir> per body.
# 5. statistics: per-body, per-direction mean / rms / peak.
name: cf_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: cp_with_forces
kind: force_contribution
source: cp_grouped
field: cp
nominal_area: 100.0
directions: [x, y, z]
- id: cf_x
kind: field_series_for_groups
source: cp_with_forces
grouping: body
field: cf_x
agg: sum
- id: cf_y
kind: field_series_for_groups
source: cp_with_forces
grouping: body
field: cf_y
agg: sum
- id: cf_z
kind: field_series_for_groups
source: cp_with_forces
grouping: body
field: cf_z
agg: sum
outputs:
cf_x: {source: cf_x, path: ./out/cf_x.time_series}
cf_y: {source: cf_y, path: ./out/cf_y.time_series}
cf_z: {source: cf_z, path: ./out/cf_z.time_series}
Run the pipeline¶
[4]:
bindings = run_template(load_template(workdir / "templates" / "cf.yaml"), storage=storage)
sorted(bindings)
[2026-07-06 01:02:47,977] [INFO] - cfdmod - apply_groupings: 1 grouping(s) on mesh with 2915 triangle(s) and 20 surface(s)
[2026-07-06 01:02:47,979] [INFO] - cfdmod - [0] BySurfaceGrouping produced 1 group(s): ['building']
[4]:
['cf_x', 'cf_y', 'cf_z', 'cp', 'cp_grouped', 'cp_with_forces', 'cp_with_mesh']
Per-body force coefficients¶
Each cf_<dir> output is a GroupsDataSource with one row per body. We assemble the per-body mean force coefficient in each direction.
[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"cf_{direction}"]
rows[f"cf_{direction}"] = ds.fields.read(f"cf_{direction}").mean(axis=1)
labels = group_labels(bindings["cf_x"])
df = pd.DataFrame(rows, index=labels)
df.index.name = "body"
df
[5]:
| cf_x | cf_y | cf_z | |
|---|---|---|---|
| body | |||
| building | -7.504354 | 15.665015 | 91.532796 |
Time history of the force coefficient¶
The full time series is available per body; here is cf_x for each body over time.
[6]:
import matplotlib.pyplot as plt
cf_x = bindings["cf_x"]
times = cf_x.time.times()
series = cf_x.fields.read("cf_x")
fig, ax = plt.subplots(figsize=(7, 3.5))
for row, label in enumerate(group_labels(cf_x)):
ax.plot(times, series[row], label=label)
ax.set_xlabel("time")
ax.set_ylabel("cf_x")
ax.set_title("Force coefficient (x) per body")
ax.legend(loc="best", fontsize=8)
plt.tight_layout()
plt.show()