Source code for cfdmod.geometry.grouping.kinds.by_percentile

"""Quantile binning along one axis: each bucket holds ~equal triangle count.

For each candidate triangle, take its centroid coordinate along ``axis``;
compute ``n_quantiles`` equal-count bins via the empirical quantiles of
that 1D distribution; assign the triangle to its bucket. Useful when
triangle density along the axis varies, so equal-width cells (as in
:class:`ByDivisionsGrouping`) would produce wildly uneven counts.
"""

from __future__ import annotations

from typing import Annotated, Literal

import numpy as np
from lnas import LnasFormat
from pydantic import BaseModel, Field

_AXIS_INDEX = {"x": 0, "y": 1, "z": 2}


[docs] class ByPercentileGrouping(BaseModel): """Equal-count quantile binning along one axis. Args: 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. """ kind: Literal["by_percentile"] = "by_percentile" axis: Annotated[ Literal["x", "y", "z"], Field(description="Axis to bin along."), ] n_quantiles: Annotated[ int, Field(ge=1, description="Number of equal-count bins."), ] name_template: Annotated[ str, Field("q{idx}", description="Format string; placeholder: {idx}."), ] restrict_to: Annotated[ list[str] | None, Field(None, description="Optional list of earlier group names to restrict to."), ]
def apply_by_percentile( spec: ByPercentileGrouping, mesh: LnasFormat, allowed: np.ndarray | None, ) -> dict[str, np.ndarray]: """Bin candidate centroids by empirical quantiles along the chosen axis.""" triangles = mesh.geometry.triangle_vertices centroids = np.mean(triangles, axis=1) n_parent = centroids.shape[0] if allowed is not None: cand = np.asarray(allowed, dtype=np.int64) else: cand = np.arange(n_parent, dtype=np.int64) if cand.size == 0: return {} coords = centroids[cand, _AXIS_INDEX[spec.axis]].astype(np.float64) n = spec.n_quantiles if n == 1: return {spec.name_template.format(idx=0): np.sort(cand)} edges = np.quantile(coords, np.linspace(0.0, 1.0, n + 1)) # Pad upper edge so the max-coordinate centroid lands in the last cell. edges[-1] = np.nextafter(edges[-1], np.inf) out: dict[str, np.ndarray] = {} for i in range(n): lo, hi = edges[i], edges[i + 1] in_cell = (coords >= lo) & (coords < hi) cell_idxs = cand[in_cell] if cell_idxs.size == 0: continue name = spec.name_template.format(idx=i) if name in out: raise ValueError( f"ByPercentileGrouping: name_template {spec.name_template!r} produced " f"duplicate group name {name!r}; include {{idx}} for uniqueness" ) out[name] = cell_idxs return out