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.
rootdefaults to the directory containing the YAML file so relativepath:entries insideinputs:/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 throughstorage.write_data_sourceas 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:
BaseModelA 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(orfn(ds, rhs, params)for a binary op) and aparams_cls, then calling this. The op is then a first-class citizen: it is usable in YAML/dict templates under itskind, validated byvalidate_template(), and listed bylist_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_clsshould subclasscfdmod.core.ops.OpParamsand set those class attributes; a plainBaseModelstill 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:
BaseModelBase frozen value object.
Subclasses lock
kindand (optionally) the admissibleTopology.cell_type. Methods on this base never mutate; they always return a new instance viamodel_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.
- topology¶
Mesh connectivity / coordinates, when applicable.
Noneis permitted for some kinds (notablymodes).- Type:
- 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
FieldStoredecides 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:
DataSourceFaces (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:
DataSource3D 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:
DataSourceBare points / probes / vertical profiles.
Covers the existing
InflowData(probe array + per-component timeseries) ands1.profile.Profile(1-D vertical profile, no time axis). Topology cell type ispoint; 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:
DataSourceOne 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
Topologyplus aGroupingmapping 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.
- 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:
DataSourceModal axis: one row per mode, fields are generalised-displacement timeseries.
No spatial topology; the original mesh / structural data lives alongside in the recipe context.
elementstypically 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) usesn_timesteps == 0to 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_normalizedconvention in cfdmod’s h5 layout. Defaults toinitial_timeso that without explicit normalization,time_normalized[0] == 0.- Type:
float | None
- 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.
- time_at(index: int) float[source]¶
Time corresponding to a single index. Negative index counts from the end.
- 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
Topologyrather 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 intovertices. Forpoint->(0, 0)(no connectivity). Forcell(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.pyexactly:/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).
- 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
-1marks “ungrouped”. Groups are addressed by integer id; an optionalid_to_labelmaps 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, runcallback(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
functo every value.If
poolis supplied, fanout runs throughpool.mapand 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
DataSourceobjects in a Python dict.read_data_sourceandwrite_data_sourceare O(1) hash lookups; nothing is copied.A
MemoryStorageis mutable: new keys are added bywrite_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]¶
Storagefor 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_sourcealso (re)generates<key>.xdmfnext 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:
BaseModelCp 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 / Lfor 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:
BaseModelCf recipe parameters.
- grouping¶
Name of the grouping in
ds.groupingsthat 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:
BaseModelCm recipe parameters.
- grouping¶
Name of the grouping in
ds.groupingsmapping elements to body ids.- Type:
str
- directions¶
Force/moment directions to aggregate. For each entry
dthe 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:
BaseModelCe recipe parameters.
- grouping¶
Name of the zoning grouping in
ds.groupings(one group per region), typically produced by thezoning_groupingop.- 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:
BaseModelS1 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:
BaseModelDynamic-analysis recipe parameters.
- mode_shapes¶
(n_load_elements, n_modes)mode-shape matrix at the load points (used to computeQ). For most cases the same mode shapes also drive recomposition (seerecomposition_mode_shapes).- Type:
Any
- recomposition_mode_shapes¶
Optional
(n_target_elements, n_modes)matrix evaluated at the target coordinates. IfNone,mode_shapesis 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:
BaseModelPedestrian 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 -> indicesmap which is merged into the running result; duplicate group names raiseValueError(no silent merging).A spec may carry a
restrict_to: list[str] | Nonefield. When set, that spec only considers triangles whose index is in the union of the named earlier groups; this is how the legacysurface -> sub_bodynesting is expressed.- Parameters:
mesh – Parent mesh.
groupings – Specs in application order.
- Returns:
GroupingResultovermesh.- Raises:
ValueError – If
groupingsis empty, two specs produce the same group name, or arestrict_toreferences 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.trianglesarray (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) andgroup_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:
BaseModelGroup triangles by named LNAS surfaces.
- Parameters:
kind – Discriminator literal, always
"by_surface".sets –
group_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.surfacesnot referenced insetsare 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:
BaseModelAxis-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:
BaseModelCartesian 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:
BaseModelCartesian 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:
BaseModelGroup 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 socc0is 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:
BaseModelGroup 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.0every 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:
BaseModelBin 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:
BaseModelEqual-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:
BaseModelCartesian 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:
BaseModelGrouping 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
kinddiscriminator soload_groupings()can route it back to the correct spec class.- Parameters:
groupings – Chain of validated spec instances.
- Returns:
list[dict]suitable forwrite_processing_metadataor 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
GroupingSpecdiscriminated union, so each entry must carry akindkey matching one of the registered spec classes.- Parameters:
serialized – Output of
dump_groupings()(or any list of dicts with a validkinddiscriminator).- 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
LnasFormatfrom an HDF5 file’s/Triangles + /Geometrydatasets, with one synthetic surface covering every triangle.Used both by
load_mesh()(for.h5/.xdmfinputs) and as the fallback when a pipeline call omitsmesh_pathand 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
groupas HDF5 attributes on that group, plus the YAML serialization as a sibling string dataset for round-trip reproducibility.The
configdict 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](seecfdmod.pressure.filters)."groupings":list[dict]produced bycfdmod.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_metadataparses the YAML back to a dict; the per-pipelineload_*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 viah5dumpeven 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 anyextrakeys 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
trianglesorregionsis 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_normalizedwith one column per retained triangle / region representative.- Raises:
ValueError – If the file has no
/{group}group, the timestep filter yields no rows, bothtrianglesandregionsare requested, or the unfiltered column count exceedsmax_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.Axesto 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_tolon the unit normal andplane_tolon 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 alogger.debugmessage 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^2so 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_triangleshas dtypeint32.
- 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_reductioncan collapse them aggressively. ARuntimeWarningis emitted in that case; consider runningfast_simplification.simplifydirectly withlossless=Trueif 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.9keeps 10%).<= 0returns the input unchanged.aggressiveness –
aggparameter passed through tofast_simplification.simplify(default 7 matches the library).
- Returns:
(new_vertices, new_triangles).new_triangleshas dtypeint32.- Raises:
ImportError – if
fast-simplificationis not installed. Install it viapip 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, runmerge_coplanar()(ifcoplanar_merge) and thendecimate_qem()(iftarget_reduction > 0), and restitch the per-surface outputs into a freshLnasFormatwith 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 flatNxN- subdivided square inside one surface comes out as 2 triangles; a curved patch comes out unchanged.- Parameters:
mesh – Input
LnasFormatwhosesurfacesmap names to triangle index arrays.coplanar_merge – Run
merge_coplanar()per surface. Default True.target_reduction – If
> 0, rundecimate_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.0disables and falls back to exact-equality dedup. Tolerance-based dedup matters oncedecimate_qem()is enabled because QEM can synthesise new vertex positions that drift below float-equality.
- Returns:
A fresh
LnasFormatwith one named surface per input surface (insertion order preserved). Empty surfaces (no triangles) and surfaces that fully collapse during merge are kept as emptysurfacesentries 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:
BaseModelTop-level config for the regroup pipeline.
- Parameters:
groupings – Chain of regroup specs (every standard
GroupingSpecplus the regroup-localBySizeRoundedPerComponent). Specs are applied left to right;BySizeRoundedPerComponententries are expanded byrun_regroupagainst 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 onlygeometry.lnas;"lnas_and_stl"also writes ageometry.stlcompanion (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 syntheticunassignedgroup/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:
BaseModelPer-parent-group round-to-nearest size-based subdivision.
Expanded by
expand_size_rounded_chain()beforeapply_groupings()is called: for each group produced by the prior chain, derive per-axisn_div = max(min_n_div, round(extent / target))from the restricted centroid bbox, then append aByDivisionsGroupingwithrestrict_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 innerByDivisionsGrouping.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
groupingsagainst a transformed copy ofmesh.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 ofgrouping.groups, with an optional trailingunassignedbucket). 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]tooutput_h5[group]per the index.Writes
/Triangles + /Geometry(from the regrouped mesh), one/{group}/t{T}dataset per input timestep, and/metacarrying the originaltime_steps/time_normalizedplus per-output- triangleregion_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
GroupingSpecentries.Returns
(expanded_specs, consumed_group_names, parent_intervals, parent_triangles).consumednames are intermediate parent groups thatBySizeRoundedPerComponenthas fanned out over;parent_intervalsandparent_trianglescarry 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.stlifcfg.output_geometry_format == "lnas_and_stl".{cfg.timeseries_group}.regrouped.h5and 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:
BaseModelImmutable 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
anchorspluspressure_coefficient/force_coefficient/moment_coefficientblocks). 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_refis a scalar reference pressure or a points/surface DataSource (broadcast per timestep). Passstatisticsto 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. Seecf_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 themethodtrade-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
gfor a narrow-band process.g = sqrt(2 ln(nu T)) + 0.5772 / sqrt(2 ln(nu T))withnu = f0the mean up-crossing rate (Hz) andT = duration(s). Withfull=Falseonly the leadingsqrt(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"(needsf0), or"gumbel".f0 – response frequency (Hz), required for
"peak-factor".duration – full-scale averaging window (s) for the gust factor.
absolute – if True,
"max"usesmax(|series|)and"peak-factor"builds the peak off|mean| + g*std.n_blocks – number of blocks for
"gumbel"block maxima.non_exceedance – design fractile
pfor"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
PointsDataSourceover the floors with displacement fieldsdisp_x/disp_y/rot_zand static-equivalent load fieldsfeq_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
responsewithacc_x/acc_y/acc_magfor the comfort assessment.pointis 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.445withcoeff4.08 (residential) or 6.12 (commercial);f0the fundamental sway frequency in Hz. The0.01converts 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)withf0the fundamental sway frequency (Hz),Rthe return period in years and600the averaging window (s). Returns m/s^2. The paper states this over 0.06 < f0 < 1.0 Hz and 0.5 < R < 10 years.
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
maxload; 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 byunit_conversion). Mirrors thehfpi_analysisnotebook’sgenerate_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}.csvviawriter.save_csv.A leading
floorcolumn (andzwhenfloor_heightsis given) is materialized becauseDebugWriter.save_csvdefaults toindex=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:
BaseModelThe 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_namefrom<case_data_dir>/global_data.json.bodies/cp_configsoverride 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_fnout over every(direction, body, cp_config)key.Collects the responses in a
Container[StaticCaseKey, PointsDataSource](group withcontainer.join_by(lambda k: k.direction)). Withpoolthe fan-out runs throughpool.map. When bothwriterandcaseare 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:
BaseModelAssembled 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.
Nonekeeps 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
BuildingDynamicConfigfrom these structural arrays.field_namesoverrides 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 bysqrt(mm); natural frequencies shift byfrequency_multiplier / sqrt(mm)(the1/sqrt(mm)from the softer/heavier structure, the explicitfrequency_multiplierapplied 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_*.TXTand newerPORTELSSE_*.TXTfile names (matched by suffix). When a*_PISOS.TXTfloor table is present it is used to sanity-check the recovered floor count.- Parameters:
source – Directory containing the
PORTELS(SE)_*.TXTfiles.active_modes – 1-based mode numbers to keep (
Nonekeeps 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.TXTfiles andmodes.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 (
Nonekeeps 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...andFORMAS_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 (
Nonekeeps all).
- Returns:
Per-floor structural data (floors ascending by elevation, mass- normalized mode shapes; storey names in
floor_labelsand the storey heights infloor_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
NodalModelinto a per-floorBuildingStructuralData.- Parameters:
tol_z – Elevation clustering tolerance (m) for the fallback grouping; nodes whose Z rounds to the same multiple of
tol_zbelong to one slab. Ignored whenfloor_levelsis 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 withtol_z.active_modes – 1-based mode numbers to keep (
Nonekeeps all).drop_massless – Drop levels with zero total mass (non-slab levels: foundation, roof). When
Falsethey raise instead.
- Returns:
A
BuildingStructuralDatawith floors ordered by ascending elevation, mass-normalized mode shapes, andcm_positionsset 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:
BaseModelUnit 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:
WindProfileData 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:
WindProfileData 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.