Skip to main content

Generating CFD Domain Primitives in PyPrimeMesh Using Custom PyAnsys Geometry Functions

alan.varghese@… | 09.23.2026

Introduction

Geometric primitives — boxes, cylinders, spheres, hemispheres, and pill capsules — are the building blocks of CFD domain setup. The same five shapes serve three distinct purposes depending on how they are sized relative to the geometry:

  • Body of Influence (BOI): localised refinement volume for wakes, shear layers, and junctions.
  • MRF zone: cylindrical or pill-shaped rotating reference frame region surrounding a fan, propeller, or rotor.
  • Enclosure: large outer bounding volume establishing the fluid domain boundary.

Each primitive type maps naturally to one or more of these roles:

boi_type Shape BOI MRF zone Enclosure
"box" Rectangular cuboid Wake boxes, junction refinement — Outer fluid domain
"cylinder" Right circular cylinder Axisymmetric wake refinement Fan, propeller, rotor region Cylindrical tunnel domain
"hemisphere" Dome cap Nose/leading-edge refinement — —
"sphere" Full sphere Compact omnidirectional source — Spherical far-field domain
"pill" Cylinder + hemisphere cap Streamlined body BOI Elongated rotor region —
"frustum" Tapered cylinder Tapered wake/nacelle refinement Tapered rotor region Tapered tunnel domain

Supported primitive types: box, cylinder, hemisphere, sphere, and pill

This guide shows how to eliminate that step by writing custom Python helper classes that generate all geometry programmatically, sized from the mesh's own bounding box at runtime. Key advantages over a manual CAD workflow:

  • One tool, three use cases — primitive type and offset ratio determine the intent.
  • Always up-to-date — bounding boxes derive from live mesh zonelets, adapting automatically when geometry changes.
  • Fully parameterised — axis direction, primitive type, and offsets are all runtime parameters.
  • Single script — mesh reading, CAD creation, and re-import run in one Python session.

The solution bridges two PyAnsys packages:

Module architecture and data flow between PyPrimeMesh and PyAnsys Geometry

Package Role
PyPrimeMesh (ansys.meshing.prime) Reads mesh, computes bounding boxes from zonelets, imports final primitive
PyAnsys Geometry (ansys.geometry.core) Constructs and exports the CAD solid as an .fmd file

About the .fmd format. FMD (Fluent Meshing Data) is Ansys's platform-agnostic binary CAD exchange format. It stores the solid body in a self-contained, OS-independent representation readable on Windows, Linux, or any Ansys-supported platform — no native CAD kernel required. This makes .fmd the ideal intermediate format when the geometry server and meshing session run on different machines or operating systems.

No CAD application is opened by the user. The entire pipeline — from reading a mesh file to appending a fully formed primitive volume — runs from a single Python script.


Code Structure

The solution is built around three Python classes you will implement:

_BBox                — plain data container for six bounding-box coordinates
_PrimitiveGenerator  — drives PyAnsys Geometry to build one primitive solid
  ├─ box             — rectangular profile extruded along an axis
  ├─ cylinder        — circular profile extruded along an axis
  ├─ hemisphere      — quarter-circle profile revolved 360° to form a dome cap
  ├─ sphere          — circumscribed sphere centred on the bounding box midpoint
  ├─ pill            — cylinder with a hemispherical cap merged at one end
  └─ frustum         — tapered circular profile extruded along an axis
PrimitiveCreator     — public API; orchestrates bounding-box extraction,
                       offsetting, CAD generation, and Prime re-import

_BBox — Bounding Box Container

class _BBox:
    def __init__(self, xmin, ymin, zmin, xmax, ymax, zmax) -> None:
        self.xmin = xmin
        self.ymin = ymin
        self.zmin = zmin
        self.xmax = xmax
        self.ymax = ymax
        self.zmax = zmax

A lightweight value object holding the six axis-aligned extents of the target region. It can be populated manually or converted from a Prime BoundingBox result.


Step 1 — Querying and Sizing the Bounding Box

