Skip to content

Core data types

The dataclasses ADMESH uses to model meshes, domains, and boundaries.

Mesh

admesh.Mesh dataclass

Triangular mesh with optional bathymetry and per-element quality.

See specs/001-pythonize-and-fort14-integration/data-model.md. Coordinates are 0-based and bathymetry follows the elevation (positive-up) convention. The fort.14 reader/writer apply the 1-based ↔ 0-based and elevation ↔ depth conversions strictly at the I/O boundary.

Source code in src/admesh/api.py
@dataclass(frozen=True, slots=True)
class Mesh:
    """Triangular mesh with optional bathymetry and per-element quality.

    See ``specs/001-pythonize-and-fort14-integration/data-model.md``.
    Coordinates are 0-based and bathymetry follows the elevation
    (positive-up) convention. The fort.14 reader/writer apply the
    1-based ↔ 0-based and elevation ↔ depth conversions strictly at the
    I/O boundary.
    """

    nodes: np.ndarray
    elements: np.ndarray
    boundaries: tuple[BoundarySegment, ...] = ()
    bathymetry: np.ndarray | None = None
    quality: np.ndarray | None = None
    title: str = ""

    @property
    def n_nodes(self) -> int:
        return int(self.nodes.shape[0])

    @property
    def n_elements(self) -> int:
        return int(self.elements.shape[0])

    @property
    def n_boundaries(self) -> int:
        return len(self.boundaries)

    def to_fort14(self, path: "str | os.PathLike[str] | TextIO") -> None:
        """Serialize this mesh to ADCIRC v55 fort.14.

        Implementation lands in T024 (``admesh.fort14.write_fort14``).
        """
        from admesh.fort14 import write_fort14

        write_fort14(self, path)

    def to_msh(self, path: "str | os.PathLike[str] | TextIO") -> None:
        """Serialize this mesh to Gmsh ASCII v2.2 (``admesh.gmsh.write_msh``)."""
        from admesh.gmsh import write_msh

        write_msh(self, path)

    def plot(self, ax=None, **kwargs):
        """Draw the mesh wireframe via chilmesh.

        Delegates to ``admesh.viz.plot_mesh`` → ``chilmesh.CHILmesh.plot``.

        Raises
        ------
        ImportError
            If chilmesh is not installed. Install with
            ``pip install admesh2D[viz]``.
        """
        from admesh.viz import plot_mesh

        return plot_mesh(self, ax=ax, **kwargs)

    def plot_quality(self, ax=None, cmap="cool", **kwargs):
        """Colormap elements by shape quality via chilmesh.

        Delegates to ``admesh.viz.plot_mesh_quality`` →
        ``chilmesh.CHILmesh.plot_quality``.

        Raises
        ------
        ImportError
            If chilmesh is not installed. Install with
            ``pip install admesh2D[viz]``.
        """
        from admesh.viz import plot_mesh_quality

        return plot_mesh_quality(self, ax=ax, cmap=cmap, **kwargs)

    def plot_layers(self, ax=None, cmap="viridis", **kwargs):
        """Color mesh elements by onion-peel layer via chilmesh.

        Delegates to ``admesh.viz.plot_mesh_layers`` →
        ``chilmesh.CHILmesh.plot_layer``.

        Raises
        ------
        ImportError
            If chilmesh is not installed. Install with
            ``pip install admesh2D[viz]``.
        """
        from admesh.viz import plot_mesh_layers

        return plot_mesh_layers(self, ax=ax, cmap=cmap, **kwargs)

    def equals(self, other: "Mesh", *, atol: float = 1e-10, rtol: float = 0.0) -> bool:
        """Tolerance-aware equality check for round-trip tests.

        Connectivity (``elements``, per-segment BC labels and node ids)
        is compared exactly; coordinates and bathymetry use
        ``np.allclose`` with the supplied tolerances. ``quality`` is
        ignored — it's a derived attribute and the round-trip path
        does not preserve it.
        """
        if not isinstance(other, Mesh):
            return NotImplemented  # type: ignore[return-value]
        if self.nodes.shape != other.nodes.shape:
            return False
        if self.elements.shape != other.elements.shape:
            return False
        if not np.array_equal(self.elements, other.elements):
            return False
        if not np.allclose(self.nodes, other.nodes, atol=atol, rtol=rtol):
            return False
        if (self.bathymetry is None) != (other.bathymetry is None):
            return False
        if self.bathymetry is not None and other.bathymetry is not None:
            if self.bathymetry.shape != other.bathymetry.shape:
                return False
            if not np.allclose(
                self.bathymetry, other.bathymetry, atol=atol, rtol=rtol
            ):
                return False
        if len(self.boundaries) != len(other.boundaries):
            return False
        for a, b in zip(self.boundaries, other.boundaries):
            if a.is_open != b.is_open:
                return False
            if int(a.bc_type) != int(b.bc_type):
                return False
            if not np.array_equal(a.node_ids, b.node_ids):
                return False
        return True

    def __repr__(self) -> str:
        if self.quality is not None and self.quality.size > 0:
            min_q = float(self.quality.min())
            mean_q = float(self.quality.mean())
            q_part = f", min_q={min_q:.2f}, mean_q={mean_q:.2f}"
        else:
            q_part = ""
        return (
            f"Mesh(n_nodes={self.n_nodes}, n_elements={self.n_elements}"
            f"{q_part}, n_boundaries={self.n_boundaries})"
        )

    def __str__(self) -> str:
        lines = ["Mesh"]
        lines.append(f"  nodes:      {self.n_nodes} × 2 (float64)")
        lines.append(f"  elements:   {self.n_elements} × 3 (int64)")
        if self.quality is not None and self.quality.size > 0:
            qmin = float(self.quality.min())
            qmean = float(self.quality.mean())
            qmax = float(self.quality.max())
            lines.append(
                f"  quality:    min={qmin:.2f}, mean={qmean:.2f}, max={qmax:.2f}"
            )
        else:
            lines.append("  quality:    not computed")
        if self.boundaries:
            lines.append(f"  boundaries: {self.n_boundaries} segments")
            for i, seg in enumerate(self.boundaries):
                if isinstance(seg.bc_type, BoundaryType):
                    label = seg.bc_type.name
                else:
                    label = f"code={int(seg.bc_type)}"
                lines.append(
                    f"    [{i}] {label:<13} ({seg.node_ids.size} nodes)"
                )
        else:
            lines.append("  boundaries: none")
        if self.bathymetry is None:
            lines.append("  bathymetry: not set")
        else:
            lines.append(
                f"  bathymetry: {self.bathymetry.size} samples (float64)"
            )
        return "\n".join(lines)

