API Reference

This page documents the public Python surface of cfdmod. Every symbol listed in cfdmod.__all__ is reachable via from cfdmod import X; deeper module paths are also stable.

Top-level entry points

Post-processing is expressed as a pipeline template: a YAML document declaring inputs, a sequence of composable ops, and outputs. Templates run from the command line or in Python; the same recipe code runs whether the backend is an in-memory store (notebooks, tests) or the on-disk XDMF+H5 store (production). See Data sources, ops, and pipelines (v3 paradigm) for the paradigm and Migrating to the v3 paradigm for the mapping from the legacy per-coefficient functions.

cfdmod run path/to/template.yaml
cfdmod.load_template(path: Path | str) PipelineTemplate[source]

Load a YAML template from disk.

root defaults to the directory containing the YAML file so relative path: entries inside inputs: / outputs: are resolved against the template’s own location, not the caller’s cwd.

cfdmod.run_template(template: PipelineTemplate, *, storage: Storage) dict[str, DataSource][source]

Run a parsed template against a Storage.

Returns the dict of all named values (inputs + step outputs) so callers can inspect intermediates. The outputs: block is written through storage.write_data_source as a side effect.

class cfdmod.PipelineTemplate(*, name: str = 'pipeline', root: str | None = None, inputs: dict[str, ~cfdmod.core.pipeline_yaml.InputSpec] = <factory>, pipeline: list[~cfdmod.core.pipeline_yaml.OpSpec] = <factory>, outputs: dict[str, ~cfdmod.core.pipeline_yaml.OutputSpec] = <factory>)[source]

Bases: BaseModel

A complete YAML template.

model_config: ClassVar[ConfigDict] = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

The op registry backing the template loader is extensible; register a new op with cfdmod.register_op() and inspect the built-ins in cfdmod.OP_REGISTRY.

cfdmod.register_op(kind: str, fn: Callable[[...], DataSource], params_cls: type[BaseModel], *, arity: Literal['unary', 'binary'] = 'unary') None[source]

Register an op under kind – the public extension point.

A consumer adds a custom op by writing a function fn(ds, params) -> DataSource (or fn(ds, rhs, params) for a binary op) and a params_cls, then calling this. The op is then a first-class citizen: it is usable in YAML/dict templates under its kind, validated by validate_template(), and listed by list_ops().

For the op’s data-source contract (consumes / produces / requires_element_meta / …) to be picked up by the catalog and the template linter, params_cls should subclass cfdmod.core.ops.OpParams and set those class attributes; a plain BaseModel still registers but is treated as unconstrained.

Idempotent: re-registering the same kind replaces the entry, so a consumer can also override a built-in.

Data sources

Every result – pressures on a surface, cell values in a volume, probe timeseries, group aggregates, modal coordinates – is carried by a frozen cfdmod.DataSource: elements on one axis, timesteps on the other, one or more named fields sharing that shape, plus element / time / field metadata. Ops consume and produce data sources; a cfdmod.Pipeline (built with cfdmod.compose()) chains them.

class cfdmod.DataSource(*, kind: ~typing.Literal['surface', 'volume', 'points', 'groups', 'modes'], time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>)[source]

Bases: BaseModel

Base frozen value object.

Subclasses lock kind and (optionally) the admissible Topology.cell_type. Methods on this base never mutate; they always return a new instance via model_copy(update=...).

kind

One of surface, volume, points, groups, modes. Locked by each subclass.

Type:

Literal[‘surface’, ‘volume’, ‘points’, ‘groups’, ‘modes’]

time

Affine time axis. Time-aggregated outputs use n_timesteps == 0.

Type:

cfdmod.core.time_axis.TimeAxis

topology

Mesh connectivity / coordinates, when applicable. None is permitted for some kinds (notably modes).

Type:

cfdmod.core.topology.Topology | None

elements

Per-element scalar / vector attributes.

Type:

cfdmod.core.topology.ElementMeta

groupings

Mapping of grouping name -> Grouping.

Type:

dict[str, cfdmod.core.grouping.Grouping]

fields

A FieldStore. Carries the heavy arrays.

Type:

cfdmod.core.protocols.FieldStore

field_meta

Mapping of field name -> FieldMeta.

Type:

dict[str, cfdmod.core.field_meta.FieldMeta]

attrs

Free-form source-level metadata.

Type:

dict[str, Any]

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

property n_elements: int

Number of elements (rows) on this data source.

with_field(name: str, value: ndarray, meta: FieldMeta | None = None) DataSource[source]

Add or replace a field. The FieldStore decides whether the array is shared by reference or copied.

with_grouping(grouping: Grouping) DataSource[source]

Add or replace a grouping. The grouping name is the key.

with_time(new_time: TimeAxis) DataSource[source]

Return a copy with a new TimeAxis. Field shapes must already match the new axis – this is a metadata update only.

class cfdmod.SurfaceDataSource(*, kind: ~typing.Literal['surface'] = 'surface', time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>)[source]

Bases: DataSource

Faces (2D triangular cells) with optional timesteps.

Topology cell type must be triangle. Mirrors the existing cfdmod XDMF+H5 timeseries layout: /Triangles, /Geometry, /{group}/t{T}.

class cfdmod.VolumeDataSource(*, kind: ~typing.Literal['volume'] = 'volume', time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>)[source]

Bases: DataSource

3D cells with optional timesteps.

Topology cell type must be cell. Reserved – not a Phase 1 target. The class is here so volume export can be added later additively rather than as a schema change.

class cfdmod.PointsDataSource(*, kind: ~typing.Literal['points'] = 'points', time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>)[source]

Bases: DataSource

Bare points / probes / vertical profiles.

Covers the existing InflowData (probe array + per-component timeseries) and s1.profile.Profile (1-D vertical profile, no time axis). Topology cell type is point; connectivity is empty.

class cfdmod.GroupsDataSource(*, kind: ~typing.Literal['groups'] = 'groups', time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>, parent_topology: ~cfdmod.core.topology.Topology, parent_grouping: ~cfdmod.core.grouping.Grouping)[source]

Bases: DataSource

One row per group: an aggregation over a parent surface.

A groups data source carries fields whose leading axis is the group index, not the original element index. Its topology is chained: it borrows the parent surface’s Topology plus a Grouping mapping each parent element to a group.

The class does not own a triangulation of the groups themselves (each group is in general not a single triangle). This avoids the “non-triangular faces” trap.

parent_topology

The parent surface’s triangle topology.

Type:

Topology

parent_grouping

A Grouping over the parent surface’s elements that determines membership.

Type:

Grouping

class cfdmod.ModesDataSource(*, kind: ~typing.Literal['modes'] = 'modes', time: ~cfdmod.core.time_axis.TimeAxis, topology: ~cfdmod.core.topology.Topology | None, elements: ~cfdmod.core.topology.ElementMeta, groupings: dict[str, ~cfdmod.core.grouping.Grouping] = <factory>, fields: ~cfdmod.core.protocols.FieldStore, field_meta: dict[str, ~cfdmod.core.field_meta.FieldMeta] = <factory>, attrs: dict[str, ~typing.Any] = <factory>)[source]

Bases: DataSource

Modal axis: one row per mode, fields are generalised-displacement timeseries.

No spatial topology; the original mesh / structural data lives alongside in the recipe context. elements typically carries an annotation column with mode labels.

Axes and topology

class cfdmod.TimeAxis(*, initial_time: float, timestep_size: Annotated[float, Ge(ge=0)], n_timesteps: Annotated[int, Ge(ge=0)], time_normalized_offset: float | None = None)[source]

Affine time index.

A time-aggregated DataSource (statistics output, a single snapshot) uses n_timesteps == 0 to mean “no time axis”. The field store side mirrors this: shape (n_elements,) instead of (n_elements, n_timesteps).

initial_time

Time at index 0.

Type:

float

timestep_size

Timestep delta. Must be > 0 when n_timesteps > 0; otherwise the value is irrelevant (kept at 0.0 by convention).

Type:

float

n_timesteps

Number of timesteps. 0 -> no time axis.

Type:

int

time_normalized_offset

Optional offset applied when reporting “normalized” time (i.e. simulation time minus a chosen reference). Mirrors the existing meta/time_normalized convention in cfdmod’s h5 layout. Defaults to initial_time so that without explicit normalization, time_normalized[0] == 0.

Type:

float | None

index_for_time(t: float) int[source]

Round-to-nearest index for t. Useful for window selection.