The key differentiator of this workflow is deriving the bounding box directly from face zonelets rather than hard-coding coordinates. Implement a PrimitiveCreator class whose constructor queries the tight axis-aligned bounding box of any list of face zonelet IDs using prime.SurfaceUtilities.get_bounding_box_of_zonelets:

surf = prime.SurfaceUtilities(model=self.model)
computed_bounding_box = surf.get_bounding_box_of_zonelets(
    zonelets=self.face_zonelets_list
)

Once the bounding box is known, expand each face independently using per-axis offset ratios before passing it to _PrimitiveGenerator. A small ratio keeps the primitive snug for a BOI; a large ratio expands it into an enclosure:

offset_ratio = {
    "xmin_ratio": 0.0,   # no offset upstream (e.g. inlet flush with geometry)
    "ymin_ratio": 0.2,   # expand 20 % of y-length on the negative-y face
    "zmin_ratio": 0.2,
    "xmax_ratio": 0.1,   # expand 10 % of x-length on the positive-x face
    "ymax_ratio": 0.2,
    "zmax_ratio": 0.2,
}

Recompute the coordinates with the ratios applied:

self.bounding_box_coordinates = prime.BoundingBox(
    model=self.model,
    xmin=bbox.xmin - x_length * offset_ratio["xmin_ratio"],
    ymin=bbox.ymin - y_length * offset_ratio["ymin_ratio"],
    zmin=bbox.zmin - z_length * offset_ratio["zmin_ratio"],
    xmax=bbox.xmax + x_length * offset_ratio["xmax_ratio"],
    ymax=bbox.ymax + y_length * offset_ratio["ymax_ratio"],
    zmax=bbox.zmax + z_length * offset_ratio["zmax_ratio"],
)

Ratios greater than 1 produce large enclosure volumes; values near 0 produce snug BOI or MRF refinement zones. Expose a single public method add_boi that drives the full pipeline:

def add_boi(self):
    self.__create_boi()   # generates and saves the .fmd file via PyAnsys Geometry
    self.__append_boi()   # imports the .fmd back into the Prime model

Step 2 — Implementing the Primitive Geometry Functions

All five primitives are implemented inside a _PrimitiveGenerator class. Each method follows the same pattern:

  1. Set length units to millimetres via DEFAULT_UNITS.LENGTH = UNITS.mm.
  2. Derive axis lengths and cross-section radius from the bounding box.
  3. Build a Sketch on a Plane anchored at the correct origin.
  4. Extrude or revolve the sketch into a solid body.

Shared Helpers

Before looking at individual primitives, two private helpers are used by all of them:

def __bbox_axis_lengths(self):
    distance_x = abs(self.bbox.xmax - self.bbox.xmin)
    distance_y = abs(self.bbox.ymax - self.bbox.ymin)
    distance_z = abs(self.bbox.zmax - self.bbox.zmin)
    return distance_x, distance_y, distance_z

def __cross_section_radius(self, distance_x, distance_y, distance_z):
    # Inscribed radius of the cross-section perpendicular to axis_direction
    if self.axis_direction == "x":
        return min(distance_y, distance_z) / 2
    elif self.axis_direction == "y":
        return min(distance_x, distance_z) / 2  # fixed: was min(distance_x + distance_z)
    elif self.axis_direction == "z":
        return min(distance_x, distance_y) / 2  # fixed: was min(distance_x + distance_y)
    else:
        raise ValueError(
            f"axis_direction must be 'x', 'y', or 'z', got '{self.axis_direction}'"
        )

__cross_section_radius returns the largest circle that fits within the rectangular cross-section perpendicular to the chosen axis — i.e. half the shorter dimension of that face.


1. Box

The simplest primitive. A rectangular profile is sketched on a plane perpendicular to X and extruded along X by the full bounding-box length.