to_fort14

to_fort14(path: 'str | os.PathLike[str] | TextIO') -> None

Serialize this mesh to ADCIRC v55 fort.14.

Implementation lands in T024 (admesh.fort14.write_fort14).

Source code in src/admesh/api.py
def to_fort14(self, path: "str | os.PathLike[str] | TextIO") -> None:
    """Serialize this mesh to ADCIRC v55 fort.14.

    Implementation lands in T024 (``admesh.fort14.write_fort14``).
    """
    from admesh.fort14 import write_fort14

    write_fort14(self, path)

to_msh

to_msh(path: 'str | os.PathLike[str] | TextIO') -> None

Serialize this mesh to Gmsh ASCII v2.2 (admesh.gmsh.write_msh).

Source code in src/admesh/api.py
def to_msh(self, path: "str | os.PathLike[str] | TextIO") -> None:
    """Serialize this mesh to Gmsh ASCII v2.2 (``admesh.gmsh.write_msh``)."""
    from admesh.gmsh import write_msh

    write_msh(self, path)

plot

plot(ax=None, **kwargs)

Draw the mesh wireframe via chilmesh.

Delegates to admesh.viz.plot_mesh → chilmesh.CHILmesh.plot.

Raises:

Type Description
ImportError

If chilmesh is not installed. Install with pip install admesh2D[viz].

Source code in src/admesh/api.py
def plot(self, ax=None, **kwargs):
    """Draw the mesh wireframe via chilmesh.

    Delegates to ``admesh.viz.plot_mesh`` → ``chilmesh.CHILmesh.plot``.

    Raises
    ------
    ImportError
        If chilmesh is not installed. Install with
        ``pip install admesh2D[viz]``.
    """
    from admesh.viz import plot_mesh

    return plot_mesh(self, ax=ax, **kwargs)

plot_quality

plot_quality(ax=None, cmap='cool', **kwargs)

Colormap elements by shape quality via chilmesh.

Delegates to admesh.viz.plot_mesh_quality → chilmesh.CHILmesh.plot_quality.

Raises:

Type Description
ImportError

If chilmesh is not installed. Install with pip install admesh2D[viz].

