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:

\[C_{f,i} = \frac{1}{A_{ref}} \sum_{k} C_{p,k}\, A_k\, n_{k,i}, \qquad i \in \{x, y, z\}.\]

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()
../../../_images/use_cases_pressure_coefficients_calculate_Cf_12_0.png