def __create_box(self):
    DEFAULT_UNITS.LENGTH = UNITS.mm
    distance_x, distance_y, distance_z = self.__bbox_axis_lengths()

    sketch_plane = Plane(
        origin=Point3D([self.bbox.xmin, 0, 0]),
        direction_x=UNITVECTOR3D_Y,
        direction_y=UNITVECTOR3D_Z,
    )
    bounding_box_sketch = Sketch(sketch_plane)
    bounding_box_sketch.box(
        center=Point2D([
            self.bbox.ymin + (distance_y / 2),
            self.bbox.zmin + (distance_z / 2),
        ]),
        width=distance_y,
        height=distance_z,
    )
    self.design.extrude_sketch(
        name=self.boi_name, sketch=bounding_box_sketch, distance=distance_x
    )

Key geometry call: Sketch.box() + Design.extrude_sketch()

The sketch plane is placed at xmin with its local axes along Y and Z, so the extrusion travels in the +X direction for distance_x.


Note: Beyond the box, several other primitive geometries can be produced by the same generator simply by changing the orientation and profile passed to the sketch/extrude/revolve calls — for example a cylinder (circular profile extruded along axis_direction), a hemisphere (quarter-circle profile revolved 360° to form a dome cap), a sphere (circumscribed about the bounding box), a pill/capsule (cylinder with a hemisphere cap merged via Boolean union), and a frustum (tapered profile extruded along an axis). These follow the same bounding-box-driven approach shown above and are omitted here for brevity.


Step 3 — Dispatching the Correct Primitive

Add a single public create_boi method to _PrimitiveGenerator that selects the geometry function by name, writes the .fmd file, and shuts down the geometry service:

def create_boi(self):
    if self.boi_type == "box":
        self.__create_box()
    elif self.boi_type == "cylinder":
        self.__create_cylinder()
    elif self.boi_type == "sphere":
        self.__create_sphere()
    elif self.boi_type == "hemisphere":
        self.__create_hemisphere()
    elif self.boi_type == "pill":
        self.__create_pill()
    elif self.boi_type == "frustum":
        self.__create_frustum()
    self.__write_cad_file()
    self.__close_geometry_services()

Once the solid is built the design is exported to an .fmd file — a platform-agnostic format readable on any Ansys-supported OS — and the geometry service is shut down.


Step 4 — Appending to the PyPrimeMesh Session

After the CAD file is written, __append_boi uses prime.FileIO to import it back as a new body part — appended to the existing model rather than replacing it. Mesh zonelets are pruned immediately so only the geometry zonelets remain, ready for size-field assignment, MRF setup, or outer-domain meshing:

def __append_boi(self):
    file_io = prime.FileIO(self.model)
    cad_import_params = prime.ImportCadParams(
        self.model,
        part_creation_type=prime.PartCreationType.BODY,
        append=True,
    )
    file_io.import_cad(
        os.path.join(self.working_dir, f"{self.boi_name}.fmd"),
        params=cad_import_params,
    )
    part = self.model.get_part_by_name(self.boi_name)
    part.delete_topo_entities(
        prime.DeleteTopoEntitiesParams(
            self.model,
            delete_geom_zonelets=False,
            delete_mesh_zonelets=True,
        )
    )

End-to-End Usage Example

The following example shows a complete workflow: launching Prime, reading a mesh, and generating three primitives — a pill-shaped BOI, a cylinder MRF zone, and a box enclosure — each derived from a different set of zonelets.

import os
from ansys.meshing import prime

# --- Assumes PrimitiveCreator, _BBox, _PrimitiveGenerator are defined as described above ---

# --- Launch Prime ---
ansys_install = os.getenv("AWP_ROOT261")
client = prime.launch_prime(
    prime_root=os.path.join(ansys_install, "meshing", "Prime")
)
model  = client.model
mesher = prime.lucid.Mesh(model)
wrk_dir = os.path.dirname(os.path.abspath(__file__))

mesher.read(os.path.join(wrk_dir, "input_mesh.msh.gz"))

part = model.get_part_by_name("origin-solid")

