Skip to content

Sections — g.sections

Parametric structural cross-section builders. Each method creates 3D geometry directly in the active session and returns an Instance with named sub-regions (flanges, web, end faces) ready for constraints and loads.

g.sections

apeGmsh.sections._builder.SectionsBuilder

SectionsBuilder(parent: '_SessionBase')

Bases: _HasLogging

Direct in-session section builder (g.sections).

Source code in src/apeGmsh/sections/_builder.py
def __init__(self, parent: "_SessionBase") -> None:
    self._parent = parent

W_solid

W_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', label: str = 'W_solid', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a W-shape solid directly in the current session.

Same geometry as :func:apeGmsh.sections.W_solid but without a Part intermediary. Returns an Instance with .labels accessor.

Parameters

anchor : str or (x, y, z), default "start" Re-origin the section in its local frame before optional align and before the user's translate/rotate. See :func:apeGmsh.core._section_placement.compute_anchor_offset. align : str or (ax, ay, az), default "z" Reorient the local +Z axis to a world direction. See :func:apeGmsh.core._section_placement.compute_alignment_rotation. lc : float Target element size for this section's BRep points. Default 1e22 imposes no constraint — element size is governed by :meth:set_global_size alone.

Example

::

with apeGmsh("frame") as g:
    col = g.sections.W_solid(
        bf=150, tf=20, h=300, tw=10, length=2000,
        label="col", lc=50,
    )
    g.mesh.sizing.set_global_size(100)
    g.mesh.generation.generate(3)