Source code in src/admesh/api.py
def plot_quality(self, ax=None, cmap="cool", **kwargs):
    """Colormap elements by shape quality via chilmesh.

    Delegates to ``admesh.viz.plot_mesh_quality`` →
    ``chilmesh.CHILmesh.plot_quality``.

    Raises
    ------
    ImportError
        If chilmesh is not installed. Install with
        ``pip install admesh2D[viz]``.
    """
    from admesh.viz import plot_mesh_quality

    return plot_mesh_quality(self, ax=ax, cmap=cmap, **kwargs)

plot_layers

plot_layers(ax=None, cmap='viridis', **kwargs)

Color mesh elements by onion-peel layer via chilmesh.

Delegates to admesh.viz.plot_mesh_layers → chilmesh.CHILmesh.plot_layer.

Raises:

Type Description
ImportError

If chilmesh is not installed. Install with pip install admesh2D[viz].

Source code in src/admesh/api.py
def plot_layers(self, ax=None, cmap="viridis", **kwargs):
    """Color mesh elements by onion-peel layer via chilmesh.

    Delegates to ``admesh.viz.plot_mesh_layers`` →
    ``chilmesh.CHILmesh.plot_layer``.

    Raises
    ------
    ImportError
        If chilmesh is not installed. Install with
        ``pip install admesh2D[viz]``.
    """
    from admesh.viz import plot_mesh_layers

    return plot_mesh_layers(self, ax=ax, cmap=cmap, **kwargs)

equals

equals(other: 'Mesh', *, atol: float = 1e-10, rtol: float = 0.0) -> bool

Tolerance-aware equality check for round-trip tests.

Connectivity (elements, per-segment BC labels and node ids) is compared exactly; coordinates and bathymetry use np.allclose with the supplied tolerances. quality is ignored — it's a derived attribute and the round-trip path does not preserve it.

Source code in src/admesh/api.py
def equals(self, other: "Mesh", *, atol: float = 1e-10, rtol: float = 0.0) -> bool:
    """Tolerance-aware equality check for round-trip tests.

    Connectivity (``elements``, per-segment BC labels and node ids)
    is compared exactly; coordinates and bathymetry use
    ``np.allclose`` with the supplied tolerances. ``quality`` is
    ignored — it's a derived attribute and the round-trip path
    does not preserve it.
    """
    if not isinstance(other, Mesh):
        return NotImplemented  # type: ignore[return-value]
    if self.nodes.shape != other.nodes.shape:
        return False
    if self.elements.shape != other.elements.shape:
        return False
    if not np.array_equal(self.elements, other.elements):
        return False
    if not np.allclose(self.nodes, other.nodes, atol=atol, rtol=rtol):
        return False
    if (self.bathymetry is None) != (other.bathymetry is None):
        return False
    if self.bathymetry is not None and other.bathymetry is not None:
        if self.bathymetry.shape != other.bathymetry.shape:
            return False
        if not np.allclose(
            self.bathymetry, other.bathymetry, atol=atol, rtol=rtol
        ):
            return False
    if len(self.boundaries) != len(other.boundaries):
        return False
    for a, b in zip(self.boundaries, other.boundaries):
        if a.is_open != b.is_open:
            return False
        if int(a.bc_type) != int(b.bc_type):
            return False
        if not np.array_equal(a.node_ids, b.node_ids):
            return False
    return True

Domain

admesh.Domain dataclass

Geometric description for :func:triangulate.

Attributes:

Name Type Description
sdf Callable[[ndarray], ndarray]

Signed-distance function: (N, 2) -> (N,). Negative inside, positive outside, zero on the boundary.

bbox tuple[float, float, float, float]

(xmin, ymin, xmax, ymax) extent (matches Shapely / matplotlib convention).

pfix ndarray | None

Optional (K, 2) array of fixed points the triangulator must keep.

pts ndarray | None

Optional (P, 2) array of boundary discretization points (used by stages that take an explicit polyline input).

bc_segments tuple[BoundarySegment, ...]

Optional pre-labeled boundary metadata that flows through to the output mesh.

bathymetry Callable[[ndarray], ndarray] | None

Optional callable for bathymetric elevation sampling; used by the default size-field stack.

