Source code for upxo.meshing.gsmesh2d

"""
gsmesh2d.py — 2D grain-structure FE meshing orchestrator.

Dispatches to conformal or non-conformal meshing backends.
Non-conformal support is a placeholder for future implementation.

Public API
----------
mesh_gs          : Orchestrate meshing; returns a unified result dict.
visualize_gs_mesh: Visualize a mesh_gs result dict.

Shared helpers
--------------
_flatten_cells   : Expand MultiPolygons → individual Polygons (shared by all backends).
"""

from __future__ import annotations

import time

import numpy as np


# ─────────────────────────────────────────────────────────────────────────────
# Shared pre-processing
# ─────────────────────────────────────────────────────────────────────────────

def _flatten_cells(cells: dict) -> tuple[dict, dict]:
    """
    Expand any MultiPolygon values into individual Polygon entries.

    Parameters
    ----------
    cells : {gid: Shapely Polygon or MultiPolygon}

    Returns
    -------
    flat_cells : {flat_id: Polygon}  — contiguous integer keys starting at 1.
    gid_map    : {flat_id: original_gid}
    """
    from shapely.geometry import Polygon, MultiPolygon
    flat_cells: dict = {}
    gid_map: dict = {}
    flat_id = 1
    for gid, geom in cells.items():
        if isinstance(geom, Polygon):
            parts = [geom]
        elif isinstance(geom, MultiPolygon):
            parts = list(geom.geoms)
        else:
            parts = []
        for part in parts:
            if part is not None and part.area > 0:
                flat_cells[flat_id] = part
                gid_map[flat_id] = gid
                flat_id += 1
    return flat_cells, gid_map


# ─────────────────────────────────────────────────────────────────────────────
# Backend: conformal (gmsh via confMesh2dGMSH)
# ─────────────────────────────────────────────────────────────────────────────

def _mesh_conformal(
    cells: dict,
    mesh_size_gb: float,
    mesh_size_bulk: float,
    mesh_order: int,
    mesh_algo: int,
    recombine_to_quads: bool,
    dist_min: float,
    dist_max: float,
    out_dir: str | None,
    basename: str,
    formats: list | None,
    verbose: bool,
) -> dict:
    """ mesh conformal."""
    from upxo.meshing.conformal_mesher2d import confMesh2dGMSH

    flat_cells, gid_map = _flatten_cells(cells)
    if verbose:
        print(f'  [gsmesh2d] {len(flat_cells)} flat polygons from {len(cells)} grains')

    m = confMesh2dGMSH()
    t0 = time.perf_counter()
    m.femesh_gmsh(
        flat_cells, gid_map,
        mesh_size_gb=mesh_size_gb,
        mesh_size_bulk=mesh_size_bulk,
        mesh_algo=mesh_algo,
        mesh_order=mesh_order,
        recombine_to_quads=recombine_to_quads,
        out_dir=out_dir,
        basename=basename,
        formats=formats,
    )
    elapsed = time.perf_counter() - t0

    pts, _, triangles, quads = m.get_mesh_geometry()
    valid_mask = ~np.isnan(pts[:, 0])
    n_nodes = int(valid_mask.sum())
    n_tri   = len(triangles) if triangles is not None else 0
    n_quad  = len(quads)     if quads     is not None else 0

    return {
        'method':     'conformal',
        'mesher':     m,
        'flat_cells': flat_cells,
        'gid_map':    gid_map,
        'n_nodes':    n_nodes,
        'n_tri':      n_tri,
        'n_quad':     n_quad,
        'elapsed':    elapsed,
        'exported':   list(getattr(m, '_exported', [])),
    }


# ─────────────────────────────────────────────────────────────────────────────
# Public API
# ─────────────────────────────────────────────────────────────────────────────