Source code in src/apeGmsh/sections/_builder.py
def W_solid(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "W_solid",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a W-shape solid directly in the current session.

    Same geometry as :func:`apeGmsh.sections.W_solid` but
    without a Part intermediary.  Returns an Instance with
    ``.labels`` accessor.

    Parameters
    ----------
    anchor : str or (x, y, z), default ``"start"``
        Re-origin the section in its local frame before optional
        ``align`` and before the user's ``translate``/``rotate``.
        See :func:`apeGmsh.core._section_placement.compute_anchor_offset`.
    align : str or (ax, ay, az), default ``"z"``
        Reorient the local +Z axis to a world direction.
        See :func:`apeGmsh.core._section_placement.compute_alignment_rotation`.
    lc : float
        Target element size for this section's BRep points.
        Default ``1e22`` imposes no constraint — element size
        is governed by :meth:`set_global_size` alone.

    Example
    -------
    ::

        with apeGmsh("frame") as g:
            col = g.sections.W_solid(
                bf=150, tf=20, h=300, tw=10, length=2000,
                label="col", lc=50,
            )
            g.mesh.sizing.set_global_size(100)
            g.mesh.generation.generate(3)
    """
    def _build():
        geo = self._parent.model.geometry
        boo = self._parent.model.boolean
        tr  = self._parent.model.transforms

        total_h = 2 * tf + h
        outer  = geo.add_rectangle(x=-bf/2, y=-total_h/2, z=0, dx=bf, dy=total_h)
        void_l = geo.add_rectangle(x=-bf/2, y=-h/2, z=0, dx=bf/2-tw/2, dy=h)
        void_r = geo.add_rectangle(x=tw/2,  y=-h/2, z=0, dx=bf/2-tw/2, dy=h)
        boo.cut(outer, [void_l, void_r], dim=2)

        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        with silent_section_slices():
            geo.slice(axis='x', offset=-tw/2)
            geo.slice(axis='x', offset=tw/2)
            geo.slice(axis='y', offset=h/2)
            geo.slice(axis='y', offset=-h/2)

        # Label volumes by structural role
        labels = self._parent.labels
        # Prefix labels with the instance label
        top_tags, bot_tags, web_tags = [], [], []
        for _, tag in gmsh.model.getEntities(3):
            com = gmsh.model.occ.getCenterOfMass(3, tag)
            if com[1] > h/2:
                top_tags.append(tag)
            elif com[1] < -h/2:
                bot_tags.append(tag)
            else:
                web_tags.append(tag)
        if top_tags:
            labels.add(3, top_tags, name=f"{label}.top_flange")
        if bot_tags:
            labels.add(3, bot_tags, name=f"{label}.bottom_flange")
        if web_tags:
            labels.add(3, web_tags, name=f"{label}.web")

        # End faces
        start_tags, end_tags = [], []
        for _, tag in gmsh.model.getEntities(2):
            try:
                com = gmsh.model.occ.getCenterOfMass(2, tag)
            except Exception:
                continue
            if abs(com[2]) < 1e-3:
                start_tags.append(tag)
            elif abs(com[2] - length) < 1e-3:
                end_tags.append(tag)
        if start_tags:
            labels.add(2, start_tags, name=f"{label}.start_face")
        if end_tags:
            labels.add(2, end_tags, name=f"{label}.end_face")

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

rect_solid

rect_solid(b: float, h: float, length: float, *, anchor='start', align='z', label: str = 'rect', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a solid rectangular bar directly in the session.

Parameters

lc : float Target element size for this section's BRep points. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def rect_solid(
    self,
    b: float,
    h: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "rect",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a solid rectangular bar directly in the session.

    Parameters
    ----------
    lc : float
        Target element size for this section's BRep points.
        Default ``1e22`` imposes no constraint.
    """
    def _build():
        tag = self._parent.model.geometry.add_box(
            -b/2, -h/2, 0, b, h, length,
        )
        self._parent.labels.add(3, [tag], name=f"{label}.body")
    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

rect_hollow

rect_hollow(b: float, h: float, t: float, length: float, *, anchor='start', align='z', label: str = 'rect_hollow', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a hollow rectangular tube (HSS) directly in the session.

Parameters

b : float Outer width (X-direction). h : float Outer height (Y-direction). t : float Wall thickness. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def rect_hollow(
    self,
    b: float,
    h: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "rect_hollow",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a hollow rectangular tube (HSS) directly in the session.

    Parameters
    ----------
    b : float
        Outer width (X-direction).
    h : float
        Outer height (Y-direction).
    t : float
        Wall thickness.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        geo = self._parent.model.geometry
        outer = geo.add_box(-b/2, -h/2, 0, b, h, length)
        inner = geo.add_box(-b/2 + t, -h/2 + t, 0, b - 2*t, h - 2*t, length)
        self._parent.model.boolean.cut(outer, [inner])
        lbl = _PrefixedLabels(self._parent.labels, label)
        for _, tag in gmsh.model.getEntities(3):
            lbl.add(3, [tag], name="body")
            break
        classify_end_faces(length, lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

pipe_solid

pipe_solid(r: float, length: float, *, anchor='start', align='z', label: str = 'pipe_solid', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a solid circular bar directly in the session.

Parameters

r : float Radius. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def pipe_solid(
    self,
    r: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "pipe_solid",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a solid circular bar directly in the session.

    Parameters
    ----------
    r : float
        Radius.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        tag = self._parent.model.geometry.add_cylinder(0, 0, 0, 0, 0, length, r)
        lbl = _PrefixedLabels(self._parent.labels, label)
        lbl.add(3, [tag], name="body")
        classify_end_faces(length, lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

pipe_hollow

pipe_hollow(r_outer: float, t: float, length: float, *, anchor='start', align='z', label: str = 'pipe_hollow', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a hollow circular pipe directly in the session.

Parameters

r_outer : float Outer radius. t : float Wall thickness. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def pipe_hollow(
    self,
    r_outer: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "pipe_hollow",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a hollow circular pipe directly in the session.

    Parameters
    ----------
    r_outer : float
        Outer radius.
    t : float
        Wall thickness.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        geo = self._parent.model.geometry
        outer = geo.add_cylinder(0, 0, 0, 0, 0, length, r_outer)
        inner = geo.add_cylinder(0, 0, 0, 0, 0, length, r_outer - t)
        self._parent.model.boolean.cut(outer, [inner])
        lbl = _PrefixedLabels(self._parent.labels, label)
        for _, tag in gmsh.model.getEntities(3):
            lbl.add(3, [tag], name="body")
            break
        classify_end_faces(length, lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

angle_solid

angle_solid(b: float, h: float, t: float, length: float, *, anchor='start', align='z', label: str = 'angle_solid', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build an L-shape (angle) directly in the session.

Parameters

b : float Horizontal leg width (X-direction). h : float Vertical leg height (Y-direction). t : float Thickness of both legs. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def angle_solid(
    self,
    b: float,
    h: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "angle_solid",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build an L-shape (angle) directly in the session.

    Parameters
    ----------
    b : float
        Horizontal leg width (X-direction).
    h : float
        Vertical leg height (Y-direction).
    t : float
        Thickness of both legs.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        geo = self._parent.model.geometry
        boo = self._parent.model.boolean
        tr  = self._parent.model.transforms

        h_leg = geo.add_rectangle(x=0, y=0, z=0, dx=b, dy=t)
        v_leg = geo.add_rectangle(x=0, y=0, z=0, dx=t, dy=h)
        boo.fuse([h_leg], [v_leg], dim=2)

        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        with silent_section_slices():
            geo.slice(axis='x', offset=t)
            geo.slice(axis='y', offset=t)

        lbl = _PrefixedLabels(self._parent.labels, label)
        h_tags, v_tags = [], []
        for _, tag in gmsh.model.getEntities(3):
            com = gmsh.model.occ.getCenterOfMass(3, tag)
            if com[1] < t:
                h_tags.append(tag)
            else:
                v_tags.append(tag)
        if h_tags:
            lbl.add(3, h_tags, name="horizontal_leg")
        if v_tags:
            lbl.add(3, v_tags, name="vertical_leg")
        classify_end_faces(length, lbl)
        classify_angle_outer_faces(lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

channel_solid

channel_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', label: str = 'channel_solid', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a C-shape (channel) directly in the session.

Parameters

bf : float Flange width. tf : float Flange thickness. h : float Clear web height (between flanges). tw : float Web thickness. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def channel_solid(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "channel_solid",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a C-shape (channel) directly in the session.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness.
    h : float
        Clear web height (between flanges).
    tw : float
        Web thickness.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        geo = self._parent.model.geometry
        boo = self._parent.model.boolean
        tr  = self._parent.model.transforms

        total_h = h + 2 * tf
        outer = geo.add_rectangle(x=0, y=-total_h/2, z=0, dx=bf, dy=total_h)
        void  = geo.add_rectangle(x=tw, y=-h/2, z=0, dx=bf - tw, dy=h)
        boo.cut(outer, [void], dim=2)

        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        with silent_section_slices():
            geo.slice(axis='x', offset=tw)
            geo.slice(axis='y', offset=h/2)
            geo.slice(axis='y', offset=-h/2)

        lbl = _PrefixedLabels(self._parent.labels, label)
        classify_w_volumes(h, tw, tf, bf, lbl)
        classify_end_faces(length, lbl)
        classify_w_outer_faces(h, tf, lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

tee_solid

tee_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', label: str = 'tee_solid', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a T-shape (tee) directly in the session.

Parameters

bf : float Flange width. tf : float Flange thickness. h : float Stem height. tw : float Stem thickness. length : float Extrusion length (Z-direction). lc : float Target element size. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def tee_solid(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "tee_solid",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a T-shape (tee) directly in the session.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness.
    h : float
        Stem height.
    tw : float
        Stem thickness.
    length : float
        Extrusion length (Z-direction).
    lc : float
        Target element size. Default ``1e22`` imposes no constraint.
    """
    def _build():
        geo = self._parent.model.geometry
        boo = self._parent.model.boolean
        tr  = self._parent.model.transforms

        flange = geo.add_rectangle(x=-bf/2, y=0, z=0, dx=bf, dy=tf)
        stem   = geo.add_rectangle(x=-tw/2, y=-h, z=0, dx=tw, dy=h)
        boo.fuse([flange], [stem], dim=2)

        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        with silent_section_slices():
            geo.slice(axis='x', offset=-tw/2)
            geo.slice(axis='x', offset=tw/2)
            geo.slice(axis='y', offset=0)

        lbl = _PrefixedLabels(self._parent.labels, label)
        flange_tags, stem_tags = [], []
        for _, tag in gmsh.model.getEntities(3):
            com = gmsh.model.occ.getCenterOfMass(3, tag)
            if com[1] >= 0:
                flange_tags.append(tag)
            else:
                stem_tags.append(tag)
        if flange_tags:
            lbl.add(3, flange_tags, name="flange")
        if stem_tags:
            lbl.add(3, stem_tags, name="stem")
        classify_end_faces(length, lbl)
        classify_tee_outer_faces(h, tf, lbl)

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

W_face

W_face(bf: float, tf: float, h: float, tw: float, *, label: str = 'W_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

W-shape cross-section face (centred on the origin).

Same profile as :meth:W_solid (bf flange width, tf flange thickness, h clear web height, tw web thickness) without the extrusion. rotate is an in-plane angle in degrees about the world origin, applied before translate.

Source code in src/apeGmsh/sections/_builder.py
def W_face(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    *,
    label: str = "W_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """W-shape cross-section face (centred on the origin).

    Same profile as :meth:`W_solid` (``bf`` flange width, ``tf``
    flange thickness, ``h`` clear web height, ``tw`` web
    thickness) without the extrusion.  ``rotate`` is an in-plane
    angle in degrees about the world origin, applied before
    ``translate``.
    """
    def _profile():
        geo = self._parent.model.geometry
        boo = self._parent.model.boolean
        total_h = 2 * tf + h
        outer  = geo.add_rectangle(
            x=-bf/2, y=-total_h/2, z=0, dx=bf, dy=total_h,
        )
        void_l = geo.add_rectangle(
            x=-bf/2, y=-h/2, z=0, dx=bf/2 - tw/2, dy=h,
        )
        void_r = geo.add_rectangle(
            x=tw/2, y=-h/2, z=0, dx=bf/2 - tw/2, dy=h,
        )
        boo.cut(outer, [void_l, void_r], dim=2)

    return self._build_face(_profile, label, translate, rotate, lc)

rect_face

rect_face(b: float, h: float, *, label: str = 'rect_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

Solid rectangular cross-section face (centred on the origin).

Source code in src/apeGmsh/sections/_builder.py
def rect_face(
    self,
    b: float,
    h: float,
    *,
    label: str = "rect_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """Solid rectangular cross-section face (centred on the origin)."""
    def _profile():
        self._parent.model.geometry.add_rectangle(
            x=-b/2, y=-h/2, z=0, dx=b, dy=h,
        )

    return self._build_face(_profile, label, translate, rotate, lc)

rect_hollow_face

rect_hollow_face(b: float, h: float, t: float, *, label: str = 'rect_hollow_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

Hollow rectangular (HSS) cross-section face: outer b × h, wall thickness t, centred on the origin.

Source code in src/apeGmsh/sections/_builder.py
def rect_hollow_face(
    self,
    b: float,
    h: float,
    t: float,
    *,
    label: str = "rect_hollow_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """Hollow rectangular (HSS) cross-section face: outer ``b × h``,
    wall thickness ``t``, centred on the origin."""
    def _profile():
        geo = self._parent.model.geometry
        outer = geo.add_rectangle(x=-b/2, y=-h/2, z=0, dx=b, dy=h)
        inner = geo.add_rectangle(
            x=-b/2 + t, y=-h/2 + t, z=0, dx=b - 2*t, dy=h - 2*t,
        )
        self._parent.model.boolean.cut(outer, [inner], dim=2)

    return self._build_face(_profile, label, translate, rotate, lc)

pipe_face

pipe_face(r: float, *, label: str = 'pipe_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

Solid circular cross-section face of radius r at the origin.

Source code in src/apeGmsh/sections/_builder.py
def pipe_face(
    self,
    r: float,
    *,
    label: str = "pipe_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """Solid circular cross-section face of radius ``r`` at the
    origin."""
    def _profile():
        geo = self._parent.model.geometry
        c = geo.add_circle(0.0, 0.0, 0.0, r)
        loop = geo.add_curve_loop([c])
        geo.add_plane_surface([loop])

    return self._build_face(_profile, label, translate, rotate, lc)

pipe_hollow_face

pipe_hollow_face(r: float, t: float, *, label: str = 'pipe_hollow_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

Hollow circular (pipe) cross-section face: outer radius r, wall thickness t, centred on the origin.

Source code in src/apeGmsh/sections/_builder.py
def pipe_hollow_face(
    self,
    r: float,
    t: float,
    *,
    label: str = "pipe_hollow_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """Hollow circular (pipe) cross-section face: outer radius
    ``r``, wall thickness ``t``, centred on the origin."""
    def _profile():
        geo = self._parent.model.geometry
        outer = geo.add_curve_loop(
            [geo.add_circle(0.0, 0.0, 0.0, r)]
        )
        inner = geo.add_curve_loop(
            [geo.add_circle(0.0, 0.0, 0.0, r - t)]
        )
        geo.add_plane_surface([outer, inner])

    return self._build_face(_profile, label, translate, rotate, lc)

angle_face

angle_face(b: float, h: float, t: float, *, label: str = 'angle_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

L-shape (angle) cross-section face — heel at the origin, horizontal leg b and vertical leg h of thickness t (same profile as :meth:angle_solid).

Source code in src/apeGmsh/sections/_builder.py
def angle_face(
    self,
    b: float,
    h: float,
    t: float,
    *,
    label: str = "angle_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """L-shape (angle) cross-section face — heel at the origin,
    horizontal leg ``b`` and vertical leg ``h`` of thickness ``t``
    (same profile as :meth:`angle_solid`)."""
    def _profile():
        geo = self._parent.model.geometry
        h_leg = geo.add_rectangle(x=0, y=0, z=0, dx=b, dy=t)
        v_leg = geo.add_rectangle(x=0, y=0, z=0, dx=t, dy=h)
        self._parent.model.boolean.fuse([h_leg], [v_leg], dim=2)

    return self._build_face(_profile, label, translate, rotate, lc)

channel_face

channel_face(bf: float, tf: float, h: float, tw: float, *, label: str = 'channel_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

C-shape (channel) cross-section face — web on the y-axis, opening toward +x (same profile as :meth:channel_solid).

Source code in src/apeGmsh/sections/_builder.py
def channel_face(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    *,
    label: str = "channel_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """C-shape (channel) cross-section face — web on the y-axis,
    opening toward +x (same profile as :meth:`channel_solid`)."""
    def _profile():
        geo = self._parent.model.geometry
        total_h = h + 2 * tf
        outer = geo.add_rectangle(
            x=0, y=-total_h/2, z=0, dx=bf, dy=total_h,
        )
        void = geo.add_rectangle(
            x=tw, y=-h/2, z=0, dx=bf - tw, dy=h,
        )
        self._parent.model.boolean.cut(outer, [void], dim=2)

    return self._build_face(_profile, label, translate, rotate, lc)

tee_face

tee_face(bf: float, tf: float, h: float, tw: float, *, label: str = 'tee_face', lc: float = 1e+22, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> 'Instance'

T-shape (tee) cross-section face — flange on top (y in [0, tf]), stem hanging below (same profile as :meth:tee_solid).

Source code in src/apeGmsh/sections/_builder.py
def tee_face(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    *,
    label: str = "tee_face",
    lc: float = 1e22,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> "Instance":
    """T-shape (tee) cross-section face — flange on top (y in
    ``[0, tf]``), stem hanging below (same profile as
    :meth:`tee_solid`)."""
    def _profile():
        geo = self._parent.model.geometry
        flange = geo.add_rectangle(x=-bf/2, y=0, z=0, dx=bf, dy=tf)
        stem   = geo.add_rectangle(x=-tw/2, y=-h, z=0, dx=tw, dy=h)
        self._parent.model.boolean.fuse([flange], [stem], dim=2)

    return self._build_face(_profile, label, translate, rotate, lc)

plot_faces

plot_faces(*, ax=None, annotate: bool = True)

Preview the session's flat-face geometry before meshing.

Draws the boundary polylines of every dim-2 entity in the current (synchronized) model and, when annotate=True, marks each face's centroid with the plain physical-group name covering it (the *_face builders' auto-PG) — falling back to the entity tag. Matplotlib only; returns the Axes.

Source code in src/apeGmsh/sections/_builder.py
def plot_faces(self, *, ax=None, annotate: bool = True):
    """Preview the session's flat-face geometry **before meshing**.

    Draws the boundary polylines of every dim-2 entity in the
    current (synchronized) model and, when ``annotate=True``, marks
    each face's centroid with the plain physical-group name
    covering it (the ``*_face`` builders' auto-PG) — falling back
    to the entity tag.  Matplotlib only; returns the ``Axes``.
    """
    from apeGmsh.core._compose_errors import raise_if_no_live_kernel
    raise_if_no_live_kernel(
        self._parent, "g.sections.plot_faces()",
        alternative=(
            "BRep geometry is not stored in model.h5, so there is "
            "nothing to preview — run this in the session that "
            "still owns the geometry, before saving"
        ),
    )
    import matplotlib.pyplot as plt
    import numpy as np

    if ax is None:
        _, ax = plt.subplots()

    # plain (non-label) PG name per dim-2 entity tag
    pg_of: dict[int, str] = {}
    for dim, pg_tag in gmsh.model.getPhysicalGroups(2):
        name = gmsh.model.getPhysicalName(dim, pg_tag)
        if name.startswith("_label:"):
            continue
        for t in gmsh.model.getEntitiesForPhysicalGroup(dim, pg_tag):
            pg_of[int(t)] = name

    for _, face in gmsh.model.getEntities(2):
        for cdim, ctag in gmsh.model.getBoundary(
            [(2, face)], oriented=False, recursive=False,
        ):
            if cdim != 1:
                continue
            lo, hi = gmsh.model.getParametrizationBounds(1, abs(ctag))
            ts = np.linspace(float(lo[0]), float(hi[0]), 48)
            pts = np.array([
                gmsh.model.getValue(1, abs(ctag), [t]) for t in ts
            ])
            ax.plot(pts[:, 0], pts[:, 1], "-", color="0.2",
                    linewidth=1.0)
        if annotate:
            com = gmsh.model.occ.getCenterOfMass(2, face)
            ax.annotate(
                pg_of.get(int(face), f"face {face}"),
                (com[0], com[1]),
                ha="center", va="center", fontsize=8, color="tab:blue",
            )
    ax.set_aspect("equal")
    ax.set_title("section faces (geometry preview)")
    return ax

W_shell

W_shell(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', label: str = 'W_shell', lc: float = 1e+22, translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, ...] | None = None) -> 'Instance'

Build a W-shape as 3 mid-surface shell rectangles.

Parameters

lc : float Target element size for this section's BRep points. Default 1e22 imposes no constraint.

Source code in src/apeGmsh/sections/_builder.py
def W_shell(
    self,
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    label: str = "W_shell",
    lc: float = 1e22,
    translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
    rotate: tuple[float, ...] | None = None,
) -> "Instance":
    """Build a W-shape as 3 mid-surface shell rectangles.

    Parameters
    ----------
    lc : float
        Target element size for this section's BRep points.
        Default ``1e22`` imposes no constraint.
    """
    def _build():
        from apeGmsh.sections.shell import _build_rect_surface
        geo = self._parent.model.geometry
        y_top = h/2 + tf/2
        y_bot = -(h/2 + tf/2)

        _build_rect_surface(
            geo,
            -bf/2, y_top, 0,  bf/2, y_top, 0,
            bf/2, y_top, length,  -bf/2, y_top, length,
            label=f"{label}.top_flange",
        )
        _build_rect_surface(
            geo,
            -bf/2, y_bot, 0,  bf/2, y_bot, 0,
            bf/2, y_bot, length,  -bf/2, y_bot, length,
            label=f"{label}.bottom_flange",
        )
        _build_rect_surface(
            geo,
            0, -h/2, 0,  0, h/2, 0,
            0, h/2, length,  0, -h/2, length,
            label=f"{label}.web",
        )
        self._parent.model.sync()

    return self._build_section(
        _build, label, translate, rotate, lc=lc,
        anchor=anchor, align=align, length=length,
    )

Solid sections

3D volumes suitable for solid elements (dim=3).

apeGmsh.sections.solid.W_solid

W_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', name: str = 'W_solid') -> Part

Create a W-shape (wide flange) as a 3D solid Part.

The section is built as an extruded I-profile, then sliced into 7 hex-compatible volumes by 4 axis-aligned cuts.

Parameters

bf : float Flange width. tf : float Flange thickness. h : float Clear web height (between flanges, not including flanges). tw : float Web thickness. length : float Extrusion length along Z. anchor : str or (x, y, z), default "start" Re-origin the section in its local frame before optional align. See :func:apeGmsh.core._section_placement.compute_anchor_offset. align : str or (ax, ay, az), default "z" Reorient the local +Z axis to a world direction. See :func:apeGmsh.core._section_placement.compute_alignment_rotation. name : str, default "W_solid" Part name.

Labels created

top_flange 3 volumes forming the top flange (y > h/2). bottom_flange 3 volumes forming the bottom flange (y < −h/2). web 1 volume for the web (|y| ≤ h/2). top_flange_face Outer +y skin surfaces of the top flange (face-to-face stacking target for align_to). bottom_flange_face Outer −y skin surfaces of the bottom flange. web_left_face, web_right_face Exposed −x / +x outer faces of the web (within |y| ≤ h/2). start_face, end_face Cross-section profile faces at z=0 and z=length.

Returns

Part

Example

::

col = W_solid(bf=150, tf=20, h=300, tw=10, length=3000)

with apeGmsh("frame") as g:
    g.parts.add(col, label="col_A")
    g.mesh.structured.set_transfinite_automatic()
    g.mesh.sizing.set_global_size(50)
    g.mesh.generation.generate(3)
Source code in src/apeGmsh/sections/solid.py
def W_solid(
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "W_solid",
) -> Part:
    """Create a W-shape (wide flange) as a 3D solid Part.

    The section is built as an extruded I-profile, then sliced
    into **7 hex-compatible volumes** by 4 axis-aligned cuts.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness.
    h : float
        Clear web height (between flanges, not including flanges).
    tw : float
        Web thickness.
    length : float
        Extrusion length along Z.
    anchor : str or (x, y, z), default ``"start"``
        Re-origin the section in its local frame before optional align.
        See :func:`apeGmsh.core._section_placement.compute_anchor_offset`.
    align : str or (ax, ay, az), default ``"z"``
        Reorient the local +Z axis to a world direction.
        See :func:`apeGmsh.core._section_placement.compute_alignment_rotation`.
    name : str, default "W_solid"
        Part name.

    Labels created
    --------------
    ``top_flange``
        3 volumes forming the top flange (y > h/2).
    ``bottom_flange``
        3 volumes forming the bottom flange (y < −h/2).
    ``web``
        1 volume for the web (|y| ≤ h/2).
    ``top_flange_face``
        Outer +y skin surfaces of the top flange (face-to-face
        stacking target for ``align_to``).
    ``bottom_flange_face``
        Outer −y skin surfaces of the bottom flange.
    ``web_left_face``, ``web_right_face``
        Exposed −x / +x outer faces of the web (within |y| ≤ h/2).
    ``start_face``, ``end_face``
        Cross-section profile faces at z=0 and z=length.

    Returns
    -------
    Part

    Example
    -------
    ::

        col = W_solid(bf=150, tf=20, h=300, tw=10, length=3000)

        with apeGmsh("frame") as g:
            g.parts.add(col, label="col_A")
            g.mesh.structured.set_transfinite_automatic()
            g.mesh.sizing.set_global_size(50)
            g.mesh.generation.generate(3)
    """
    from ._classify import classify_w_volumes

    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean
        tr  = part.model.transforms

        # Profile: outer rectangle minus two voids
        total_h = 2 * tf + h
        outer  = geo.add_rectangle(x=-bf / 2, y=-total_h / 2, z=0, dx=bf, dy=total_h)
        void_l = geo.add_rectangle(x=-bf / 2, y=-h / 2, z=0, dx=bf / 2 - tw / 2, dy=h)
        void_r = geo.add_rectangle(x=tw / 2,  y=-h / 2, z=0, dx=bf / 2 - tw / 2, dy=h)
        profile = boo.cut(outer, [void_l, void_r], dim=2)

        # Extrude along Z
        tr.extrude(profile, 0, 0, length)

        # Slice into 7 hex-compatible regions
        with silent_section_slices():
            geo.slice(axis='x', offset=-tw / 2)
            geo.slice(axis='x', offset=tw / 2)
            geo.slice(axis='y', offset=h / 2)
            geo.slice(axis='y', offset=-h / 2)

        # Label by structural role
        classify_w_volumes(h, tw, tf, bf, part.labels)

        # Label end-cap surfaces for BC / load application
        classify_end_faces(length, part.labels)

        # Label outer +y / -y skin surfaces for face-to-face stacking
        classify_w_outer_faces(h, tf, part.labels)
        classify_w_web_side_faces(h, tw, part.labels)

        # Apply anchor + align AFTER classification so the z=0/length
        # end-face heuristic still matches the as-extruded geometry.
        apply_placement(anchor, align, length=length)

    return part

apeGmsh.sections.solid.rect_solid

rect_solid(b: float, h: float, length: float, *, anchor='start', align='z', name: str = 'rect_solid') -> Part

Create a solid rectangular bar as a 3D Part.

Parameters

b : float Width (X-direction). h : float Height (Y-direction). length : float Length (Z-direction). name : str Part name.

Labels created

body — the single volume.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def rect_solid(
    b: float,
    h: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "rect_solid",
) -> Part:
    """Create a solid rectangular bar as a 3D Part.

    Parameters
    ----------
    b : float
        Width (X-direction).
    h : float
        Height (Y-direction).
    length : float
        Length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``body`` — the single volume.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        part.model.geometry.add_box(-b / 2, -h / 2, 0, b, h, length, label="body")
        classify_end_faces(length, part.labels)
        apply_placement(anchor, align, length=length)
    return part

apeGmsh.sections.solid.rect_hollow

rect_hollow(b: float, h: float, t: float, length: float, *, anchor='start', align='z', name: str = 'rect_hollow') -> Part

Create a hollow rectangular tube (HSS) as a 3D solid Part.

Parameters

b : float Outer width (X-direction). h : float Outer height (Y-direction). t : float Wall thickness. length : float Length (Z-direction). name : str Part name.

Labels created

body — the hollow tube volume.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def rect_hollow(
    b: float,
    h: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "rect_hollow",
) -> Part:
    """Create a hollow rectangular tube (HSS) as a 3D solid Part.

    Parameters
    ----------
    b : float
        Outer width (X-direction).
    h : float
        Outer height (Y-direction).
    t : float
        Wall thickness.
    length : float
        Length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``body`` — the hollow tube volume.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean

        outer = geo.add_box(-b / 2, -h / 2, 0, b, h, length)
        inner = geo.add_box(-b / 2 + t, -h / 2 + t, 0,
                            b - 2 * t, h - 2 * t, length)
        boo.cut(outer, [inner])

        # Label the surviving volume
        for _, tag in gmsh.model.getEntities(3):
            part.labels.add(3, [tag], name="body")
            break
        classify_end_faces(length, part.labels)
        apply_placement(anchor, align, length=length)

    return part

apeGmsh.sections.solid.pipe_solid

pipe_solid(r: float, length: float, *, anchor='start', align='z', name: str = 'pipe_solid') -> Part

Create a solid circular bar as a 3D Part.

Parameters

r : float Radius. length : float Length (Z-direction). name : str Part name.

Labels created

body — the single cylinder volume.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def pipe_solid(
    r: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "pipe_solid",
) -> Part:
    """Create a solid circular bar as a 3D Part.

    Parameters
    ----------
    r : float
        Radius.
    length : float
        Length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``body`` — the single cylinder volume.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        part.model.geometry.add_cylinder(0, 0, 0, 0, 0, length, r, label="body")
        classify_end_faces(length, part.labels)
        apply_placement(anchor, align, length=length)
    return part

apeGmsh.sections.solid.pipe_hollow

pipe_hollow(r_outer: float, t: float, length: float, *, anchor='start', align='z', name: str = 'pipe_hollow') -> Part

Create a hollow circular pipe as a 3D solid Part.

Parameters

r_outer : float Outer radius. t : float Wall thickness. length : float Length (Z-direction). name : str Part name.

Labels created

body — the hollow pipe volume.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def pipe_hollow(
    r_outer: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "pipe_hollow",
) -> Part:
    """Create a hollow circular pipe as a 3D solid Part.

    Parameters
    ----------
    r_outer : float
        Outer radius.
    t : float
        Wall thickness.
    length : float
        Length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``body`` — the hollow pipe volume.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean

        outer = geo.add_cylinder(0, 0, 0, 0, 0, length, r_outer)
        inner = geo.add_cylinder(0, 0, 0, 0, 0, length, r_outer - t)
        boo.cut(outer, [inner])

        for _, tag in gmsh.model.getEntities(3):
            part.labels.add(3, [tag], name="body")
            break
        classify_end_faces(length, part.labels)
        apply_placement(anchor, align, length=length)

    return part

apeGmsh.sections.solid.angle_solid

angle_solid(b: float, h: float, t: float, length: float, *, anchor='start', align='z', name: str = 'angle_solid') -> Part

Create an L-shape (angle) as a 3D solid Part.

The angle is placed with its corner at the origin, legs extending in +X and +Y. Sliced at the corner junction for hex-compatible meshing.

Parameters

b : float Horizontal leg width (X-direction). h : float Vertical leg height (Y-direction). t : float Thickness of both legs. length : float Extrusion length (Z-direction). name : str Part name.

Labels created

horizontal_leg — volumes in the horizontal leg (y < t). vertical_leg — volumes in the vertical leg (x < t). horizontal_leg_face — underside of h-leg at y=0. vertical_leg_face — back of v-leg at x=0. start_face, end_face — profile faces at z=0 and z=length.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def angle_solid(
    b: float,
    h: float,
    t: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "angle_solid",
) -> Part:
    """Create an L-shape (angle) as a 3D solid Part.

    The angle is placed with its corner at the origin, legs
    extending in +X and +Y.  Sliced at the corner junction
    for hex-compatible meshing.

    Parameters
    ----------
    b : float
        Horizontal leg width (X-direction).
    h : float
        Vertical leg height (Y-direction).
    t : float
        Thickness of both legs.
    length : float
        Extrusion length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``horizontal_leg`` — volumes in the horizontal leg (y < t).
    ``vertical_leg`` — volumes in the vertical leg (x < t).
    ``horizontal_leg_face`` — underside of h-leg at y=0.
    ``vertical_leg_face`` — back of v-leg at x=0.
    ``start_face``, ``end_face`` — profile faces at z=0 and z=length.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean
        tr  = part.model.transforms

        # Build L-profile as two rectangles fused
        h_leg = geo.add_rectangle(x=0, y=0, z=0, dx=b, dy=t)
        v_leg = geo.add_rectangle(x=0, y=0, z=0, dx=t, dy=h)
        boo.fuse([h_leg], [v_leg], dim=2)

        # Extrude
        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        # Slice at the corner junction
        with silent_section_slices():
            geo.slice(axis='x', offset=t)
            geo.slice(axis='y', offset=t)

        # Label by structural role
        h_tags = []
        v_tags = []
        for _, tag in gmsh.model.getEntities(3):
            com = gmsh.model.occ.getCenterOfMass(3, tag)
            if com[1] < t:
                h_tags.append(tag)
            else:
                v_tags.append(tag)
        if h_tags:
            part.labels.add(3, h_tags, name="horizontal_leg")
        if v_tags:
            part.labels.add(3, v_tags, name="vertical_leg")
        classify_end_faces(length, part.labels)
        classify_angle_outer_faces(part.labels)
        apply_placement(anchor, align, length=length)

    return part

apeGmsh.sections.solid.channel_solid

channel_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', name: str = 'channel_solid') -> Part

Create a C-shape (channel) as a 3D solid Part.

The channel opens in the +X direction. The web is at x=0, flanges extend in the +X direction from the web.

Parameters

bf : float Flange width (depth of flanges in X). tf : float Flange thickness. h : float Clear web height (between flanges). tw : float Web thickness. length : float Extrusion length (Z-direction). name : str Part name.

Labels created

top_flange — volumes in the top flange (y > h/2). bottom_flange — volumes in the bottom flange (y < −h/2). web — volumes in the web. top_flange_face — outer +y skin of top flange. bottom_flange_face — outer −y skin of bottom flange. start_face, end_face — profile faces at z=0 and z=length.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def channel_solid(
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "channel_solid",
) -> Part:
    """Create a C-shape (channel) as a 3D solid Part.

    The channel opens in the +X direction.  The web is at x=0,
    flanges extend in the +X direction from the web.

    Parameters
    ----------
    bf : float
        Flange width (depth of flanges in X).
    tf : float
        Flange thickness.
    h : float
        Clear web height (between flanges).
    tw : float
        Web thickness.
    length : float
        Extrusion length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``top_flange`` — volumes in the top flange (y > h/2).
    ``bottom_flange`` — volumes in the bottom flange (y < −h/2).
    ``web`` — volumes in the web.
    ``top_flange_face`` — outer +y skin of top flange.
    ``bottom_flange_face`` — outer −y skin of bottom flange.
    ``start_face``, ``end_face`` — profile faces at z=0 and z=length.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean
        tr  = part.model.transforms

        total_h = h + 2 * tf

        # C-profile: outer rectangle minus one void (the open side)
        outer = geo.add_rectangle(x=0, y=-total_h / 2, z=0, dx=bf, dy=total_h)
        void  = geo.add_rectangle(x=tw, y=-h / 2, z=0, dx=bf - tw, dy=h)
        boo.cut(outer, [void], dim=2)

        # Extrude
        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        # Slice for hex readiness
        with silent_section_slices():
            geo.slice(axis='x', offset=tw)
            geo.slice(axis='y', offset=h / 2)
            geo.slice(axis='y', offset=-h / 2)

        # Label by role
        from ._classify import classify_w_volumes
        classify_w_volumes(h, tw, tf, bf, part.labels)
        classify_end_faces(length, part.labels)
        classify_w_outer_faces(h, tf, part.labels)
        apply_placement(anchor, align, length=length)

    return part

apeGmsh.sections.solid.tee_solid

tee_solid(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', name: str = 'tee_solid') -> Part

Create a T-shape (tee / WT) as a 3D solid Part.

The flange is at the top (+Y), the stem hangs down. Centered on the web at x=0.

Parameters

bf : float Flange width. tf : float Flange thickness. h : float Stem height (from bottom of flange to bottom of stem). tw : float Stem (web) thickness. length : float Extrusion length (Z-direction). name : str Part name.

Labels created

flange — volumes in the flange. stem — volumes in the stem. flange_face — outer +y skin of the flange (top). stem_face — outer −y skin of the stem (bottom). start_face, end_face — profile faces at z=0 and z=length.

Returns

Part

Source code in src/apeGmsh/sections/solid.py
def tee_solid(
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "tee_solid",
) -> Part:
    """Create a T-shape (tee / WT) as a 3D solid Part.

    The flange is at the top (+Y), the stem hangs down.  Centered
    on the web at x=0.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness.
    h : float
        Stem height (from bottom of flange to bottom of stem).
    tw : float
        Stem (web) thickness.
    length : float
        Extrusion length (Z-direction).
    name : str
        Part name.

    Labels created
    --------------
    ``flange`` — volumes in the flange.
    ``stem`` — volumes in the stem.
    ``flange_face`` — outer +y skin of the flange (top).
    ``stem_face`` — outer −y skin of the stem (bottom).
    ``start_face``, ``end_face`` — profile faces at z=0 and z=length.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean
        tr  = part.model.transforms

        # T-profile: flange rectangle + stem rectangle, fused
        flange = geo.add_rectangle(x=-bf / 2, y=0, z=0, dx=bf, dy=tf)
        stem   = geo.add_rectangle(x=-tw / 2, y=-h, z=0, dx=tw, dy=h)
        boo.fuse([flange], [stem], dim=2)

        surfs = gmsh.model.getEntities(2)
        if surfs:
            tr.extrude(surfs[0], 0, 0, length)

        # Slice at the flange-stem junction
        with silent_section_slices():
            geo.slice(axis='x', offset=-tw / 2)
            geo.slice(axis='x', offset=tw / 2)
            geo.slice(axis='y', offset=0)

        # Label by role
        flange_tags = []
        stem_tags = []
        for _, tag in gmsh.model.getEntities(3):
            com = gmsh.model.occ.getCenterOfMass(3, tag)
            if com[1] >= 0:
                flange_tags.append(tag)
            else:
                stem_tags.append(tag)
        if flange_tags:
            part.labels.add(3, flange_tags, name="flange")
        if stem_tags:
            part.labels.add(3, stem_tags, name="stem")
        classify_end_faces(length, part.labels)
        classify_tee_outer_faces(h, tf, part.labels)
        apply_placement(anchor, align, length=length)

    return part

Shell sections

Mid-surface geometry for shell elements (dim=2).

apeGmsh.sections.shell.W_shell

W_shell(bf: float, tf: float, h: float, tw: float, length: float, *, anchor='start', align='z', name: str = 'W_shell') -> Part

Create a W-shape as 3 mid-surface shell rectangles.

The I-section is represented by:

  • top flange — horizontal rectangle at y = h/2 + tf/2 (flange mid-plane), width bf, length length.
  • bottom flange — horizontal rectangle at y = -(h/2 + tf/2).
  • web — vertical rectangle at x = 0, height h, length length.

Parameters

bf : float Flange width. tf : float Flange thickness (positions the mid-surface; the shell element's section definition carries the actual thickness). h : float Clear web height (between flange mid-surfaces). tw : float Web thickness (informational — the mid-surface is at x=0). length : float Extrusion length along Z. name : str Part name.

Labels created

top_flange The top flange mid-surface. bottom_flange The bottom flange mid-surface. web The web mid-surface.

Returns

Part

Source code in src/apeGmsh/sections/shell.py
def W_shell(
    bf: float,
    tf: float,
    h: float,
    tw: float,
    length: float,
    *,
    anchor="start",
    align="z",
    name: str = "W_shell",
) -> Part:
    """Create a W-shape as 3 mid-surface shell rectangles.

    The I-section is represented by:

    * **top flange** — horizontal rectangle at ``y = h/2 + tf/2``
      (flange mid-plane), width ``bf``, length ``length``.
    * **bottom flange** — horizontal rectangle at ``y = -(h/2 + tf/2)``.
    * **web** — vertical rectangle at ``x = 0``, height ``h``,
      length ``length``.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness (positions the mid-surface; the shell
        element's section definition carries the actual thickness).
    h : float
        Clear web height (between flange mid-surfaces).
    tw : float
        Web thickness (informational — the mid-surface is at x=0).
    length : float
        Extrusion length along Z.
    name : str
        Part name.

    Labels created
    --------------
    ``top_flange``
        The top flange mid-surface.
    ``bottom_flange``
        The bottom flange mid-surface.
    ``web``
        The web mid-surface.

    Returns
    -------
    Part
    """
    part = Part(name)
    with part:
        geo = part.model.geometry

        y_top = h / 2 + tf / 2
        y_bot = -(h / 2 + tf / 2)

        # Top flange: rectangle in XZ plane at y = y_top
        _build_rect_surface(
            geo,
            -bf / 2, y_top, 0,
            bf / 2,  y_top, 0,
            bf / 2,  y_top, length,
            -bf / 2, y_top, length,
            label="top_flange",
        )

        # Bottom flange: rectangle in XZ plane at y = y_bot
        _build_rect_surface(
            geo,
            -bf / 2, y_bot, 0,
            bf / 2,  y_bot, 0,
            bf / 2,  y_bot, length,
            -bf / 2, y_bot, length,
            label="bottom_flange",
        )

        # Web: rectangle in YZ plane at x = 0
        _build_rect_surface(
            geo,
            0, -h / 2, 0,
            0,  h / 2, 0,
            0,  h / 2, length,
            0, -h / 2, length,
            label="web",
        )

        part.model.sync()
        apply_placement(anchor, align, length=length)

    return part

Profile sections

2D cross-sections for fiber analysis or sweep operations.

apeGmsh.sections.profile.W_profile

W_profile(bf: float, tf: float, h: float, tw: float, *, anchor='start', align='z', name: str = 'W_profile') -> Part

Create a W-shape 2D cross-section (no extrusion).

The I-shaped surface sits in the XY plane at z=0, centered on the origin.

Parameters

bf : float Flange width. tf : float Flange thickness. h : float Clear web height. tw : float Web thickness. name : str Part name.

Labels created

profile — the I-shaped surface.

Returns

Part

Example

::

section = W_profile(bf=150, tf=20, h=300, tw=10)
# section.has_file -> True (auto-persisted)
# Use for fiber analysis or sweep along a path
Source code in src/apeGmsh/sections/profile.py
def W_profile(
    bf: float,
    tf: float,
    h: float,
    tw: float,
    *,
    anchor="start",
    align="z",
    name: str = "W_profile",
) -> Part:
    """Create a W-shape 2D cross-section (no extrusion).

    The I-shaped surface sits in the XY plane at z=0, centered
    on the origin.

    Parameters
    ----------
    bf : float
        Flange width.
    tf : float
        Flange thickness.
    h : float
        Clear web height.
    tw : float
        Web thickness.
    name : str
        Part name.

    Labels created
    --------------
    ``profile`` — the I-shaped surface.

    Returns
    -------
    Part

    Example
    -------
    ::

        section = W_profile(bf=150, tf=20, h=300, tw=10)
        # section.has_file -> True (auto-persisted)
        # Use for fiber analysis or sweep along a path
    """
    part = Part(name)
    with part:
        geo = part.model.geometry
        boo = part.model.boolean

        total_h = 2 * tf + h
        outer  = geo.add_rectangle(x=-bf / 2, y=-total_h / 2, z=0, dx=bf, dy=total_h)
        void_l = geo.add_rectangle(x=-bf / 2, y=-h / 2, z=0, dx=bf / 2 - tw / 2, dy=h)
        void_r = geo.add_rectangle(x=tw / 2,  y=-h / 2, z=0, dx=bf / 2 - tw / 2, dy=h)
        boo.cut(outer, [void_l, void_r], dim=2)

        # Label the surviving surface
        import gmsh
        for _, tag in gmsh.model.getEntities(2):
            part.labels.add(2, [tag], name="profile")
            break

        # Profile has no extrusion length; only "start" and tuple
        # anchors apply.  Pass length=None — named modes other than
        # "start" raise (consistent with helper contract).
        apply_placement(anchor, align, length=None)

    return part

Section documents

Declarative, versioned JSON descriptions of a cross-section — the source of truth the Qt builder edits. See Author a section document.

apeGmsh.sections._document.SectionDocument

SectionDocument(data: dict[str, Any])

Declarative section description (continuum lane, ADR 0080 B1).

Construct blank via :meth:new, load via :meth:open, mutate via the add_* / set_* methods (the same surface the builder GUI drives), persist via :meth:save, and realize via :meth:build — which runs a private apeGmsh session (builders → booleans → mesh) and returns a :class:~apeGmsh.sections.SectionProperties.

Source code in src/apeGmsh/sections/_document.py
def __init__(self, data: dict[str, Any]) -> None:
    _validate(data)
    self._data = data

new classmethod

new(*, name: str | None = None, kind: Literal['continuum', 'fiber'] = 'continuum', units: str = '') -> 'SectionDocument'

A blank document. units is a display label only — apeGmsh stays unit-agnostic.

Source code in src/apeGmsh/sections/_document.py
@classmethod
def new(
    cls,
    *,
    name: str | None = None,
    kind: Literal["continuum", "fiber"] = "continuum",
    units: str = "",
) -> "SectionDocument":
    """A blank document. ``units`` is a display label only —
    apeGmsh stays unit-agnostic."""
    data: dict[str, Any] = {
        "section_doc_version": SECTION_DOC_VERSION,
        "kind": kind,
        "name": name,
        "notes": "",
        "units": units,
        "materials": {},
    }
    if kind == "fiber":
        data |= {
            "patches": [], "layers": [], "points": [],
            "templates": [], "GJ": None,
        }
    else:
        data |= {
            "shapes": [], "booleans": [], "bars": [],
            "mesh": {"lc": None, "order": 2},
            "disconnected": "raise",
        }
    return cls(data)

open classmethod

open(path: str | Path) -> 'SectionDocument'

Load a .section.json document (version-window checked).

Source code in src/apeGmsh/sections/_document.py
@classmethod
def open(cls, path: str | Path) -> "SectionDocument":
    """Load a ``.section.json`` document (version-window checked)."""
    try:
        data = json.loads(Path(path).read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        raise SectionDocumentError(
            f"SectionDocument.open: cannot read {path!s}: {e}"
        ) from e
    return cls(data)

export_script

export_script(path: 'str | Path | None' = None) -> str

Render the document as a readable, runnable apeGmsh script (ADR 0080 B4) and return the text; write it to path when given. One-way export — round-trip editing is the JSON document's job. Continuum scripts bind the analyzer to sec; fiber scripts define build_section(ops).

Source code in src/apeGmsh/sections/_document.py
def export_script(self, path: "str | Path | None" = None) -> str:
    """Render the document as a readable, runnable apeGmsh script
    (ADR 0080 B4) and return the text; write it to ``path`` when
    given. One-way export — round-trip editing is the JSON
    document's job. Continuum scripts bind the analyzer to
    ``sec``; fiber scripts define ``build_section(ops)``."""
    from ._script_export import render_script

    text = render_script(self)
    if path is not None:
        Path(path).write_text(text, encoding="utf-8")
    return text

save

save(path: str | Path) -> None

Write the document as deterministic, diff-friendly JSON (strict spec - non-finite floats refuse rather than emitting non-portable NaN/Infinity tokens).

Source code in src/apeGmsh/sections/_document.py
def save(self, path: str | Path) -> None:
    """Write the document as deterministic, diff-friendly JSON
    (strict spec - non-finite floats refuse rather than emitting
    non-portable ``NaN``/``Infinity`` tokens)."""
    try:
        text = json.dumps(
            self._data, indent=2, sort_keys=False, allow_nan=False,
        )
    except ValueError as e:
        raise SectionDocumentError(
            f"document contains non-finite numbers and cannot be "
            f"saved as portable JSON: {e}"
        ) from e
    Path(path).write_text(text + "\n", encoding="utf-8")

to_dict

to_dict() -> dict[str, Any]

Deep copy of the underlying document dict.

Source code in src/apeGmsh/sections/_document.py
def to_dict(self) -> dict[str, Any]:
    """Deep copy of the underlying document dict."""
    return json.loads(json.dumps(self._data))

set_material

set_material(name: str, *, E: float | None = None, nu: float | None = None, G: float | None = None, fy: float | None = None, density: float | None = None, uniaxial: 'tuple[str, dict[str, Any]] | None' = None) -> None

Define (or redefine) a named material. Dual-role: the continuum build needs E+nu on used materials; the fiber handoff needs uniaxial=("<Type>", {kwargs}) — resolved as ops.uniaxialMaterial.<Type>(**kwargs). Give either role or both; continuum-parameter validation defers to :class:SectionMaterial at build so the rules stay single.

Source code in src/apeGmsh/sections/_document.py
def set_material(
    self,
    name: str,
    *,
    E: float | None = None,
    nu: float | None = None,
    G: float | None = None,
    fy: float | None = None,
    density: float | None = None,
    uniaxial: "tuple[str, dict[str, Any]] | None" = None,
) -> None:
    """Define (or redefine) a named material. Dual-role: the
    continuum build needs ``E``+``nu`` on used materials; the
    fiber handoff needs ``uniaxial=("<Type>", {kwargs})`` —
    resolved as ``ops.uniaxialMaterial.<Type>(**kwargs)``. Give
    either role or both; continuum-parameter validation defers to
    :class:`SectionMaterial` at build so the rules stay single."""
    if E is None and uniaxial is None:
        raise SectionDocumentError(
            f"material {name!r}: give the continuum role "
            f"(E=, nu=) and/or the fiber role (uniaxial=)."
        )
    if (E is None) != (nu is None):
        raise SectionDocumentError(
            f"material {name!r}: E and nu come together."
        )
    entry: dict[str, Any] = {
        "E": E, "nu": nu, "G": G, "fy": fy, "density": density,
    }
    if uniaxial is not None:
        u_type, u_params = uniaxial
        entry["uniaxial"] = {
            "type": str(u_type), "params": dict(u_params),
        }
    # same value gate the loader applies (numeric, finite)
    _validate_materials({"materials": {str(name): entry}})
    self._data["materials"][str(name)] = entry

add_shape

add_shape(shape: str, *, id: str, material: str | None = None, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None, **params: float) -> None

Add a parametric shape. id becomes the physical-group label; material defaults to id when materials are used.

Source code in src/apeGmsh/sections/_document.py
def add_shape(
    self,
    shape: str,
    *,
    id: str,
    material: str | None = None,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
    **params: float,
) -> None:
    """Add a parametric shape. ``id`` becomes the physical-group
    label; ``material`` defaults to ``id`` when materials are
    used."""
    self._require_lane("continuum", "add_shape")
    if shape not in _SHAPE_PARAMS:
        raise SectionDocumentError(
            f"unknown shape {shape!r}; expected one of "
            f"{sorted(_SHAPE_PARAMS)} (or add_polygon)."
        )
    missing = [k for k in _SHAPE_PARAMS[shape] if k not in params]
    extra = [k for k in params if k not in _SHAPE_PARAMS[shape]]
    if missing or extra:
        raise SectionDocumentError(
            f"shape {shape!r} ({id!r}): missing params {missing}, "
            f"unknown params {extra}."
        )
    self._check_new_id(id)
    tx, ty = _pair(translate, f"shape {id!r} translate")
    self._data["shapes"].append({
        "id": str(id), "shape": shape,
        "params": {
            k: _num(v, f"shape {id!r} param {k}")
            for k, v in params.items()
        },
        "material": material,
        "translate": [tx, ty],
        "rotate": None if rotate is None
        else _num(rotate, f"shape {id!r} rotate"),
    })

add_polygon

add_polygon(points: 'list[tuple[float, float]]', *, id: str, material: str | None = None, translate: tuple[float, float] = (0.0, 0.0), rotate: float | None = None) -> None

Add a freehand straight-segment polygon (the canvas tool's output). Points are authoring-plane vertices in order; the loop closes automatically.

Source code in src/apeGmsh/sections/_document.py
def add_polygon(
    self,
    points: "list[tuple[float, float]]",
    *,
    id: str,
    material: str | None = None,
    translate: tuple[float, float] = (0.0, 0.0),
    rotate: float | None = None,
) -> None:
    """Add a freehand straight-segment polygon (the canvas tool's
    output). Points are authoring-plane vertices in order; the
    loop closes automatically."""
    self._require_lane("continuum", "add_polygon")
    if len(points) < 3:
        raise SectionDocumentError(
            f"polygon {id!r}: needs at least 3 points, "
            f"got {len(points)}."
        )
    self._check_new_id(id)
    tx, ty = _pair(translate, f"polygon {id!r} translate")
    self._data["shapes"].append({
        "id": str(id), "shape": "polygon",
        "points": [
            list(_pair(pt, f"polygon {id!r} point {i}"))
            for i, pt in enumerate(points)
        ],
        "material": material,
        "translate": [tx, ty],
        "rotate": None if rotate is None
        else _num(rotate, f"polygon {id!r} rotate"),
    })

add_embed

add_embed(outer: str, inner: str) -> None

The composite-partition primitive: carve inner out of outer (cut, tool kept) then fragment the pair conformally. The double-cover trap is unrepresentable through this op.

Source code in src/apeGmsh/sections/_document.py
def add_embed(self, outer: str, inner: str) -> None:
    """The composite-partition primitive: carve ``inner`` out of
    ``outer`` (cut, tool kept) then fragment the pair conformally.
    The double-cover trap is unrepresentable through this op."""
    self._require_lane("continuum", "add_embed")
    self._check_shape_ref(outer)
    self._check_shape_ref(inner)
    if outer == inner:
        raise SectionDocumentError(
            f"embed needs two different shapes, both are {outer!r}."
        )
    self._data["booleans"].append(
        {"op": "embed", "outer": outer, "inner": inner}
    )

add_cut

add_cut(target: str, tool: str, *, remove_tool: bool = True) -> None

Raw boolean cut (e.g. punching holes with a sacrificial tool shape). For overlapping material regions use :meth:add_embed instead.

Source code in src/apeGmsh/sections/_document.py
def add_cut(self, target: str, tool: str, *, remove_tool: bool = True) -> None:
    """Raw boolean cut (e.g. punching holes with a sacrificial
    tool shape). For overlapping *material* regions use
    :meth:`add_embed` instead."""
    self._require_lane("continuum", "add_cut")
    self._check_shape_ref(target)
    self._check_shape_ref(tool)
    if target == tool:
        raise SectionDocumentError(
            f"cut needs two different shapes, both are {target!r}."
        )
    self._data["booleans"].append({
        "op": "cut", "target": target, "tool": tool,
        "remove_tool": bool(remove_tool),
    })

add_fragment_pair

add_fragment_pair(a: str, b: str) -> None

Raw conformal fragment of two touching (non-overlapping) shapes.

Source code in src/apeGmsh/sections/_document.py
def add_fragment_pair(self, a: str, b: str) -> None:
    """Raw conformal fragment of two touching (non-overlapping)
    shapes."""
    self._require_lane("continuum", "add_fragment_pair")
    self._check_shape_ref(a)
    self._check_shape_ref(b)
    if a == b:
        raise SectionDocumentError(
            f"fragment_pair needs two different shapes, both are "
            f"{a!r}."
        )
    self._data["booleans"].append({"op": "fragment_pair", "a": a, "b": b})

add_bar

add_bar(*, material: str, x: float, y: float, area: float) -> None

One discrete rebar on a continuum section, in authoring (x, y) coordinates. Rides the kind="fiber" lowering at :meth:to_section; concrete area is not deducted.

Source code in src/apeGmsh/sections/_document.py
def add_bar(
    self, *, material: str, x: float, y: float, area: float,
) -> None:
    """One discrete rebar on a continuum section, in **authoring
    (x, y)** coordinates. Rides the ``kind="fiber"`` lowering at
    :meth:`to_section`; concrete area is not deducted."""
    self._require_lane("continuum", "add_bar")
    entry = {
        "kind": "point", "material": str(material),
        "x": x, "y": y, "area": area,
    }
    _check_bar_entry(entry)
    self._data.setdefault("bars", []).append(entry)

add_bar_line

add_bar_line(*, material: str, n: int, area: float, start: tuple[float, float], end: tuple[float, float]) -> None

n equally spaced bars from start to end (endpoints included, n >= 2), authoring coordinates. Stored parametric and expanded at handoff.

Source code in src/apeGmsh/sections/_document.py
def add_bar_line(
    self,
    *,
    material: str,
    n: int,
    area: float,
    start: tuple[float, float],
    end: tuple[float, float],
) -> None:
    """``n`` equally spaced bars from ``start`` to ``end``
    (endpoints included, ``n >= 2``), authoring coordinates.
    Stored parametric and expanded at handoff."""
    self._require_lane("continuum", "add_bar_line")
    entry = {
        "kind": "line", "material": str(material),
        "n": n, "area": area,
        "start": list(start), "end": list(end),
    }
    _check_bar_entry(entry)
    self._data.setdefault("bars", []).append(entry)

add_template

add_template(template: str, *, materials: 'Mapping[str, str]', **params: Any) -> None

Add a parametric RC template (stored as parameters, re-expanded on every build). materials maps the template's roles to material-table names — exact cover required.

Source code in src/apeGmsh/sections/_document.py
def add_template(
    self,
    template: str,
    *,
    materials: "Mapping[str, str]",
    **params: Any,
) -> None:
    """Add a parametric RC template (stored as parameters,
    re-expanded on every build). ``materials`` maps the template's
    roles to material-table names — exact cover required."""
    self._require_lane("fiber", "add_template")
    _expand_template_checked(template, dict(params))  # validate now
    roles = set(template_roles(dict(params)))
    given = set(materials)
    if roles != given:
        raise SectionDocumentError(
            f"template {template!r}: materials= must cover roles "
            f"{sorted(roles)} exactly — missing "
            f"{sorted(roles - given)}, unknown {sorted(given - roles)}."
        )
    self._data["templates"].append({
        "template": template,
        "params": dict(params),
        "materials": {str(k): str(v) for k, v in materials.items()},
    })

build

build() -> 'SectionProperties | FiberRecipe'

Realize the document.

Continuum lane: private apeGmsh session → builders → booleans → mesh → :class:SectionProperties (which snapshots the fem, so the session is closed before returning). Documents with an empty materials table build in the analyzer's geometric-only mode; otherwise every shape's material (explicit or defaulted to its id) must exist in the table — fail-loud here, before any session is opened.

Fiber lane: templates expand deterministically and merge with the literal patches/layers/points into a :class:FiberRecipe (no session, no bridge objects) — hand it to :meth:to_section for the OpenSees handoff.

Source code in src/apeGmsh/sections/_document.py
def build(self) -> "SectionProperties | FiberRecipe":
    """Realize the document.

    Continuum lane: private apeGmsh session → builders → booleans
    → mesh → :class:`SectionProperties` (which snapshots the fem,
    so the session is closed before returning). Documents with an
    empty ``materials`` table build in the analyzer's
    geometric-only mode; otherwise every shape's material
    (explicit or defaulted to its id) must exist in the table —
    fail-loud here, before any session is opened.

    Fiber lane: templates expand deterministically and merge with
    the literal patches/layers/points into a :class:`FiberRecipe`
    (no session, no bridge objects) — hand it to
    :meth:`to_section` for the OpenSees handoff.
    """
    if self.kind == "fiber":
        return self._build_fiber()
    return self._build_continuum()

to_section

to_section(ops: Any, *, name: str | None = None) -> Any

Resolve the document on an apeSees bridge.

Fiber lane: construct each used material's uniaxial spec via ops.uniaxialMaterial.<Type>(**params) (one bridge material per document material name) and register the section as ops.section.Fiber(...).

Continuum lane (ADR 0080 B3): build the analyzer, resolve the region materials' uniaxial specs, expand the bars overlay, and register ops.section.ComputedSection(kind="fiber", fibers=..., bars=...) — the Gauss-fiber lowering plus discrete rebar. (For the elastic lowering, call ops.section.ComputedSection(analysis=doc.build()) directly.) Fail-loud on any used material with no uniaxial role.

Source code in src/apeGmsh/sections/_document.py
def to_section(self, ops: Any, *, name: str | None = None) -> Any:
    """Resolve the document on an ``apeSees`` bridge.

    Fiber lane: construct each used material's ``uniaxial`` spec
    via ``ops.uniaxialMaterial.<Type>(**params)`` (one bridge
    material per document material name) and register the section
    as ``ops.section.Fiber(...)``.

    Continuum lane (ADR 0080 B3): build the analyzer, resolve the
    region materials' ``uniaxial`` specs, expand the ``bars``
    overlay, and register
    ``ops.section.ComputedSection(kind="fiber", fibers=...,
    bars=...)`` — the Gauss-fiber lowering plus discrete rebar.
    (For the elastic lowering, call
    ``ops.section.ComputedSection(analysis=doc.build())``
    directly.) Fail-loud on any used material with no ``uniaxial``
    role."""
    if self.kind == "continuum":
        return self._to_computed_fiber(ops, name=name)
    recipe = self._build_fiber()
    used = sorted({
        i["material"]
        for i in (*recipe.patches, *recipe.layers, *recipe.points)
    })
    mats = self._resolve_uniaxial(ops, used)
    typed_patches, typed_layers, typed_points = typed_fiber_items(
        recipe, mats,
    )
    return ops.section.Fiber(
        patches=typed_patches,
        layers=typed_layers,
        fibers=typed_points,
        GJ=recipe.GJ,
        name=name if name is not None else self.name,
    )

analysis_from_fem

analysis_from_fem(fem: Any) -> 'SectionProperties'

Wrap a :class:FEMData that :meth:build_fem produced for this document into its analyzer.

Split out so the meshing can happen somewhere else — the ADR 0080 B6 properties worker meshes in a subprocess and calls this with the FEMData that came back (see :mod:apeGmsh.sections._mesh_proc).

Source code in src/apeGmsh/sections/_document.py
def analysis_from_fem(self, fem: Any) -> "SectionProperties":
    """Wrap a :class:`FEMData` that :meth:`build_fem` produced for
    *this* document into its analyzer.

    Split out so the meshing can happen somewhere else — the ADR
    0080 B6 properties worker meshes in a subprocess and calls this
    with the ``FEMData`` that came back (see
    :mod:`apeGmsh.sections._mesh_proc`).
    """
    from ._analysis import SectionProperties

    materials = self._resolve_materials()
    return SectionProperties(
        fem,
        materials=materials or None,
        name=self.name,
        disconnected=self._data.get("disconnected", "raise"),
    )

build_fem

build_fem() -> Any

The gmsh half of a continuum build: run the private session (builders → booleans → mesh) and return the FEMData snapshot. No materials, no analyzer, no solve.

This is the only part of a section build that touches Gmsh, and Gmsh is one process-global, non-reentrant C++ runtime — so this is also the only part that has to be serialized against other threads, or moved out of the process entirely.

Source code in src/apeGmsh/sections/_document.py
def build_fem(self) -> Any:
    """The **gmsh half** of a continuum build: run the private
    session (builders → booleans → mesh) and return the ``FEMData``
    snapshot. No materials, no analyzer, no solve.

    This is the only part of a section build that touches Gmsh, and
    Gmsh is one process-global, non-reentrant C++ runtime — so this
    is also the only part that has to be serialized against other
    threads, or moved out of the process entirely.
    """
    from apeGmsh import apeGmsh

    data = self._data
    self._require_mesh_lc()

    sacrificial = self._sacrificial_ids()

    g = apeGmsh(model_name=self.name or "section_doc", verbose=False)
    g.begin()
    try:
        instances: dict[str, Any] = {}
        for sh in data["shapes"]:
            if sh["shape"] == "polygon":
                instances[sh["id"]] = _build_polygon(
                    g, sh, pg=sh["id"] not in sacrificial,
                )
            else:
                builder = getattr(g.sections, sh["shape"])
                instances[sh["id"]] = builder(
                    **sh["params"],
                    label=sh["id"],
                    translate=tuple(sh["translate"]),
                    rotate=sh["rotate"],
                )
        for op in data["booleans"]:
            _apply_boolean(g, op, instances)
        if sacrificial:
            g.model.geometry.remove_orphans()
        g.mesh.sizing.set_global_size(float(data["mesh"]["lc"]))
        g.mesh.generation.generate(dim=2)
        if int(data["mesh"].get("order", 2)) > 1:
            g.mesh.generation.set_order(2)
        fem = g.mesh.queries.get_fem_data(dim=2)
    finally:
        g.end()
    return fem

apeGmsh.sections._document.FiberRecipe dataclass

FiberRecipe(patches: tuple[dict[str, Any], ...], layers: tuple[dict[str, Any], ...], points: tuple[dict[str, Any], ...], GJ: float | None)

A fiber-lane document, fully expanded (ADR 0080 B2).

Plain data — patch / layer / point dicts carrying material names from the document's table, templates already expanded. :meth:SectionDocument.to_section turns it into a registered bridge Fiber; tests and the GUI read it directly.

areas_by_material

areas_by_material() -> dict[str, float]

Total fiber area per material name (patches by geometry, layers/points by n·A) — the exact-sum test surface.

Source code in src/apeGmsh/sections/_document.py
def areas_by_material(self) -> dict[str, float]:
    """Total fiber area per material name (patches by geometry,
    layers/points by ``n·A``) — the exact-sum test surface."""
    out: dict[str, float] = {}

    def _add(name: str, a: float) -> None:
        out[name] = out.get(name, 0.0) + a

    for p in self.patches:
        if p["kind"] == "rect":
            _add(
                p["material"],
                abs((p["yJ"] - p["yI"]) * (p["zJ"] - p["zI"])),
            )
        else:
            frac = (p.get("end_ang", 360.0) - p.get("start_ang", 0.0)) / 360.0
            _add(
                p["material"],
                math.pi * (p["ext_rad"] ** 2 - p["int_rad"] ** 2) * frac,
            )
    for la in self.layers:
        _add(la["material"], la["n_bars"] * la["area"])
    for pt in self.points:
        _add(pt["material"], pt["area"])
    return out

apeGmsh.sections._builder_gui.launch_builder

launch_builder(path_or_doc: 'str | Path | SectionDocument | None' = None, *, blocking: bool = True) -> 'SectionBuilderWindow'

Open the section builder.

path_or_doc is a .section.json path, an existing :class:SectionDocument, or None for a blank continuum document. blocking=True enters the Qt event loop; blocking=False returns immediately with the window alive (notebooks: %gui qt).

Source code in src/apeGmsh/sections/_builder_gui.py
def launch_builder(
    path_or_doc: "str | Path | SectionDocument | None" = None,
    *,
    blocking: bool = True,
) -> "SectionBuilderWindow":
    """Open the section builder.

    ``path_or_doc`` is a ``.section.json`` path, an existing
    :class:`SectionDocument`, or ``None`` for a blank continuum
    document. ``blocking=True`` enters the Qt event loop;
    ``blocking=False`` returns immediately with the window alive
    (notebooks: ``%gui qt``).
    """
    QtWidgets, _QtCore, _QtGui = _import_qt()

    from apeGmsh.viewers.ui._qt_env import prepare_qt_environment
    prepare_qt_environment()
    app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])

    if sys.platform == "win32" and app.platformName().lower() == "offscreen":
        raise RuntimeError(
            "Qt is running on the 'offscreen' platform, which cannot "
            "host the section builder window on Windows. Unset "
            "QT_QPA_PLATFORM (or start a fresh process without it) to "
            "open the builder."
        )

    doc = _coerce_document(path_or_doc)
    on_disk = (
        None if path_or_doc is None or isinstance(path_or_doc, SectionDocument)
        else path_or_doc
    )
    win = SectionBuilderWindow(doc, path=on_disk)
    win.set_live_properties(True)   # real launches solve live (B6)
    win.show()
    if blocking:
        app.exec_()
    else:
        _LIVE_BUILDERS.append(win)
    return win

apeGmsh.sections._handoff.handoff_snippet

handoff_snippet(doc: 'SectionDocument', *, path: 'str | Path | None' = None) -> str

Render the paste-ready bridge handoff for doc.

Parameters

doc The section document, either lane. path Where the document lives on disk. Continuum lane only — the snippet re-opens the document, so it needs the path; a placeholder derived from the document name is emitted when this is omitted (the GUI passes the file it has open). Ignored by the fiber lane, which inlines everything.

Returns

str Python source assuming an apeSees bridge bound to ops. Always compile()-able.

Source code in src/apeGmsh/sections/_handoff.py
def handoff_snippet(
    doc: "SectionDocument", *, path: "str | Path | None" = None,
) -> str:
    """Render the paste-ready bridge handoff for ``doc``.

    Parameters
    ----------
    doc
        The section document, either lane.
    path
        Where the document lives on disk. **Continuum lane only** — the
        snippet re-opens the document, so it needs the path; a
        placeholder derived from the document name is emitted when this
        is omitted (the GUI passes the file it has open). Ignored by the
        fiber lane, which inlines everything.

    Returns
    -------
    str
        Python source assuming an ``apeSees`` bridge bound to ``ops``.
        Always ``compile()``-able.
    """
    if doc.kind == "fiber":
        return _fiber_snippet(doc)
    return _continuum_snippet(doc, path=path)

Moment–curvature

apeGmsh.sections._mc.moment_curvature

moment_curvature(doc: 'SectionDocument', *, axis: "Literal['z', 'y']" = 'z', kappa_max: float, n_steps: int = 40, axial: float = 0.0, tol: float = 1e-08, max_iter: int = 25) -> MomentCurvature

Push a fiber-lane document's section to kappa_max and record its moment–curvature response.

Parameters

doc A fiber-lane :class:~apeGmsh.sections.SectionDocument. Every material it uses needs a uniaxial spec. (Continuum documents lower to fibers only through a bridge — hand doc.to_section(ops) to a frame model for those.) axis "z" (default) bends about the section's z-axis — DOF 6, whose elastic slope is the analyzer's EIxx_c. "y" is DOF 5 / EIyy_c. kappa_max Target curvature. Signed: a negative value walks the curve into the other quadrant, which is how an asymmetric section's two directions get compared. n_steps Displacement-control steps from 0 to kappa_max. axial Constant axial force held during the push, applied first and frozen (loadConst). OpenSees convention — compression is negative. tol, max_iter Newton convergence test (NormDispIncr) parameters.

Returns

MomentCurvature

Raises

MomentCurvatureError The document is not fiber-lane, an argument is out of range, a material has no usable uniaxial spec, or the axial pre-load did not converge. ImportError No OpenSees backend is installed. Raised after the document is resolved, so a malformed document reports its own problem whether or not a solver is present.

Notes

Wipes the process-global OpenSees domain — see the module docstring.

Source code in src/apeGmsh/sections/_mc.py
def moment_curvature(
    doc: "SectionDocument",
    *,
    axis: "Literal['z', 'y']" = "z",
    kappa_max: float,
    n_steps: int = 40,
    axial: float = 0.0,
    tol: float = 1e-8,
    max_iter: int = 25,
) -> MomentCurvature:
    """Push a fiber-lane document's section to ``kappa_max`` and record
    its moment–curvature response.

    Parameters
    ----------
    doc
        A **fiber-lane** :class:`~apeGmsh.sections.SectionDocument`.
        Every material it uses needs a ``uniaxial`` spec. (Continuum
        documents lower to fibers only through a bridge — hand
        ``doc.to_section(ops)`` to a frame model for those.)
    axis
        ``"z"`` (default) bends about the section's z-axis — DOF 6,
        whose elastic slope is the analyzer's ``EIxx_c``. ``"y"`` is
        DOF 5 / ``EIyy_c``.
    kappa_max
        Target curvature. **Signed**: a negative value walks the curve
        into the other quadrant, which is how an asymmetric section's
        two directions get compared.
    n_steps
        Displacement-control steps from 0 to ``kappa_max``.
    axial
        Constant axial force held during the push, applied first and
        frozen (``loadConst``). OpenSees convention — **compression is
        negative**.
    tol, max_iter
        Newton convergence test (``NormDispIncr``) parameters.

    Returns
    -------
    MomentCurvature

    Raises
    ------
    MomentCurvatureError
        The document is not fiber-lane, an argument is out of range, a
        material has no usable uniaxial spec, or the axial pre-load did
        not converge.
    ImportError
        No OpenSees backend is installed. Raised **after** the document
        is resolved, so a malformed document reports its own problem
        whether or not a solver is present.

    Notes
    -----
    Wipes the process-global OpenSees domain — see the module
    docstring.
    """
    if doc.kind != "fiber":
        raise MomentCurvatureError(
            f"moment_curvature is a fiber-lane operation; this document "
            f"is kind={doc.kind!r}. Lower a continuum document on a "
            f"bridge instead: doc.to_section(ops)."
        )
    if axis not in _DOF_OF_AXIS:
        raise MomentCurvatureError(
            f"axis must be 'z' or 'y', got {axis!r}."
        )
    if kappa_max == 0.0:
        raise MomentCurvatureError("kappa_max must be non-zero.")
    if n_steps < 1:
        raise MomentCurvatureError(
            f"n_steps must be >= 1, got {n_steps}."
        )

    # resolve the document FIRST: its errors are the user's to fix and
    # do not depend on a solver being installed.
    mats, section = _prepare_section(doc)

    ops = _ops_module()
    dof = _DOF_OF_AXIS[axis]

    ops.wipe()
    ops.model("basic", "-ndm", 3, "-ndf", 6)
    ops.node(1, 0.0, 0.0, 0.0)
    ops.node(2, 0.0, 0.0, 0.0)
    ops.fix(1, 1, 1, 1, 1, 1, 1)
    # a fiber section carries no shear stiffness (and no torsional
    # stiffness without -GJ): free axial + both rotations, restrain the
    # rest — the same restraint set gate G-D used.
    ops.fix(2, 0, 1, 1, 1, 0, 0)

    _emit_section(mats, section)
    ops.element("zeroLengthSection", 1, 1, 2, 1)

    ops.system("BandGeneral")
    ops.numberer("Plain")
    ops.constraints("Plain")

    if axial != 0.0:
        ops.timeSeries("Constant", 1)
        ops.pattern("Plain", 1, 1)
        ops.load(2, float(axial), 0.0, 0.0, 0.0, 0.0, 0.0)
        ops.integrator("LoadControl", 1.0)
        ops.test("NormDispIncr", tol, max_iter)
        ops.algorithm("Newton")
        ops.analysis("Static")
        if ops.analyze(1) != 0:
            raise MomentCurvatureError(
                f"the axial pre-load ({axial}) did not converge; the "
                f"section cannot carry it."
            )
        ops.loadConst("-time", 0.0)

    # unit reference moment → the load factor IS the moment
    ops.timeSeries("Linear", 2)
    ops.pattern("Plain", 2, 2)
    reference = [0.0] * 6
    reference[dof - 1] = 1.0
    ops.load(2, *reference)

    dkappa = float(kappa_max) / n_steps
    ops.integrator("DisplacementControl", 2, dof, dkappa)
    ops.test("NormDispIncr", tol, max_iter)
    ops.algorithm("Newton")
    ops.analysis("Static")

    curvature = [0.0]
    moment = [0.0]
    complete = True
    for _step in range(n_steps):
        if ops.analyze(1) != 0:
            complete = False
            break
        curvature.append(float(ops.nodeDisp(2, dof)))
        moment.append(float(ops.getLoadFactor(2)))

    return MomentCurvature(
        axis=axis,
        curvature=tuple(curvature),
        moment=tuple(moment),
        axial=float(axial),
        complete=complete,
    )

apeGmsh.sections._mc.MomentCurvature dataclass

MomentCurvature(axis: str, curvature: tuple[float, ...], moment: tuple[float, ...], axial: float, complete: bool)

One moment–curvature curve (ADR 0080 B7).

curvature and moment are parallel, start at (0, 0), and carry the sign of kappa_max. complete is False when a step failed to converge before n_steps — the curve up to that point is still valid, which is the normal end of an RC section that crushes.

EI0 property

EI0: float

Initial (first-step secant) flexural stiffness M/κ.

For elastic materials this is the fiber-sum Σ E·A·r² about the bending axis exactly — the identity the B7 gate checks.

M_max property

M_max: float

Largest |M| reached, carrying its sign.