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.
@@ -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
+ [![CI](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml/badge.svg)](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml/badge.svg)](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![CI](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml/badge.svg)](https://github.com/Jeanbjoseph/mesh2flac3d/actions/workflows/ci.yml)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,2 @@
1
+ [console_scripts]
2
+ mesh2flac3d = mesh2flac3d.cli:main
@@ -0,0 +1,6 @@
1
+ meshio>=5
2
+ numpy>=1.20
3
+
4
+ [test]
5
+ pytest>=7
6
+ gmsh>=4
@@ -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()