Skip to content

shore.mesh.Mesh

The OOP entry point for SHORE: a multi-block structured mesh. Mesh.from_stl(...) builds the blocks (surface projection + topology wiring) and Mesh.march() extrudes them into a volume by hyperbolic marching. Every shore mesh CLI invocation is a thin wrapper around this class.

python
from shore.mesh import CubedSphereTopology, Mesh

mesh = Mesh.from_stl(
    "sphere.stl",
    topology=CubedSphereTopology(),
    ni=40, nj=60, nk=30,
    theta_cap_deg=30.0,
)
mesh.march(ds=1e-3, growth=1.15)
mesh.write_geo("sphere")          # 6 sphere_<label>.geo files

Class

python
class Mesh:
    body:     Body
    blocks:   list[HexBlock]
    topology: Topology
AttributeDescription
bodyThe triangulated body surface and its cached BVH (only set when built via from_stl; None when built via from_array).
blocksThe list of HexBlock instances produced by the topology. Order matches the topology's canonical block list (sub0..sub3, cap_north, cap_south for CubedSphereTopology).
topologyThe Topology instance that produced blocks. Determines which marcher march() dispatches to (cubed-sphere, O-grid, C-grid, or OH hybrid).

Mesh.from_stl

python
@classmethod
def from_stl(
    cls,
    path: str | Path,
    topology: Topology,
    ni: int,
    nj: int,
    nk: int,
    theta_cap: float | None = None,
    theta_cap_deg: float | None = None,
    i_spacing: str = "uniform",
    i_beta: float = 3.0,
    j_spacing: str = "uniform",
    j_beta: float = 3.0,
    j_cluster_lon: float | None = None,
    spacing_k: Spacing1D | None = None,
) -> Mesh

Load an STL, project the surface, and wire the blocks. Does not march — call Mesh.march() afterwards to fill the wall-normal layers.

Parameters