property is_time_aggregated: bool

True for stats outputs / single snapshots (no time axis).

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

property normalization_offset: float

Resolved offset used for normalized time.

rescale(factor: float) TimeAxis[source]

Multiply initial_time and timestep_size by factor.

time_at(index: int) float[source]

Time corresponding to a single index. Negative index counts from the end.

times() ndarray[source]

Materialise the full time array. Avoid in hot paths.

times_normalized() ndarray[source]

Materialise the full normalized time array.

translate(new_initial_time: float) TimeAxis[source]

Set initial_time; keep step size and length.

window(start: float, end: float) tuple[TimeAxis, slice][source]

Time-axis form of “window selection”.

Returns the new TimeAxis plus the index slice callers can apply to their field arrays. The slice is open on the right, matching numpy convention.

with_normalization_offset(offset: float) TimeAxis[source]

Override the normalization offset (defaults to initial_time).

class cfdmod.Topology(*, cell_type: Literal['triangle', 'point', 'cell'], connectivity: ndarray, vertices: ndarray)[source]

Mesh connectivity + vertex coordinates for a data source.

Frozen and immutable. Geometric ops (rigid-body transformation, rescale) produce a new Topology rather than mutating in place.

cell_type

One of CellType. Locks the connectivity schema.

Type:

Literal[‘triangle’, ‘point’, ‘cell’]

connectivity

For triangle -> (n_elements, 3) int32 indices into vertices. For point -> (0, 0) (no connectivity). For cell (reserved) -> implementation specific.

Type:

numpy.ndarray

vertices

(n_vertices, 3) float64 vertex coordinates.

Type:

numpy.ndarray

The shapes mirror the existing on-disk layout in cfdmod/io/xdmf.py exactly: /Triangles -> int32 (N, 3), /Geometry -> float64 (V, 3). No format change.

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

property n_elements: int

Number of elements (triangles, points, or cells).

classmethod points(vertices: Any) Topology[source]

Build a points topology from a (n_points, 3) array.

classmethod triangles(connectivity: Any, vertices: Any) Topology[source]

Build a triangle topology from connectivity + vertex arrays.

class cfdmod.FieldMeta(*, name: str, unit: str = '-', scale: float = 1.0)[source]

Metadata for one field on a DataSource.

name

Display name (e.g. "pressure", "u_x").

Type:

str

unit

SI-ish unit string (e.g. "Pa", "m/s", "-").

Type:

str

scale

Multiplicative scale relative to the unit. Defaults to 1.0; nonzero values let a recipe carry “the field is in unit * scale” without rewriting the array.

Type:

float

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.Grouping(*, name: str, indices: ndarray, id_to_label: dict[int, str] | None = None)[source]

One grouping over a data source’s element axis.

Each element gets exactly one group id under this grouping; the sentinel -1 marks “ungrouped”. Groups are addressed by integer id; an optional id_to_label maps id to a human label (mirroring the legacy "{idx}-{body}" convention used in cfdmod’s pressure pipeline).

name

Grouping name (e.g. "surface", "zoning").

Type:

str

indices

(n_elements,) int32 array of group ids. -1 = ungrouped.

Type:

numpy.ndarray

id_to_label

Optional dict mapping group id -> string label.

Type:

dict[int, str] | None

label(group_id: int) str[source]

Resolve a group id to a label.

Falls back to str(group_id) if no mapping is registered or the id is missing from the mapping.

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Containers

A cfdmod.Container aggregates many data sources under complex keys (case, direction, …), the way a parametric study fans out over inflow angles.

class cfdmod.Container(*, items: dict[~cfdmod.core.container.K, ~cfdmod.core.container.V] = <factory>)[source]

Hashable-keyed map of values, with parallel fanout and partition.

Frozen at the model level; the underlying dict is replaced by construction of a new container rather than mutated.

filter_by(predicate: Callable[[K], bool]) Container[source]

Return a sub-container of entries whose key satisfies predicate.

join_by(callback: Callable[[K], T]) dict[T, Container[K, V]][source]

Partition by a derived key.

Mirrors HFPIAnalysisResults.join_by: for every entry, run callback(key) to derive a partition key, then collect entries sharing each partition key into their own container.

map_values(func: Callable[[V], Any], *, pool: Pool | None = None) Container[K, Any][source]

Apply func to every value.

If pool is supplied, fanout runs through pool.map and the entries’ order is preserved by re-zipping with the keys. Without a pool the work runs sequentially in insertion order.

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Pipelines and storage

cfdmod.Pipeline

alias of Callable[[DataSource], DataSource]

cfdmod.compose(*ops: Callable[[DataSource], DataSource]) Callable[[DataSource], DataSource][source]

Compose a sequence of single-arg callables left-to-right.

The first op runs first; the last op’s output is the pipeline’s output. With no ops, returns identity().

class cfdmod.MemoryStorage[source]

Dict-backed Storage.

Stores complete DataSource objects in a Python dict. read_data_source and write_data_source are O(1) hash lookups; nothing is copied.

A MemoryStorage is mutable: new keys are added by write_data_source(). The data sources themselves remain frozen, so this is consistent with the functional-core principle.

class cfdmod.XdmfH5Storage(root: Path, *, write_xdmf: bool = True)[source]

Storage for the XDMF + H5 byte layout.

Parameters:
  • root – Directory under which keys resolve. read_data_source("bodies.foo") opens <root>/bodies.foo.h5.

  • write_xdmf – When True, write_data_source also (re)generates <key>.xdmf next to the h5. Default True.

Recipe configs

The pressure and wind recipes ship as small-data Pydantic configs under cfdmod.recipes; each mirrors one legacy *CaseConfig and builds the equivalent pipeline. Example templates live under fixtures/tests/pressure/templates/.

class cfdmod.recipes.CpRecipeConfig(*, field: str = 'pressure', out: str = 'cp', dynamic_pressure: Annotated[float, Gt(gt=0)], time_rescale_factor: float | None = None, statistics: list[Literal['mean', 'rms', 'min', 'max', 'peak_min', 'peak_max', 'skewness', 'kurtosis']] | None = None)[source]

Bases: BaseModel

Cp pipeline parameters.

field

Source pressure field name. Defaults to "pressure".

Type:

str

out

Output Cp field name. Defaults to "cp".

Type:

str

dynamic_pressure

0.5 * rho * U_ref^2; Cp’s denominator. Required.

Type:

float

time_rescale_factor

Optional scalar applied to the time axis (e.g. U_ref / L for convective time). None -> leave time axis untouched.

Type:

float | None

statistics

Optional list of statistics to compute on the resulting Cp series. None -> no statistics step (the output keeps its full time axis).

Type:

list[Literal[‘mean’, ‘rms’, ‘min’, ‘max’, ‘peak_min’, ‘peak_max’, ‘skewness’, ‘kurtosis’]] | None

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.CfRecipeConfig(*, grouping: str, directions: list[Literal['x', 'y', 'z']] = ['x', 'y', 'z'], prefix: str = 'cp')[source]

Bases: BaseModel

Cf recipe parameters.

grouping

Name of the grouping in ds.groupings that maps triangles to body ids.

Type:

str

directions

Field-name suffixes per force direction. The recipe aggregates cp_<dir> for each entry. Defaults to the three Cartesian components.

Type:

list[Literal[‘x’, ‘y’, ‘z’]]

prefix

Source field prefix ("cp" for the standard convention).

Type:

str

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.CmRecipeConfig(*, grouping: str, directions: list[Literal['x', 'y', 'z']] = ['x', 'y', 'z'], prefix: str = 'cm')[source]

Bases: BaseModel

Cm recipe parameters.

grouping

Name of the grouping in ds.groupings mapping elements to body ids.

Type:

str

directions

Force/moment directions to aggregate. For each entry d the recipe sums the <prefix>_<d> field over each body (net moment).

Type:

list[Literal[‘x’, ‘y’, ‘z’]]

prefix

Source field prefix; "cm" for the standard per-element moment-contribution convention.

Type:

str

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.CeRecipeConfig(*, grouping: str, field: str = 'cp', out: str = 'ce')[source]

Bases: BaseModel

Ce recipe parameters.

grouping

Name of the zoning grouping in ds.groupings (one group per region), typically produced by the zoning_grouping op.

Type:

str

field

Source field to aggregate ("cp" by convention).

Type:

str

out

Output field name on the resulting groups source.

Type:

str

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.S1RecipeConfig(*, field: str = 'u', out: str = 's1', wall_threshold: float = 1e-06)[source]

Bases: BaseModel

S1 recipe parameters.

field

Velocity field on both profiles. Defaults to "u".

Type:

str

out

Output field name. Defaults to "s1".

Type:

str

wall_threshold

Reference values whose absolute value falls below this threshold (and the wall row at z=0) are dropped from the output. Mirrors the legacy Profile.__truediv__ check.

Type:

float

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.DynamicAnalysisConfig(*, mode_shapes: Any, target_points: Any, recomposition_mode_shapes: Any | None = None, load_field: str = 'force', response_field: str = 'u')[source]

Bases: BaseModel

Dynamic-analysis recipe parameters.

mode_shapes

(n_load_elements, n_modes) mode-shape matrix at the load points (used to compute Q). For most cases the same mode shapes also drive recomposition (see recomposition_mode_shapes).

Type:

Any

recomposition_mode_shapes

Optional (n_target_elements, n_modes) matrix evaluated at the target coordinates. If None, mode_shapes is reused (load and target coincide).

Type:

Any | None

target_points

(n_target_elements, 3) coordinates for the recomposed response.

Type:

Any

load_field

Field name carrying the load timeseries on the input data source. Defaults to "force".

Type:

str

response_field

Field name on the output points data source. Defaults to "u".

Type:

str

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.recipes.PedestrianComfortConfig(*, probes: Any, field: str = 'u_mag', statistics: list[Literal['mean', 'rms', 'min', 'max', 'peak_min', 'peak_max', 'skewness', 'kurtosis']] = ['mean', 'rms', 'peak_max'])[source]

Bases: BaseModel

Pedestrian comfort recipe parameters.

probes

(n_probes, 3) probe positions.

Type:

Any

field

Velocity field on the source (e.g. "u_mag").

Type:

str

statistics

Statistics to compute per probe.

Type:

list[Literal[‘mean’, ‘rms’, ‘min’, ‘max’, ‘peak_min’, ‘peak_max’, ‘skewness’, ‘kurtosis’]]

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Triangle-grouping pipeline

A grouping pipeline partitions or selects triangles of a parent lnas.LnasFormat mesh into named groups: specs are Pydantic models in a discriminated union, composed left-to-right with cfdmod.apply_groupings(), and a triangle may belong to zero, one, or many groups.

The force / moment / shape recipes (Cf, Cm, Ce) are built on top of this abstraction: the body_grouping, zoning_grouping and connectivity_grouping ops attach a grouping to a cfdmod.SurfaceDataSource, and per-group field series are then aggregated onto a cfdmod.GroupsDataSource. body_grouping splits by surface name, zoning_grouping by a rectangular centroid-binned grid, and connectivity_grouping by shared-edge connected component (one physical body per component, no axis projection). More elaborate partitions are expressed by composing the built-in grouping kinds below.

Driver

cfdmod.apply_groupings(mesh: LnasFormat, groupings: list[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]]) GroupingResult[source]

Apply a chain of grouping specs to a parent mesh.

Specs are applied left to right. Each spec produces a fresh name -> indices map which is merged into the running result; duplicate group names raise ValueError (no silent merging).

A spec may carry a restrict_to: list[str] | None field. When set, that spec only considers triangles whose index is in the union of the named earlier groups; this is how the legacy surface -> sub_body nesting is expressed.

Parameters:
  • mesh – Parent mesh.

  • groupings – Specs in application order.

Returns:

GroupingResult over mesh.

Raises:

ValueError – If groupings is empty, two specs produce the same group name, or a restrict_to references an unknown group.

class cfdmod.GroupingResult(parent_n_triangles: int, groups: dict[str, ~numpy.ndarray] = <factory>)[source]

Result of applying a chain of groupings to a parent mesh.

Triangle indices are into the parent LnasFormat.geometry.triangles array (0..parent_n_triangles-1). A triangle may appear in zero, one, or many groups.

parent_n_triangles

Number of triangles in the parent mesh.

Type:

int

groups

Mapping of group_name -> sorted np.int64 triangle indices.

Type:

dict[str, numpy.ndarray]

membership_long() pd.DataFrame[source]

Long-form (triangle_idx, group_name) table.

One row per (triangle, group) pair; the table is the natural place to express overlapping or absent group membership.

Returns:

DataFrame with columns triangle_idx (int64) and group_name (str). Empty if no groups were produced.

to_region_idx(sep: str = '|', unassigned: str = '') ndarray[source]

Single-label-per-triangle view, sep-joined when overlapping.

Triangles in no group are labelled unassigned (default empty string). Useful for legacy code paths that expect one region label per triangle.

Parameters:
  • sep – Separator used when a triangle belongs to several groups.

  • unassigned – Label for triangles in no group.

Returns:

Object-dtype array of length parent_n_triangles.

Built-in grouping kinds

Each kind is dispatched on its kind discriminator in the cfdmod.GroupingSpec union. A new kind is added by defining a new Pydantic model under cfdmod/geometry/grouping/kinds/ with a unique kind literal, registering it in the union, and adding a dispatch branch in cfdmod.geometry.grouping.base._dispatch.

class cfdmod.BySurfaceGrouping(*, kind: ~typing.Literal['by_surface'] = 'by_surface', sets: dict[str, list[str]] = <factory>, include_unlisted: bool = False)[source]

Bases: BaseModel

Group triangles by named LNAS surfaces.

