Skip to content

Quickstart

This page walks through the most common ADMESH workflows: triangulating a polygon, round-tripping an ADCIRC fort.14, composing a custom size field, and loading a domain from the admesh-domains registry.

Setup

import admesh
import numpy as np

All examples assume ADMESH ≥ 0.1.0.

Triangulate a polygon

The simplest workflow: define a polygon by its vertex ring, call triangulate, and read out the mesh.

# Unit square with one hole.
outer = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
hole = np.array([[0.4, 0.4], [0.6, 0.4], [0.6, 0.6], [0.4, 0.6]])

domain = admesh.Domain(
    sdf=lambda p: ...,  # signed-distance function over the multi-ring polygon
    bbox=(0.0, 0.0, 1.0, 1.0),
)

mesh = admesh.triangulate(domain, h_max=0.05)

print(mesh.n_nodes, mesh.n_elements)
print("min quality:", mesh.quality.min())

For polygon-based domains, the canonical builder is admesh.load_domain_from_json — see that page for the expected JSON schema. The admesh.Domain constructor accepts a signed- distance callable directly when you want full control.

Round-trip an ADCIRC fort.14

src = admesh.read_fort14("examples/coast.14")
src.to_fort14("examples/coast_out.14")

roundtripped = admesh.read_fort14("examples/coast_out.14")
assert src.equals(roundtripped)

Mesh.equals tolerates atol=1e-5 on node coordinates and exact match on connectivity + boundary segments. The Fort14ParseError exception carries line_no / expected / actual for any malformed input.

Build a Domain from an existing mesh

When you have a mesh and want to re-triangulate it with new size-field defaults, use Domain.from_mesh:

src = admesh.read_fort14("coastal_fixture.14")
domain = admesh.Domain.from_mesh(src)

fresh = admesh.triangulate(domain, h_min=10.0, h_max=200.0)

Domain.from_mesh extracts the outer boundary ring (largest signed area) and any interior holes (smaller rings), then builds a Shapely-backed SDF plus a bathymetry interpolant from the source mesh's depth values.

Custom size-field contribution

Pass a callable that maps (N, 2) points to (N,) desired edge lengths. Multiple contributions can be combined via compose_size_field.

def refine_near_inlet(points):
    """Tighten edges within 500 m of the inlet at x=1500."""
    return 50.0 + 0.2 * np.abs(points[:, 0] - 1500.0)

def refine_near_island(points):
    """Tighten edges within 100 m of a circular island at (3000, 2000)."""
    d = np.linalg.norm(points - np.array([3000.0, 2000.0]), axis=1)
    return 20.0 + 0.5 * np.clip(d - 100.0, 0.0, None)

mesh = admesh.triangulate(
    domain,
    h_min=20.0,
    h_max=500.0,
    user_contribs=(refine_near_inlet, refine_near_island),
    combine=np.minimum.reduce,  # take the tightest contribution at each point
)

The default combine is np.minimum.reduce — at every point, the tightest of the contributions wins. Pass np.add or np.maximum.reduce to change the policy.

Load from the admesh-domains registry

⚠️ Status as of 2026-05-15: registry loaders are documented but the adapter is broken against admesh-domains 0.3.x (see issue #64). Use file loaders below until the fix lands.

When the adapter is repaired, the registry workflow will be:

# Discover available domains.
for mesh_id, desc in sorted(admesh.list_available_domains().items()):
    print(f"{mesh_id}: {desc}")

# Load a registered domain by id.
domain = admesh.load_domain_from_registry("BaranjaHill")
mesh = admesh.triangulate(domain)

# Or load with provenance metadata.
domain, meta = admesh.load_domain_with_metadata("BaranjaHill")
print(f"License: {meta.get('license')}, contributor: {meta.get('contributed_by')}")

Load from a local fort.14 or polygon file

# fort.14 — full ADCIRC mesh, including boundary types.
domain = admesh.load_domain_from_fort14("examples/coast.14")

# JSON — polygon rings + fixed points.
domain = admesh.load_domain_from_json("examples/inlet.json")

# TOML — same content, TOML-formatted.
domain = admesh.load_domain_from_toml("examples/inlet.toml")

See Domain loaders for the JSON / TOML schemas.

Pre-quad smoother (spec 004)

If you plan to convert the triangulation to quads downstream, run the right-isoceles smoother first:

domain = admesh.load_domain_from_fort14("coast.14")
mesh = admesh.triangulate(domain)

# Nudge triangles toward right-isoceles shape so pair-fusion is clean.
p_new, _ = admesh.smooth_for_quadrangulation(
    mesh.nodes, mesh.elements, domain.sdf,
)

# Quality check.
q = admesh.right_iso_quality(p_new, mesh.elements)
print("right-isoceles quality (mean):", q.mean())

Valence balancing (issue #27)

After triangulation, you may want to balance the number of triangles meeting at each node (the valence). The default heuristic targets 6 interior-node valence and 4 boundary-node valence.

result = admesh.balance_valence_triangles(mesh, max_passes=3)
print(f"Flipped {result.n_flips} edges across {result.n_passes} passes")
print(f"Valence after: mean={result.stats.mean_valence:.2f}")

See BalanceConfig for tuning the targets.

Visualization

Install the [viz] extra (pip install admesh2D[viz]) for the matplotlib adapter:

import matplotlib.pyplot as plt
from admesh.viz import plot_mesh

fig, ax = plt.subplots(figsize=(8, 6))
plot_mesh(mesh, ax=ax, show_boundary=True)
plt.show()

Where to go next

  • API Reference — every public symbol documented.
  • Porting notes — how the MATLAB stages map to Python, with recorded behavior differences.
  • Contributing — dev setup, branch contract, filing issues.