Metadata-Version: 2.4
Name: geneuler
Version: 0.1.1
Summary: Lightweight mesh generation and I/O (Gmsh+meshio) for GenEuler.
Project-URL: Homepage, https://github.com/your-org/geneuler
Project-URL: Issues, https://github.com/your-org/geneuler/issues
Author-email: Mohamad Kokh <mohamad.kokh@gmail.com>
License: MIT License
        
        Copyright (c) 2025 Mohamad Kokh
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: gmsh,mesh,step,stl,tetrahedral,xdmf
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Requires-Dist: gmsh>=4.11
Requires-Dist: meshio>=5.3
Requires-Dist: numpy>=1.23
Requires-Dist: ufl==2024.1.*
Description-Content-Type: text/markdown

# GenEuler Mesh — Lightweight meshing utilities for rapid FEA prep

GenEuler’s `Mesh` module is a tiny, dependency-light wrapper around **Gmsh** and **meshio** that helps you:

* Generate tetrahedral meshes from an axis-aligned bounding box
* Import user geometry (STL/OBJ/PLY/STEP/IGES) and build an outer shell (and, when possible, a watertight volume)
* Tag common boundary node sets (outer box, “floor”, attachment regions)
* Export meshes to `.msh` and `.xdmf`/`.h5` for downstream FEM

---

## Contents