Parameters:
  • kind – Discriminator literal, always "by_surface".

  • setsgroup_name -> list of LnasFormat.surfaces keys. Each named set becomes one group containing the union of those surfaces’ triangle indices.

  • include_unlisted – When True, all surfaces in LnasFormat.surfaces not referenced in sets are added as singleton groups keyed by surface name.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByZoningGrouping(*, kind: ~typing.Literal['by_zoning'] = 'by_zoning', x_intervals: list[float] = <factory>, y_intervals: list[float] = <factory>, z_intervals: list[float] = <factory>, name_template: str = 'r{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Axis-aligned centroid binning into a Cartesian grid of regions.

Parameters:
  • kind – Discriminator literal, always "by_zoning".

  • x_intervals – Strictly ascending, non-repeating bin edges. [-inf, inf] (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • y_intervals – Strictly ascending, non-repeating bin edges. [-inf, inf] (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • z_intervals – Strictly ascending, non-repeating bin edges. [-inf, inf] (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • name_template – Format string for group names. Available placeholders: {idx} (linear region index, 0-based), {ix}, {iy}, {iz} (per-axis cell indices).

  • restrict_to – Optional list of earlier group names; when set, only triangles in (the union of) those groups are considered. Triangles outside the restriction are not assigned by this spec.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByDivisionsGrouping(*, kind: Literal['by_divisions'] = 'by_divisions', n_div_x: Annotated[int | None, Ge(ge=1)] = None, n_div_y: Annotated[int | None, Ge(ge=1)] = None, n_div_z: Annotated[int | None, Ge(ge=1)] = None, name_template: str = 'r{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Cartesian binning by uniform division count per axis.

Parameters:
  • kind – Discriminator literal, always "by_divisions".

  • n_div_x – Number of cells along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • n_div_y – Number of cells along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • n_div_z – Number of cells along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • name_template – Format string for group names. Available placeholders: {idx} (linear region index, 0-based), {ix}, {iy}, {iz} (per-axis cell indices).

  • restrict_to – Optional list of earlier group names; when set, only triangles in (the union of) those groups are considered and the bounding box is computed from their centroids only.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.BySizeGrouping(*, kind: Literal['by_size'] = 'by_size', size_x: Annotated[float | None, Gt(gt=0.0)] = None, size_y: Annotated[float | None, Gt(gt=0.0)] = None, size_z: Annotated[float | None, Gt(gt=0.0)] = None, name_template: str = 'r{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Cartesian binning by fixed cell size per axis.

Parameters:
  • kind – Discriminator literal, always "by_size".

  • size_x – Cell size along each axis (must be > 0). None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • size_y – Cell size along each axis (must be > 0). None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • size_z – Cell size along each axis (must be > 0). None (the default) means “do not bin along this axis”; the axis contributes a single cell spanning [-inf, inf].

  • name_template – Format string for group names. Available placeholders: {idx} (linear region index, 0-based), {ix}, {iy}, {iz} (per-axis cell indices).

  • restrict_to – Optional list of earlier group names; when set, only triangles in (the union of) those groups are considered and the bounding box is computed from their centroids only.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByConnectivityGrouping(*, kind: Literal['by_connectivity'] = 'by_connectivity', name_template: str = 'cc{idx}', min_triangles: Annotated[int, Ge(ge=1)] = 1, restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Group triangles by connected component (shared-edge adjacency).

Parameters:
  • kind – Discriminator literal, always "by_connectivity".

  • name_template – Format string for group names. Available placeholder: {idx} (component index, 0-based; components are ordered by descending triangle count so cc0 is the largest).

  • min_triangles – Components smaller than this are dropped.

  • restrict_to – Optional list of earlier group names; when set, only triangles in (the union of) those groups participate in the connectivity analysis. Edges to triangles outside the restriction are ignored.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByNormalGrouping(*, kind: ~typing.Literal['by_normal'] = 'by_normal', axes: list[~typing.Literal['+x', '-x', '+y', '-y', '+z', '-z']] = <factory>, tolerance_deg: ~typing.Annotated[float, ~annotated_types.Gt(gt=0.0), ~annotated_types.Le(le=90.0)] = 45.0, name_template: str = 'n_{axis}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Group triangles by best-fit cardinal direction of their outward normal.

Parameters:
  • kind – Discriminator literal, always "by_normal".

  • axes – Cardinal directions to produce buckets for. Subset of {"+x", "-x", "+y", "-y", "+z", "-z"}.

  • tolerance_deg – Maximum angle (degrees) between a triangle normal and its best-fit cardinal direction. With the default 45.0 every non-degenerate normal lands in exactly one bucket; a tighter value (e.g. 30.0) excludes oblique faces from all buckets.

  • name_template – Format string. Placeholder {axis} is the literal axis token ("+x""-z").

  • restrict_to – Optional list of earlier group names to scope to.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByPlaneGrouping(*, kind: ~typing.Literal['by_plane'] = 'by_plane', point: tuple[float, float, float], normal: tuple[float, float, float], intervals: list[float] = <factory>, name_template: str = 'r{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Bin triangle centroids by signed distance from an oriented plane.

Parameters:
  • kind – Discriminator literal, always "by_plane".

  • point – A point on the plane (3-vector).

  • normal – Plane normal (3-vector, auto-normalised; must be non-zero).

  • intervals – Strictly ascending bin edges along normal (signed distances). Default [-inf, 0.0, inf] (two half-spaces).

  • name_template – Format string. Placeholder {idx}.

  • restrict_to – Optional list of earlier group names to scope to.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByPercentileGrouping(*, kind: Literal['by_percentile'] = 'by_percentile', axis: Literal['x', 'y', 'z'], n_quantiles: Annotated[int, Ge(ge=1)], name_template: str = 'q{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Equal-count quantile binning along one axis.

Parameters:
  • kind – Discriminator literal, always "by_percentile".

  • axis – Which axis to bin along: "x", "y", or "z".

  • n_quantiles – Number of equal-count bins (>= 1).

  • name_template – Format string. Placeholder {idx} (0-based).

  • restrict_to – Optional list of earlier group names to scope to.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.ByCylindricalGrouping(*, kind: Literal['by_cylindrical'] = 'by_cylindrical', origin: tuple[float, float, float], axis: Literal['x', 'y', 'z'] = 'z', r_intervals: list[float] | None = None, theta_intervals_deg: list[float] | None = None, axial_intervals: list[float] | None = None, name_template: str = 'r{idx}', restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Cartesian product of (r, theta, axial) bins around a cylinder axis.

Parameters:
  • kind – Discriminator literal, always "by_cylindrical".

  • origin – Point on the cylinder axis (3-vector).

  • axis – Cylinder axis: "x", "y", or "z".

  • r_intervals – Radial bin edges (>= 0, ascending), or None.

  • theta_intervals_deg – Angular bin edges in degrees, in [0, 360], ascending, or None.

  • axial_intervals – Bin edges along axis (ascending), or None.

  • name_template – Format string. Placeholders: {idx} (linear), {ir}, {it}, {iz} (per-axis cell indices).

  • restrict_to – Optional list of earlier group names to scope to.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.CustomGrouping(*, kind: ~typing.Literal['by_custom'] = 'by_custom', callback: ~typing.Any, params: dict[str, ~typing.Any] = <factory>, restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Grouping defined by a user-supplied Python callback.

Parameters:
  • kind – Discriminator literal, always "by_custom".

  • callback – Either an importable dotted path string ("my_pkg.my_func") or a Python callable matching the signature documented in the module docstring.

  • params – User-defined parameters passed verbatim to the callback. Keep JSON/YAML-safe if you intend to persist the chain via dump_groupings().

  • restrict_to – Optional list of earlier group names; when set, the callback receives only triangles in (the union of) those groups as candidate_idxs.

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Persistence

A grouping chain can be recorded alongside a coefficient’s processing_metadata block via the convention {"groupings": cfdmod.dump_groupings(chain)} (sibling to "filters"); cfdmod.load_groupings() rehydrates the typed spec instances on read.

cfdmod.dump_groupings(groupings: list[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]]) list[dict[str, Any]][source]

Serialize a chain of grouping specs to plain dicts (YAML/JSON-safe).

Each entry retains its kind discriminator so load_groupings() can route it back to the correct spec class.

Parameters:

groupings – Chain of validated spec instances.

Returns:

list[dict] suitable for write_processing_metadata or any YAML/JSON serializer.

cfdmod.load_groupings(serialized: list[dict[str, Any]]) list[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]][source]

Re-hydrate a chain of grouping specs from their dict form.

Uses the GroupingSpec discriminated union, so each entry must carry a kind key matching one of the registered spec classes.

Parameters:

serialized – Output of dump_groupings() (or any list of dicts with a valid kind discriminator).

Returns:

Validated spec instances ready to feed to cfdmod.geometry.grouping.apply_groupings().

Raises:

pydantic.ValidationError – If any entry is not a valid spec.

I/O helpers

Mesh resolver

cfdmod.load_mesh(source: Path | LnasFormat) LnasFormat[source]

Resolve a mesh from any supported source.

Parameters:

source – Either an LnasFormat (returned as-is) or a path ending in .lnas, .stl, .h5, or .xdmf.

Returns:

LnasFormat ready to drive the pressure pipeline.

cfdmod.mesh_from_h5(h5_path: Path) LnasFormat[source]

Build an LnasFormat from an HDF5 file’s /Triangles + /Geometry datasets, with one synthetic surface covering every triangle.

Used both by load_mesh() (for .h5/.xdmf inputs) and as the fallback when a pipeline call omits mesh_path and the geometry has to come straight from the body or cp timeseries file.

Embedded post-processing metadata

Every pipeline output H5 carries a processing_metadata group with the config used to produce it. The two helpers below let external pipelines write or read that block without depending on the layout details.

cfdmod.write_processing_metadata(h5_path: Path, group: str, config: dict, *, extra: dict | None = None) None[source]

Embed the post-processing parameters used to produce group as HDF5 attributes on that group, plus the YAML serialization as a sibling string dataset for round-trip reproducibility.

The config dict is generic - callers compose whatever keys they need. By convention, pipeline chains live under top-level keys named after the pipeline kind:

  • "filters": list[dict] produced by [spec.model_dump() for spec in filter_specs] (see cfdmod.pressure.filters).

  • "groupings": list[dict] produced by cfdmod.geometry.grouping.dump_groupings() for the chain of triangle-grouping specs that defined this group’s regions.

Either or both may be present. read_processing_metadata parses the YAML back to a dict; the per-pipeline load_* helpers (e.g. cfdmod.geometry.grouping.load_groupings()) re-hydrate the typed spec instances.

cfdmod.config_yaml

full config serialized to YAML (string)

cfdmod.produced_at

ISO-8601 UTC timestamp

cfdmod.cfdmod_version

package version

plus every key from ``extra`` (e.g. {'body_h5'

‘…’, ‘probe_h5’: ‘…’})

Stored under {group}/processing_metadata/config.yaml (string dataset) so it remains human-inspectable via h5dump even when attribute inspection isn’t available.

cfdmod.read_processing_metadata(h5_path: Path, group: str) dict[source]

Read the metadata written by write_processing_metadata().

Returns dict with keys config (parsed YAML), produced_at, cfdmod_version, and any extra keys recorded at write time.

Timeseries access

Pull a coefficient timeseries out of any output H5 into a wide-form pandas.DataFrame, save it as CSV for spreadsheet ingest, or plot it with one matplotlib call.

cfdmod.read_timeseries_df(h5_path: Path | str, group: str, *, triangles: Iterable[int] | None = None, regions: bool = False, timestep_range: tuple[float, float] | None = None, max_columns: int = 200) DataFrame[source]

Read a coefficient timeseries from an XDMF+H5 into a wide-form DataFrame.

Parameters:
  • h5_path – Timeseries H5 path (e.g. cp.default.time_series.h5, Cf.containers.pack.time_series.h5).

  • group – Coefficient group inside the file. Examples: "cp" (in a Cp file), "cf_x" / "cf_y" / "cf_z" (in a Cf file), "cm_x" / "cm_y" / "cm_z" (Cm file).

  • triangles – Optional list/iterable of triangle indices to keep as columns. Mutually exclusive with regions=True.

  • regions – When True, deduplicate columns by their value pattern – one representative triangle per unique value vector, named by the chosen triangle’s integer index. Correct for Cf/Cm files where each region contributes a constant value to all its triangles. Do not use on per-triangle data (Cp).

  • timestep_range – Optional (t_min, t_max) filter applied on the raw time keys (t{T} keys, not on the normalized index).

  • max_columns – Refuse to return a DataFrame wider than this many columns unless triangles or regions is set; protects callers that forget to filter on a per-triangle file (an 80k-tri Cp would be 80k columns wide, way past spreadsheet usability).

Returns:

DataFrame indexed by time_normalized with one column per retained triangle / region representative.

Raises:

ValueError – If the file has no /{group} group, the timestep filter yields no rows, both triangles and regions are requested, or the unfiltered column count exceeds max_columns.

cfdmod.to_csv(df: DataFrame, path: Path | str, **kwargs) None[source]

Save a timeseries DataFrame as CSV.

Wide-form by default: first column is time_normalized (from the index), one column per retained triangle/region. Drops straight into Google Sheets / Excel via Open / Import.

Extra keyword arguments are forwarded to pandas.DataFrame.to_csv().

cfdmod.plot_timeseries(df: pd.DataFrame, columns: Iterable[int] | None = None, *, ax: Axes | None = None, title: str | None = None, ylabel: str = 'value', **plot_kwargs) Axes[source]

One-line matplotlib plot of a timeseries DataFrame.

Parameters:
  • df – A DataFrame returned by read_timeseries_df().

  • columns – Optional subset of columns (triangle indices) to plot; defaults to all columns currently in the DataFrame.

  • ax – Existing matplotlib.axes.Axes to draw into; a new figure is created if omitted.

  • title – Plot title.

  • ylabel – Y-axis label (default "value" – override per coefficient, e.g. "Cp" / "Cf_x").

  • **plot_kwargs – Extra args passed to DataFrame.plot.

Returns:

The matplotlib Axes the plot was drawn into.

Geometry I/O (STL)

cfdmod.read_stl(filename: Path) tuple[ndarray, ndarray][source]

Read buffer content as STL file

Parameters:

buff (io.BufferedReader) – buffer to read from

Returns:

return STL representation as (triangles, normals).

Return type:

tuple[np.ndarray, np.ndarray]

cfdmod.export_stl(filename: Path, triangle_vertices: ndarray, normals: ndarray)[source]

Export geometry in STL format

Parameters:
  • filename (pathlib.Path) – Filename to save to.

  • triangle_vertices (np.ndarray) – Array of the vertices of the triangles.

  • normals (np.ndarray) – Array of triangles normals.

Remesh (geometry coarsening)

cfdmod.remesh is a small API-only module for coarsening grouped LnasFormat meshes – typically the output of the regroup pipeline, where each named surface holds many triangles that came out of the aggregation="sliced" 90-degree cuts. The default path is exact coplanar-fan collapse (lossless, numpy-only); a flat NxN-subdivided square inside one surface comes back as 2 triangles, a curved patch unchanged.

The module follows a small convention split. The two array-level functions take raw (vertices, triangles) and operate on a single sub-mesh:

cfdmod.merge_coplanar(vertices: ndarray, triangles: ndarray, normal_tol: float = 1e-06, plane_tol: float = 1e-09, collinear_rel_tol: float = 1e-09) tuple[ndarray, ndarray][source]

Collapse coplanar adjacent triangles into the minimum triangulation of their region.

Within each connected component of edge-adjacent triangles that share a plane (within normal_tol on the unit normal and plane_tol on the plane offset, with anti-parallel normals treated as the same plane), the interior triangulation is replaced by a fresh ear-clipped triangulation of the component’s boundary loop. Components with multiple boundary loops (annular topology) or for which ear-clipping fails to make progress are kept as-is, and a logger.debug message is emitted so callers can see when fallback triggers.

Parameters:
  • vertices(V, 3) input vertex array.

  • triangles(T, 3) input triangle array of vertex indices.

  • normal_tol – Max angular deviation (as 1 - |cos(theta)|) for two adjacent triangles to be considered coplanar. Uses the absolute cosine so flipped (anti-parallel) triangles on the same physical plane are also merged.

  • plane_tol – Max absolute deviation of plane offsets (n . v0) for two adjacent triangles to be considered coplanar.

  • collinear_rel_tol – Relative tolerance for the collinear-vertex drop pass on the boundary loop. The absolute threshold is collinear_rel_tol * bbox_diagonal^2 so the behaviour is independent of mesh units.

Returns:

(new_vertices, new_triangles). Unused vertices are dropped; the remaining vertex order matches the surviving input vertex order. new_triangles has dtype int32.

cfdmod.decimate_qem(vertices: ndarray, triangles: ndarray, target_reduction: float, aggressiveness: float = 7.0) tuple[ndarray, ndarray][source]

QEM decimation via fast-simplification.

Mesh boundaries (vertices and edges on the boundary of the input sub-mesh) are preserved implicitly by the underlying algorithm and are never collapsed; a per-surface call therefore leaves the group boundary intact and adjacent groups still match exactly at their shared edges after each is decimated independently.

Closed surfaces (sub-meshes with no boundary edges, e.g. a watertight sphere) have no boundary for the algorithm to protect, so a high target_reduction can collapse them aggressively. A RuntimeWarning is emitted in that case; consider running fast_simplification.simplify directly with lossless=True if you need a bounded-error path.

Parameters:
  • vertices(V, 3) input vertex array.

  • triangles(T, 3) input triangle array of vertex indices.

  • target_reduction – Fraction of triangles to remove (0.9 keeps 10%). <= 0 returns the input unchanged.

  • aggressivenessagg parameter passed through to fast_simplification.simplify (default 7 matches the library).

Returns:

(new_vertices, new_triangles). new_triangles has dtype int32.

Raises:

ImportError – if fast-simplification is not installed. Install it via pip install 'aerosim-cfdmod[remesh]'.

decimate_qem requires the optional fast-simplification dep (pip install 'aerosim-cfdmod[remesh]'); calling it without that extra raises ImportError with a clear install hint.

The top-level entry point dispatches both array-level operations over every named surface in an LnasFormat and restitches the per-surface outputs back into a fresh LnasFormat (same surface names, vertex order recompacted, optional tolerance-based seam dedup):

cfdmod.remesh_per_group(mesh: LnasFormat, coplanar_merge: bool = True, target_reduction: float = 0.0, aggressiveness: float = 7.0, normal_tol: float = 1e-06, plane_tol: float = 1e-09, seam_rel_tol: float = 1e-09) LnasFormat[source]

Per-surface remesh of an LnasFormat.

For each surface in mesh.surfaces, extract its triangles into a sub-mesh, run merge_coplanar() (if coplanar_merge) and then decimate_qem() (if target_reduction > 0), and restitch the per-surface outputs into a fresh LnasFormat with the same surface names.

With the defaults (coplanar_merge=True, target_reduction=0.0) the operation is geometrically lossless: every output vertex is either an input vertex or lies exactly on the input surface. A flat NxN- subdivided square inside one surface comes out as 2 triangles; a curved patch comes out unchanged.

Parameters:
  • mesh – Input LnasFormat whose surfaces map names to triangle index arrays.

  • coplanar_merge – Run merge_coplanar() per surface. Default True.

  • target_reduction – If > 0, run decimate_qem() per surface after the coplanar pass with this reduction fraction.

  • aggressiveness – Forwarded to decimate_qem.

  • normal_tol – Forwarded to merge_coplanar.

  • plane_tol – Forwarded to merge_coplanar.

  • seam_rel_tol – Relative tolerance for merging shared boundary vertices between adjacent surfaces in the restitched output. The absolute threshold is seam_rel_tol * bbox_diagonal. 0 disables and falls back to exact-equality dedup. Tolerance-based dedup matters once decimate_qem() is enabled because QEM can synthesise new vertex positions that drift below float-equality.

Returns:

A fresh LnasFormat with one named surface per input surface (insertion order preserved). Empty surfaces (no triangles) and surfaces that fully collapse during merge are kept as empty surfaces entries to preserve the name mapping.

Regroup (disk regroup pipeline)

cfdmod.regroup takes a geometry plus a per-triangle HDF5 timeseries, applies a chain of triangle-grouping specs, and writes two aligned outputs: a new LnasFormat mesh with one named surface per group, and a new HDF5 timeseries whose columns line up with the new triangle order (or are area-weighted aggregates per group). It reuses the grouping kinds above and adds one regroup-local spec that fans out per-component target-size subdivisions. Runnable as python -m cfdmod.regroup.

class cfdmod.RegroupConfig(*, groupings: Annotated[list[Annotated[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')] | BySizeRoundedPerComponent, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]], MinLen(min_length=1)], transformation: TransformationConfig | None = None, aggregation: Literal['per_triangle', 'area_weighted_mean', 'sliced'] = 'area_weighted_mean', timeseries_group: str = 'cp', output_geometry_format: Literal['lnas', 'lnas_and_stl'] = 'lnas', unassigned_policy: Literal['drop', 'keep_as_unassigned'] = 'drop')[source]

Bases: BaseModel

Top-level config for the regroup pipeline.

Parameters:
  • groupings – Chain of regroup specs (every standard GroupingSpec plus the regroup-local BySizeRoundedPerComponent). Specs are applied left to right; BySizeRoundedPerComponent entries are expanded by run_regroup against the groups produced by the prior prefix of the chain.

  • transformation – Optional rigid-body transform applied to a mesh copy before binning. Output geometry vertices stay in world coordinates; only the binning frame moves. Mirrors Ce.

  • aggregation – Per-group HDF5 column policy. "per_triangle" reorders the input columns; one output column per (parent) triangle. "area_weighted_mean" writes one aggregated value per group, broadcast over the post-slice triangles of that group (so geometry and timeseries cardinality match for ParaView).

  • timeseries_group – HDF5 group name used in the input (under which t{T} datasets live) and reused in the output.

  • output_geometry_format"lnas" writes only geometry.lnas; "lnas_and_stl" also writes a geometry.stl companion (for quick ParaView lookup; surface labels are lost in STL).

  • unassigned_policy – What to do with parent triangles that fall in zero groups. "drop" excludes them; "keep_as_unassigned" adds a synthetic unassigned group/surface.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.BySizeRoundedPerComponent(*, kind: Literal['by_size_rounded_per_component'] = 'by_size_rounded_per_component', target_size_x: Annotated[float | None, Gt(gt=0.0)] = None, target_size_y: Annotated[float | None, Gt(gt=0.0)] = None, target_size_z: Annotated[float | None, Gt(gt=0.0)] = None, name_template: str = '{parent}_r{idx}', min_n_div: Annotated[int, Ge(ge=1)] = 1, restrict_to: list[str] | None = None)[source]

Bases: BaseModel

Per-parent-group round-to-nearest size-based subdivision.

Expanded by expand_size_rounded_chain() before apply_groupings() is called: for each group produced by the prior chain, derive per-axis n_div = max(min_n_div, round(extent / target)) from the restricted centroid bbox, then append a ByDivisionsGrouping with restrict_to=[parent_name] to the expanded chain.

Parameters:
  • kind – Discriminator literal, always "by_size_rounded_per_component".

  • target_size_x – Approximate cell size along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • target_size_y – Approximate cell size along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • target_size_z – Approximate cell size along each axis. None (the default) means “do not bin along this axis”; the axis contributes a single cell.

  • name_template – Format string for emitted group names. {parent} is replaced with the parent group’s name and {idx}, {ix}, {iy}, {iz} are forwarded to the inner ByDivisionsGrouping.

  • min_n_div – Floor for the rounded division count per axis; defaults to 1.

  • restrict_to – Optional list of earlier group names whose triangles define the parent components to fan out over. None (the default) means “use every group produced by the prior chain”.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

cfdmod.build_regroup_mapping(mesh: LnasFormat, groupings: list[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]], transformation: TransformationConfig | None) GroupingResult[source]

Apply groupings against a transformed copy of mesh.

Mirrors Ce’s transformed-frame binning convention: vertices are moved on a copy before binning, so the parent mesh’s surfaces and triangle indices remain valid for the caller.

cfdmod.build_regrouped_mesh(mesh: LnasFormat, grouping: GroupingResult, *, aggregation: Literal['per_triangle', 'area_weighted_mean'], unassigned_policy: Literal['drop', 'keep_as_unassigned']) tuple[LnasFormat, RegroupIndex][source]

Build the output LnasFormat and the input->output column mapping.

Output triangles appear in the concatenation order [group0_parents, group1_parents, ...] (insertion order of grouping.groups, with an optional trailing unassigned bucket). Surfaces on the returned LnasFormat carry one entry per group.

cfdmod.apply_regroup_to_timeseries(input_h5: Path, output_h5: Path, *, group: str, regroup_index: RegroupIndex, new_triangles: ndarray, new_vertices: ndarray) None[source]

Stream-rewrite input_h5[group] to output_h5[group] per the index.

Writes /Triangles + /Geometry (from the regrouped mesh), one /{group}/t{T} dataset per input timestep, and /meta carrying the original time_steps / time_normalized plus per-output- triangle region_labels (the group each triangle belongs to).

cfdmod.expand_regroup_chain(chain: list[Annotated[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')] | BySizeRoundedPerComponent, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]], mesh: LnasFormat, transformation: TransformationConfig | None) tuple[list[Annotated[BySurfaceGrouping | ByZoningGrouping | ByDivisionsGrouping | BySizeGrouping | ByConnectivityGrouping | ByNormalGrouping | ByPlaneGrouping | ByPercentileGrouping | ByCylindricalGrouping | CustomGrouping, FieldInfo(annotation=NoneType, required=True, discriminator='kind')]], set[str], dict[str, tuple[list[float], list[float], list[float]]], dict[str, ndarray]][source]

Resolve any regroup-local specs into plain GroupingSpec entries.

Returns (expanded_specs, consumed_group_names, parent_intervals, parent_triangles). consumed names are intermediate parent groups that BySizeRoundedPerComponent has fanned out over; parent_intervals and parent_triangles carry the per-parent cut planes and triangle indices needed to drive the "sliced" aggregation mode (empty dicts when no fan-out happened).

cfdmod.run_regroup(cfg: RegroupConfig, geometry: Path | LnasFormat, timeseries: Path, output_dir: Path) None[source]

Run the full regroup pipeline and write outputs to output_dir.

Outputs:
  • geometry.lnas (always); geometry.stl if cfg.output_geometry_format == "lnas_and_stl".

  • {cfg.timeseries_group}.regrouped.h5 and a sibling .xdmf.

Building wind-load post-processing

cfdmod.building turns a pressure timeseries on a building surface into the engineering deliverables of a wind study: per-floor force and moment coefficients, the modal dynamic response, occupant-comfort accelerations, design load cases, and a multi-direction / multi-body fan-out driver. It composes the v3 recipes and ops; nothing here is high-rise-specific – the same helpers serve low-rise studies. Import from the sub-package:

from cfdmod.building import BuildingCase, cf_per_floor, solve_building_response

Case aggregation

class cfdmod.building.BuildingCase(*, name: str, reference_height: Annotated[float, Gt(gt=0)], characteristic_length: Annotated[float, Gt(gt=0)], basic_wind_speed: Annotated[float, Gt(gt=0)], fluid_density: Annotated[float, Gt(gt=0)] = 1.225, simul_reference_velocity: Annotated[float, Gt(gt=0)], reference_velocity: float | None = None, nominal_area: Annotated[float, Gt(gt=0)], nominal_volume: Annotated[float, Gt(gt=0)], floor_heights: Annotated[list[float], MinLen(min_length=2)], lever_origin: list[float] = [0.0, 0.0, 0.0], directions: list[str] = [], body_name: str = 'building')[source]

Bases: BaseModel

Immutable aggregate of a high-rise case’s post-processing inputs.

property dynamic_pressure: float

q = 0.5 * rho * U_H^2 (Pa).

classmethod from_case_data(case_data_dir: str | Path, params_name: str, *, body_name: str | None = None) BuildingCase[source]

Build from a case_data/ dir containing global_data.json + a params yaml.

Parses the consulting params layout (top-level anchors plus pressure_coefficient / force_coefficient / moment_coefficient blocks). Missing optional fields fall back to sensible defaults.

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

property u_h: float

measured if set, else simulation.

Type:

Reference velocity actually used

with_reference_velocity(u_ref: float) BuildingCase[source]

Return a copy whose Cp normalisation uses the measured u_ref.

cfdmod.building.cp_from_pressure(body: DataSource, p_ref, case: BuildingCase, *, statistics: list[str] | None = None, time_rescale_factor: float | None = None) DataSource[source]

Cp time series (or stats) non-dimensionalised by the case dynamic pressure.

p_ref is a scalar reference pressure or a points/surface DataSource (broadcast per timestep). Pass statistics to collapse the time axis to mean/rms/peak fields instead of the full series.

Per-floor coefficients

cfdmod.building.cf_per_floor(cp_ds: DataSource, mesh_path: str, case: BuildingCase, *, directions: tuple[str, ...] = ('x', 'y'), method: Literal['face_cut', 'centroid'] = 'centroid') GroupsDataSource[source]

Per-floor force coefficients cf_<dir>, one row per floor slice.

method="centroid" (default) assigns each whole triangle to a floor by its centroid – fast, bounded memory, and matching the v2 sub-body grouping. method="face_cut" slices triangles at the floor edges for an exact partial-area split, but fragments the mesh (much heavier); prefer centroid at production sizes. See cf_cm_per_floor() when you need both Cf and Cm. Precision follows the Cp field’s dtype (float32 for solver output).

cfdmod.building.cm_per_floor(cp_ds: DataSource, mesh_path: str, case: BuildingCase, *, directions: tuple[str, ...] = ('z',), method: Literal['face_cut', 'centroid'] = 'centroid') GroupsDataSource[source]

Per-floor moment coefficients cm_<dir> about the case lever origin.

See cf_per_floor() for the method trade-off.

Peak statistics

The peak of a fluctuating series is taken by one of three selectable methods (raw maximum, gust peak-factor, or a Gumbel fit).

cfdmod.building.gust_peak_factor(f0: float, duration: float = 600.0, *, full: bool = True) float[source]

Davenport gust peak factor g for a narrow-band process.

g = sqrt(2 ln(nu T)) + 0.5772 / sqrt(2 ln(nu T)) with nu = f0 the mean up-crossing rate (Hz) and T = duration (s). With full=False only the leading sqrt(2 ln(nu T)) term is returned (the form used in the quick-look notebooks).

cfdmod.building.peak_value(series: ndarray, method: Literal['max', 'peak-factor', 'gumbel'] = 'peak-factor', *, f0: float | None = None, duration: float = 600.0, absolute: bool = True, n_blocks: int = 10, non_exceedance: float = 0.78) float[source]

Reduce a response time series to a single design peak.

Parameters:
  • method"max", "peak-factor" (needs f0), or "gumbel".

  • f0 – response frequency (Hz), required for "peak-factor".

  • duration – full-scale averaging window (s) for the gust factor.

  • absolute – if True, "max" uses max(|series|) and "peak-factor" builds the peak off |mean| + g*std.

  • n_blocks – number of blocks for "gumbel" block maxima.

  • non_exceedance – design fractile p for "gumbel" (x_p = loc - scale ln(-ln p)).

Dynamic response

cfdmod.building.solve_building_response(load_source: PointsDataSource, structure: BuildingStructuralData, *, damping_ratio: float = 0.02) PointsDataSource[source]

Floor loads + structure -> per-floor dynamic response.

Returns a PointsDataSource over the floors with displacement fields disp_x / disp_y / rot_z and static-equivalent load fields feq_x / feq_y / meq_z (each (n_floors, n_t)).

cfdmod.building.floor_accelerations(response: PointsDataSource, structure: BuildingStructuralData, *, point: tuple[float, float] = (0.0, 0.0)) PointsDataSource[source]

Per-floor horizontal accelerations at an off-centre occupant point.

Augments response with acc_x / acc_y / acc_mag for the comfort assessment. point is in the same frame as the structure’s CM offsets.

cfdmod.building.peak_response_table(response: PointsDataSource, accelerations: PointsDataSource, case: BuildingCase) DataFrame[source]

Per-floor peak magnitudes for the engineer-facing deliverable table.

One row per floor: mid-height Z, peak absolute displacement / rotation, peak static-equivalent loads, and peak acceleration magnitude. Peaks are the maximum absolute value over the time record.

Occupant comfort

Peak top-floor accelerations are checked against the comfort limits of three standards – NBR 6123, Melbourne (1992) and the NBCC. comfort_limit dispatches on the selected standard and occupancy; the per-standard helpers are also exposed directly.

cfdmod.building.comfort_limit(f0: float | ndarray, standard: Literal['nbr', 'melbourne', 'nbcc'], *, occupancy: Literal['residential', 'commercial'] = 'residential', return_period_years: float = 10.0) float | ndarray[source]

Dispatch to an acceleration-limit curve by standard.

Parameters:
  • f0 – fundamental sway frequency (Hz); scalar or array.

  • standard"nbr", "melbourne" or "nbcc".

  • occupancy"residential" or "commercial" (NBR / NBCC).

  • return_period_years – return period (Melbourne only).

Returns the limit in m/s^2 (matching the shape of f0, except the flat NBCC limit which is a scalar).

cfdmod.building.nbr6123_acceleration_limit(f0: float | ndarray, occupancy: Literal['residential', 'commercial'] = 'residential') float | ndarray[source]

ABNT NBR 6123 serviceability acceleration limit (sec. 9.6.2).

a_lim = 0.01 * coeff * f0**-0.445 with coeff 4.08 (residential) or 6.12 (commercial); f0 the fundamental sway frequency in Hz. The 0.01 converts the standard’s cm/s^2 expression to m/s^2. Returns m/s^2. The standard states this over 0.06-1.00 Hz at a 1-year return period.

cfdmod.building.melbourne1992_acceleration_limit(f0: float | ndarray, return_period_years: float = 10.0) float | ndarray[source]

Melbourne & Palmer (1992) serviceable peak-acceleration limit (Eq. 3).

a_lim = sqrt(2 ln(600 f0)) * (0.68 + ln(R) / 5) * exp(-3.65 - 0.41 ln f0) with f0 the fundamental sway frequency (Hz), R the return period in years and 600 the averaging window (s). Returns m/s^2. The paper states this over 0.06 < f0 < 1.0 Hz and 0.5 < R < 10 years.

cfdmod.building.nbcc_acceleration_limit(occupancy: Literal['residential', 'commercial'] = 'residential') float[source]

NBCC flat occupant-comfort limit (10-year return period), in m/s^2.

Frequency-independent: 15 milli-g residential, 25 milli-g office / commercial.

Design load cases

cfdmod.building.generate_load_cases(max_dict: dict[str, dict[str, ndarray]], min_dict: dict[str, dict[str, ndarray]], *, unit_conversion: float = 0.00010197162129779283) dict[int, dict[str, ndarray]][source]

Eberick companion-load cases from the per-direction envelopes.

For each principal axis the critical direction is the one with the largest mean max load; the principal sign (max / min) is then combined with all four companion-axis sign combinations. Returns {case_id: {"Fx","Fy","Mz": per-floor array}} (loads scaled by unit_conversion). Mirrors the hfpi_analysis notebook’s generate_load_cases.

cfdmod.building.save_load_case_tables(stats: dict[str, dict[str, DataFrame]], writer, *, deliverable: bool = True, floor_heights: ndarray | None = None, prefix: str = 'loadcase', skip_if_exists: bool = False) dict[str, Path][source]

Write each {prefix}_{stat}_{Fx|Fy|Mz}.csv via writer.save_csv.

A leading floor column (and z when floor_heights is given) is materialized because DebugWriter.save_csv defaults to index=False. Returns {csv name: written path}.

Multi-direction fan-out

A single driver runs the whole per-floor / dynamic / comfort chain over every (direction, body, config) combination of a parametric study.

class cfdmod.building.FanoutPlan(*, batch_name: str = '', categories: list[str] = [], directions_by_category: dict[str, list[str]] = {}, bodies: list[str], cp_configs: list[str] = ['base'])[source]

Bases: BaseModel

The fan-out axes parsed from global_data.json (or built directly).

property directions: list[str]

Order-preserving union of directions across the selected categories.

classmethod from_global_data(case_data_dir: str | Path, *, bodies: list[str] | None = None, cp_configs: list[str] | None = None) FanoutPlan[source]

Read analysis.categories / directions_cat* / body_name / batch_name from <case_data_dir>/global_data.json.

bodies / cp_configs override the single body / default config the JSON implies (real cases fan over several).

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

cfdmod.building.run_fanout(plan: FanoutPlan, solve_fn: Callable[[StaticCaseKey], PointsDataSource], *, pool: Pool | None = None, writer=None, case: BuildingCase | None = None) Container[StaticCaseKey, PointsDataSource][source]

Fan solve_fn out over every (direction, body, cp_config) key.

Collects the responses in a Container[StaticCaseKey, PointsDataSource] (group with container.join_by(lambda k: k.direction)). With pool the fan-out runs through pool.map. When both writer and case are given, the provenance dump (region info + resolved config) is written once beside the outputs.

Structural model import (dynamics)

The building dynamic-response recipe needs a modal model of the structure – per-floor mass, polar inertia, centre of mass, natural periods and the per-floor mode shapes. cfdmod.dynamics reads that model out of the structural engineer’s design software (TQS, Eberick) and converts it to the internal BuildingStructuralData. See Structural Model Import (Dynamics) for the supported file formats and the conversion in detail.

from cfdmod.dynamics import read_tqs_portels, read_tqs_portico, read_eberick

Structural model

class cfdmod.dynamics.structural.BuildingStructuralData(*, mode_shapes: Any, natural_frequencies: Any, floor_points: Any, cm_positions: Any, floors_mass: Any, floors_radius: Any, floor_labels: list[str] | None = None, floor_metadata: dict[str, list] | None = None)[source]

Bases: BaseModel

Assembled structural inputs for the building dynamic-response recipe.

Holds numpy arrays ready to pass to BuildingDynamicConfig. Mode shapes are stored mass-normalized.

classmethod from_csvs(modes_csv: Path, floors_csv: Path, mode_shape_csvs: list[Path], *, active_modes: list[int] | None = None) BuildingStructuralData[source]

Build from the modes / floors / per-mode mode-shape CSVs.

Parameters:

active_modes – 1-based mode numbers to keep. None keeps every mode that has a mode-shape CSV.

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

to_config(damping_ratio: float, **field_names: str)[source]

Build a BuildingDynamicConfig from these structural arrays.

field_names overrides the load-coefficient field names (field_x / field_y / field_mz).

with_multipliers(*, mass_multiplier: float = 1.0, frequency_multiplier: float = 1.0) BuildingStructuralData[source]

Apply per-case mass / frequency multipliers.

Mirrors the legacy HFPI case knobs: scaling every floor mass by mass_multiplier (mm) scales the generalized mass uniformly, so mass-normalized shapes divide by sqrt(mm); natural frequencies shift by frequency_multiplier / sqrt(mm) (the 1/sqrt(mm) from the softer/heavier structure, the explicit frequency_multiplier applied on top). Radius of gyration is left unchanged (matches the legacy path).

Returns a new BuildingStructuralData.

Importers

cfdmod.dynamics.read_tqs_portels(source: str | Path, *, active_modes: list[int] | None = None, tol_z: float = 0.05) BuildingStructuralData[source]

Read a TQS PORTELS export directory into a BuildingStructuralData.

Handles both the older PORTELS_*.TXT and newer PORTELSSE_*.TXT file names (matched by suffix). When a *_PISOS.TXT floor table is present it is used to sanity-check the recovered floor count.

Parameters:
  • source – Directory containing the PORTELS(SE)_*.TXT files.

  • active_modes – 1-based mode numbers to keep (None keeps all).

  • tol_z – Slab elevation clustering tolerance (m).

Returns:

Per-floor structural data ready for the building dynamic recipe.

cfdmod.dynamics.read_tqs_portico(source: str | Path, *, masses_file: str | Path | None = None, modos_file: str | Path | None = None, modes_file: str | Path | None = None, units: EberickUnits | None = None, active_modes: list[int] | None = None) BuildingStructuralData[source]

Read a TQS Portico per-floor export into a BuildingStructuralData.

Parameters:
  • source – Directory containing the PORTICO_*_PAVIMENTO.TXT files and modes.csv.

  • modes_file (masses_file / modos_file /) – Explicit paths overriding the in-directory lookup (for renamed files).

  • units – Unit conversions (default cm -> m, tf.s^2/cm -> kg).

  • active_modes – 1-based mode numbers to keep (None keeps all).

Returns:

Per-floor structural data (floors ascending by elevation, mass-normalized mode shapes).

cfdmod.dynamics.read_eberick(source: str | Path, *, masses_file: str | Path | None = None, formas_file: str | Path | None = None, units: EberickUnits | None = None, active_modes: list[int] | None = None) BuildingStructuralData[source]

Read an Eberick export directory into a BuildingStructuralData.

Parameters:
  • source – Directory holding the DISTRIBUICAO_DAS_MASSAS... and FORMAS_MODAIS... workbooks (matched case/accent-insensitively).

  • formas_file (masses_file /) – Explicit workbook paths overriding the in-directory lookup (for renamed files).

  • units – Unit conversions (default cm -> m, tf.s^2/cm -> kg).

  • active_modes – 1-based mode numbers to keep (None keeps all).

Returns:

Per-floor structural data (floors ascending by elevation, mass- normalized mode shapes; storey names in floor_labels and the storey heights in floor_metadata['altura_cm']).

Conversion

cfdmod.dynamics.imports.aggregate_to_building(model: NodalModel, *, tol_z: float = 0.05, floor_levels: list[float] | None = None, floor_labels: list[str] | None = None, active_modes: list[int] | None = None, drop_massless: bool = True) BuildingStructuralData[source]

Aggregate a NodalModel into a per-floor BuildingStructuralData.

Parameters:
  • tol_z – Elevation clustering tolerance (m) for the fallback grouping; nodes whose Z rounds to the same multiple of tol_z belong to one slab. Ignored when floor_levels is given.

  • floor_levels – Authoritative slab elevations (e.g. from a TQS PISOS table). When given, every node is assigned to its nearest level, which collapses the many intermediate FE node elevations (beams, landings) onto the real floors. When None, elevations are discovered by clustering node Z with tol_z.

  • active_modes – 1-based mode numbers to keep (None keeps all).

  • drop_massless – Drop levels with zero total mass (non-slab levels: foundation, roof). When False they raise instead.

Returns:

A BuildingStructuralData with floors ordered by ascending elevation, mass-normalized mode shapes, and cm_positions set to the per-floor centre of mass.

class cfdmod.dynamics.imports.EberickUnits(*, length_to_m: float = 0.01, mass_to_kg: float = 980665.0)[source]

Bases: BaseModel

Unit conversions for an Eberick export (defaults: cm -> m, tf.s^2/cm -> kg).

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Analytical wind profiles

Code-based mean-velocity profiles \(U(z)\) for reference and inflow target curves.

class cfdmod.WindProfile_NBR(*, U_H_overwrite: float | None = None, directional_data: DataFrame, V0: float)[source]

Bases: WindProfile

Data for wind analysis and calculation

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class cfdmod.WindProfile_EU(*, U_H_overwrite: float | None = None, directional_data: DataFrame, Vb: float)[source]

Bases: WindProfile

Data for wind analysis and calculation for EU standard EN1991

model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Notebook utilities

cfdmod.mesh_summary(path: Path) None[source]

Print a summary of an LNAS or STL mesh file.

Parameters:

path – Path to .lnas or .stl file.

cfdmod.show_config(config: BaseModel) None[source]

Pretty-print a Pydantic config as a dictionary.

Parameters:

config – Any Pydantic BaseModel instance (LoftCaseConfig, CpRecipeConfig, etc.).

cfdmod.load_lnas(path: str | Path) LnasFormat[source]

Load an LNAS file and return the LnasFormat object.

Parameters:

path – Path to .lnas file.

Returns:

LnasFormat object ready for use.