[docs] def mesh_gs( cells: dict, method: str = 'conformal', mesh_size_gb: float = 0.75, mesh_size_bulk: float = 4.5, mesh_order: int = 1, mesh_algo: int = 8, recombine_to_quads: bool = True, dist_min: float = 0.5, dist_max: float = 5.0, out_dir: str | None = None, basename: str = 'gs_mesh', formats: list | None = None, verbose: bool = False, ) -> dict: """ Orchestrate FE meshing for a dict of Shapely grain polygons. Parameters ---------- cells : {gid: Shapely Polygon or MultiPolygon} method : 'conformal' (default). 'non_conformal' reserved for future use. mesh_size_gb : Target element size on grain boundaries. mesh_size_bulk : Target element size in grain interiors. mesh_order : Element order (1 = linear, 2 = quadratic). mesh_algo : Gmsh algorithm ID (6=Frontal, 8=Frontal-Delaunay quads). recombine_to_quads: Recombine triangles into quads after generation. dist_min : Distance field DistMin (passed through; currently stored in result). dist_max : Distance field DistMax (passed through; currently stored in result). out_dir : Directory for exported files. No export when None. basename : Filename stem (extension appended per format). formats : List of format extensions, e.g. ``['msh', 'inp', 'vtk']``. verbose : Print progress messages. Returns ------- dict method str — meshing method used. mesher confMesh2dGMSH instance with populated nodes / elConn. flat_cells {flat_id: Polygon} gid_map {flat_id: original_gid} n_nodes int n_tri int n_quad int elapsed float — total wall-clock seconds. exported list[str] — paths of files written. """ if method == 'conformal': return _mesh_conformal( cells, mesh_size_gb=mesh_size_gb, mesh_size_bulk=mesh_size_bulk, mesh_order=mesh_order, mesh_algo=mesh_algo, recombine_to_quads=recombine_to_quads, dist_min=dist_min, dist_max=dist_max, out_dir=out_dir, basename=basename, formats=formats, verbose=verbose, ) elif method == 'non_conformal': raise NotImplementedError('Non-conformal meshing orchestration: coming soon.') else: raise ValueError(f'Unknown meshing method: {method!r}')
[docs] def visualize_gs_mesh( result: dict, figsize: tuple = (12, 9), dpi: int = 150, tri_edgecolor: str = 'steelblue', tri_linewidth: float = 0.2, tri_alpha: float = 0.7, quad_edgecolor: str = 'darkorange', quad_linewidth: float = 0.2, quad_alpha: float = 0.7, show_axis: bool = True, show_stats: bool = True, lw_gb: float = 0.6, color_gb: str = 'k', ) -> tuple: """ Visualize a mesh_gs result dict using see_femesh + grain boundary overlay. Parameters ---------- result : dict returned by mesh_gs. figsize : Figure size in inches. dpi : Figure DPI. tri_* : Styling for triangle elements. quad_* : Styling for quad elements. show_axis : Show axis ticks. show_stats : Show element count statistics in the figure. lw_gb : Linewidth for grain boundary overlay. color_gb : Colour for grain boundary overlay. Returns ------- (fig, ax) """ from upxo.viz.meshviz import see_femesh from matplotlib.collections import LineCollection import numpy as np m = result['mesher'] pts, _, triangles, quads = m.get_mesh_geometry() title = ( f'Conformal FE mesh — ' f'{result["n_tri"]:,} tri + {result["n_quad"]:,} quad, ' f'{result["n_nodes"]:,} nodes' ) fig, ax = see_femesh( points_2d=pts, lines=None, triangles=triangles, quads=quads, figsize=figsize, dpi=dpi, tri_edgecolor=tri_edgecolor, tri_linewidth=tri_linewidth, tri_alpha=tri_alpha, quad_edgecolor=quad_edgecolor, quad_linewidth=quad_linewidth, quad_alpha=quad_alpha, show_axis=show_axis, show_stats=show_stats, title=title, ) segments = [np.column_stack(poly.exterior.xy) for poly in result['flat_cells'].values()] ax.add_collection(LineCollection(segments, colors=color_gb, linewidths=lw_gb, zorder=2)) return fig, ax