* [Installation](#installation)
* [Units & conventions](#units--conventions)
* [Quick start](#quick-start)
* [API overview](#api-overview)

  * [Constructors](#constructors)
  * [Instance methods](#instance-methods)
* [Workflows](#workflows)

  * [1) Axis-aligned box](#1-axis-aligned-box)
  * [2) Importing surfaces or CAD](#2-importing-surfaces-or-cad)
* [Boundary helpers](#boundary-helpers)
* [Saving meshes](#saving-meshes)
* [Mesh quality & watertightness notes](#mesh-quality--watertightness-notes)
* [Troubleshooting](#troubleshooting)
* [FAQ](#faq)
* [Minimal, complete examples](#minimal-complete-examples)
* [License](#license)

---

## Installation

```bash
# prerequisites
pip install gmsh meshio numpy

# then install GenEuler (replace with your package name if different)
pip install geneuler
```

> **macOS tip (Apple Silicon):** If you hit issues with `gmsh` wheels, ensure Python ≥3.10 and a recent pip. Gmsh publishes prebuilt wheels for common platforms.

---

## Units & conventions

* **Default units:** millimeters (mm)
* **Characteristic length `h`:** direct target element size fed to Gmsh
* **Coordinate system:** right-handed `(x, y, z)`
  By convention, **y** is “vertical” (used by `floor_nodes(y=0)`).

---

## Quick start

```python
from geneuler.mesh import Mesh

# 1) Mesh a box with free h, and write a .msh
m = Mesh.from_bbox(
    {"x": (-60, 60), "y": (0, 150), "z": (-120, 120)},
    h=6.0,
    msh_path="out/box.msh",
)

# 2) Boundary sets
floor_idx = m.floor_nodes(y=0.0)  # all nodes at y≈0
attachments = [((0, 130, -110), 6.0), ((20, 140, 0), 8.0)]
attach_idx = m.attachment_nodes(attachments)  # union of both groups

# 3) Export to XDMF/H5 for FEM
xdmf, h5 = m.save_xdmf("out/box")
print(xdmf, h5)
```

---

## API overview

### `Mesh` dataclass

```python
@dataclass
class Mesh:
    points: np.ndarray                # (N, 3) float
    tets: np.ndarray                  # (M, 4) int (may be empty for shell-only)
    tris: Optional[np.ndarray] = None # (K, 3) int (outer surface triangles)
```

### Constructors

* `Mesh.from_msh(path: str) -> Mesh`
  Load existing Gmsh `.msh` (v2/v4). Works for surface-only or volume meshes.

* `Mesh.from_bbox(bbox, *, h=4.0, refine_ball_center=None, refine_ball_radius=15.0, refine_factor=0.25, msh_path="out/box.msh") -> Mesh`
  Generate a tetrahedral mesh of an axis-aligned box. Optional local refinement “ball”.

  * `bbox`: `{"x": (x0,x1), "y": (y0,y1), "z": (z0,z1)}` or flat `(x0,x1,y0,y1,z0,z1)`
  * `h`: free characteristic length (recommended start: 4–8 mm for \~100–150 mm boxes)
  * `refine_ball_center/radius/factor`: inside ball, local `h' = h * refine_factor`
  * `msh_path`: optional write-through to `.msh`

* `Mesh.from_surface(path, *, h=4.0, reconstruct_volume=True, angle_deg=40.0, include_boundary=True, force_parametrization=False, curve_angle_deg=180.0, msh_path="out/surface.msh", allow_shell_fallback=True) -> Mesh`
  Import STL/OBJ/PLY or STEP/IGES and mesh it. For STL/OBJ/PLY, a quick watertight preflight decides whether to attempt volume reconstruction.

  * If watertight and `reconstruct_volume=True`: attempts tetra volume
  * If not watertight: generates **surface-only** triangles (shell)
  * Includes robust fallbacks if parametrization/volume steps fail

### Instance methods

* `save_msh(path: str) -> None`
  Write `.msh` (triangles and/or tets).

* `save_xdmf(path_without_ext: str) -> tuple[str, str]`
  Write `.xdmf` (and possibly a `.h5` sidecar). Returns `(xdmf_path, h5_path_or_empty)`.

* `boundary_nodes(bbox=DEFAULT_BBOX, tol=1e-6) -> np.ndarray`
  Node indices lying on the 6 faces of the given box.

* `floor_nodes(y: Optional[float] = 0.0, tol=1e-6) -> np.ndarray`
  If `y` is provided, pick nodes with `Y≈y`; if `y=None`, pick nodes at the minimum `Y`.

* `attachment_nodes(attachments, *, default_radius=5.0, return_groups=False)`
  `attachments` can be:

  ```python
  [ ( (x,y,z), r ), ( (x,y,z), r ), ... ]  # explicit radii
  # or
  [ (x,y,z), (x,y,z), ... ]                # uses default_radius
  ```

  Returns the **union** of all groups (indices) by default, or `{i: idxs}` when `return_groups=True`.

---

## Workflows

### 1) Axis-aligned box

```python
m = Mesh.from_bbox(
    {"x": (-50, 50), "y": (0, 130), "z": (-130, 130)},
    h=4.0,                                   # free value
    refine_ball_center=(0, 130, -110),       # optional
    refine_ball_radius=15.0,
    refine_factor=0.25,                      # inside ball: h' = h * refine_factor
    msh_path="out/baseline.msh",
)
print(m.points.shape, m.tets.shape, m.tris.shape if m.tris is not None else None)
```

> **Rule of thumb:** For a \~100–150 mm box, start with `h ≈ 4–8 mm`.
> Decrease `h` for more elements; increase `h` for a coarser preview.

### 2) Importing surfaces or CAD

#### STL / OBJ / PLY

```python
m = Mesh.from_surface(
    "roundedBox1-40x40x40_boxOnly.stl",
    h=3.0,
    reconstruct_volume=True,   # attempt volume if watertight; else shell-only
    msh_path="out/chassis.msh",
)
# If not watertight: triangles (shell). If watertight: tetrahedra (and likely triangles too).
```

Under the hood:

1. **Watertight preflight** (via `meshio`) checks boundary & non-manifold edges.
2. If non-watertight and `reconstruct_volume=True`: skip parametrization/volume; **mesh shell only**.
3. If watertight: classify & parametrize surfaces, build a volume; retry with looser options on failure; finally **fall back** to shell-only if needed.

#### STEP / IGES (CAD solids)

```python
m = Mesh.from_surface(
    "bracket.step",
    h=2.5,
    reconstruct_volume=True,   # if solids present, creates a tetra volume
    msh_path="out/bracket.msh",
)
```

---

## Boundary helpers

```python
# Outer box faces (pass the same bbox you used or any envelope)
b_idx = m.boundary_nodes({"x": (-60, 60), "y": (0, 150), "z": (-120, 120)})

# Floor nodes (default y=0; pass y=None to pick the min Y in the mesh)
floor_idx = m.floor_nodes(y=0.0)

# Attachments: pass list of centers (optionally with radius)
attachments = [((0,130,-110), 6.0), (20,140,0)]  # second uses default_radius
idx_union = m.attachment_nodes(attachments)
idx_groups = m.attachment_nodes(attachments, return_groups=True)  # {0: idx0, 1: idx1}
```

---

## Saving meshes

```python
# 1) Gmsh format (v2/v4 handled by meshio)
m.save_msh("out/model.msh")

# 2) XDMF (Dolfinx/FEniCSx friendly). meshio may emit a sidecar HDF5 if large.
xdmf_path, h5_path = m.save_xdmf("out/model")
print(xdmf_path, h5_path)  # h5_path is "" if no sidecar was written
```

* Surface-only meshes produce **triangles**; volume meshes include **tetrahedra** (and may also include triangles).
* Many FEM solvers need **volume cells**. If you imported a non-watertight STL and only have triangles, either repair the geometry or create a background box and use boolean operations (e.g., Gmsh OCC) before re-meshing.

---

## Mesh quality & watertightness notes

* **Watertightness matters** for volume meshing from STL/OBJ/PLY. The quick check treats any **boundary edges** or **non-manifold edges** as non-watertight → shell-only.
* **Parametrization failures** (e.g., “Wrong topology of boundary mesh” or PETSc convergence issues) are common with noisy triangulations. The importer automatically:

  1. Retries with looser angles and forced parametrization.
  2. Falls back to shell-only meshing if that still fails (unless `allow_shell_fallback=False`).

---

## Troubleshooting

**“No tetrahedral elements were generated.”**
You likely requested `reconstruct_volume=True` on a non-watertight STL/OBJ. Either set `reconstruct_volume=False` (surface only) or repair the mesh and retry.

**“Wrong topology of boundary mesh for parametrization.”**
The input triangulation isn’t suitable for Gmsh’s parametrization. A more permissive retry is attempted automatically. If it still fails, shell-only meshing is returned. Consider:

* repairing the STL (Meshlab/Blender/Netfabb),
* simplifying tiny features/degenerate triangles,
* exporting a STEP/IGES from CAD if available.

**XDMF created but no `.h5`**
`save_xdmf()` returns an empty string for the HDF5 path when meshio inlines data into the XDMF (common for small meshes).

**Apple Silicon issues with `gmsh`**
Use a recent Python and pip; ensure you’re getting the official `gmsh` wheel.

---

## FAQ

**What’s a good starting value for `h`?**

* Boxes \~100–150 mm: **4–8 mm**
* Smaller parts / higher accuracy: **2–4 mm**
* Very coarse preview: **10–15 mm**

**Do I have to pass `bbox` to `boundary_nodes`?**
It should correspond to the envelope whose faces you want to select. If you pass a different box, you’ll tag different nodes.

**Can I tag multiple attachment regions with different radii?**
Yes. Pass `[((x,y,z), r), ((x2,y2,z2), r2), ...]`. Use `return_groups=True` to keep groups separate.

**Will `from_surface` always return tetrahedra?**
Only when the geometry is watertight and volume reconstruction succeeds. Otherwise, you’ll get a triangle shell.

---

## Minimal, complete examples

**A. Box mesh ready for FEM**

```python
from geneuler.mesh import Mesh

bbox = {"x": (-60, 60), "y": (0, 150), "z": (-120, 120)}
m = Mesh.from_bbox(bbox, h=6.0, msh_path="out/box.msh")
b_nodes = m.boundary_nodes(bbox)
floor = m.floor_nodes(y=0.0)
attach = m.attachment_nodes([((0,130,-110), 6.0), ((20,140,0), 8.0)])

xdmf, h5 = m.save_xdmf("out/box")
print("Wrote:", xdmf, h5)
```

**B. Import STL; shell-only fallback handled automatically**

```python
m = Mesh.from_surface(
    "roundedBox1-40x40x40_boxOnly.stl",
    h=4.0,
    reconstruct_volume=True,      # will try; falls back to shell if not watertight
    msh_path="out/shell_or_volume.msh",
)
print("Points:", m.points.shape, "Triangles:", None if m.tris is None else m.tris.shape, "Tets:", m.tets.shape)
m.save_xdmf("out/shell_or_volume")
```

**C. Import STEP and volume mesh**

```python
m = Mesh.from_surface("bracket.step", h=2.5, reconstruct_volume=True, msh_path="out/bracket.msh")
m.save_xdmf("out/bracket")
```

---

## License

This project is distributed under the terms specified in the `LICENSE` file.