NameTypeDefaultDescription
pathstr | PathPath to the body's STL file. Loaded via load_stl.
topologyTopologyA wired Topology builder — typically CubedSphereTopology(), OGridTopology(), CGridTopology(...), or OHTopology(...).
ni, njintSurface node counts (the (ni, nj) shape of each block's k=0 layer). For CubedSphereTopology, nj must be a multiple of 4 (rounded down with a warning if not).
nkintWall-normal node count including the wall row at k=0. With nk = 30 the marcher extrudes 29 cells from the wall.
theta_capfloat | None0.05*piPolar exclusion angle in radians. Mutually exclusive with theta_cap_deg.
theta_cap_degfloat | NoneSame as theta_cap in degrees.
i_spacingstr"uniform"Surface i-direction spacing law: "uniform" / "tanh" / "tanh2".
i_betafloat3.0Clustering strength for tanh / tanh2 on i.
j_spacingstr"uniform"Surface j-direction spacing law: "uniform" or "tanh2".
j_betafloat3.0Clustering strength for tanh2 on j.
j_cluster_lonfloat | NoneNone(legacy single-block / O-grid only) longitude in radians to cluster the j-distribution toward. Ignored by CubedSphereTopology.
spacing_kSpacing1D | NoneSpacing1D(law="geometric", ds=1e-3, growth=1.15)Wall-normal spacing law. See Spacing1D.

Returns

Mesh — ready to call .march().

Raises

  • MeshInputError — STL malformed / not watertight / not star-shaped from anchor.
  • ParameterError — invalid spacing law, conflicting angle args, theta_cap outside (0, π/2).

Mesh.from_array

python
@classmethod
def from_array(
    cls,
    surface: np.ndarray,
    topology: Topology,
    nk: int,
    spacing_k: Spacing1D | None = None,
    **kwargs,
) -> Mesh

Build a Mesh from a pre-built surface array (no STL). Used by topologies whose input is a numeric array — typically a cross-section polyline from another mesh — rather than a triangulated body.

The resulting Mesh has body = None; methods that depend on self.body (e.g. blend_normals_k > 0 STL-normal blending in Mesh.march) will fail loudly if called.

For CGridTipCap, surface is a (ni_body, 3) cross-section polyline and nk is the cap's spanwise (k-axis) node count.

python
from shore.io.geo import read_geo
from shore.mesh import CGridTipCap, Mesh

body = read_geo("naca0012_body.geo")
cross = body[:, 0, 0, :]                       # jlo tip, k=0 (the wall)
topo = CGridTipCap(
    le_cut_upper_idx=39, le_cut_lower_idx=80,
    span_dir=(0.0, 0.0, -1.0), tip_inset=0.02, n_cap_j=12,
)
cap = Mesh.from_array(surface=cross, topology=topo, nk=20)
cap.write_geo("cap_jlo")

Mesh.march

python
def march(
    self,
    ds: float = 1e-3,
    growth: float = 1.15,
    smooth_strength: float = 0.2,
    smooth_iters: int = 2,
    steps: np.ndarray | None = None,
    layer_callback: Callable[[int, float], None] | None = None,
    blend_normals_k: int = 0,
) -> None

Extrude every block in self.blocks from layer k=0 outward, in place. Dispatches to the per-topology marcher (CubedSphereTopology_march_caps, OGridTopology_march_ogrid, CGridTopology_march_cgrid_* per block count, OHTopology_march_caps_blocks for the wrap then OHTopology.fill_channel_blocks for the deferred channel interior). For CGridTipCap, the cap is fully volumetric at build time and march() is a silent no-op.

Parameters

NameTypeDefaultDescription
dsfloat1e-3First-layer thickness for the geometric spacing law. Ignored when steps is supplied or when spacing_k overrides via per-edge data.
growthfloat1.15Layer-to-layer thickness ratio for the geometric law.
smooth_strengthfloat0.2Laplacian blend factor per layer in [0, 1].
smooth_itersint2Number of Laplacian sweeps applied after each extrusion step.
stepsnp.ndarray | NoneNonePre-computed per-layer step sizes, shape (nk-1,). Highest precedence — overrides ds/growth and any per-edge spacing_k.
layer_callbackCallable[[int, float], None] | NoneNoneCalled as layer_callback(k, jmin_global) after each accepted layer. The CLI's Rich progress bar uses this hook.
blend_normals_kint0Number of layers over which to blend STL surface normals into the grid-computed normals. 0 disables. At layer k < blend_normals_k, blend weight is alpha = 1 - k / blend_normals_k, so k=0 extrudes along pure STL normals and the blend reaches zero at k = blend_normals_k. Useful for sharp-feature bodies. Requires self.body.mesh.

k-step precedence

Mesh.march resolves the wall-normal step schedule by precedence (highest first):

  1. The steps argument, when supplied.
  2. Block 0's k-edges: if all 4 carry a user-overridden Spacing1D (replaced after construction), and every other block agrees, use that schedule. Disagreement → UserWarning, fall back.
  3. Block 0's spacing_k attribute set at from_stl time.
  4. The (ds, growth) geometric defaults.

All blocks share one schedule because k-coupled SHARED / DIRICHLET seams require coincident node positions layer by layer. See Per-edge spacing for how to use precedence (2).

Raises

  • MarchingError — negative-Jacobian cell detected (with tol = -1e-6 to absorb the corner-junction noise floor).
  • NotImplementedErrorself.topology has no registered marcher.

I/O methods

Mesh.write_geo

python
def write_geo(self, stem: str | Path) -> None

Write each block to <stem>_<label>.geo. For CubedSphereTopology: 6 files (<stem>_sub0.geo ... <stem>_cap_south.geo). For OGridTopology: one file (<stem>.geo). For OHTopology: 12 files (6 wrap + 6 <stem>_chan_<label>.geo).

Mesh.write_vtk / Mesh.export

python
def write_vtk(self, stem: str | Path, with_jacobian: bool = True) -> None
def export   (self, stem: str | Path, with_jacobian: bool = True) -> None

Write all blocks as a single PyVista MultiBlock .vtm for ParaView. export is an alias for write_vtk matching the CLI verb. Per-cell Jacobian is attached as CellData by default. See shore.io.vtk.

Inspection methods

Mesh.info

python
def info(self) -> dict

Return mesh metadata: n_blocks, ordered labels, and a per-block dict with shape, n_points, n_cells, xyz_min, xyz_max. The shore info CLI is the one-line wrapper.

Mesh.check

python
def check(self, warn_threshold: float = 1e-6) -> dict

Per-block layer-by-layer Jacobian check. Returns {"ok": bool, "blocks": {label: {"jmins": [...], "any_negative": ..., "any_warn": ...}}}. The marcher's per-layer guard catches inversions during generation; check is the independent post-hoc cross-check.

Mesh.view

python
def view(
    self, *,
    jupyter: bool = False,
    off_screen: bool = False,
    show_edges: bool = True,
    opacity: float = 0.6,
    colorby: str = "Jacobian",
) -> pv.Plotter

Open an interactive PyVista window with all blocks rendered. The shore view CLI is the wrapper. Requires the [vis] extra.

HexBlock attribute

Each entry in Mesh.blocks is a HexBlock:

AttributeDescription
labelBlock name ("sub0", "cap_north", ...).
nodesCoordinate array, shape (ni, nj, nk, 3). After march(), layers 1..nk-1 are filled.
facesSix Face instances, one per face name.
edgesTwelve Edge instances, one per axis-parallel edge.

See also

Released under the MIT License.