# --- Extract zonelets by label ---
fuselage_zonelets = part.get_face_zonelets_of_label_name_pattern(
    "fuselage", name_pattern_params=prime.NamePatternParams(model)
)
blade_zonelets = part.get_face_zonelets_of_label_name_pattern(
    "engine_blade", name_pattern_params=prime.NamePatternParams(model)
)
all_zonelets = part.get_face_zonelets()

# --- Pill primitive as BOI around fuselage (x-axis, asymmetric offsets) ---
PrimitiveCreator(
    face_zonelets_list=fuselage_zonelets,
    use_defined_bounding_box=False,
    bounding_box_coordinates={},
    axis_direction="x",
    use_offset_ratio=True,
    offset_ratio={
        "xmin_ratio": 0.0, "ymin_ratio": 0.2, "zmin_ratio": 0.2,
        "xmax_ratio": 0.1, "ymax_ratio": 0.2, "zmax_ratio": 0.2,
    },
    boi_name="boi-fuselage",
    boi_type="pill",
    model=model,
    working_dir=wrk_dir,
).add_boi()

# --- Cylinder primitive as MRF zone around rotating blade ---
PrimitiveCreator(
    face_zonelets_list=blade_zonelets,
    use_defined_bounding_box=False,
    bounding_box_coordinates={},
    axis_direction="x",
    use_offset_ratio=True,
    offset_ratio={k: 0.1 for k in
                  ("xmin_ratio","ymin_ratio","zmin_ratio",
                   "xmax_ratio","ymax_ratio","zmax_ratio")},
    boi_name="mrf-blade",
    boi_type="cylinder",
    model=model,
    working_dir=wrk_dir,
).add_boi()

# --- Box primitive as outer domain enclosure ---
PrimitiveCreator(
    face_zonelets_list=all_zonelets,
    use_defined_bounding_box=False,
    bounding_box_coordinates={},
    axis_direction="x",
    use_offset_ratio=True,
    offset_ratio={
        "xmin_ratio": 5, "ymin_ratio": 5,  "zmin_ratio": 5,
        "xmax_ratio": 5, "ymax_ratio": -0.01, "zmax_ratio": 5,
    },
    boi_name="enclosure",
    boi_type="box",
    model=model,
    working_dir=wrk_dir,
).add_boi()

mesher.write(os.path.join(wrk_dir, "mesh_with_bois.msh.gz"))

Alternatively, if you already know the exact bounding box coordinates, pass use_defined_bounding_box=True and supply a _BBox instance directly:

# --- Assumes _BBox and _PrimitiveGenerator are defined as described above ---

bbox = _BBox(xmin=-1750, ymin=-100, zmin=0, xmax=-917, ymax=0, zmax=100)
PrimitiveCreator(
    face_zonelets_list=[],
    use_defined_bounding_box=True,
    bounding_box_coordinates=bbox,
    axis_direction="x",
    use_offset_ratio=True,
    offset_ratio={
        "xmin_ratio": 5, "ymin_ratio": 5,  "zmin_ratio": 5,
        "xmax_ratio": 5, "ymax_ratio": -0.01, "zmax_ratio": 5,
    },
    boi_name="enclosure",
    boi_type="box",
    model=model,
    working_dir=wrk_dir,
).add_boi()

Summary

This guide has shown how to write custom Python helper classes that generate geometric primitive volumes directly from PyPrimeMesh face zonelets, without opening a CAD application. Five primitive types — box, cylinder, hemisphere, sphere, and pill — cover the full range of CFD domain objects: localised BOI refinement zones, MRF rotating regions, and outer domain enclosures. Bounding boxes are computed at runtime from face zonelets, ensuring every primitive is automatically sized to the current geometry. Per-axis offset ratios give fine-grained control over how tightly or loosely each primitive wraps its target region. Once generated, each solid is exported as an .fmd file and appended back into the Prime model in the same Python session, making the workflow fully reproducible and easy to integrate into automated meshing pipelines.

For more details, feel free to reach out on the Synopsys Developer Forum.

Connect with Ansys