Source code in src/admesh/api.py
@dataclass(frozen=True, slots=True)
class Domain:
    """Geometric description for :func:`triangulate`.

    Attributes
    ----------
    sdf
        Signed-distance function: ``(N, 2) -> (N,)``. Negative inside,
        positive outside, zero on the boundary.
    bbox
        ``(xmin, ymin, xmax, ymax)`` extent (matches Shapely / matplotlib
        convention).
    pfix
        Optional ``(K, 2)`` array of fixed points the triangulator must
        keep.
    pts
        Optional ``(P, 2)`` array of boundary discretization points
        (used by stages that take an explicit polyline input).
    bc_segments
        Optional pre-labeled boundary metadata that flows through to
        the output mesh.
    bathymetry
        Optional callable for bathymetric elevation sampling;
        used by the default size-field stack.
    """

    sdf: Callable[[np.ndarray], np.ndarray]
    bbox: tuple[float, float, float, float]
    pfix: np.ndarray | None = None
    pts: np.ndarray | None = None
    bc_segments: tuple[BoundarySegment, ...] = ()
    bathymetry: Callable[[np.ndarray], np.ndarray] | None = field(default=None, compare=False)

    @classmethod
    def from_mesh(cls, mesh: "Mesh") -> "Domain":
        """Recover a multiply-connected domain from a mesh.

        Extracts the boundary rings from the mesh, identifies the outer ring
        (by signed area), and constructs a Domain suitable for re-triangulation.

        Parameters
        ----------
        mesh : Mesh
            Source mesh with nodes and elements.

        Returns
        -------
        Domain
            Domain with SDF derived from mesh boundary and interior bathymetry
            (if available), and bc_segments recovered from the mesh boundary.
        """

        # Extract boundary segments (rings) from the mesh
        bc_segments = _derive_boundary_segments(mesh.elements, mesh.nodes)
        if not bc_segments:
            raise ValueError("Mesh has no boundary (is it fully 2D connected?)")

        # Compute domain bbox from all nodes
        bbox = (
            float(mesh.nodes[:, 0].min()),
            float(mesh.nodes[:, 1].min()),
            float(mesh.nodes[:, 0].max()),
            float(mesh.nodes[:, 1].max()),
        )

        # Build SDF using:
        #  1. Dense-sampled boundary polyline for accurate distance (fixes #38 gradient spikes)
        #  2. in_polygon winding test for correct sign — enabled by the junction-aware ring
        #     walk in _derive_boundary_segments which now produces closed rings even for
        #     real-world ADCIRC meshes with pinch-point boundary nodes.
        from scipy.spatial import cKDTree
        from admesh._stages.in_polygon import in_polygon as _in_polygon

        outer_ring_nodes = bc_segments[0].node_ids
        outer_ring_pts = mesh.nodes[outer_ring_nodes]
        hole_rings_pts = [mesh.nodes[seg.node_ids] for seg in bc_segments[1:]]

        diag = float(np.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]))
        h_samp = max(diag / 500, 1e-8)

        def _dense_sample_ring(ring_pts: np.ndarray, h: float) -> np.ndarray:
            segs = []
            n = len(ring_pts)
            for i in range(n):
                a = ring_pts[i]
                b = ring_pts[(i + 1) % n]
                seg_len = float(np.linalg.norm(b - a))
                n_samp = max(2, int(np.ceil(seg_len / h)))
                ts = np.linspace(0.0, 1.0, n_samp)[:-1]
                segs.append(a + ts[:, None] * (b - a))
            return np.vstack(segs) if segs else ring_pts

        dense_parts = [_dense_sample_ring(outer_ring_pts, h_samp)]
        for hr in hole_rings_pts:
            dense_parts.append(_dense_sample_ring(hr, h_samp))
        tree = cKDTree(np.vstack(dense_parts))

        def sdf(points: np.ndarray) -> np.ndarray:
            distances, _ = tree.query(points)
            in_outer, _ = _in_polygon(
                points[:, 0], points[:, 1],
                outer_ring_pts[:, 0], outer_ring_pts[:, 1],
            )
            in_hole = np.zeros(len(points), dtype=bool)
            for hr in hole_rings_pts:
                ih, _ = _in_polygon(
                    points[:, 0], points[:, 1],
                    hr[:, 0], hr[:, 1],
                )
                in_hole |= ih
            inside = in_outer & ~in_hole
            return np.where(inside, -distances, distances)

        if getattr(mesh, "bathymetry", None) is not None:
            from scipy.interpolate import NearestNDInterpolator
            bathy_interp = NearestNDInterpolator(
                mesh.nodes,
                mesh.bathymetry,
                rescale=False,
            )
        else:
            bathy_interp = None

        return cls(sdf=sdf, bbox=bbox, bc_segments=bc_segments, bathymetry=bathy_interp)

