diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..27dffffaf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = [ + "setuptools", + "wheel", + "scikit-build", + "cmake", + "pybind11-stubgen==2.5", + "netgen-mesher", + "mkl", + "intel-cmplr-lib-rt", + "requests", + "packaging", +] +build-backend = "setuptools.build_meta" diff --git a/utils/vol_to_gmsh/README.md b/utils/vol_to_gmsh/README.md new file mode 100644 index 000000000..b67866858 --- /dev/null +++ b/utils/vol_to_gmsh/README.md @@ -0,0 +1,123 @@ +# Netgen .vol to Gmsh/VTK Converter + +## Overview + +Convert Netgen `.vol` mesh files to Gmsh `.msh` or VTK `.vtk` format for visualization. + +## Requirements + +- NGSolve / Netgen (installed) +- Gmsh (optional, for viewing .msh files) +- ParaView (optional, for viewing .vtk files) + +## Usage + +### Basic Gmsh Conversion + +```bash +# Convert .vol to .msh (Gmsh format) +python vol_to_gmsh.py input.vol + +# Output: input.msh +``` + +### VTK Conversion + +```bash +# Convert .vol to .vtk (VTK format) +python vol_to_gmsh.py input.vol --vtk + +# Output: input.vtk +``` + +### Custom Output Name + +```bash +# Specify output filename +python vol_to_gmsh.py input.vol output.msh +python vol_to_gmsh.py input.vol output.vtk --vtk +``` + +### View in Gmsh + +```bash +# Convert and open in Gmsh +python vol_to_gmsh.py input.vol --view +``` + +## Examples + +### Example 1: Convert cube mesh + +```bash +python vol_to_gmsh.py cube.vol +``` + +### Example 2: Convert to VTK format for ParaView + +```bash +python vol_to_gmsh.py mesh.vol --vtk +``` + +## Gmsh Format Details + +The script exports to **Gmsh Format 2.x** (ASCII format): + +- Section `$Nodes`: Vertex coordinates (1-indexed) +- Section `$Elements`: Elements with type IDs +- Supports up to 2nd order elements + +### Element Type IDs (Gmsh 2.x) + +| Type ID | Element | Nodes | +|---------|---------|-------| +| 2 | Triangle | 3 | +| 4 | Tetrahedron | 4 | +| 5 | Hexahedron | 8 | +| 6 | Wedge/Prism | 6 | + +For details, see the [Gmsh documentation](https://gmsh.info/doc/texinfo/gmsh.html). + +## VTK Format Details + +VTK format is suitable for: +- ParaView visualization +- Python analysis with PyVista +- Legacy VTK readers + +## Tested Files + +Successfully converted: +- `cube.vol` (228 vertices, 756 elements) +- `coil.vol` (331 vertices, 1709 elements) +- `shaft.vol` (558 vertices, 1622 elements) +- `square.vol` (2D mesh) + +## Troubleshooting + +### Error: File not found + +Check that the input `.vol` file exists: +```bash +ls -l input.vol +``` + +### Error: Gmsh not found in PATH + +Install Gmsh from https://gmsh.info/ or disable `--view` option. + +### Unicode encoding errors (Windows) + +Script now uses ASCII characters `[OK]` instead of Unicode checkmarks for Windows cp932 compatibility. + +## Related Tools + +- **Gmsh**: https://gmsh.info/ +- **ParaView**: https://www.paraview.org/ +- **PyVista**: https://docs.pyvista.org/ + +## Notes + +- Gmsh format version can be selected with `--format gmsh` or `--format gmsh2` +- Default: Gmsh 2.x format (widely supported) +- VTK format includes mesh geometry only (no field data) diff --git a/utils/vol_to_gmsh/test_vol_to_gmsh.py b/utils/vol_to_gmsh/test_vol_to_gmsh.py new file mode 100644 index 000000000..bd54cabcb --- /dev/null +++ b/utils/vol_to_gmsh/test_vol_to_gmsh.py @@ -0,0 +1,68 @@ +"""Tests for vol_to_gmsh converter.""" + +import os +import pytest +from pathlib import Path +from vol_to_gmsh import convert_vol_to_gmsh, convert_vol_to_vtk + + +@pytest.fixture +def sample_mesh(tmp_path): + """Create a simple test mesh.""" + from netgen.csg import unit_cube + mesh = unit_cube.GenerateMesh(maxh=0.5) + vol_file = str(tmp_path / "test.vol") + mesh.Save(vol_file) + return vol_file + + +def test_convert_to_gmsh(sample_mesh, tmp_path): + """Test basic Gmsh conversion.""" + output = str(tmp_path / "test.msh") + result = convert_vol_to_gmsh(sample_mesh, output) + assert os.path.exists(result) + assert result == output + + with open(result) as f: + content = f.read() + assert "$MeshFormat" in content + assert "$Nodes" in content + assert "$Elements" in content + + +def test_convert_to_gmsh_default_name(sample_mesh): + """Test Gmsh conversion with auto-generated filename.""" + result = convert_vol_to_gmsh(sample_mesh) + expected = str(Path(sample_mesh).with_suffix('.msh')) + assert result == expected + assert os.path.exists(result) + + +def test_convert_to_vtk(sample_mesh, tmp_path): + """Test VTK conversion.""" + output = str(tmp_path / "test") + result = convert_vol_to_vtk(sample_mesh, output) + assert os.path.exists(result) + assert result.endswith('.vtu') + + +def test_missing_file(): + """Test error handling for missing file.""" + with pytest.raises(RuntimeError): + convert_vol_to_gmsh("nonexistent.vol") + + +def test_mixed_elements(tmp_path): + """Test conversion of mesh with multiple element types.""" + from netgen.occ import Box, OCCGeometry, Pnt + + box = Box(Pnt(0, 0, 0), Pnt(1, 1, 1)) + geo = OCCGeometry(box) + mesh = geo.GenerateMesh(maxh=0.5) + + vol_file = str(tmp_path / "mixed.vol") + mesh.Save(vol_file) + + output = str(tmp_path / "mixed.msh") + result = convert_vol_to_gmsh(vol_file, output) + assert os.path.exists(result) diff --git a/utils/vol_to_gmsh/vol_to_gmsh.py b/utils/vol_to_gmsh/vol_to_gmsh.py new file mode 100644 index 000000000..eefa1ef80 --- /dev/null +++ b/utils/vol_to_gmsh/vol_to_gmsh.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +""" +Convert Netgen .vol mesh to Gmsh .msh format for visualization. + +Usage: + python vol_to_gmsh.py input.vol [output.msh] + +Features: +- Converts Netgen volume mesh (.vol) to Gmsh format (.msh) +- Preserves element types (hex, tet, wedge) +- Supports NGSolve mesh with materials +- Can open in Gmsh for visualization + +Requirements: +- NGSolve / Netgen +- Gmsh (for viewing, optional) + +License: + MIT License + Copyright (c) 2026 NGSolve Contributors + +Author: + Created for NGSolve project mesh conversion utilities +""" + +import sys +import os +from pathlib import Path + + +def convert_vol_to_gmsh(vol_file, gmsh_file=None, format='gmsh2'): + """ + Convert Netgen .vol to Gmsh .msh format. + + Parameters: + ----------- + vol_file : str + Input .vol file path + gmsh_file : str, optional + Output .msh file path (default: same as input with .msh extension) + format : str + 'gmsh' or 'gmsh2' (default: gmsh2 for modern Gmsh) + + Returns: + -------- + str + Path to the generated Gmsh file + + Raises: + ------- + RuntimeError + If mesh loading or export fails + """ + from netgen.meshing import Mesh as NetgenMesh + + # Load Netgen mesh + print(f"Loading: {vol_file}") + ngmesh = NetgenMesh() + + try: + ngmesh.Load(vol_file) + except Exception as e: + raise RuntimeError(f"Failed to load mesh file: {e}") + + # Get mesh info (updated API) + nv = len(ngmesh.Points()) + ne = len(ngmesh.Elements3D()) + nse = len(ngmesh.Elements2D()) + + print(f" Vertices: {nv}") + print(f" Volume elements: {ne}") + print(f" Surface elements: {nse}") + + # Check for empty mesh + if nv == 0: + raise RuntimeError("Mesh has no vertices") + + # Warn if no volume elements (surface-only mesh) + if ne == 0 and nse > 0: + print(" Note: Surface-only mesh (no volume elements)") + + # Determine output filename + if gmsh_file is None: + vol_path = Path(vol_file) + gmsh_file = str(vol_path.with_suffix('.msh')) + + # Export to Gmsh format + print(f"Exporting to Gmsh format: {gmsh_file}") + + # Note: Netgen Export() uses format string to determine output format + # For Gmsh: use 'Gmsh Format' or 'Gmsh2 Format' + if format == 'gmsh2': + format_str = 'Gmsh2 Format' + else: + format_str = 'Gmsh Format' + + try: + ngmesh.Export(gmsh_file, format_str) + except Exception as e: + raise RuntimeError(f"Failed to export to Gmsh format: {e}") + + print(f"[OK] Conversion complete: {gmsh_file}") + print(f"\nTo view in Gmsh:") + print(f" gmsh {gmsh_file}") + + return gmsh_file + + +def convert_vol_to_vtk(vol_file, vtk_file=None): + """ + Convert Netgen .vol to VTK format (alternative to Gmsh). + + Uses NGSolve VTKOutput for conversion. + + Parameters: + ----------- + vol_file : str + Input .vol file path + vtk_file : str, optional + Output .vtk file path (default: same as input with .vtk extension) + + Returns: + -------- + str + Path to the generated VTK file + + Raises: + ------- + RuntimeError + If mesh loading or export fails + """ + from ngsolve import Mesh, VTKOutput + + print(f"Loading NGSolve mesh: {vol_file}") + + try: + mesh = Mesh(vol_file) + except Exception as e: + raise RuntimeError(f"Failed to load mesh with NGSolve: {e}") + + # Determine output filename + if vtk_file is None: + vol_path = Path(vol_file) + vtk_file = str(vol_path.with_suffix('')) # VTKOutput adds .vtk + + print(f"Exporting to VTK: {vtk_file}.vtu") + + # Export using VTKOutput (creates .vtu file - VTK Unstructured Grid) + try: + vtk = VTKOutput(mesh, coefs=[], names=[], filename=vtk_file) + vtk.Do() + except Exception as e: + raise RuntimeError(f"Failed to export to VTK format: {e}") + + print(f"[OK] VTK export complete: {vtk_file}.vtu") + print(f"\nTo view in ParaView:") + print(f" paraview {vtk_file}.vtu") + + return vtk_file + '.vtu' + + +def main(): + """Main entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description='Convert Netgen .vol to Gmsh .msh or VTK format', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Convert to Gmsh (default) + python vol_to_gmsh.py mesh.vol + + # Convert to Gmsh with custom output name + python vol_to_gmsh.py mesh.vol output.msh + + # Convert to VTK instead + python vol_to_gmsh.py mesh.vol --vtk + + # View in Gmsh after conversion + python vol_to_gmsh.py mesh.vol --view + """ + ) + + parser.add_argument('vol_file', help='Input .vol file') + parser.add_argument('output_file', nargs='?', help='Output .msh or .vtk file (optional)') + parser.add_argument('--vtk', action='store_true', help='Export to VTK instead of Gmsh') + parser.add_argument('--format', choices=['gmsh', 'gmsh2'], default='gmsh2', + help='Gmsh format version (default: gmsh2)') + parser.add_argument('--view', action='store_true', help='Open in Gmsh after conversion') + + args = parser.parse_args() + + # Check input file exists + if not os.path.exists(args.vol_file): + print(f"Error: File not found: {args.vol_file}") + sys.exit(1) + + # Convert + try: + if args.vtk: + output_file = convert_vol_to_vtk(args.vol_file, args.output_file) + else: + output_file = convert_vol_to_gmsh(args.vol_file, args.output_file, args.format) + except RuntimeError as e: + print(f"Error during conversion: {e}") + sys.exit(1) + except Exception as e: + print(f"Unexpected error: {e}") + sys.exit(1) + + # View in Gmsh if requested + if args.view and not args.vtk: + print(f"\nLaunching Gmsh...") + import subprocess + try: + subprocess.run(['gmsh', output_file]) + except FileNotFoundError: + print("Error: Gmsh not found in PATH") + print("Install Gmsh: https://gmsh.info/") + except Exception as e: + print(f"Error launching Gmsh: {e}") + + +if __name__ == '__main__': + main()