sgio 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl
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.
- sgio/_version.py +1 -1
- sgio/core/builder.py +2 -1
- sgio/iofunc/gmsh/_gmsh41_refactored.py +1115 -0
- sgio/model/failure.py +7 -9
- sgio/model/solid.py +405 -440
- {sgio-0.3.1.dist-info → sgio-0.3.2.dist-info}/METADATA +1 -1
- {sgio-0.3.1.dist-info → sgio-0.3.2.dist-info}/RECORD +10 -9
- {sgio-0.3.1.dist-info → sgio-0.3.2.dist-info}/WHEEL +0 -0
- {sgio-0.3.1.dist-info → sgio-0.3.2.dist-info}/entry_points.txt +0 -0
- {sgio-0.3.1.dist-info → sgio-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import BinaryIO, TextIO, Union, Tuple, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
from meshio import CellBlock, Mesh
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from ._common import (
|
|
13
|
+
_fast_forward_over_blank_lines,
|
|
14
|
+
_fast_forward_to_end_block,
|
|
15
|
+
_gmsh_to_meshio_order,
|
|
16
|
+
_gmsh_to_meshio_type,
|
|
17
|
+
_meshio_to_gmsh_order,
|
|
18
|
+
_meshio_to_gmsh_type,
|
|
19
|
+
_read_data,
|
|
20
|
+
_read_physical_names,
|
|
21
|
+
_write_data,
|
|
22
|
+
_write_physical_names,
|
|
23
|
+
num_nodes_per_cell,
|
|
24
|
+
cell_data_from_raw,
|
|
25
|
+
)
|
|
26
|
+
from sgio.iofunc._meshio import (
|
|
27
|
+
warn,
|
|
28
|
+
raw_from_cell_data,
|
|
29
|
+
WriteError,
|
|
30
|
+
ReadError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
"""Gmsh 4.1 format mesh I/O.
|
|
34
|
+
|
|
35
|
+
This module implements reading and writing of Gmsh 4.1 MSH file format.
|
|
36
|
+
The format is specified at:
|
|
37
|
+
http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format
|
|
38
|
+
|
|
39
|
+
Main entry points:
|
|
40
|
+
- read_buffer(): Read a Gmsh 4.1 format mesh from a file buffer
|
|
41
|
+
- write_buffer(): Write a mesh to a file buffer in Gmsh 4.1 format
|
|
42
|
+
|
|
43
|
+
Both binary and ASCII formats are supported.
|
|
44
|
+
|
|
45
|
+
Format Structure:
|
|
46
|
+
- $MeshFormat section: Version and mode information
|
|
47
|
+
- $PhysicalNames section: Named physical groups (optional)
|
|
48
|
+
- $Entities section: Geometric entities and their relationships (optional)
|
|
49
|
+
- $Nodes section: Node coordinates and tags
|
|
50
|
+
- $Elements section: Element connectivity
|
|
51
|
+
- $Periodic section: Periodic boundary conditions (optional)
|
|
52
|
+
- $NodeData/$ElementData sections: Field data (optional)
|
|
53
|
+
- $ElementNodeData sections: Nodal data per element (optional)
|
|
54
|
+
|
|
55
|
+
Key Features:
|
|
56
|
+
- Unified GmshWriter class for binary/ASCII output
|
|
57
|
+
- Helper functions for modular code organization
|
|
58
|
+
- Comprehensive type hints for IDE support
|
|
59
|
+
- Support for entity tags, physical groups, and bounding entities
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
c_int = np.dtype("i")
|
|
65
|
+
c_size_t = np.dtype("P")
|
|
66
|
+
c_double = np.dtype("d")
|
|
67
|
+
|
|
68
|
+
# Type aliases
|
|
69
|
+
FileHandle = Union[BinaryIO, TextIO]
|
|
70
|
+
DimTag = Tuple[int, int] # (dimension, tag)
|
|
71
|
+
PhysicalTags = Tuple[Dict[int, List[int]], ...] # indexed by dimension
|
|
72
|
+
BoundingEntities = Tuple[Dict[int, npt.NDArray], ...]
|
|
73
|
+
|
|
74
|
+
# Gmsh format constants
|
|
75
|
+
GMSH_MAX_DIM = 4 # Maximum entity dimension (point=0, curve=1, surface=2, volume=3)
|
|
76
|
+
GMSH_BBOX_POINT_COORDS = 3 # Bounding box coordinates for point entities
|
|
77
|
+
GMSH_BBOX_ENTITY_COORDS = 6 # Bounding box coordinates for entities with dim > 0
|
|
78
|
+
GMSH_NODE_TAG_OFFSET = 1 # Gmsh uses 1-based indexing, Python uses 0-based
|
|
79
|
+
|
|
80
|
+
# Component count constraints (from Gmsh spec)
|
|
81
|
+
GMSH_VALID_COMPONENTS = (1, 3, 9) # Valid number of components per data field
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _size_type(data_size):
|
|
85
|
+
return np.dtype(f"u{data_size}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GmshWriter:
|
|
89
|
+
"""Helper class to handle binary/ASCII writing modes uniformly."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, fh: FileHandle, binary: bool):
|
|
92
|
+
self.fh = fh
|
|
93
|
+
self.binary = binary
|
|
94
|
+
|
|
95
|
+
def write_string(self, text: str) -> None:
|
|
96
|
+
"""Write string in appropriate format."""
|
|
97
|
+
if self.binary:
|
|
98
|
+
self.fh.write(text.encode() if isinstance(text, str) else text)
|
|
99
|
+
else:
|
|
100
|
+
self.fh.write(text)
|
|
101
|
+
|
|
102
|
+
def write_array(self, data, dtype, sep: str = " ", fmt: str = None) -> None:
|
|
103
|
+
"""Write numpy array in appropriate format."""
|
|
104
|
+
arr = np.asarray(data, dtype=dtype)
|
|
105
|
+
if self.binary:
|
|
106
|
+
arr.tofile(self.fh)
|
|
107
|
+
else:
|
|
108
|
+
if fmt:
|
|
109
|
+
arr = np.atleast_2d(arr)
|
|
110
|
+
np.savetxt(self.fh, arr, fmt=fmt, delimiter=sep, newline=sep)
|
|
111
|
+
else:
|
|
112
|
+
arr.tofile(self.fh, sep, "%d")
|
|
113
|
+
|
|
114
|
+
def write_section_header(self, section_name: str) -> None:
|
|
115
|
+
"""Write section header like $Entities."""
|
|
116
|
+
self.write_string(f"${section_name}\n")
|
|
117
|
+
|
|
118
|
+
def write_section_footer(self, section_name: str) -> None:
|
|
119
|
+
"""Write section footer like $EndEntities."""
|
|
120
|
+
self.write_string(f"$End{section_name}\n")
|
|
121
|
+
|
|
122
|
+
def newline(self) -> None:
|
|
123
|
+
"""Write a newline."""
|
|
124
|
+
self.write_string("\n" if not self.binary else b"\n")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ====================================================================
|
|
128
|
+
# Readers
|
|
129
|
+
# ====================================================================
|
|
130
|
+
|
|
131
|
+
def read_buffer(f: FileHandle, is_ascii: bool, data_size: int) -> Mesh:
|
|
132
|
+
"""Read gmsh 4.1 format mesh from buffer.
|
|
133
|
+
|
|
134
|
+
The format is specified at
|
|
135
|
+
<http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format>.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
f: File handle to read from
|
|
139
|
+
is_ascii: Whether the file is in ASCII mode
|
|
140
|
+
data_size: Size of size_t type in bytes
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Mesh object containing the mesh data
|
|
144
|
+
"""
|
|
145
|
+
# Initialize the optional data fields
|
|
146
|
+
points = []
|
|
147
|
+
cells = None
|
|
148
|
+
field_data = {}
|
|
149
|
+
cell_data_raw = {}
|
|
150
|
+
cell_tags = {}
|
|
151
|
+
point_data = {}
|
|
152
|
+
physical_tags = None
|
|
153
|
+
bounding_entities = None
|
|
154
|
+
cell_sets = {}
|
|
155
|
+
periodic = None
|
|
156
|
+
|
|
157
|
+
while True:
|
|
158
|
+
# fast-forward over blank lines
|
|
159
|
+
line, is_eof = _fast_forward_over_blank_lines(f)
|
|
160
|
+
if is_eof:
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if line[0] != "$":
|
|
164
|
+
raise ReadError(f"Unexpected line {repr(line)}")
|
|
165
|
+
|
|
166
|
+
environ = line[1:].strip()
|
|
167
|
+
|
|
168
|
+
if environ == "PhysicalNames":
|
|
169
|
+
_read_physical_names(f, field_data)
|
|
170
|
+
elif environ == "Entities":
|
|
171
|
+
# Read physical tags and information on bounding entities.
|
|
172
|
+
physical_tags, bounding_entities = _read_entities(f, is_ascii, data_size)
|
|
173
|
+
elif environ == "Nodes":
|
|
174
|
+
points, point_tags, point_entities = _read_nodes(f, is_ascii, data_size)
|
|
175
|
+
elif environ == "Elements":
|
|
176
|
+
cells, cell_tags, cell_sets = _read_elements(
|
|
177
|
+
f,
|
|
178
|
+
point_tags,
|
|
179
|
+
physical_tags,
|
|
180
|
+
bounding_entities,
|
|
181
|
+
is_ascii,
|
|
182
|
+
data_size,
|
|
183
|
+
field_data,
|
|
184
|
+
)
|
|
185
|
+
elif environ == "Periodic":
|
|
186
|
+
periodic = _read_periodic(f, is_ascii, data_size)
|
|
187
|
+
elif environ == "NodeData":
|
|
188
|
+
_read_data(f, "NodeData", point_data, data_size, is_ascii)
|
|
189
|
+
elif environ == "ElementData":
|
|
190
|
+
_read_data(f, "ElementData", cell_data_raw, data_size, is_ascii)
|
|
191
|
+
else:
|
|
192
|
+
# Skip unrecognized sections
|
|
193
|
+
_fast_forward_to_end_block(f, environ)
|
|
194
|
+
|
|
195
|
+
if cells is None:
|
|
196
|
+
raise ReadError("$Element section not found.")
|
|
197
|
+
|
|
198
|
+
cell_data = cell_data_from_raw(cells, cell_data_raw)
|
|
199
|
+
cell_data.update(cell_tags)
|
|
200
|
+
|
|
201
|
+
# Add node entity information to the point data
|
|
202
|
+
point_data.update({"gmsh:dim_tags": point_entities})
|
|
203
|
+
|
|
204
|
+
return Mesh(
|
|
205
|
+
points,
|
|
206
|
+
cells,
|
|
207
|
+
point_data=point_data,
|
|
208
|
+
cell_data=cell_data,
|
|
209
|
+
field_data=field_data,
|
|
210
|
+
cell_sets=cell_sets,
|
|
211
|
+
gmsh_periodic=periodic,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _read_entities(f: FileHandle, is_ascii: bool, data_size: int) -> Tuple[PhysicalTags, BoundingEntities]:
|
|
216
|
+
"""Read the entity section.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
f: File handle to read from
|
|
220
|
+
is_ascii: Whether the file is in ASCII mode
|
|
221
|
+
data_size: Size of size_t type in bytes
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Tuple of (physical_tags, bounding_entities) where:
|
|
225
|
+
- physical_tags: Physical tags indexed by dimension
|
|
226
|
+
- bounding_entities: Bounding entities indexed by dimension
|
|
227
|
+
"""
|
|
228
|
+
fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
|
|
229
|
+
c_size_t = _size_type(data_size)
|
|
230
|
+
physical_tags = ({}, {}, {}, {})
|
|
231
|
+
bounding_entities = ({}, {}, {}, {})
|
|
232
|
+
number = fromfile(f, c_size_t, 4) # dims 0, 1, 2, 3
|
|
233
|
+
|
|
234
|
+
for d, n in enumerate(number):
|
|
235
|
+
for _ in range(n):
|
|
236
|
+
(tag,) = fromfile(f, c_int, 1)
|
|
237
|
+
fromfile(f, c_double, 3 if d == 0 else 6) # discard bounding-box
|
|
238
|
+
(num_physicals,) = fromfile(f, c_size_t, 1)
|
|
239
|
+
physical_tags[d][tag] = list(fromfile(f, c_int, num_physicals))
|
|
240
|
+
if d > 0:
|
|
241
|
+
# Number of bounding entities
|
|
242
|
+
num_BREP_ = fromfile(f, c_size_t, 1)[0]
|
|
243
|
+
# Store bounding entities
|
|
244
|
+
bounding_entities[d][tag] = fromfile(f, c_int, num_BREP_)
|
|
245
|
+
|
|
246
|
+
_fast_forward_to_end_block(f, "Entities")
|
|
247
|
+
return physical_tags, bounding_entities
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _read_nodes(f: FileHandle, is_ascii: bool, data_size: int) -> Tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
|
|
251
|
+
"""Read node data: Node coordinates and tags.
|
|
252
|
+
|
|
253
|
+
Also find the entities of the nodes, and store this as point_data.
|
|
254
|
+
Note that entity tags are 1-offset within each dimension, thus it is
|
|
255
|
+
necessary to keep track of both tag and dimension of the entity.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
f: File handle to read from
|
|
259
|
+
is_ascii: Whether the file is in ASCII mode
|
|
260
|
+
data_size: Size of size_t type in bytes
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (points, tags, dim_tags) where:
|
|
264
|
+
- points: Node coordinates (N, 3)
|
|
265
|
+
- tags: Node tags (N,)
|
|
266
|
+
- dim_tags: Entity dimension and tags for each node (N, 2)
|
|
267
|
+
"""
|
|
268
|
+
fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
|
|
269
|
+
c_size_t = _size_type(data_size)
|
|
270
|
+
|
|
271
|
+
# numEntityBlocks numNodes minNodeTag maxNodeTag (all size_t)
|
|
272
|
+
num_entity_blocks, total_num_nodes, _, _ = fromfile(f, c_size_t, 4)
|
|
273
|
+
|
|
274
|
+
points = np.empty((total_num_nodes, 3), dtype=float)
|
|
275
|
+
tags = np.empty(total_num_nodes, dtype=int)
|
|
276
|
+
dim_tags = np.empty((total_num_nodes, 2), dtype=int)
|
|
277
|
+
|
|
278
|
+
idx = 0
|
|
279
|
+
for _ in range(num_entity_blocks):
|
|
280
|
+
# entityDim(int) entityTag(int) parametric(int) numNodes(size_t)
|
|
281
|
+
dim, entity_tag, parametric = fromfile(f, c_int, 3)
|
|
282
|
+
if parametric != 0:
|
|
283
|
+
raise ReadError("parametric nodes not implemented")
|
|
284
|
+
num_nodes = int(fromfile(f, c_size_t, 1)[0])
|
|
285
|
+
|
|
286
|
+
ixx = slice(idx, idx + num_nodes)
|
|
287
|
+
tags[ixx] = fromfile(f, c_size_t, num_nodes) - 1
|
|
288
|
+
|
|
289
|
+
# x(double) y(double) z(double) (* numNodes)
|
|
290
|
+
points[ixx] = fromfile(f, c_double, num_nodes * 3).reshape((num_nodes, 3))
|
|
291
|
+
|
|
292
|
+
# Entity tag and entity dimension of the nodes
|
|
293
|
+
dim_tags[ixx, 0] = dim
|
|
294
|
+
dim_tags[ixx, 1] = entity_tag
|
|
295
|
+
idx += num_nodes
|
|
296
|
+
|
|
297
|
+
_fast_forward_to_end_block(f, "Nodes")
|
|
298
|
+
return points, tags, dim_tags
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _read_elements(
|
|
302
|
+
f: FileHandle,
|
|
303
|
+
point_tags: npt.NDArray,
|
|
304
|
+
physical_tags: Optional[PhysicalTags],
|
|
305
|
+
bounding_entities: Optional[BoundingEntities],
|
|
306
|
+
is_ascii: bool,
|
|
307
|
+
data_size: int,
|
|
308
|
+
field_data: Dict[str, Tuple[int, int]]
|
|
309
|
+
) -> Tuple[List[CellBlock], Dict, Dict]:
|
|
310
|
+
"""Read element data from gmsh 4.1 file.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
f: File handle to read from
|
|
314
|
+
point_tags: Point tags array
|
|
315
|
+
physical_tags: Physical tags indexed by dimension
|
|
316
|
+
bounding_entities: Bounding entities indexed by dimension
|
|
317
|
+
is_ascii: Whether the file is in ASCII mode
|
|
318
|
+
data_size: Size of size_t type in bytes
|
|
319
|
+
field_data: Field data dictionary
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Tuple of (cells, cell_data, cell_sets)
|
|
323
|
+
"""
|
|
324
|
+
fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
|
|
325
|
+
c_size_t = _size_type(data_size)
|
|
326
|
+
|
|
327
|
+
# numEntityBlocks numElements minElementTag maxElementTag (all size_t)
|
|
328
|
+
num_entity_blocks, _, _, _ = fromfile(f, c_size_t, 4)
|
|
329
|
+
|
|
330
|
+
data = []
|
|
331
|
+
cell_data = {}
|
|
332
|
+
cell_sets = {k: [None] * num_entity_blocks for k in field_data.keys()}
|
|
333
|
+
|
|
334
|
+
for k in range(num_entity_blocks):
|
|
335
|
+
# entityDim(int) entityTag(int) elementType(int) numElements(size_t)
|
|
336
|
+
dim, tag, type_ele = fromfile(f, c_int, 3)
|
|
337
|
+
(num_ele,) = fromfile(f, c_size_t, 1)
|
|
338
|
+
for physical_name, cell_set in cell_sets.items():
|
|
339
|
+
cell_set[k] = np.arange(
|
|
340
|
+
num_ele
|
|
341
|
+
if (
|
|
342
|
+
physical_tags
|
|
343
|
+
and field_data[physical_name][1] == dim
|
|
344
|
+
and field_data[physical_name][0] in physical_tags[dim][tag]
|
|
345
|
+
)
|
|
346
|
+
else 0,
|
|
347
|
+
dtype=type(num_ele),
|
|
348
|
+
)
|
|
349
|
+
tpe = _gmsh_to_meshio_type[type_ele]
|
|
350
|
+
num_nodes_per_ele = num_nodes_per_cell[tpe]
|
|
351
|
+
d = fromfile(f, c_size_t, int(num_ele * (1 + num_nodes_per_ele))).reshape(
|
|
352
|
+
(num_ele, -1)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Find physical tag, if defined; else it is None.
|
|
356
|
+
pt = None if not physical_tags else physical_tags[dim][tag]
|
|
357
|
+
# Bounding entities (of lower dimension) if defined.
|
|
358
|
+
if dim > 0 and bounding_entities:
|
|
359
|
+
be = bounding_entities[dim][tag]
|
|
360
|
+
else:
|
|
361
|
+
be = None
|
|
362
|
+
data.append((pt, be, tag, tpe, d))
|
|
363
|
+
|
|
364
|
+
_fast_forward_to_end_block(f, "Elements")
|
|
365
|
+
|
|
366
|
+
# Inverse point tags
|
|
367
|
+
inv_tags = np.full(np.max(point_tags) + 1, -1, dtype=int)
|
|
368
|
+
inv_tags[point_tags] = np.arange(len(point_tags))
|
|
369
|
+
|
|
370
|
+
# Note that the first column in the data array is the element tag; discard it.
|
|
371
|
+
# Ensure integer types for node indices
|
|
372
|
+
data = [
|
|
373
|
+
(physical_tag, bound_entity, geom_tag, tpe,
|
|
374
|
+
inv_tags[(d[:, 1:].astype(int) - 1)])
|
|
375
|
+
for physical_tag, bound_entity, geom_tag, tpe, d in data
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
cells = []
|
|
379
|
+
for physical_tag, bound_entity, geom_tag, key, values in data:
|
|
380
|
+
# Ensure the cell data is integer type
|
|
381
|
+
cells.append(CellBlock(key, _gmsh_to_meshio_order(key, values.astype(int))))
|
|
382
|
+
if physical_tag:
|
|
383
|
+
if "gmsh:physical" not in cell_data:
|
|
384
|
+
cell_data["gmsh:physical"] = []
|
|
385
|
+
cell_data["gmsh:physical"].append(
|
|
386
|
+
np.full(len(values), physical_tag[0], int)
|
|
387
|
+
)
|
|
388
|
+
if "gmsh:geometrical" not in cell_data:
|
|
389
|
+
cell_data["gmsh:geometrical"] = []
|
|
390
|
+
cell_data["gmsh:geometrical"].append(np.full(len(values), geom_tag, int))
|
|
391
|
+
|
|
392
|
+
# The bounding entities is stored in the cell_sets.
|
|
393
|
+
if bounding_entities:
|
|
394
|
+
if "gmsh:bounding_entities" not in cell_sets:
|
|
395
|
+
cell_sets["gmsh:bounding_entities"] = []
|
|
396
|
+
cell_sets["gmsh:bounding_entities"].append(bound_entity)
|
|
397
|
+
|
|
398
|
+
return cells, cell_data, cell_sets
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _read_periodic(f: FileHandle, is_ascii: bool, data_size: int) -> List:
|
|
402
|
+
"""Read periodic information from gmsh 4.1 file.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
f: File handle to read from
|
|
406
|
+
is_ascii: Whether the file is in ASCII mode
|
|
407
|
+
data_size: Size of size_t type in bytes
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of periodic boundary information
|
|
411
|
+
"""
|
|
412
|
+
fromfile = partial(np.fromfile, sep=" " if is_ascii else "")
|
|
413
|
+
c_size_t = _size_type(data_size)
|
|
414
|
+
periodic = []
|
|
415
|
+
# numPeriodicLinks(size_t)
|
|
416
|
+
num_periodic = int(fromfile(f, c_size_t, 1)[0])
|
|
417
|
+
for _ in range(num_periodic):
|
|
418
|
+
# entityDim(int) entityTag(int) entityTagMaster(int)
|
|
419
|
+
edim, stag, mtag = fromfile(f, c_int, 3)
|
|
420
|
+
# numAffine(size_t) value(double) ...
|
|
421
|
+
num_affine = int(fromfile(f, c_size_t, 1)[0])
|
|
422
|
+
affine = fromfile(f, c_double, num_affine)
|
|
423
|
+
# numCorrespondingNodes(size_t)
|
|
424
|
+
num_nodes = int(fromfile(f, c_size_t, 1)[0])
|
|
425
|
+
# nodeTag(size_t) nodeTagMaster(size_t) ...
|
|
426
|
+
slave_master = fromfile(f, c_size_t, num_nodes * 2).reshape(-1, 2)
|
|
427
|
+
slave_master = slave_master - 1 # Subtract one, Python is 0-based
|
|
428
|
+
periodic.append([edim, (stag, mtag), affine, slave_master])
|
|
429
|
+
|
|
430
|
+
_fast_forward_to_end_block(f, "Periodic")
|
|
431
|
+
return periodic
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ====================================================================
|
|
435
|
+
# Writers
|
|
436
|
+
# ====================================================================
|
|
437
|
+
|
|
438
|
+
def write_buffer(
|
|
439
|
+
file: FileHandle,
|
|
440
|
+
mesh: Mesh,
|
|
441
|
+
float_fmt: str,
|
|
442
|
+
mesh_only: bool,
|
|
443
|
+
binary: bool,
|
|
444
|
+
**kwargs
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Write mesh to msh file format.
|
|
447
|
+
|
|
448
|
+
Format specification:
|
|
449
|
+
<http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format>
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
file: File handle to write to
|
|
453
|
+
mesh: Mesh object to write
|
|
454
|
+
float_fmt: Format string for floating point numbers
|
|
455
|
+
mesh_only: If True, skip writing entity information
|
|
456
|
+
binary: Whether to write in binary mode
|
|
457
|
+
**kwargs: Additional keyword arguments (currently unused)
|
|
458
|
+
"""
|
|
459
|
+
logger.debug('writing gmsh buffer...')
|
|
460
|
+
logger.debug(locals())
|
|
461
|
+
|
|
462
|
+
# Filter the point data: gmsh:dim_tags are tags, the rest is actual point data.
|
|
463
|
+
point_data = {}
|
|
464
|
+
for key, d in mesh.point_data.items():
|
|
465
|
+
if key not in ["gmsh:dim_tags"]:
|
|
466
|
+
point_data[key] = d
|
|
467
|
+
|
|
468
|
+
# Split the cell data: gmsh:physical and gmsh:geometrical are tags, the rest is
|
|
469
|
+
# actual cell data.
|
|
470
|
+
tag_data = {}
|
|
471
|
+
cell_data = {}
|
|
472
|
+
for key, d in mesh.cell_data.items():
|
|
473
|
+
if key in ["gmsh:physical", "gmsh:geometrical", "cell_tags"]:
|
|
474
|
+
tag_data[key] = d
|
|
475
|
+
else:
|
|
476
|
+
cell_data[key] = d
|
|
477
|
+
|
|
478
|
+
# with open(filename, "wb") as fh:
|
|
479
|
+
file_type = 1 if binary else 0
|
|
480
|
+
data_size = c_size_t.itemsize
|
|
481
|
+
file.write(f"$MeshFormat\n")
|
|
482
|
+
if binary:
|
|
483
|
+
file.write(f"4.1 {file_type} {data_size}\n".encode())
|
|
484
|
+
else:
|
|
485
|
+
file.write(f"4.1 {file_type} {data_size}\n")
|
|
486
|
+
|
|
487
|
+
if binary:
|
|
488
|
+
np.array([1], dtype=c_int).tofile(file)
|
|
489
|
+
file.write(b"\n")
|
|
490
|
+
file.write(b"$EndMeshFormat\n")
|
|
491
|
+
else:
|
|
492
|
+
file.write("$EndMeshFormat\n")
|
|
493
|
+
|
|
494
|
+
if mesh.field_data:
|
|
495
|
+
_write_physical_names(file, mesh.field_data)
|
|
496
|
+
|
|
497
|
+
if not mesh_only:
|
|
498
|
+
_write_entities(
|
|
499
|
+
file, mesh.cells, tag_data, mesh.cell_sets, mesh.point_data, binary
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
_write_nodes(file, mesh.points, mesh.cells, mesh.point_data, float_fmt, binary)
|
|
503
|
+
_write_elements(file, mesh.cells, tag_data, binary)
|
|
504
|
+
if mesh.gmsh_periodic is not None:
|
|
505
|
+
_write_periodic(file, mesh.gmsh_periodic, float_fmt, binary)
|
|
506
|
+
|
|
507
|
+
# if not mesh_only:
|
|
508
|
+
for name, dat in point_data.items():
|
|
509
|
+
_write_data(file, "NodeData", name, dat, binary)
|
|
510
|
+
cell_data_raw = raw_from_cell_data(cell_data)
|
|
511
|
+
for name, dat in cell_data_raw.items():
|
|
512
|
+
_write_data(file, "ElementData", name, dat, binary)
|
|
513
|
+
|
|
514
|
+
# Write cell_point_data (element nodal data) to ElementNodeData sections
|
|
515
|
+
if hasattr(mesh, 'cell_point_data') and mesh.cell_point_data:
|
|
516
|
+
_write_cell_point_data(file, mesh, binary)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _prepare_entity_data(
|
|
520
|
+
cells: List[CellBlock],
|
|
521
|
+
tag_data: Dict,
|
|
522
|
+
point_data: Dict,
|
|
523
|
+
cell_sets: Dict
|
|
524
|
+
) -> Tuple[npt.NDArray, npt.NDArray, bool]:
|
|
525
|
+
"""Prepare entity-related data structures.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
cells: List of cell blocks
|
|
529
|
+
tag_data: Tag data dictionary
|
|
530
|
+
point_data: Point data dictionary
|
|
531
|
+
cell_sets: Cell sets dictionary
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Tuple of (node_dim_tags, cell_dim_tags, has_bounding_entities) where:
|
|
535
|
+
- node_dim_tags: Unique (dim, tag) pairs from point data
|
|
536
|
+
- cell_dim_tags: (dim, tag) pairs for each cell block
|
|
537
|
+
- has_bounding_entities: Whether bounding entity info exists
|
|
538
|
+
"""
|
|
539
|
+
# Uniquify node dimension-tags
|
|
540
|
+
node_dim_tags = np.unique(point_data["gmsh:dim_tags"], axis=0)
|
|
541
|
+
|
|
542
|
+
# Prepare cell dimension-tags
|
|
543
|
+
cell_dim_tags = np.empty((len(cells), 2), dtype=int)
|
|
544
|
+
for ci, cell_block in enumerate(cells):
|
|
545
|
+
cell_dim_tags[ci] = [
|
|
546
|
+
cell_block.dim,
|
|
547
|
+
tag_data["gmsh:geometrical"][ci][0],
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
has_bounding_entities = "gmsh:bounding_entities" in cell_sets
|
|
551
|
+
|
|
552
|
+
return node_dim_tags, cell_dim_tags, has_bounding_entities
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _write_entity_bbox(writer: GmshWriter, dim: int) -> None:
|
|
556
|
+
"""Write bounding box coordinates (zeros placeholder).
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
writer: GmshWriter instance
|
|
560
|
+
dim: Entity dimension (0 for point, >0 for higher-dimensional entities)
|
|
561
|
+
"""
|
|
562
|
+
num_coords = GMSH_BBOX_POINT_COORDS if dim == 0 else GMSH_BBOX_ENTITY_COORDS
|
|
563
|
+
writer.write_array(np.zeros(num_coords), c_double)
|
|
564
|
+
if not writer.binary:
|
|
565
|
+
writer.fh.write(" ")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _write_physical_and_bounding_tags(
|
|
569
|
+
writer: GmshWriter,
|
|
570
|
+
dim: int,
|
|
571
|
+
matching_cell_block: npt.NDArray,
|
|
572
|
+
tag_data: Dict,
|
|
573
|
+
cell_sets: Dict,
|
|
574
|
+
has_bounding_entities: bool
|
|
575
|
+
) -> None:
|
|
576
|
+
"""Write physical tags and bounding entities for an entity.
|
|
577
|
+
|
|
578
|
+
Handles three cases:
|
|
579
|
+
1. Entity with physical tag
|
|
580
|
+
2. Entity without physical tag
|
|
581
|
+
3. Bounding entities (for dim > 0)
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
writer: GmshWriter instance
|
|
585
|
+
dim: Entity dimension
|
|
586
|
+
matching_cell_block: Matching cell block indices
|
|
587
|
+
tag_data: Tag data dictionary
|
|
588
|
+
cell_sets: Cell sets dictionary
|
|
589
|
+
has_bounding_entities: Whether bounding entity info exists
|
|
590
|
+
"""
|
|
591
|
+
# Guard clause: no matching cell block
|
|
592
|
+
if matching_cell_block.size == 0:
|
|
593
|
+
writer.write_array([0], c_size_t)
|
|
594
|
+
if not writer.binary:
|
|
595
|
+
writer.fh.write("0 ")
|
|
596
|
+
|
|
597
|
+
if dim > 0:
|
|
598
|
+
writer.write_array([0], c_size_t)
|
|
599
|
+
if not writer.binary:
|
|
600
|
+
writer.fh.write("0\n")
|
|
601
|
+
elif not writer.binary:
|
|
602
|
+
writer.fh.write("\n")
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
# Main path: has matching cell block
|
|
606
|
+
# Write physical tag if available
|
|
607
|
+
try:
|
|
608
|
+
physical_tag = tag_data["gmsh:physical"][matching_cell_block[0]][0]
|
|
609
|
+
writer.write_array([1], c_size_t)
|
|
610
|
+
writer.write_array([physical_tag], c_int)
|
|
611
|
+
if not writer.binary:
|
|
612
|
+
writer.fh.write(" ")
|
|
613
|
+
except KeyError:
|
|
614
|
+
writer.write_array([0], c_size_t)
|
|
615
|
+
if not writer.binary:
|
|
616
|
+
writer.fh.write("0 ")
|
|
617
|
+
|
|
618
|
+
# Write bounding entities for dim > 0
|
|
619
|
+
if dim > 0:
|
|
620
|
+
if has_bounding_entities:
|
|
621
|
+
bounds = cell_sets["gmsh:bounding_entities"][matching_cell_block[0]]
|
|
622
|
+
num_bounds = len(bounds)
|
|
623
|
+
|
|
624
|
+
if num_bounds > 0:
|
|
625
|
+
writer.write_array([num_bounds], c_size_t)
|
|
626
|
+
writer.write_array(bounds, c_int)
|
|
627
|
+
if not writer.binary:
|
|
628
|
+
writer.fh.write("\n")
|
|
629
|
+
else:
|
|
630
|
+
writer.write_array([0], c_size_t)
|
|
631
|
+
if not writer.binary:
|
|
632
|
+
writer.fh.write("0\n")
|
|
633
|
+
else:
|
|
634
|
+
writer.write_array([0], c_size_t)
|
|
635
|
+
if not writer.binary:
|
|
636
|
+
writer.fh.write("0\n")
|
|
637
|
+
else:
|
|
638
|
+
# Dimension 0: enforce line change for ASCII
|
|
639
|
+
if not writer.binary:
|
|
640
|
+
writer.fh.write("\n")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _write_entities(
|
|
644
|
+
fh: FileHandle,
|
|
645
|
+
cells: List[CellBlock],
|
|
646
|
+
tag_data: Dict,
|
|
647
|
+
cell_sets: Dict,
|
|
648
|
+
point_data: Dict,
|
|
649
|
+
binary: bool
|
|
650
|
+
) -> None:
|
|
651
|
+
"""Write entity section in a .msh file.
|
|
652
|
+
|
|
653
|
+
The entity section links three kinds of information:
|
|
654
|
+
1) The geometric objects represented in the mesh
|
|
655
|
+
2) Physical tags of geometric objects
|
|
656
|
+
3) Bounding entities (for objects of dimension >= 1)
|
|
657
|
+
|
|
658
|
+
The entities of all geometric objects are pulled from point_data['gmsh:dim_tags'].
|
|
659
|
+
Physical tags are specified as tag_data, while the boundary of a geometric
|
|
660
|
+
object is specified in cell_sets.
|
|
661
|
+
"""
|
|
662
|
+
# Early return if no entity data
|
|
663
|
+
if "gmsh:dim_tags" not in point_data:
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
writer = GmshWriter(fh, binary)
|
|
667
|
+
writer.write_section_header("Entities")
|
|
668
|
+
|
|
669
|
+
# Prepare data structures
|
|
670
|
+
node_dim_tags, cell_dim_tags, has_bounding_entities = _prepare_entity_data(
|
|
671
|
+
cells, tag_data, point_data, cell_sets
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Write number of entities per dimension
|
|
675
|
+
num_occ = np.bincount(node_dim_tags[:, 0], minlength=GMSH_MAX_DIM)
|
|
676
|
+
if num_occ.size > GMSH_MAX_DIM:
|
|
677
|
+
raise ValueError(f"Encountered entity with dimension > {GMSH_MAX_DIM - 1}")
|
|
678
|
+
|
|
679
|
+
writer.write_array(num_occ, c_size_t)
|
|
680
|
+
if not binary:
|
|
681
|
+
fh.write("\n")
|
|
682
|
+
|
|
683
|
+
# Write entity data
|
|
684
|
+
for dim, tag in node_dim_tags:
|
|
685
|
+
# Find matching cell block
|
|
686
|
+
matching_cell_block = np.where(
|
|
687
|
+
np.logical_and(cell_dim_tags[:, 0] == dim, cell_dim_tags[:, 1] == tag)
|
|
688
|
+
)[0]
|
|
689
|
+
|
|
690
|
+
if matching_cell_block.size > 1:
|
|
691
|
+
raise ValueError("Encountered non-unique CellBlock dim_tag")
|
|
692
|
+
|
|
693
|
+
# Write entity tag
|
|
694
|
+
writer.write_array([tag], c_int)
|
|
695
|
+
if not writer.binary:
|
|
696
|
+
writer.fh.write(" ")
|
|
697
|
+
|
|
698
|
+
# Write bounding box (placeholder zeros)
|
|
699
|
+
_write_entity_bbox(writer, dim)
|
|
700
|
+
|
|
701
|
+
# Write physical tags and bounding entities
|
|
702
|
+
_write_physical_and_bounding_tags(
|
|
703
|
+
writer, dim, matching_cell_block, tag_data, cell_sets, has_bounding_entities
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
writer.newline()
|
|
707
|
+
writer.write_section_footer("Entities")
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _prepare_node_dim_tags(
|
|
711
|
+
point_data: Dict,
|
|
712
|
+
cells: List[CellBlock],
|
|
713
|
+
n: int
|
|
714
|
+
) -> Tuple[npt.NDArray, npt.NDArray]:
|
|
715
|
+
"""Prepare node dimension-tag mappings.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
point_data: Point data dictionary
|
|
719
|
+
cells: List of cell blocks
|
|
720
|
+
n: Number of points
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Tuple of (node_dim_tags, reverse_index_map) where:
|
|
724
|
+
- node_dim_tags: Unique (dim, tag) combinations
|
|
725
|
+
- reverse_index_map: Maps each node to its dim_tag index
|
|
726
|
+
"""
|
|
727
|
+
if "gmsh:dim_tags" in point_data:
|
|
728
|
+
# reverse_index_map maps from all nodes to their respective representation in
|
|
729
|
+
# (the uniquified) node_dim_tags. This approach works for general orderings of
|
|
730
|
+
# the nodes
|
|
731
|
+
node_dim_tags, reverse_index_map = np.unique(
|
|
732
|
+
point_data["gmsh:dim_tags"],
|
|
733
|
+
axis=0,
|
|
734
|
+
return_inverse=True,
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
# If entity information is not provided, assign the same entity for all nodes.
|
|
738
|
+
# This only makes sense if the cells are of a single type
|
|
739
|
+
if len(cells) != 1:
|
|
740
|
+
raise WriteError(
|
|
741
|
+
"Specify entity information (gmsh:dim_tags in point_data) "
|
|
742
|
+
"to deal with more than one cell type."
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
dim = cells[0].dim
|
|
746
|
+
tag = GMSH_NODE_TAG_OFFSET
|
|
747
|
+
node_dim_tags = np.array([[dim, tag]])
|
|
748
|
+
# All nodes map to the (single) dimension-entity object
|
|
749
|
+
reverse_index_map = np.full(n, 0, dtype=int)
|
|
750
|
+
|
|
751
|
+
return node_dim_tags, reverse_index_map
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _write_node_block(
|
|
755
|
+
writer: GmshWriter,
|
|
756
|
+
dim: int,
|
|
757
|
+
tag: int,
|
|
758
|
+
node_tags: npt.NDArray,
|
|
759
|
+
points: npt.NDArray,
|
|
760
|
+
float_fmt: str
|
|
761
|
+
) -> None:
|
|
762
|
+
"""Write a single node entity block.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
writer: GmshWriter instance
|
|
766
|
+
dim: Entity dimension
|
|
767
|
+
tag: Entity tag
|
|
768
|
+
node_tags: Node indices for this block
|
|
769
|
+
points: All point coordinates
|
|
770
|
+
float_fmt: Format string for floating point numbers
|
|
771
|
+
"""
|
|
772
|
+
num_points_this = node_tags.size
|
|
773
|
+
is_parametric = 0
|
|
774
|
+
|
|
775
|
+
writer.write_array([dim, tag, is_parametric], c_int)
|
|
776
|
+
writer.write_array([num_points_this], c_size_t)
|
|
777
|
+
|
|
778
|
+
if writer.binary:
|
|
779
|
+
(node_tags + GMSH_NODE_TAG_OFFSET).astype(c_size_t).tofile(writer.fh)
|
|
780
|
+
points[node_tags].tofile(writer.fh)
|
|
781
|
+
else:
|
|
782
|
+
(node_tags + GMSH_NODE_TAG_OFFSET).astype(c_size_t).tofile(writer.fh, "\n", "%d")
|
|
783
|
+
writer.fh.write("\n")
|
|
784
|
+
np.savetxt(writer.fh, points[node_tags], delimiter=" ", fmt="%" + float_fmt)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _write_nodes(
|
|
788
|
+
fh: FileHandle,
|
|
789
|
+
points: npt.NDArray,
|
|
790
|
+
cells: List[CellBlock],
|
|
791
|
+
point_data: Dict,
|
|
792
|
+
float_fmt: str,
|
|
793
|
+
binary: bool
|
|
794
|
+
) -> None:
|
|
795
|
+
"""Write node information.
|
|
796
|
+
|
|
797
|
+
If data on dimension and tags of the geometric entities which the nodes belong to
|
|
798
|
+
is available, the nodes will be grouped accordingly. This data is specified as
|
|
799
|
+
point_data, using the key 'gmsh:dim_tags' and data as a num_points x 2 numpy
|
|
800
|
+
array (first column is the dimension of the geometric entity of this node,
|
|
801
|
+
second is the tag).
|
|
802
|
+
|
|
803
|
+
If dim_tags are not available, all nodes will be assigned the same tag. This only
|
|
804
|
+
makes sense if a single cell block is present in the mesh; an error will be raised
|
|
805
|
+
if len(cells) > 1.
|
|
806
|
+
"""
|
|
807
|
+
# Ensure 3D points (msh4 requires 3D points)
|
|
808
|
+
if points.shape[1] == 2:
|
|
809
|
+
points = np.column_stack([points, np.zeros_like(points[:, 0])])
|
|
810
|
+
|
|
811
|
+
writer = GmshWriter(fh, binary)
|
|
812
|
+
writer.write_section_header("Nodes")
|
|
813
|
+
|
|
814
|
+
n = points.shape[0]
|
|
815
|
+
min_tag = GMSH_NODE_TAG_OFFSET
|
|
816
|
+
max_tag = n
|
|
817
|
+
|
|
818
|
+
# Prepare dimension-tag mappings
|
|
819
|
+
node_dim_tags, reverse_index_map = _prepare_node_dim_tags(point_data, cells, n)
|
|
820
|
+
num_blocks = node_dim_tags.shape[0]
|
|
821
|
+
|
|
822
|
+
# Validate and prepare points for binary mode
|
|
823
|
+
if binary and points.dtype != c_double:
|
|
824
|
+
warn(f"Binary Gmsh needs c_double points (got {points.dtype}). Converting.")
|
|
825
|
+
points = points.astype(c_double)
|
|
826
|
+
|
|
827
|
+
# Write preamble
|
|
828
|
+
writer.write_array([num_blocks, n, min_tag, max_tag], c_size_t)
|
|
829
|
+
if not binary:
|
|
830
|
+
fh.write("\n")
|
|
831
|
+
|
|
832
|
+
# Write node blocks
|
|
833
|
+
for j in range(num_blocks):
|
|
834
|
+
dim, tag = node_dim_tags[j]
|
|
835
|
+
node_tags = np.where(reverse_index_map == j)[0]
|
|
836
|
+
_write_node_block(writer, dim, tag, node_tags, points, float_fmt)
|
|
837
|
+
|
|
838
|
+
writer.newline()
|
|
839
|
+
writer.write_section_footer("Nodes")
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _write_element_block(
|
|
843
|
+
writer: GmshWriter,
|
|
844
|
+
cell_block: CellBlock,
|
|
845
|
+
tag_data: Dict,
|
|
846
|
+
block_index: int,
|
|
847
|
+
tag0: int
|
|
848
|
+
) -> int:
|
|
849
|
+
"""Write a single element entity block.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
writer: GmshWriter instance
|
|
853
|
+
cell_block: CellBlock to write
|
|
854
|
+
tag_data: Tag data dictionary
|
|
855
|
+
block_index: Index of this cell block
|
|
856
|
+
tag0: Starting element tag
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Updated tag counter for next block
|
|
860
|
+
"""
|
|
861
|
+
node_idcs = _meshio_to_gmsh_order(cell_block.type, cell_block.data)
|
|
862
|
+
|
|
863
|
+
# Get entity tag
|
|
864
|
+
if "gmsh:geometrical" in tag_data:
|
|
865
|
+
entity_tag = tag_data["gmsh:geometrical"][block_index][0]
|
|
866
|
+
else:
|
|
867
|
+
# Note: Binary uses 0, ASCII uses 1 as default (preserving original behavior)
|
|
868
|
+
entity_tag = 0 if writer.binary else 1
|
|
869
|
+
|
|
870
|
+
cell_type = _meshio_to_gmsh_type[cell_block.type]
|
|
871
|
+
n = len(cell_block.data)
|
|
872
|
+
|
|
873
|
+
# Write block header
|
|
874
|
+
writer.write_array([cell_block.dim, entity_tag, cell_type], c_int)
|
|
875
|
+
writer.write_array([n], c_size_t)
|
|
876
|
+
|
|
877
|
+
# Write element data
|
|
878
|
+
if writer.binary:
|
|
879
|
+
if node_idcs.dtype != c_size_t:
|
|
880
|
+
warn(f"Binary Gmsh cells need c_size_t (got {node_idcs.dtype}). Converting.")
|
|
881
|
+
node_idcs = node_idcs.astype(c_size_t)
|
|
882
|
+
|
|
883
|
+
np.column_stack([
|
|
884
|
+
np.arange(tag0, tag0 + n, dtype=c_size_t),
|
|
885
|
+
node_idcs + GMSH_NODE_TAG_OFFSET,
|
|
886
|
+
]).tofile(writer.fh)
|
|
887
|
+
else:
|
|
888
|
+
np.savetxt(
|
|
889
|
+
writer.fh,
|
|
890
|
+
np.column_stack([
|
|
891
|
+
np.arange(tag0, tag0 + n),
|
|
892
|
+
node_idcs + GMSH_NODE_TAG_OFFSET
|
|
893
|
+
]),
|
|
894
|
+
"%d",
|
|
895
|
+
" ",
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
return tag0 + n
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _write_elements(fh: FileHandle, cells: List[CellBlock], tag_data: Dict, binary: bool) -> None:
|
|
902
|
+
"""Write the $Elements block.
|
|
903
|
+
|
|
904
|
+
$Elements
|
|
905
|
+
numEntityBlocks(size_t)
|
|
906
|
+
numElements(size_t) minElementTag(size_t) maxElementTag(size_t)
|
|
907
|
+
entityDim(int) entityTag(int) elementType(int) numElementsInBlock(size_t)
|
|
908
|
+
elementTag(size_t) nodeTag(size_t) ...
|
|
909
|
+
...
|
|
910
|
+
...
|
|
911
|
+
$EndElements
|
|
912
|
+
"""
|
|
913
|
+
writer = GmshWriter(fh, binary)
|
|
914
|
+
writer.write_section_header("Elements")
|
|
915
|
+
|
|
916
|
+
total_num_cells = sum(len(c) for c in cells)
|
|
917
|
+
num_blocks = len(cells)
|
|
918
|
+
min_element_tag = GMSH_NODE_TAG_OFFSET
|
|
919
|
+
max_element_tag = total_num_cells
|
|
920
|
+
|
|
921
|
+
writer.write_array(
|
|
922
|
+
[num_blocks, total_num_cells, min_element_tag, max_element_tag],
|
|
923
|
+
c_size_t
|
|
924
|
+
)
|
|
925
|
+
if not binary:
|
|
926
|
+
fh.write("\n")
|
|
927
|
+
|
|
928
|
+
tag0 = GMSH_NODE_TAG_OFFSET
|
|
929
|
+
for ci, cell_block in enumerate(cells):
|
|
930
|
+
tag0 = _write_element_block(writer, cell_block, tag_data, ci, tag0)
|
|
931
|
+
|
|
932
|
+
writer.newline()
|
|
933
|
+
writer.write_section_footer("Elements")
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _write_periodic(fh: FileHandle, periodic, float_fmt: str, binary: bool) -> None:
|
|
937
|
+
"""Write the $Periodic block.
|
|
938
|
+
|
|
939
|
+
Specified as:
|
|
940
|
+
|
|
941
|
+
$Periodic
|
|
942
|
+
numPeriodicLinks(size_t)
|
|
943
|
+
entityDim(int) entityTag(int) entityTagMaster(int)
|
|
944
|
+
numAffine(size_t) value(double) ...
|
|
945
|
+
numCorrespondingNodes(size_t)
|
|
946
|
+
nodeTag(size_t) nodeTagMaster(size_t)
|
|
947
|
+
...
|
|
948
|
+
...
|
|
949
|
+
$EndPeriodic
|
|
950
|
+
"""
|
|
951
|
+
writer = GmshWriter(fh, binary)
|
|
952
|
+
|
|
953
|
+
writer.write_section_header("Periodic")
|
|
954
|
+
writer.write_array(len(periodic), c_size_t)
|
|
955
|
+
|
|
956
|
+
for dim, (stag, mtag), affine, slave_master in periodic:
|
|
957
|
+
writer.write_array([dim, stag, mtag], c_int)
|
|
958
|
+
|
|
959
|
+
if affine is None or len(affine) == 0:
|
|
960
|
+
writer.write_array(0, c_size_t)
|
|
961
|
+
else:
|
|
962
|
+
writer.write_array(len(affine), c_size_t, sep=" ")
|
|
963
|
+
writer.write_array(affine, c_double, fmt=float_fmt)
|
|
964
|
+
|
|
965
|
+
slave_master = np.array(slave_master, dtype=c_size_t)
|
|
966
|
+
slave_master = slave_master.reshape(-1, 2)
|
|
967
|
+
slave_master = slave_master + GMSH_NODE_TAG_OFFSET # Gmsh is 1-based
|
|
968
|
+
writer.write_array(len(slave_master), c_size_t)
|
|
969
|
+
writer.write_array(slave_master, c_size_t)
|
|
970
|
+
|
|
971
|
+
writer.newline()
|
|
972
|
+
writer.write_section_footer("Periodic")
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _determine_component_count(cell_blocks_data: List[npt.NDArray]) -> Optional[int]:
|
|
976
|
+
"""Determine number of components from cell_point_data shape.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
cell_blocks_data: List of cell block data arrays
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
Number of components, or None if shape is invalid
|
|
983
|
+
"""
|
|
984
|
+
first_block_data = cell_blocks_data[0]
|
|
985
|
+
|
|
986
|
+
if len(first_block_data.shape) == 2:
|
|
987
|
+
# Shape: (n_elements, n_nodes_per_element) - single component
|
|
988
|
+
return 1
|
|
989
|
+
elif len(first_block_data.shape) == 3:
|
|
990
|
+
# Shape: (n_elements, n_nodes_per_element, n_components)
|
|
991
|
+
return first_block_data.shape[2]
|
|
992
|
+
else:
|
|
993
|
+
return None
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _write_element_node_data_header(
|
|
997
|
+
writer: GmshWriter,
|
|
998
|
+
field_name: str,
|
|
999
|
+
num_components: int,
|
|
1000
|
+
total_elements: int
|
|
1001
|
+
) -> None:
|
|
1002
|
+
"""Write ElementNodeData section header.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
writer: GmshWriter instance
|
|
1006
|
+
field_name: Name of the data field
|
|
1007
|
+
num_components: Number of components per node
|
|
1008
|
+
total_elements: Total number of elements
|
|
1009
|
+
"""
|
|
1010
|
+
writer.write_section_header("ElementNodeData")
|
|
1011
|
+
|
|
1012
|
+
# String tags
|
|
1013
|
+
writer.write_string(f"{1}\n")
|
|
1014
|
+
writer.write_string(f'"{field_name}"\n')
|
|
1015
|
+
|
|
1016
|
+
# Real tags
|
|
1017
|
+
writer.write_string(f"{1}\n")
|
|
1018
|
+
writer.write_string(f"{0.0}\n")
|
|
1019
|
+
|
|
1020
|
+
# Integer tags: time step, num components, num elements
|
|
1021
|
+
writer.write_string(f"{3}\n")
|
|
1022
|
+
writer.write_string(f"{0}\n") # time step
|
|
1023
|
+
writer.write_string(f"{num_components}\n")
|
|
1024
|
+
writer.write_string(f"{total_elements}\n")
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _write_element_node_values(
|
|
1028
|
+
writer: GmshWriter,
|
|
1029
|
+
elem_id: int,
|
|
1030
|
+
elem_data: npt.NDArray,
|
|
1031
|
+
num_components: int
|
|
1032
|
+
) -> None:
|
|
1033
|
+
"""Write nodal data for a single element.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
writer: GmshWriter instance
|
|
1037
|
+
elem_id: Element ID
|
|
1038
|
+
elem_data: Nodal data for this element
|
|
1039
|
+
num_components: Number of components per node
|
|
1040
|
+
"""
|
|
1041
|
+
if num_components == 1:
|
|
1042
|
+
# Single component: elem_data is 1D array
|
|
1043
|
+
num_nodes = len(elem_data)
|
|
1044
|
+
writer.write_array([elem_id, num_nodes], c_int)
|
|
1045
|
+
|
|
1046
|
+
if writer.binary:
|
|
1047
|
+
elem_data.astype(c_double).tofile(writer.fh)
|
|
1048
|
+
else:
|
|
1049
|
+
for val in elem_data:
|
|
1050
|
+
writer.fh.write(f" {float(val)}")
|
|
1051
|
+
writer.fh.write("\n")
|
|
1052
|
+
else:
|
|
1053
|
+
# Multi-component: elem_data is 2D array (n_nodes, n_components)
|
|
1054
|
+
num_nodes = elem_data.shape[0]
|
|
1055
|
+
writer.write_array([elem_id, num_nodes], c_int)
|
|
1056
|
+
|
|
1057
|
+
if writer.binary:
|
|
1058
|
+
# Flatten the data: all components for node 1, then node 2, etc.
|
|
1059
|
+
elem_data.astype(c_double).flatten().tofile(writer.fh)
|
|
1060
|
+
else:
|
|
1061
|
+
for node_vals in elem_data:
|
|
1062
|
+
for val in node_vals:
|
|
1063
|
+
writer.fh.write(f" {float(val)}")
|
|
1064
|
+
writer.fh.write("\n")
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _write_cell_point_data(fh: FileHandle, mesh, binary: bool) -> None:
|
|
1068
|
+
"""Write cell_point_data (element nodal data) to $ElementNodeData sections.
|
|
1069
|
+
|
|
1070
|
+
The cell_point_data structure is:
|
|
1071
|
+
{name: [array_for_cell_block_0, array_for_cell_block_1, ...]}
|
|
1072
|
+
where each array has shape (n_elements, n_nodes_per_element) for single
|
|
1073
|
+
component or (n_elements, n_nodes_per_element, n_components) for multi-
|
|
1074
|
+
component data.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
fh: File handle to write to
|
|
1078
|
+
mesh: Mesh object containing cell_point_data
|
|
1079
|
+
binary: Whether to write in binary mode
|
|
1080
|
+
"""
|
|
1081
|
+
# Validate element_id exists
|
|
1082
|
+
if 'element_id' not in mesh.cell_data:
|
|
1083
|
+
logger.warning("Cannot write cell_point_data: mesh.cell_data['element_id'] not found")
|
|
1084
|
+
return
|
|
1085
|
+
|
|
1086
|
+
element_ids = mesh.cell_data['element_id']
|
|
1087
|
+
writer = GmshWriter(fh, binary)
|
|
1088
|
+
|
|
1089
|
+
# Process each field
|
|
1090
|
+
for field_name, cell_blocks_data in mesh.cell_point_data.items():
|
|
1091
|
+
# Determine component count
|
|
1092
|
+
num_components = _determine_component_count(cell_blocks_data)
|
|
1093
|
+
if num_components is None:
|
|
1094
|
+
logger.warning(
|
|
1095
|
+
f"Unexpected shape for cell_point_data '{field_name}': "
|
|
1096
|
+
f"{cell_blocks_data[0].shape}"
|
|
1097
|
+
)
|
|
1098
|
+
continue
|
|
1099
|
+
|
|
1100
|
+
total_elements = sum(len(block_data) for block_data in cell_blocks_data)
|
|
1101
|
+
|
|
1102
|
+
# Write header
|
|
1103
|
+
_write_element_node_data_header(writer, field_name, num_components, total_elements)
|
|
1104
|
+
|
|
1105
|
+
# Write data for each element
|
|
1106
|
+
for block_idx, block_data in enumerate(cell_blocks_data):
|
|
1107
|
+
block_element_ids = element_ids[block_idx]
|
|
1108
|
+
|
|
1109
|
+
for elem_idx, elem_data in enumerate(block_data):
|
|
1110
|
+
elem_id = int(block_element_ids[elem_idx])
|
|
1111
|
+
_write_element_node_values(writer, elem_id, elem_data, num_components)
|
|
1112
|
+
|
|
1113
|
+
# Write footer
|
|
1114
|
+
writer.newline()
|
|
1115
|
+
writer.write_section_footer("ElementNodeData")
|