from_mesh classmethod

from_mesh(mesh: 'Mesh') -> 'Domain'

Recover a multiply-connected domain from a mesh.

Extracts the boundary rings from the mesh, identifies the outer ring (by signed area), and constructs a Domain suitable for re-triangulation.

Parameters:

Name Type Description Default
mesh Mesh

Source mesh with nodes and elements.

required

Returns:

Type Description
Domain

Domain with SDF derived from mesh boundary and interior bathymetry (if available), and bc_segments recovered from the mesh boundary.

Source code in src/admesh/api.py
@classmethod
def from_mesh(cls, mesh: "Mesh") -> "Domain":
    """Recover a multiply-connected domain from a mesh.

    Extracts the boundary rings from the mesh, identifies the outer ring
    (by signed area), and constructs a Domain suitable for re-triangulation.

    Parameters
    ----------
    mesh : Mesh
        Source mesh with nodes and elements.

    Returns
    -------
    Domain
        Domain with SDF derived from mesh boundary and interior bathymetry
        (if available), and bc_segments recovered from the mesh boundary.
    """

    # Extract boundary segments (rings) from the mesh
    bc_segments = _derive_boundary_segments(mesh.elements, mesh.nodes)
    if not bc_segments:
        raise ValueError("Mesh has no boundary (is it fully 2D connected?)")

    # Compute domain bbox from all nodes
    bbox = (
        float(mesh.nodes[:, 0].min()),
        float(mesh.nodes[:, 1].min()),
        float(mesh.nodes[:, 0].max()),
        float(mesh.nodes[:, 1].max()),
    )

    # Build SDF using:
    #  1. Dense-sampled boundary polyline for accurate distance (fixes #38 gradient spikes)
    #  2. in_polygon winding test for correct sign — enabled by the junction-aware ring
    #     walk in _derive_boundary_segments which now produces closed rings even for
    #     real-world ADCIRC meshes with pinch-point boundary nodes.
    from scipy.spatial import cKDTree
    from admesh._stages.in_polygon import in_polygon as _in_polygon

    outer_ring_nodes = bc_segments[0].node_ids
    outer_ring_pts = mesh.nodes[outer_ring_nodes]
    hole_rings_pts = [mesh.nodes[seg.node_ids] for seg in bc_segments[1:]]

    diag = float(np.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]))
    h_samp = max(diag / 500, 1e-8)

    def _dense_sample_ring(ring_pts: np.ndarray, h: float) -> np.ndarray:
        segs = []
        n = len(ring_pts)
        for i in range(n):
            a = ring_pts[i]
            b = ring_pts[(i + 1) % n]
            seg_len = float(np.linalg.norm(b - a))
            n_samp = max(2, int(np.ceil(seg_len / h)))
            ts = np.linspace(0.0, 1.0, n_samp)[:-1]
            segs.append(a + ts[:, None] * (b - a))
        return np.vstack(segs) if segs else ring_pts

    dense_parts = [_dense_sample_ring(outer_ring_pts, h_samp)]
    for hr in hole_rings_pts:
        dense_parts.append(_dense_sample_ring(hr, h_samp))
    tree = cKDTree(np.vstack(dense_parts))

    def sdf(points: np.ndarray) -> np.ndarray:
        distances, _ = tree.query(points)
        in_outer, _ = _in_polygon(
            points[:, 0], points[:, 1],
            outer_ring_pts[:, 0], outer_ring_pts[:, 1],
        )
        in_hole = np.zeros(len(points), dtype=bool)
        for hr in hole_rings_pts:
            ih, _ = _in_polygon(
                points[:, 0], points[:, 1],
                hr[:, 0], hr[:, 1],
            )
            in_hole |= ih
        inside = in_outer & ~in_hole
        return np.where(inside, -distances, distances)

    if getattr(mesh, "bathymetry", None) is not None:
        from scipy.interpolate import NearestNDInterpolator
        bathy_interp = NearestNDInterpolator(
            mesh.nodes,
            mesh.bathymetry,
            rescale=False,
        )
    else:
        bathy_interp = None

    return cls(sdf=sdf, bbox=bbox, bc_segments=bc_segments, bathymetry=bathy_interp)

