mesh2flac3d 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mesh2flac3d-0.1.0/LICENSE +21 -0
- mesh2flac3d-0.1.0/PKG-INFO +120 -0
- mesh2flac3d-0.1.0/README.md +97 -0
- mesh2flac3d-0.1.0/pyproject.toml +33 -0
- mesh2flac3d-0.1.0/setup.cfg +4 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d/__init__.py +19 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d/cli.py +67 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d/core.py +248 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d/dat.py +40 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/PKG-INFO +120 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/SOURCES.txt +14 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/dependency_links.txt +1 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/entry_points.txt +2 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/requires.txt +6 -0
- mesh2flac3d-0.1.0/src/mesh2flac3d.egg-info/top_level.txt +1 -0
- mesh2flac3d-0.1.0/tests/test_convert.py +148 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI SIM Engenharia Geotecnica
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mesh2flac3d
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert Gmsh/VTK meshes to Itasca FLAC3D .f3grid, preserving physical groups (ZGROUP/FGROUP) with correct zone winding.
|
|
5
|
+
Author: AI SIM Engenharia Geotecnica
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jeanbjoseph/mesh2flac3d
|
|
8
|
+
Project-URL: Issues, https://github.com/Jeanbjoseph/mesh2flac3d/issues
|
|
9
|
+
Keywords: flac3d,gmsh,mesh,geomechanics,itasca,f3grid,finite-element
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: meshio>=5
|
|
18
|
+
Requires-Dist: numpy>=1.20
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
21
|
+
Requires-Dist: gmsh>=4; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# mesh2flac3d
|
|
25
|
+
|
|
26
|
+
[](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
29
|
+
Convert meshes (Gmsh `.msh`, VTK, and anything [meshio](https://github.com/nschloe/meshio) reads) to **Itasca FLAC3D** `.f3grid` grids — **preserving physical groups**:
|
|
30
|
+
|
|
31
|
+
- **Volume** physical groups → **`ZGROUP`** (zone groups)
|
|
32
|
+
- **Surface** physical groups → **`FGROUP`** (face groups, for boundary conditions)
|
|
33
|
+
- **Correct zone winding**: every zone is reordered to FLAC3D's convention, so you never get *negative-volume zone* errors on import.
|
|
34
|
+
|
|
35
|
+
It reads meshes through `meshio` (MIT) and **does not import `gmsh`**, so it carries no GPL obligation — you can use it freely, including in commercial workflows.
|
|
36
|
+
|
|
37
|
+
## Why not just use meshio?
|
|
38
|
+
|
|
39
|
+
`meshio` has a FLAC3D writer, but for a typical geomechanics mesh (volume **and** surface physical groups) it currently:
|
|
40
|
+
|
|
41
|
+
- **crashes** (`TypeError` in `split_f_z`) when both zone and face groups are present, and
|
|
42
|
+
- **drops all 2D groups** (`"FLAC3D format only supports 3D cells. Skipping triangle…"`), so you lose the face groups you need for boundary conditions, and
|
|
43
|
+
- does not guarantee zone orientation.
|
|
44
|
+
|
|
45
|
+
`mesh2flac3d` is a focused, correct writer built for that exact case.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install mesh2flac3d # once published
|
|
51
|
+
# or, from source:
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Command line
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mesh2flac3d model.msh model.f3grid
|
|
59
|
+
mesh2flac3d model.msh # -> model.f3grid
|
|
60
|
+
mesh2flac3d model.msh out.f3grid --dat out.dat # also write a FLAC3D command skeleton
|
|
61
|
+
mesh2flac3d model.msh --no-faces # zones only
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Example output:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[mesh2flac3d] model.msh -> model.f3grid
|
|
68
|
+
points: 350 zones: 1218 faces: 522
|
|
69
|
+
zone groups: Underburden(401), Salt(414), Overburden(403)
|
|
70
|
+
face groups: Top(90), Bottom(90), Sides(342)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Python API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import mesh2flac3d as m2f
|
|
77
|
+
|
|
78
|
+
grid = m2f.convert("model.msh", "model.f3grid")
|
|
79
|
+
print(grid.zone_groups.keys()) # dict_keys(['Underburden', 'Salt', 'Overburden'])
|
|
80
|
+
print(grid.face_groups.keys()) # dict_keys(['Top', 'Bottom', 'Sides'])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Supported elements
|
|
84
|
+
|
|
85
|
+
| Family | Gmsh / meshio | FLAC3D |
|
|
86
|
+
|--------|------------------------|--------|
|
|
87
|
+
| Zones | tetra | T4 |
|
|
88
|
+
| | pyramid | P5 |
|
|
89
|
+
| | wedge / prism | W6 |
|
|
90
|
+
| | hexahedron | B8 |
|
|
91
|
+
| Faces | triangle | T3 |
|
|
92
|
+
| | quad | Q4 |
|
|
93
|
+
|
|
94
|
+
High-order variants (tetra10, hexahedron20, …) are exported using their linear
|
|
95
|
+
corner nodes.
|
|
96
|
+
|
|
97
|
+
## In Gmsh, name your groups
|
|
98
|
+
|
|
99
|
+
Give your regions and boundaries **Physical Groups** with names — those names
|
|
100
|
+
become the FLAC3D group names:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Physical Volume("Salt") = {2};
|
|
104
|
+
Physical Surface("Top") = {4};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install -e ".[test]"
|
|
111
|
+
pytest -q
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The test fixture is generated with Gmsh (`tests/fixtures/make_testmesh.py`);
|
|
115
|
+
Gmsh is a **test-only** dependency, never used by the package at runtime.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT © AI SIM Engenharia Geotécnica. FLAC3D and Itasca are trademarks of Itasca
|
|
120
|
+
Consulting Group; this project is independent and not affiliated with Itasca.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# mesh2flac3d
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Convert meshes (Gmsh `.msh`, VTK, and anything [meshio](https://github.com/nschloe/meshio) reads) to **Itasca FLAC3D** `.f3grid` grids — **preserving physical groups**:
|
|
7
|
+
|
|
8
|
+
- **Volume** physical groups → **`ZGROUP`** (zone groups)
|
|
9
|
+
- **Surface** physical groups → **`FGROUP`** (face groups, for boundary conditions)
|
|
10
|
+
- **Correct zone winding**: every zone is reordered to FLAC3D's convention, so you never get *negative-volume zone* errors on import.
|
|
11
|
+
|
|
12
|
+
It reads meshes through `meshio` (MIT) and **does not import `gmsh`**, so it carries no GPL obligation — you can use it freely, including in commercial workflows.
|
|
13
|
+
|
|
14
|
+
## Why not just use meshio?
|
|
15
|
+
|
|
16
|
+
`meshio` has a FLAC3D writer, but for a typical geomechanics mesh (volume **and** surface physical groups) it currently:
|
|
17
|
+
|
|
18
|
+
- **crashes** (`TypeError` in `split_f_z`) when both zone and face groups are present, and
|
|
19
|
+
- **drops all 2D groups** (`"FLAC3D format only supports 3D cells. Skipping triangle…"`), so you lose the face groups you need for boundary conditions, and
|
|
20
|
+
- does not guarantee zone orientation.
|
|
21
|
+
|
|
22
|
+
`mesh2flac3d` is a focused, correct writer built for that exact case.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install mesh2flac3d # once published
|
|
28
|
+
# or, from source:
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Command line
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mesh2flac3d model.msh model.f3grid
|
|
36
|
+
mesh2flac3d model.msh # -> model.f3grid
|
|
37
|
+
mesh2flac3d model.msh out.f3grid --dat out.dat # also write a FLAC3D command skeleton
|
|
38
|
+
mesh2flac3d model.msh --no-faces # zones only
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example output:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
[mesh2flac3d] model.msh -> model.f3grid
|
|
45
|
+
points: 350 zones: 1218 faces: 522
|
|
46
|
+
zone groups: Underburden(401), Salt(414), Overburden(403)
|
|
47
|
+
face groups: Top(90), Bottom(90), Sides(342)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Python API
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import mesh2flac3d as m2f
|
|
54
|
+
|
|
55
|
+
grid = m2f.convert("model.msh", "model.f3grid")
|
|
56
|
+
print(grid.zone_groups.keys()) # dict_keys(['Underburden', 'Salt', 'Overburden'])
|
|
57
|
+
print(grid.face_groups.keys()) # dict_keys(['Top', 'Bottom', 'Sides'])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Supported elements
|
|
61
|
+
|
|
62
|
+
| Family | Gmsh / meshio | FLAC3D |
|
|
63
|
+
|--------|------------------------|--------|
|
|
64
|
+
| Zones | tetra | T4 |
|
|
65
|
+
| | pyramid | P5 |
|
|
66
|
+
| | wedge / prism | W6 |
|
|
67
|
+
| | hexahedron | B8 |
|
|
68
|
+
| Faces | triangle | T3 |
|
|
69
|
+
| | quad | Q4 |
|
|
70
|
+
|
|
71
|
+
High-order variants (tetra10, hexahedron20, …) are exported using their linear
|
|
72
|
+
corner nodes.
|
|
73
|
+
|
|
74
|
+
## In Gmsh, name your groups
|
|
75
|
+
|
|
76
|
+
Give your regions and boundaries **Physical Groups** with names — those names
|
|
77
|
+
become the FLAC3D group names:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Physical Volume("Salt") = {2};
|
|
81
|
+
Physical Surface("Top") = {4};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e ".[test]"
|
|
88
|
+
pytest -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The test fixture is generated with Gmsh (`tests/fixtures/make_testmesh.py`);
|
|
92
|
+
Gmsh is a **test-only** dependency, never used by the package at runtime.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT © AI SIM Engenharia Geotécnica. FLAC3D and Itasca are trademarks of Itasca
|
|
97
|
+
Consulting Group; this project is independent and not affiliated with Itasca.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mesh2flac3d"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convert Gmsh/VTK meshes to Itasca FLAC3D .f3grid, preserving physical groups (ZGROUP/FGROUP) with correct zone winding."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "AI SIM Engenharia Geotecnica" }]
|
|
13
|
+
keywords = ["flac3d", "gmsh", "mesh", "geomechanics", "itasca", "f3grid", "finite-element"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["meshio>=5", "numpy>=1.20"]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = ["pytest>=7", "gmsh>=4"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Jeanbjoseph/mesh2flac3d"
|
|
27
|
+
Issues = "https://github.com/Jeanbjoseph/mesh2flac3d/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
mesh2flac3d = "mesh2flac3d.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mesh2flac3d — Export meshes (Gmsh .msh, VTK, ...) to Itasca FLAC3D .f3grid,
|
|
3
|
+
preserving physical groups as zone groups (ZGROUP) and boundary/face groups
|
|
4
|
+
as face groups (FGROUP), with guaranteed right-handed (positive-volume) zones.
|
|
5
|
+
|
|
6
|
+
Reads via `meshio` (any format it supports); writes a correct FLAC3D grid.
|
|
7
|
+
Does NOT import `gmsh`, so it carries no GPL obligation.
|
|
8
|
+
|
|
9
|
+
Basic use:
|
|
10
|
+
|
|
11
|
+
import mesh2flac3d as m2f
|
|
12
|
+
m2f.convert("model.msh", "model.f3grid")
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2026 AI SIM Engenharia Geotecnica. MIT License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .core import convert, write_f3grid, Grid, __version__
|
|
18
|
+
|
|
19
|
+
__all__ = ["convert", "write_f3grid", "Grid", "__version__"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Command-line interface: ``mesh2flac3d input.msh output.f3grid``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .core import convert, __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser():
|
|
12
|
+
p = argparse.ArgumentParser(
|
|
13
|
+
prog="mesh2flac3d",
|
|
14
|
+
description="Convert a mesh (Gmsh .msh, VTK, ...) to FLAC3D .f3grid, "
|
|
15
|
+
"preserving physical groups as ZGROUP/FGROUP.",
|
|
16
|
+
)
|
|
17
|
+
p.add_argument("input", help="input mesh (e.g. model.msh)")
|
|
18
|
+
p.add_argument("output", nargs="?", help="output .f3grid (default: input with .f3grid)")
|
|
19
|
+
p.add_argument("--no-faces", action="store_true",
|
|
20
|
+
help="do not export 2D physical groups as faces/FGROUP")
|
|
21
|
+
p.add_argument("--float-fmt", default=".10e", help="coordinate format (default: .10e)")
|
|
22
|
+
p.add_argument("--slot", default="Default", help="FLAC3D group slot name")
|
|
23
|
+
p.add_argument("--input-format", default=None,
|
|
24
|
+
help="force meshio input format instead of inferring")
|
|
25
|
+
p.add_argument("--dat", metavar="FILE", default=None,
|
|
26
|
+
help="also write a FLAC3D command-file skeleton (.dat)")
|
|
27
|
+
p.add_argument("-q", "--quiet", action="store_true", help="suppress summary")
|
|
28
|
+
p.add_argument("--version", action="version", version=f"mesh2flac3d {__version__}")
|
|
29
|
+
return p
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv=None):
|
|
33
|
+
args = build_parser().parse_args(argv)
|
|
34
|
+
output = args.output
|
|
35
|
+
if output is None:
|
|
36
|
+
output = args.input.rsplit(".", 1)[0] + ".f3grid"
|
|
37
|
+
|
|
38
|
+
grid = convert(
|
|
39
|
+
args.input, output,
|
|
40
|
+
keep_faces=not args.no_faces,
|
|
41
|
+
float_fmt=args.float_fmt,
|
|
42
|
+
slot=args.slot,
|
|
43
|
+
input_format=args.input_format,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if args.dat:
|
|
47
|
+
from .dat import write_dat_skeleton
|
|
48
|
+
write_dat_skeleton(args.dat, output, grid)
|
|
49
|
+
|
|
50
|
+
if not args.quiet:
|
|
51
|
+
nz = sum(len(c[1]) for c in grid.zone_cells)
|
|
52
|
+
nf = sum(len(c[1]) for c in grid.face_cells)
|
|
53
|
+
print(f"[mesh2flac3d] {args.input} -> {output}")
|
|
54
|
+
print(f" points: {len(grid.points)} zones: {nz} faces: {nf}")
|
|
55
|
+
if grid.zone_groups:
|
|
56
|
+
print(" zone groups: " + ", ".join(
|
|
57
|
+
f"{k}({len(v)})" for k, v in grid.zone_groups.items()))
|
|
58
|
+
if grid.face_groups:
|
|
59
|
+
print(" face groups: " + ", ".join(
|
|
60
|
+
f"{k}({len(v)})" for k, v in grid.face_groups.items()))
|
|
61
|
+
if args.dat:
|
|
62
|
+
print(f" command skeleton: {args.dat}")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Core conversion logic: meshio Mesh -> FLAC3D .f3grid.
|
|
2
|
+
|
|
3
|
+
The FLAC3D grid (.f3grid) ASCII format used here:
|
|
4
|
+
|
|
5
|
+
* GRIDPOINTS
|
|
6
|
+
G <gid> <x> <y> <z>
|
|
7
|
+
* ZONES
|
|
8
|
+
Z <T4|W6|P5|B8> <zid> <n1> <n2> ...
|
|
9
|
+
* ZONE GROUPS
|
|
10
|
+
ZGROUP "<name>" SLOT <slot>
|
|
11
|
+
<zid> <zid> ...
|
|
12
|
+
* FACES
|
|
13
|
+
F <T3|Q4> <fid> <n1> <n2> ...
|
|
14
|
+
* FACE GROUPS
|
|
15
|
+
FGROUP "<name>" SLOT <slot>
|
|
16
|
+
<fid> <fid> ...
|
|
17
|
+
|
|
18
|
+
Node/zone/face IDs are 1-based. Zones and faces have independent ID spaces.
|
|
19
|
+
Zone connectivity is reordered so the first four corner nodes form a
|
|
20
|
+
right-handed system (positive volume) — FLAC3D rejects inverted zones.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
# meshio cell type -> FLAC3D keyword
|
|
30
|
+
MESHIO_TO_FLAC3D_TYPE = {
|
|
31
|
+
"tetra": "T4",
|
|
32
|
+
"pyramid": "P5",
|
|
33
|
+
"wedge": "W6",
|
|
34
|
+
"hexahedron": "B8",
|
|
35
|
+
"triangle": "T3",
|
|
36
|
+
"quad": "Q4",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# 3D (zone) vs 2D (face) meshio types, mapping any high-order variant to its
|
|
40
|
+
# linear corner topology (we export corner nodes only).
|
|
41
|
+
ZONE_TYPES = {
|
|
42
|
+
"tetra": "tetra", "tetra10": "tetra",
|
|
43
|
+
"pyramid": "pyramid", "pyramid13": "pyramid",
|
|
44
|
+
"wedge": "wedge", "wedge12": "wedge", "wedge15": "wedge", "wedge18": "wedge",
|
|
45
|
+
"hexahedron": "hexahedron", "hexahedron20": "hexahedron",
|
|
46
|
+
"hexahedron24": "hexahedron", "hexahedron27": "hexahedron",
|
|
47
|
+
}
|
|
48
|
+
FACE_TYPES = {
|
|
49
|
+
"triangle": "triangle", "triangle6": "triangle", "triangle7": "triangle",
|
|
50
|
+
"quad": "quad", "quad8": "quad", "quad9": "quad",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Node reordering meshio -> FLAC3D, and the mirror ordering used when the first
|
|
54
|
+
# variant yields a left-handed (negative) zone.
|
|
55
|
+
ORDER = {
|
|
56
|
+
"tetra": [0, 1, 2, 3],
|
|
57
|
+
"pyramid": [0, 1, 3, 4, 2],
|
|
58
|
+
"wedge": [0, 1, 3, 2, 4, 5],
|
|
59
|
+
"hexahedron": [0, 1, 3, 4, 2, 7, 5, 6],
|
|
60
|
+
}
|
|
61
|
+
ORDER_MIRROR = {
|
|
62
|
+
"tetra": [0, 2, 1, 3],
|
|
63
|
+
"pyramid": [0, 3, 1, 4, 2],
|
|
64
|
+
"wedge": [0, 2, 3, 1, 5, 4],
|
|
65
|
+
"hexahedron": [0, 3, 1, 4, 2, 5, 7, 6],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# cell_set keys produced by meshio readers that are not real physical groups.
|
|
69
|
+
_INTERNAL_SET_PREFIXES = ("gmsh:", "medit:", "cell_set-")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_internal_key(key: str) -> bool:
|
|
73
|
+
return any(key.startswith(p) for p in _INTERNAL_SET_PREFIXES)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _reorder_zone(points, conn, key):
|
|
77
|
+
"""Reorder one block of zone connectivity to right-handed FLAC3D order.
|
|
78
|
+
|
|
79
|
+
`conn` is (n, k) corner indices into `points`; `key` is the linear type.
|
|
80
|
+
FLAC3D requires each zone to have positive volume: the first four corner
|
|
81
|
+
nodes must form a right-handed system, i.e. the scalar triple product
|
|
82
|
+
(p1-p0)x(p2-p0)-(p3-p0) > 0. (Verified against FLAC3D 7.0, which rejects a
|
|
83
|
+
grid with "tet volumes are <= 0" otherwise.) Returns (n, k) reordered so
|
|
84
|
+
every zone imports without a negative-volume error.
|
|
85
|
+
"""
|
|
86
|
+
o1 = ORDER[key]
|
|
87
|
+
o2 = ORDER_MIRROR[key]
|
|
88
|
+
reordered = conn[:, o1]
|
|
89
|
+
p = points[reordered[:, :4]]
|
|
90
|
+
a = p[:, 1] - p[:, 0]
|
|
91
|
+
b = p[:, 2] - p[:, 0]
|
|
92
|
+
c = p[:, 3] - p[:, 0]
|
|
93
|
+
det = np.einsum("ij,ij->i", np.cross(a, b), c)
|
|
94
|
+
# keep o1 where it already yields positive volume (det > 0); else mirror it
|
|
95
|
+
out = np.where((det > 0)[:, None], conn[:, o1], conn[:, o2])
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Grid:
|
|
100
|
+
"""A FLAC3D grid ready to be written.
|
|
101
|
+
|
|
102
|
+
Built from a meshio Mesh. Splits cells into zones (3D) and faces (2D),
|
|
103
|
+
fixes zone winding, and resolves physical groups into zone/face groups
|
|
104
|
+
keyed by name.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, points, zone_cells, face_cells, zone_groups, face_groups):
|
|
108
|
+
self.points = points # (npts, 3) float
|
|
109
|
+
self.zone_cells = zone_cells # list of (flac3d_type, (n,k) int, base_id)
|
|
110
|
+
self.face_cells = face_cells # list of (flac3d_type, (n,k) int, base_id)
|
|
111
|
+
self.zone_groups = zone_groups # {name: np.array of 1-based zone ids}
|
|
112
|
+
self.face_groups = face_groups # {name: np.array of 1-based face ids}
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_meshio(cls, mesh, keep_faces=True):
|
|
116
|
+
points = np.asarray(mesh.points, dtype=float)
|
|
117
|
+
if points.shape[1] == 2: # promote 2D coords to 3D
|
|
118
|
+
points = np.column_stack([points, np.zeros(len(points))])
|
|
119
|
+
|
|
120
|
+
# Assign global 1-based ids per block; map block index -> id offset.
|
|
121
|
+
zone_cells, face_cells = [], []
|
|
122
|
+
zone_block_base = {} # block_index -> first zone id (1-based)
|
|
123
|
+
face_block_base = {}
|
|
124
|
+
zid = 0
|
|
125
|
+
fid = 0
|
|
126
|
+
for bi, cb in enumerate(mesh.cells):
|
|
127
|
+
ct = cb.type
|
|
128
|
+
data = np.asarray(cb.data)
|
|
129
|
+
if ct in ZONE_TYPES:
|
|
130
|
+
key = ZONE_TYPES[ct]
|
|
131
|
+
corners = data[:, : len(ORDER[key])]
|
|
132
|
+
conn = _reorder_zone(points, corners, key)
|
|
133
|
+
zone_block_base[bi] = zid + 1
|
|
134
|
+
zone_cells.append((MESHIO_TO_FLAC3D_TYPE[key], conn, zid + 1))
|
|
135
|
+
zid += len(conn)
|
|
136
|
+
elif keep_faces and ct in FACE_TYPES:
|
|
137
|
+
key = FACE_TYPES[ct]
|
|
138
|
+
ncorner = 3 if key == "triangle" else 4
|
|
139
|
+
conn = data[:, :ncorner]
|
|
140
|
+
face_block_base[bi] = fid + 1
|
|
141
|
+
face_cells.append((MESHIO_TO_FLAC3D_TYPE[key], conn, fid + 1))
|
|
142
|
+
fid += len(conn)
|
|
143
|
+
|
|
144
|
+
zone_groups = cls._resolve_groups(mesh, zone_block_base, ZONE_TYPES)
|
|
145
|
+
face_groups = (
|
|
146
|
+
cls._resolve_groups(mesh, face_block_base, FACE_TYPES)
|
|
147
|
+
if keep_faces else {}
|
|
148
|
+
)
|
|
149
|
+
return cls(points, zone_cells, face_cells, zone_groups, face_groups)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _resolve_groups(mesh, block_base, type_filter):
|
|
153
|
+
"""Turn meshio cell_sets into {name: 1-based global ids} for the given
|
|
154
|
+
cell family (zone or face). Falls back to cell_data gmsh:physical if no
|
|
155
|
+
named cell_sets exist."""
|
|
156
|
+
groups = {}
|
|
157
|
+
cell_sets = getattr(mesh, "cell_sets", None) or {}
|
|
158
|
+
for name, per_block in cell_sets.items():
|
|
159
|
+
if _is_internal_key(name):
|
|
160
|
+
continue
|
|
161
|
+
ids = []
|
|
162
|
+
for bi, local in enumerate(per_block):
|
|
163
|
+
if bi not in block_base or local is None:
|
|
164
|
+
continue
|
|
165
|
+
local = np.asarray(local)
|
|
166
|
+
if local.size == 0:
|
|
167
|
+
continue
|
|
168
|
+
ids.append(local + block_base[bi])
|
|
169
|
+
if ids:
|
|
170
|
+
groups[name] = np.unique(np.concatenate(ids))
|
|
171
|
+
return groups
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _write_table(f, ids, per_line=10):
|
|
175
|
+
ids = np.asarray(ids, dtype=int)
|
|
176
|
+
for i in range(0, len(ids), per_line):
|
|
177
|
+
f.write(" " + " ".join(str(v) for v in ids[i : i + per_line]) + "\n")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def write_f3grid(grid: Grid, path, float_fmt=".10e", slot="Default"):
|
|
181
|
+
"""Write a Grid to a FLAC3D .f3grid ASCII file."""
|
|
182
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
183
|
+
f.write(f"* FLAC3D grid produced by mesh2flac3d {__version__}\n")
|
|
184
|
+
|
|
185
|
+
f.write("* GRIDPOINTS\n")
|
|
186
|
+
fmt = "G {:d} " + " ".join(["{:" + float_fmt + "}"] * 3) + "\n"
|
|
187
|
+
for i, (x, y, z) in enumerate(grid.points, start=1):
|
|
188
|
+
f.write(fmt.format(i, x, y, z))
|
|
189
|
+
|
|
190
|
+
f.write("* ZONES\n")
|
|
191
|
+
for ftype, conn, base in grid.zone_cells:
|
|
192
|
+
for j, row in enumerate(conn):
|
|
193
|
+
zid = base + j
|
|
194
|
+
nodes = " ".join(str(int(v) + 1) for v in row)
|
|
195
|
+
f.write(f"Z {ftype} {zid} {nodes}\n")
|
|
196
|
+
|
|
197
|
+
if grid.zone_groups:
|
|
198
|
+
f.write("* ZONE GROUPS\n")
|
|
199
|
+
for name, ids in grid.zone_groups.items():
|
|
200
|
+
f.write(f'ZGROUP "{name}" SLOT "{slot}"\n')
|
|
201
|
+
_write_table(f, ids)
|
|
202
|
+
|
|
203
|
+
if grid.face_cells:
|
|
204
|
+
f.write("* FACES\n")
|
|
205
|
+
for ftype, conn, base in grid.face_cells:
|
|
206
|
+
for j, row in enumerate(conn):
|
|
207
|
+
fid = base + j
|
|
208
|
+
nodes = " ".join(str(int(v) + 1) for v in row)
|
|
209
|
+
f.write(f"F {ftype} {fid} {nodes}\n")
|
|
210
|
+
|
|
211
|
+
if grid.face_groups:
|
|
212
|
+
f.write("* FACE GROUPS\n")
|
|
213
|
+
for name, ids in grid.face_groups.items():
|
|
214
|
+
f.write(f'FGROUP "{name}" SLOT "{slot}"\n')
|
|
215
|
+
_write_table(f, ids)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def convert(input_path, output_path, keep_faces=True, float_fmt=".10e",
|
|
219
|
+
slot="Default", input_format=None):
|
|
220
|
+
"""Read a mesh (any meshio-supported format) and write a FLAC3D .f3grid.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
input_path : str
|
|
225
|
+
Source mesh (e.g. Gmsh ``.msh``). Physical volume groups become zone
|
|
226
|
+
groups; physical surface groups become face groups.
|
|
227
|
+
output_path : str
|
|
228
|
+
Destination ``.f3grid``.
|
|
229
|
+
keep_faces : bool
|
|
230
|
+
Export 2D physical groups as FLAC3D faces/FGROUP (for boundary
|
|
231
|
+
conditions). Set False to export zones only.
|
|
232
|
+
float_fmt : str
|
|
233
|
+
Coordinate format spec.
|
|
234
|
+
slot : str
|
|
235
|
+
FLAC3D group slot name.
|
|
236
|
+
input_format : str, optional
|
|
237
|
+
Force a meshio input format instead of inferring from the extension.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
Grid
|
|
242
|
+
"""
|
|
243
|
+
import meshio # imported lazily; meshio is MIT-licensed
|
|
244
|
+
|
|
245
|
+
mesh = meshio.read(input_path, file_format=input_format)
|
|
246
|
+
grid = Grid.from_meshio(mesh, keep_faces=keep_faces)
|
|
247
|
+
write_f3grid(grid, output_path, float_fmt=float_fmt, slot=slot)
|
|
248
|
+
return grid
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Optional FLAC3D command-file (.dat) skeleton generator.
|
|
2
|
+
|
|
3
|
+
Emits a *generic* starting script that imports the grid and stubs out a
|
|
4
|
+
constitutive model assignment per zone group and a boundary condition per face
|
|
5
|
+
group. It contains no project-specific parameters — the user fills those in.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_dat_skeleton(path, f3grid_path, grid):
|
|
14
|
+
grid_name = os.path.basename(f3grid_path)
|
|
15
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
16
|
+
f.write("; FLAC3D command skeleton generated by mesh2flac3d\n")
|
|
17
|
+
f.write("; Fill in constitutive models, properties and BC values.\n\n")
|
|
18
|
+
f.write("model new\n")
|
|
19
|
+
f.write("model large-strain off\n")
|
|
20
|
+
f.write(f"zone import '{grid_name}'\n\n")
|
|
21
|
+
|
|
22
|
+
f.write("; --- Constitutive model per zone group ---\n")
|
|
23
|
+
if grid.zone_groups:
|
|
24
|
+
for name in grid.zone_groups:
|
|
25
|
+
f.write(f"zone cmodel assign elastic range group '{name}'\n")
|
|
26
|
+
f.write(f"; zone property young 1e9 poisson 0.3 range group '{name}'\n")
|
|
27
|
+
else:
|
|
28
|
+
f.write("; (no zone groups found)\n")
|
|
29
|
+
f.write("\n")
|
|
30
|
+
|
|
31
|
+
f.write("; --- Boundary conditions per face group ---\n")
|
|
32
|
+
if grid.face_groups:
|
|
33
|
+
for name in grid.face_groups:
|
|
34
|
+
f.write(f"; zone face apply velocity-normal 0 range group '{name}'\n")
|
|
35
|
+
else:
|
|
36
|
+
f.write("; (no face groups found)\n")
|
|
37
|
+
f.write("\n")
|
|
38
|
+
|
|
39
|
+
f.write("; model gravity 0 0 -9.81\n")
|
|
40
|
+
f.write("; model solve\n")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mesh2flac3d
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert Gmsh/VTK meshes to Itasca FLAC3D .f3grid, preserving physical groups (ZGROUP/FGROUP) with correct zone winding.
|
|
5
|
+
Author: AI SIM Engenharia Geotecnica
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jeanbjoseph/mesh2flac3d
|
|
8
|
+
Project-URL: Issues, https://github.com/Jeanbjoseph/mesh2flac3d/issues
|
|
9
|
+
Keywords: flac3d,gmsh,mesh,geomechanics,itasca,f3grid,finite-element
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: meshio>=5
|
|
18
|
+
Requires-Dist: numpy>=1.20
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
21
|
+
Requires-Dist: gmsh>=4; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# mesh2flac3d
|
|
25
|
+
|
|
26
|
+
[](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
29
|
+
Convert meshes (Gmsh `.msh`, VTK, and anything [meshio](https://github.com/nschloe/meshio) reads) to **Itasca FLAC3D** `.f3grid` grids — **preserving physical groups**:
|
|
30
|
+
|
|
31
|
+
- **Volume** physical groups → **`ZGROUP`** (zone groups)
|
|
32
|
+
- **Surface** physical groups → **`FGROUP`** (face groups, for boundary conditions)
|
|
33
|
+
- **Correct zone winding**: every zone is reordered to FLAC3D's convention, so you never get *negative-volume zone* errors on import.
|
|
34
|
+
|
|
35
|
+
It reads meshes through `meshio` (MIT) and **does not import `gmsh`**, so it carries no GPL obligation — you can use it freely, including in commercial workflows.
|
|
36
|
+
|
|
37
|
+
## Why not just use meshio?
|
|
38
|
+
|
|
39
|
+
`meshio` has a FLAC3D writer, but for a typical geomechanics mesh (volume **and** surface physical groups) it currently:
|
|
40
|
+
|
|
41
|
+
- **crashes** (`TypeError` in `split_f_z`) when both zone and face groups are present, and
|
|
42
|
+
- **drops all 2D groups** (`"FLAC3D format only supports 3D cells. Skipping triangle…"`), so you lose the face groups you need for boundary conditions, and
|
|
43
|
+
- does not guarantee zone orientation.
|
|
44
|
+
|
|
45
|
+
`mesh2flac3d` is a focused, correct writer built for that exact case.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install mesh2flac3d # once published
|
|
51
|
+
# or, from source:
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Command line
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mesh2flac3d model.msh model.f3grid
|
|
59
|
+
mesh2flac3d model.msh # -> model.f3grid
|
|
60
|
+
mesh2flac3d model.msh out.f3grid --dat out.dat # also write a FLAC3D command skeleton
|
|
61
|
+
mesh2flac3d model.msh --no-faces # zones only
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Example output:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[mesh2flac3d] model.msh -> model.f3grid
|
|
68
|
+
points: 350 zones: 1218 faces: 522
|
|
69
|
+
zone groups: Underburden(401), Salt(414), Overburden(403)
|
|
70
|
+
face groups: Top(90), Bottom(90), Sides(342)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Python API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import mesh2flac3d as m2f
|
|
77
|
+
|
|
78
|
+
grid = m2f.convert("model.msh", "model.f3grid")
|
|
79
|
+
print(grid.zone_groups.keys()) # dict_keys(['Underburden', 'Salt', 'Overburden'])
|
|
80
|
+
print(grid.face_groups.keys()) # dict_keys(['Top', 'Bottom', 'Sides'])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Supported elements
|
|
84
|
+
|
|
85
|
+
| Family | Gmsh / meshio | FLAC3D |
|
|
86
|
+
|--------|------------------------|--------|
|
|
87
|
+
| Zones | tetra | T4 |
|
|
88
|
+
| | pyramid | P5 |
|
|
89
|
+
| | wedge / prism | W6 |
|
|
90
|
+
| | hexahedron | B8 |
|
|
91
|
+
| Faces | triangle | T3 |
|
|
92
|
+
| | quad | Q4 |
|
|
93
|
+
|
|
94
|
+
High-order variants (tetra10, hexahedron20, …) are exported using their linear
|
|
95
|
+
corner nodes.
|
|
96
|
+
|
|
97
|
+
## In Gmsh, name your groups
|
|
98
|
+
|
|
99
|
+
Give your regions and boundaries **Physical Groups** with names — those names
|
|
100
|
+
become the FLAC3D group names:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Physical Volume("Salt") = {2};
|
|
104
|
+
Physical Surface("Top") = {4};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install -e ".[test]"
|
|
111
|
+
pytest -q
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The test fixture is generated with Gmsh (`tests/fixtures/make_testmesh.py`);
|
|
115
|
+
Gmsh is a **test-only** dependency, never used by the package at runtime.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT © AI SIM Engenharia Geotécnica. FLAC3D and Itasca are trademarks of Itasca
|
|
120
|
+
Consulting Group; this project is independent and not affiliated with Itasca.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/mesh2flac3d/__init__.py
|
|
5
|
+
src/mesh2flac3d/cli.py
|
|
6
|
+
src/mesh2flac3d/core.py
|
|
7
|
+
src/mesh2flac3d/dat.py
|
|
8
|
+
src/mesh2flac3d.egg-info/PKG-INFO
|
|
9
|
+
src/mesh2flac3d.egg-info/SOURCES.txt
|
|
10
|
+
src/mesh2flac3d.egg-info/dependency_links.txt
|
|
11
|
+
src/mesh2flac3d.egg-info/entry_points.txt
|
|
12
|
+
src/mesh2flac3d.egg-info/requires.txt
|
|
13
|
+
src/mesh2flac3d.egg-info/top_level.txt
|
|
14
|
+
tests/test_convert.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mesh2flac3d
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""End-to-end tests. gmsh is used ONLY to build the fixture mesh, never by the
|
|
2
|
+
package itself. Run: pytest -q (needs meshio, numpy, gmsh)."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import meshio
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import mesh2flac3d as m2f
|
|
13
|
+
|
|
14
|
+
HERE = os.path.dirname(__file__)
|
|
15
|
+
FIXGEN = os.path.join(HERE, "fixtures", "make_testmesh.py")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(scope="module")
|
|
19
|
+
def msh(tmp_path_factory):
|
|
20
|
+
out = tmp_path_factory.mktemp("mesh") / "box3.msh"
|
|
21
|
+
subprocess.run([sys.executable, FIXGEN, str(out)], check=True)
|
|
22
|
+
return str(out)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_f3grid_zones(path):
|
|
26
|
+
"""Minimal parser: returns points(dict id->xyz) and zones(list of node-id lists)."""
|
|
27
|
+
pts, zones = {}, []
|
|
28
|
+
with open(path, encoding="utf-8") as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
s = line.split()
|
|
31
|
+
if not s:
|
|
32
|
+
continue
|
|
33
|
+
if s[0] == "G":
|
|
34
|
+
pts[int(s[1])] = np.array([float(s[2]), float(s[3]), float(s[4])])
|
|
35
|
+
elif s[0] == "Z":
|
|
36
|
+
zones.append([int(x) for x in s[3:]])
|
|
37
|
+
return pts, zones
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _first4_positive(pts, zones):
|
|
41
|
+
"""Every FLAC3D zone must have its first four nodes right-handed
|
|
42
|
+
(triple product > 0) — the invariant FLAC3D checks on import."""
|
|
43
|
+
bad = 0
|
|
44
|
+
for n in zones:
|
|
45
|
+
p0, p1, p2, p3 = (pts[i] for i in n[:4])
|
|
46
|
+
if np.dot(np.cross(p1 - p0, p2 - p0), p3 - p0) <= 0:
|
|
47
|
+
bad += 1
|
|
48
|
+
return bad
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _build_mixed_mesh():
|
|
52
|
+
"""In-memory mesh with wedges (W6) and a pyramid (P5), half of them
|
|
53
|
+
deliberately inverted, to exercise winding correction without gmsh.
|
|
54
|
+
Mirrors the case validated live in FLAC3D 7.0 (vtot == 1e6, negzero == 0)."""
|
|
55
|
+
corners = np.array([[0, 0], [100, 0], [100, 100], [0, 100]], float)
|
|
56
|
+
zlev = [0.0, -30.0, -60.0, -90.0]
|
|
57
|
+
pts, idx = [], {}
|
|
58
|
+
for k, z in enumerate(zlev):
|
|
59
|
+
for c, (x, y) in enumerate(corners):
|
|
60
|
+
idx[(c, k)] = len(pts)
|
|
61
|
+
pts.append([x, y, z])
|
|
62
|
+
pts = np.array(pts, float)
|
|
63
|
+
tris = [(0, 1, 2), (0, 2, 3)]
|
|
64
|
+
wedges, layer = [], []
|
|
65
|
+
for k in range(3):
|
|
66
|
+
for a, b, c in tris:
|
|
67
|
+
wedges.append([idx[(a, k + 1)], idx[(b, k + 1)], idx[(c, k + 1)],
|
|
68
|
+
idx[(a, k)], idx[(b, k)], idx[(c, k)]])
|
|
69
|
+
layer.append(k)
|
|
70
|
+
wedges = np.array(wedges)
|
|
71
|
+
for i in range(0, len(wedges), 2): # invert every other wedge
|
|
72
|
+
wedges[i] = wedges[i][[3, 4, 5, 0, 1, 2]]
|
|
73
|
+
base_p = [idx[(0, 0)], idx[(1, 0)], idx[(2, 0)], idx[(3, 0)]]
|
|
74
|
+
apex = len(pts)
|
|
75
|
+
pts = np.vstack([pts, [50, 50, 30]])
|
|
76
|
+
pyr = np.array([[*base_p, apex]])
|
|
77
|
+
cells = [("wedge", wedges), ("pyramid", pyr)]
|
|
78
|
+
sets = {}
|
|
79
|
+
for k, nm in enumerate(["Overburden", "Salt", "Underburden"]):
|
|
80
|
+
sets[nm] = [np.array([i for i, lk in enumerate(layer) if lk == k]),
|
|
81
|
+
np.array([], dtype=int)]
|
|
82
|
+
sets["Cap"] = [np.array([], dtype=int), np.array([0])]
|
|
83
|
+
return meshio.Mesh(points=pts, cells=cells, cell_sets=sets)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_wedge_pyramid_winding(tmp_path):
|
|
87
|
+
out = str(tmp_path / "wedp5.f3grid")
|
|
88
|
+
grid = m2f.Grid.from_meshio(_build_mixed_mesh())
|
|
89
|
+
m2f.write_f3grid(grid, out)
|
|
90
|
+
pts, zones = _read_f3grid_zones(out)
|
|
91
|
+
assert len(zones) == 7 # 6 wedges + 1 pyramid
|
|
92
|
+
assert _first4_positive(pts, zones) == 0
|
|
93
|
+
assert set(grid.zone_groups) == {"Overburden", "Salt", "Underburden", "Cap"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_convert_preserves_groups(msh, tmp_path):
|
|
97
|
+
out = str(tmp_path / "box3.f3grid")
|
|
98
|
+
grid = m2f.convert(msh, out)
|
|
99
|
+
|
|
100
|
+
# zone groups: the three volume layers must survive
|
|
101
|
+
assert set(grid.zone_groups) == {"Overburden", "Salt", "Underburden"}
|
|
102
|
+
# face groups: the three boundary sets must survive as FGROUP
|
|
103
|
+
assert set(grid.face_groups) == {"Top", "Bottom", "Sides"}
|
|
104
|
+
|
|
105
|
+
# every zone assigned to exactly one group (partition of the zone set)
|
|
106
|
+
total_zones = sum(len(c[1]) for c in grid.zone_cells)
|
|
107
|
+
grouped = np.concatenate(list(grid.zone_groups.values()))
|
|
108
|
+
assert len(grouped) == total_zones
|
|
109
|
+
assert len(np.unique(grouped)) == total_zones
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_no_negative_volume_tets(msh, tmp_path):
|
|
113
|
+
out = str(tmp_path / "box3.f3grid")
|
|
114
|
+
m2f.convert(msh, out)
|
|
115
|
+
pts, zones = _read_f3grid_zones(out)
|
|
116
|
+
tets = [z for z in zones if len(z) == 4]
|
|
117
|
+
assert tets, "expected tetrahedra in the fixture"
|
|
118
|
+
bad = 0
|
|
119
|
+
for n in tets:
|
|
120
|
+
p0, p1, p2, p3 = (pts[i] for i in n)
|
|
121
|
+
# FLAC3D requires positive zone volume: triple product > 0.
|
|
122
|
+
# (Confirmed by FLAC3D 7.0, which errors on "tet volumes are <= 0".)
|
|
123
|
+
det = np.dot(np.cross(p1 - p0, p2 - p0), p3 - p0)
|
|
124
|
+
if det <= 0:
|
|
125
|
+
bad += 1
|
|
126
|
+
assert bad == 0, f"{bad} non-positive-volume tetrahedra written"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_roundtrip_with_meshio_reader(msh, tmp_path):
|
|
130
|
+
"""Our .f3grid must be readable back by meshio's FLAC3D reader."""
|
|
131
|
+
out = str(tmp_path / "box3.f3grid")
|
|
132
|
+
m2f.convert(msh, out, keep_faces=False) # meshio reader focuses on zones
|
|
133
|
+
back = meshio.read(out)
|
|
134
|
+
n_tetra = sum(len(cb.data) for cb in back.cells if cb.type == "tetra")
|
|
135
|
+
assert n_tetra > 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_cli(msh, tmp_path):
|
|
139
|
+
out = str(tmp_path / "cli.f3grid")
|
|
140
|
+
dat = str(tmp_path / "cli.dat")
|
|
141
|
+
rc = subprocess.run(
|
|
142
|
+
[sys.executable, "-m", "mesh2flac3d.cli", msh, out, "--dat", dat],
|
|
143
|
+
capture_output=True, text=True,
|
|
144
|
+
)
|
|
145
|
+
assert rc.returncode == 0, rc.stderr
|
|
146
|
+
assert os.path.exists(out) and os.path.exists(dat)
|
|
147
|
+
assert "ZGROUP" in open(out, encoding="utf-8").read()
|
|
148
|
+
assert "FGROUP" in open(out, encoding="utf-8").read()
|