BoundarySegment

admesh.BoundarySegment dataclass

A single boundary segment of a :class:Mesh.

Attributes:

Name Type Description
node_ids ndarray

Shape (N,), dtype int64. 0-based node indices in declaration order. Conversion to/from the 1-based fort.14 convention is handled at the I/O boundary by admesh.fort14.

bc_type BoundaryType | int

Either a :class:BoundaryType member (named ADCIRC code) or a plain int (unmapped code preserved for round-trip).

is_open bool

True iff this segment lives in the fort.14 open boundary block. Tracked separately from bc_type so uncommon ADCIRC codes that may appear in either block round-trip faithfully.

Source code in src/admesh/api.py
@dataclass(frozen=True, slots=True)
class BoundarySegment:
    """A single boundary segment of a :class:`Mesh`.

    Attributes
    ----------
    node_ids
        Shape ``(N,)``, dtype ``int64``. **0-based** node indices in
        declaration order. Conversion to/from the 1-based fort.14
        convention is handled at the I/O boundary by ``admesh.fort14``.
    bc_type
        Either a :class:`BoundaryType` member (named ADCIRC code) or a
        plain ``int`` (unmapped code preserved for round-trip).
    is_open
        ``True`` iff this segment lives in the fort.14 *open* boundary
        block. Tracked separately from ``bc_type`` so uncommon ADCIRC
        codes that may appear in either block round-trip faithfully.
    """

    node_ids: np.ndarray
    bc_type: BoundaryType | int
    is_open: bool

    def __post_init__(self) -> None:
        ids = self.node_ids
        if not isinstance(ids, np.ndarray):
            raise TypeError(
                f"BoundarySegment.node_ids must be a numpy.ndarray, got {type(ids).__name__}"
            )
        if ids.ndim != 1:
            raise ValueError(
                f"BoundarySegment.node_ids must be 1-D, got ndim={ids.ndim}"
            )
        if ids.dtype != np.int64:
            raise ValueError(
                f"BoundarySegment.node_ids must be int64, got dtype={ids.dtype}"
            )
        if ids.size > 0 and ids.min() < 0:
            raise ValueError(
                "BoundarySegment.node_ids contains negative indices "
                "(must be 0-based and non-negative)"
            )

BoundaryType

admesh.BoundaryType

Bases: IntEnum

v1 ADCIRC boundary-condition codes recognized by admesh.fort14.

IntEnum so each member compares equal to its ADCIRC numeric code: BoundaryType.OPEN == 0 is True. The fort.14 writer can emit codes via int(bc) without a side table.

Members

OPEN ADCIRC code 0 — open ocean / external water. MAINLAND ADCIRC code 1 — mainland boundary, no normal flux. ISLAND ADCIRC code 11 — island boundary. MAINLAND_FLUX ADCIRC code 20 — mainland with normal-flux specified. WALL Alias for MAINLAND (same int value). Retained because the faithful-port surface uses the name WALL for ADCIRC code 1.

Notes

Codes outside this set are preserved on round-trip as plain int values in BoundarySegment.bc_type. Use isinstance(bc, BoundaryType) to discriminate the named-vs-numeric case.

Source code in src/admesh/boundary_types.py
class BoundaryType(IntEnum):
    """v1 ADCIRC boundary-condition codes recognized by ``admesh.fort14``.

    ``IntEnum`` so each member compares equal to its ADCIRC numeric
    code: ``BoundaryType.OPEN == 0`` is ``True``. The fort.14 writer can
    emit codes via ``int(bc)`` without a side table.

    Members
    -------
    OPEN
        ADCIRC code 0 — open ocean / external water.
    MAINLAND
        ADCIRC code 1 — mainland boundary, no normal flux.
    ISLAND
        ADCIRC code 11 — island boundary.
    MAINLAND_FLUX
        ADCIRC code 20 — mainland with normal-flux specified.
    WALL
        Alias for ``MAINLAND`` (same int value). Retained because the
        faithful-port surface uses the name ``WALL`` for ADCIRC code 1.

    Notes
    -----
    Codes outside this set are preserved on round-trip as plain
    ``int`` values in ``BoundarySegment.bc_type``. Use
    ``isinstance(bc, BoundaryType)`` to discriminate the named-vs-numeric
    case.
    """

    OPEN = 0
    MAINLAND = 1
    ISLAND = 11
    MAINLAND_FLUX = 20
    WALL = 1