Coreform-Cubit-Mesh-Export 1.11.0__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.
cubit_mesh_export.py ADDED
@@ -0,0 +1,2738 @@
1
+ from typing import Any, List, Optional, Tuple
2
+
3
+ ########################################################################
4
+ ### Common utility functions
5
+ ########################################################################
6
+
7
+ def _block_contains_geometry(cubit: Any, block_id: int) -> bool:
8
+ """Check if a block contains geometry (volume, surface, curve, vertex) instead of mesh elements."""
9
+ try:
10
+ if len(cubit.get_block_volumes(block_id)) > 0:
11
+ return True
12
+ except:
13
+ pass
14
+ try:
15
+ if len(cubit.get_block_surfaces(block_id)) > 0:
16
+ return True
17
+ except:
18
+ pass
19
+ try:
20
+ if len(cubit.get_block_curves(block_id)) > 0:
21
+ return True
22
+ except:
23
+ pass
24
+ try:
25
+ if len(cubit.get_block_vertices(block_id)) > 0:
26
+ return True
27
+ except:
28
+ pass
29
+ return False
30
+
31
+
32
+ def _get_block_elements(cubit: Any, block_id: int, elem_type: str) -> Tuple[int, ...]:
33
+ """Get mesh elements from a block, supporting both geometry and mesh element blocks.
34
+
35
+ This function mimics Cubit's 'draw <elem_type> in block X' behavior.
36
+ When a block contains geometry (volume, surface, curve, vertex),
37
+ it uses parse_cubit_list to get the associated mesh elements.
38
+ When a block contains direct mesh elements, it uses the standard get_block_* functions.
39
+
40
+ Args:
41
+ cubit: Cubit Python interface object
42
+ block_id: Block ID
43
+ elem_type: Element type ('tet', 'hex', 'wedge', 'pyramid', 'tri', 'face',
44
+ 'quad', 'edge', 'node')
45
+
46
+ Returns:
47
+ Tuple of element IDs
48
+ """
49
+ # Check if block contains geometry (volume, surface, curve, vertex)
50
+ if _block_contains_geometry(cubit, block_id):
51
+ # Use parse_cubit_list for geometry-based blocks
52
+ # This mimics Cubit's 'draw <elem_type> in block X' behavior
53
+ try:
54
+ elements = cubit.parse_cubit_list(elem_type, f"in block {block_id}")
55
+ return tuple(elements)
56
+ except:
57
+ return ()
58
+
59
+ # For mesh element blocks, use the standard get_block_* functions
60
+ try:
61
+ if elem_type == "hex":
62
+ return cubit.get_block_hexes(block_id)
63
+ elif elem_type == "tet":
64
+ return cubit.get_block_tets(block_id)
65
+ elif elem_type == "wedge":
66
+ return cubit.get_block_wedges(block_id)
67
+ elif elem_type == "pyramid":
68
+ return cubit.get_block_pyramids(block_id)
69
+ elif elem_type == "tri":
70
+ return cubit.get_block_tris(block_id)
71
+ elif elem_type == "face":
72
+ return cubit.get_block_faces(block_id)
73
+ elif elem_type == "quad":
74
+ return cubit.get_block_quads(block_id)
75
+ elif elem_type == "edge":
76
+ return cubit.get_block_edges(block_id)
77
+ elif elem_type == "node":
78
+ return cubit.get_block_nodes(block_id)
79
+ else:
80
+ return ()
81
+ except:
82
+ return ()
83
+
84
+
85
+
86
+
87
+ def _warn_mixed_element_types_in_blocks(cubit: Any) -> None:
88
+ """Warn if any block contains multiple 3D element types.
89
+
90
+ When a block contains multiple element types (e.g., tet + hex + pyramid),
91
+ Cubit's 'block X element type' command only reports the last set type via
92
+ get_block_element_type(). While Cubit does convert all elements to 2nd order
93
+ when you issue commands like 'block 1 element type tetra10', this warning
94
+ helps users understand they need to issue multiple commands for mixed blocks.
95
+
96
+ This function prints a warning for each block with mixed 3D element types.
97
+ """
98
+ # 3D element types to check
99
+ volume_elem_types = ["hex", "tet", "wedge", "pyramid"]
100
+
101
+ for block_id in cubit.get_block_id_list():
102
+ found_types = []
103
+ for elem_type in volume_elem_types:
104
+ elements = _get_block_elements(cubit, block_id, elem_type)
105
+ if len(elements) > 0:
106
+ found_types.append(elem_type)
107
+
108
+ if len(found_types) > 1:
109
+ block_name = cubit.get_exodus_entity_name("block", block_id)
110
+ type_str = ", ".join(found_types)
111
+ print(f"WARNING: Block {block_id} ('{block_name}') contains multiple 3D element types: {type_str}")
112
+ print(f" To convert all elements to 2nd order, you need to issue separate commands:")
113
+ for elem_type in found_types:
114
+ if elem_type == "tet":
115
+ print(f" block {block_id} element type tetra10")
116
+ elif elem_type == "hex":
117
+ print(f" block {block_id} element type hex20")
118
+ elif elem_type == "wedge":
119
+ print(f" block {block_id} element type wedge15")
120
+ elif elem_type == "pyramid":
121
+ print(f" block {block_id} element type pyramid13")
122
+
123
+ ########################################################################
124
+ ### Gmsh format version 2.2
125
+ ### - Flat structure: $Nodes and $Elements as simple lists
126
+ ########################################################################
127
+
128
+ def export_Gmsh_ver2(cubit: Any, FileName: str) -> Any:
129
+ """Export mesh to Gmsh format version 2.2.
130
+
131
+ Exports 1D, 2D, and 3D mesh elements from Cubit to Gmsh v2.2 format.
132
+ Supports both first-order and second-order elements.
133
+
134
+ Args:
135
+ cubit: Cubit Python interface object
136
+ FileName: Output file path for the .msh file
137
+
138
+ Returns:
139
+ cubit: The cubit object (for method chaining)
140
+
141
+ Supported elements:
142
+ - 1st order: Point, Line, Triangle, Quad, Tetrahedron, Hexahedron, Wedge, Pyramid
143
+ - 2nd order: Line3, Triangle6, Quad8/9, Tetrahedron10/11, Hexahedron20, Wedge15, Pyramid13
144
+ """
145
+ _warn_mixed_element_types_in_blocks(cubit)
146
+
147
+ with open(FileName, 'w') as fid:
148
+
149
+ fid.write("$MeshFormat\n")
150
+ fid.write("2.2 0 8\n")
151
+ fid.write("$EndMeshFormat\n")
152
+
153
+ fid.write("$PhysicalNames\n")
154
+ fid.write(f'{cubit.get_block_count()}\n')
155
+ for block_id in cubit.get_block_id_list():
156
+ name = cubit.get_exodus_entity_name("block", block_id)
157
+ if len(_get_block_elements(cubit, block_id, "node")) > 0:
158
+ fid.write(f'0 {block_id} "{name}"\n')
159
+ elif len(_get_block_elements(cubit, block_id, "edge")) > 0:
160
+ fid.write(f'1 {block_id} "{name}"\n')
161
+ elif len(_get_block_elements(cubit, block_id, "tri")) + len(_get_block_elements(cubit, block_id, "face")) > 0:
162
+ fid.write(f'2 {block_id} "{name}"\n')
163
+ else:
164
+ fid.write(f'3 {block_id} "{name}"\n')
165
+ fid.write('$EndPhysicalNames\n')
166
+
167
+ fid.write("$Nodes\n")
168
+ node_list = set()
169
+ for block_id in cubit.get_block_id_list():
170
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
171
+ for elem_type in elem_types:
172
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
173
+ node_ids = cubit.get_expanded_connectivity(elem_type, element_id)
174
+ node_list.update(node_ids)
175
+
176
+ fid.write(f'{len(node_list)}\n')
177
+ for node_id in node_list:
178
+ coord = cubit.get_nodal_coordinates(node_id)
179
+ fid.write(f'{node_id} {coord[0]} {coord[1]} {coord[2]}\n')
180
+ fid.write('$EndNodes\n')
181
+
182
+ hex_list = set()
183
+ tet_list = set()
184
+ wedge_list = set()
185
+ pyramid_list = set()
186
+ tri_list = set()
187
+ quad_list = set()
188
+ edge_list = set()
189
+ node_list = set()
190
+
191
+ for block_id in cubit.get_block_id_list():
192
+ tet_list.update(_get_block_elements(cubit, block_id, "tet"))
193
+ hex_list.update(_get_block_elements(cubit, block_id, "hex"))
194
+ wedge_list.update(_get_block_elements(cubit, block_id, "wedge"))
195
+ pyramid_list.update(_get_block_elements(cubit, block_id, "pyramid"))
196
+ tri_list.update(_get_block_elements(cubit, block_id, "tri"))
197
+ quad_list.update(_get_block_elements(cubit, block_id, "face"))
198
+ edge_list.update(_get_block_elements(cubit, block_id, "edge"))
199
+ node_list.update(_get_block_elements(cubit, block_id, "node"))
200
+
201
+ element_id = 0
202
+ fid.write('$Elements\n')
203
+ fid.write(f'{len(hex_list) + len(tet_list) + len(wedge_list) + len(pyramid_list) + len(tri_list) + len(quad_list) + len(edge_list) + len(node_list)}\n')
204
+
205
+ for block_id in cubit.get_block_id_list():
206
+
207
+ tet_list = _get_block_elements(cubit, block_id, "tet")
208
+ for tet_id in tet_list:
209
+ element_id += 1
210
+ node_list = cubit.get_expanded_connectivity("tet", tet_id)
211
+ if len(node_list)==4:
212
+ fid.write(f'{element_id} { 4} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]}\n')
213
+ elif len(node_list)==10:
214
+ fid.write(f'{element_id} {11} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]} {node_list[9]} {node_list[8]}\n')
215
+ elif len(node_list)==11:
216
+ fid.write(f'{element_id} {35} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]} {node_list[9]} {node_list[8]} {node_list[10]}\n')
217
+
218
+ hex_list = _get_block_elements(cubit, block_id, "hex")
219
+ for hex_id in hex_list:
220
+ element_id += 1
221
+ node_list = cubit.get_expanded_connectivity("hex", hex_id)
222
+ if len(node_list)==8:
223
+ fid.write(f'{element_id} { 5} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]}\n')
224
+ elif len(node_list)==20:
225
+ fid.write(f'{element_id} {17} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]} {node_list[8]} {node_list[11]} {node_list[12]} {node_list[9]} {node_list[13]} {node_list[10]} {node_list[14]} {node_list[15]} {node_list[16]} {node_list[19]} {node_list[17]} {node_list[18]}\n')
226
+
227
+ wedge_list = _get_block_elements(cubit, block_id, "wedge")
228
+ for wedge_id in wedge_list:
229
+ element_id += 1
230
+ node_list = cubit.get_expanded_connectivity("wedge", wedge_id)
231
+ if len(node_list)==6:
232
+ fid.write(f'{element_id} { 6} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]}\n')
233
+ elif len(node_list)==15:
234
+ fid.write(f'{element_id} {18} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[8]} {node_list[9]} {node_list[7]} {node_list[10]} {node_list[11]} {node_list[12]} {node_list[14]} {node_list[13]}\n')
235
+
236
+ pyramid_list = _get_block_elements(cubit, block_id, "pyramid")
237
+ for pyramid_id in pyramid_list:
238
+ element_id += 1
239
+ node_list = cubit.get_expanded_connectivity("pyramid", pyramid_id)
240
+ if len(node_list)==5:
241
+ fid.write(f'{element_id} { 7} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]}\n')
242
+ elif len(node_list)==13:
243
+ fid.write(f'{element_id} {19} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[8]} {node_list[9]} {node_list[6]} {node_list[10]} {node_list[7]} {node_list[11]} {node_list[12]} \n')
244
+
245
+ tri_list = _get_block_elements(cubit, block_id, "tri")
246
+ for tri_id in tri_list:
247
+ element_id += 1
248
+ node_list = cubit.get_expanded_connectivity("tri", tri_id)
249
+ if len(node_list)==3:
250
+ fid.write(f'{element_id} { 2} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]}\n')
251
+ elif len(node_list)==6:
252
+ fid.write(f'{element_id} { 9} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]}\n')
253
+ elif len(node_list)==7:
254
+ fid.write(f'{element_id} {42} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]}\n')
255
+
256
+ quad_list = _get_block_elements(cubit, block_id, "face")
257
+ for quad_id in quad_list:
258
+ element_id += 1
259
+ node_list = cubit.get_expanded_connectivity("quad", quad_id)
260
+ if len(node_list)==4:
261
+ fid.write(f'{element_id} { 3} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]}\n')
262
+ elif len(node_list)==8:
263
+ fid.write(f'{element_id} {16} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]}\n')
264
+ elif len(node_list)==9:
265
+ fid.write(f'{element_id} {10} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]} {node_list[8]}\n')
266
+
267
+ edge_list = _get_block_elements(cubit, block_id, "edge")
268
+ for edge_id in edge_list:
269
+ element_id += 1
270
+ node_list = cubit.get_expanded_connectivity("edge", edge_id)
271
+ if len(node_list)==2:
272
+ fid.write(f'{element_id} {1} {2} {block_id} {block_id} {node_list[0]} {node_list[1]}\n')
273
+ elif len(node_list)==3:
274
+ fid.write(f'{element_id} {8} {2} {block_id} {block_id} {node_list[0]} {node_list[1]} {node_list[2]}\n')
275
+
276
+ node_list = _get_block_elements(cubit, block_id, "node")
277
+ for node_id in node_list:
278
+ element_id += 1
279
+ fid.write(f'{element_id} {15} {2} {block_id} {block_id} {node_id}\n')
280
+
281
+ fid.write('$EndElements\n')
282
+ return cubit
283
+
284
+ ########################################################################
285
+ ### Gmsh format version 4.1
286
+ ### - Hierarchical structure: $Entities defines geometry topology
287
+ ### - Entity-based $Nodes and $Elements grouping
288
+ ########################################################################
289
+
290
+ def export_Gmsh_ver4(cubit: Any, FileName: str, DIM: str = "auto") -> Any:
291
+ """Export mesh to Gmsh format version 4.1.
292
+
293
+ Exports 1D, 2D, and 3D mesh elements from Cubit to Gmsh v4.1 format.
294
+ Supports both first-order and second-order elements.
295
+ Includes full $Entities section with geometry topology.
296
+
297
+ Args:
298
+ cubit: Cubit Python interface object
299
+ FileName: Output file path for the .msh file
300
+ DIM: Dimension mode
301
+ - "auto": Auto-detect (3D if volume elements exist, else 2D)
302
+ - "2D": 2D mode (orient surface normals to +z direction, z-coordinates set to 0)
303
+ - "3D": 3D mode (no normal orientation)
304
+
305
+ Returns:
306
+ cubit: The cubit object (for method chaining)
307
+
308
+ Supported elements:
309
+ - 1st order: Point, Line, Triangle, Quad, Tetrahedron, Hexahedron, Wedge, Pyramid
310
+ - 2nd order: Line3, Triangle6, Quad8/9, Tetrahedron10/11, Hexahedron20, Wedge15, Pyramid13
311
+ """
312
+ _warn_mixed_element_types_in_blocks(cubit)
313
+
314
+
315
+ # ============================================================
316
+ # Gmsh element type codes (same as v2)
317
+ # ============================================================
318
+ GMSH_POINT = 15
319
+ GMSH_LINE2 = 1
320
+ GMSH_LINE3 = 8
321
+ GMSH_TRI3 = 2
322
+ GMSH_TRI6 = 9
323
+ GMSH_TRI7 = 42
324
+ GMSH_QUAD4 = 3
325
+ GMSH_QUAD8 = 16
326
+ GMSH_QUAD9 = 10
327
+ GMSH_TET4 = 4
328
+ GMSH_TET10 = 11
329
+ GMSH_TET11 = 35
330
+ GMSH_HEX8 = 5
331
+ GMSH_HEX20 = 17
332
+ GMSH_WEDGE6 = 6
333
+ GMSH_WEDGE15 = 18
334
+ GMSH_PYRAMID5 = 7
335
+ GMSH_PYRAMID13 = 19
336
+
337
+ # ============================================================
338
+ # Collect block information and determine dimension
339
+ # ============================================================
340
+
341
+ # Collect elements by type from all blocks
342
+ all_tets = set()
343
+ all_hexes = set()
344
+ all_wedges = set()
345
+ all_pyramids = set()
346
+ all_tris = set()
347
+ all_quads = set()
348
+ all_edges = set()
349
+ all_points = set()
350
+
351
+ # Map element to block_id
352
+ elem_to_block = {}
353
+
354
+ for block_id in cubit.get_block_id_list():
355
+ for tet_id in _get_block_elements(cubit, block_id, "tet"):
356
+ all_tets.add(tet_id)
357
+ elem_to_block[('tet', tet_id)] = block_id
358
+ for hex_id in _get_block_elements(cubit, block_id, "hex"):
359
+ all_hexes.add(hex_id)
360
+ elem_to_block[('hex', hex_id)] = block_id
361
+ for wedge_id in _get_block_elements(cubit, block_id, "wedge"):
362
+ all_wedges.add(wedge_id)
363
+ elem_to_block[('wedge', wedge_id)] = block_id
364
+ for pyramid_id in _get_block_elements(cubit, block_id, "pyramid"):
365
+ all_pyramids.add(pyramid_id)
366
+ elem_to_block[('pyramid', pyramid_id)] = block_id
367
+ for tri_id in _get_block_elements(cubit, block_id, "tri"):
368
+ all_tris.add(tri_id)
369
+ elem_to_block[('tri', tri_id)] = block_id
370
+ for quad_id in _get_block_elements(cubit, block_id, "face"):
371
+ all_quads.add(quad_id)
372
+ elem_to_block[('quad', quad_id)] = block_id
373
+ for edge_id in _get_block_elements(cubit, block_id, "edge"):
374
+ all_edges.add(edge_id)
375
+ elem_to_block[('edge', edge_id)] = block_id
376
+ for node_id in _get_block_elements(cubit, block_id, "node"):
377
+ all_points.add(node_id)
378
+ elem_to_block[('node', node_id)] = block_id
379
+
380
+ has_3d_elements = len(all_tets) + len(all_hexes) + len(all_wedges) + len(all_pyramids) > 0
381
+ has_2d_elements = len(all_tris) + len(all_quads) > 0
382
+
383
+ # Determine dimension mode
384
+ if DIM == "auto":
385
+ actual_dim = "3D" if has_3d_elements else "2D"
386
+ else:
387
+ actual_dim = DIM
388
+
389
+ # ============================================================
390
+ # Collect nodes
391
+ # ============================================================
392
+
393
+ node_set = set()
394
+ for block_id in cubit.get_block_id_list():
395
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
396
+ for elem_type in elem_types:
397
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
398
+ node_ids = cubit.get_expanded_connectivity(elem_type, element_id)
399
+ node_set.update(node_ids)
400
+
401
+ sorted_nodes = sorted(node_set)
402
+ num_nodes = len(sorted_nodes)
403
+ min_node_tag = min(sorted_nodes) if sorted_nodes else 0
404
+ max_node_tag = max(sorted_nodes) if sorted_nodes else 0
405
+
406
+ # ============================================================
407
+ # Collect geometry entities for $Entities section
408
+ # ============================================================
409
+
410
+ # Get volumes and surfaces associated with blocks
411
+ volume_set = set()
412
+ surface_set = set()
413
+ curve_set = set()
414
+ vertex_set = set()
415
+
416
+ # Physical tags: block_id -> set of entity IDs
417
+ volume_physical = {} # volume_id -> block_id
418
+ surface_physical = {} # surface_id -> block_id
419
+
420
+ for block_id in cubit.get_block_id_list():
421
+ # 3D blocks -> volumes
422
+ for vol_id in cubit.get_block_volumes(block_id):
423
+ volume_set.add(vol_id)
424
+ volume_physical[vol_id] = block_id
425
+ # Get surfaces of this volume
426
+ try:
427
+ vol = cubit.volume(vol_id)
428
+ for surf in vol.surfaces():
429
+ surface_set.add(surf.id())
430
+ # Get curves of this surface
431
+ for curve in surf.curves():
432
+ curve_set.add(curve.id())
433
+ # Get vertices of this curve
434
+ for vert in curve.vertices():
435
+ vertex_set.add(vert.id())
436
+ except:
437
+ # Fallback: use parse_cubit_list
438
+ surf_ids = cubit.parse_cubit_list("surface", f"in volume {vol_id}")
439
+ surface_set.update(surf_ids)
440
+
441
+ # 2D blocks -> surfaces
442
+ tri_list = _get_block_elements(cubit, block_id, "tri")
443
+ quad_list = _get_block_elements(cubit, block_id, "face")
444
+ if len(tri_list) + len(quad_list) > 0:
445
+ # Find surfaces containing these elements
446
+ for tri_id in tri_list:
447
+ try:
448
+ owner = cubit.get_geometry_owner("tri", tri_id)
449
+ if owner and "surface" in owner.lower():
450
+ surf_id = int(owner.split()[1])
451
+ surface_set.add(surf_id)
452
+ surface_physical[surf_id] = block_id
453
+ except:
454
+ pass
455
+ for quad_id in quad_list:
456
+ try:
457
+ owner = cubit.get_geometry_owner("quad", quad_id)
458
+ if owner and "surface" in owner.lower():
459
+ surf_id = int(owner.split()[1])
460
+ surface_set.add(surf_id)
461
+ surface_physical[surf_id] = block_id
462
+ except:
463
+ pass
464
+
465
+ # If no geometry found, create minimal entities based on blocks
466
+ if not volume_set and not surface_set:
467
+ # Use block IDs as entity tags
468
+ for block_id in cubit.get_block_id_list():
469
+ if len(_get_block_elements(cubit, block_id, "tet")) + len(_get_block_elements(cubit, block_id, "hex")) + \
470
+ len(_get_block_elements(cubit, block_id, "wedge")) + len(_get_block_elements(cubit, block_id, "pyramid")) > 0:
471
+ volume_set.add(block_id)
472
+ volume_physical[block_id] = block_id
473
+ elif len(_get_block_elements(cubit, block_id, "tri")) + len(_get_block_elements(cubit, block_id, "face")) > 0:
474
+ surface_set.add(block_id)
475
+ surface_physical[block_id] = block_id
476
+
477
+ # ============================================================
478
+ # Write Gmsh v4 file
479
+ # ============================================================
480
+
481
+ with open(FileName, 'w') as fid:
482
+
483
+ # ------------------------------------------------------------
484
+ # $MeshFormat
485
+ # ------------------------------------------------------------
486
+ fid.write('$MeshFormat\n')
487
+ fid.write('4.1 0 8\n')
488
+ fid.write('$EndMeshFormat\n')
489
+
490
+ # ------------------------------------------------------------
491
+ # $PhysicalNames
492
+ # ------------------------------------------------------------
493
+ fid.write('$PhysicalNames\n')
494
+ fid.write(f'{cubit.get_block_count()}\n')
495
+ for block_id in cubit.get_block_id_list():
496
+ name = cubit.get_exodus_entity_name("block", block_id)
497
+ if len(_get_block_elements(cubit, block_id, "node")) > 0:
498
+ dim = 0
499
+ elif len(_get_block_elements(cubit, block_id, "edge")) > 0:
500
+ dim = 1
501
+ elif len(_get_block_elements(cubit, block_id, "tri")) + len(_get_block_elements(cubit, block_id, "face")) > 0:
502
+ dim = 2
503
+ else:
504
+ dim = 3
505
+ fid.write(f'{dim} {block_id} "{name}"\n')
506
+ fid.write('$EndPhysicalNames\n')
507
+
508
+ # ------------------------------------------------------------
509
+ # $Entities
510
+ # ------------------------------------------------------------
511
+ num_points_ent = len(vertex_set)
512
+ num_curves_ent = len(curve_set)
513
+ num_surfaces_ent = len(surface_set) if surface_set else len([b for b in cubit.get_block_id_list() if len(_get_block_elements(cubit, b, "tri")) + len(_get_block_elements(cubit, b, "face")) > 0])
514
+ num_volumes_ent = len(volume_set) if volume_set else len([b for b in cubit.get_block_id_list() if len(_get_block_elements(cubit, b, "tet")) + len(_get_block_elements(cubit, b, "hex")) + len(_get_block_elements(cubit, b, "wedge")) + len(_get_block_elements(cubit, b, "pyramid")) > 0])
515
+
516
+ fid.write('$Entities\n')
517
+ fid.write(f'{num_points_ent} {num_curves_ent} {num_surfaces_ent} {num_volumes_ent}\n')
518
+
519
+ # Points (vertices)
520
+ for vert_id in sorted(vertex_set):
521
+ try:
522
+ coord = cubit.vertex(vert_id).coordinates()
523
+ fid.write(f'{vert_id} {coord[0]} {coord[1]} {coord[2]} 0\n')
524
+ except:
525
+ fid.write(f'{vert_id} 0 0 0 0\n')
526
+
527
+ # Curves
528
+ for curve_id in sorted(curve_set):
529
+ try:
530
+ bbox = cubit.get_bounding_box("curve", curve_id)
531
+ minx, maxx = bbox[0], bbox[1]
532
+ miny, maxy = bbox[3], bbox[4]
533
+ minz, maxz = bbox[6], bbox[7]
534
+ # Get bounding vertices
535
+ curve = cubit.curve(curve_id)
536
+ vert_ids = [v.id() for v in curve.vertices()]
537
+ num_bnd = len(vert_ids)
538
+ vert_str = ' '.join(str(v) for v in vert_ids)
539
+ fid.write(f'{curve_id} {minx} {miny} {minz} {maxx} {maxy} {maxz} 0 {num_bnd} {vert_str}\n')
540
+ except:
541
+ fid.write(f'{curve_id} 0 0 0 0 0 0 0 0\n')
542
+
543
+ # Surfaces
544
+ if surface_set:
545
+ for surf_id in sorted(surface_set):
546
+ try:
547
+ bbox = cubit.get_bounding_box("surface", surf_id)
548
+ minx, maxx = bbox[0], bbox[1]
549
+ miny, maxy = bbox[3], bbox[4]
550
+ minz, maxz = bbox[6], bbox[7]
551
+ # Physical tag
552
+ phys_tag = surface_physical.get(surf_id, 0)
553
+ num_phys = 1 if phys_tag else 0
554
+ phys_str = f' {phys_tag}' if phys_tag else ''
555
+ # Get bounding curves
556
+ try:
557
+ surf = cubit.surface(surf_id)
558
+ curve_ids = [c.id() for c in surf.curves()]
559
+ num_bnd = len(curve_ids)
560
+ curve_str = ' '.join(str(c) for c in curve_ids)
561
+ except:
562
+ num_bnd = 0
563
+ curve_str = ''
564
+ fid.write(f'{surf_id} {minx} {miny} {minz} {maxx} {maxy} {maxz} {num_phys}{phys_str} {num_bnd} {curve_str}\n')
565
+ except:
566
+ fid.write(f'{surf_id} 0 0 0 0 0 0 0 0\n')
567
+ else:
568
+ # Fallback: use blocks as surfaces
569
+ for block_id in cubit.get_block_id_list():
570
+ if len(_get_block_elements(cubit, block_id, "tri")) + len(_get_block_elements(cubit, block_id, "face")) > 0:
571
+ fid.write(f'{block_id} 0 0 0 0 0 0 1 {block_id} 0\n')
572
+
573
+ # Volumes
574
+ if volume_set:
575
+ for vol_id in sorted(volume_set):
576
+ try:
577
+ bbox = cubit.get_bounding_box("volume", vol_id)
578
+ minx, maxx = bbox[0], bbox[1]
579
+ miny, maxy = bbox[3], bbox[4]
580
+ minz, maxz = bbox[6], bbox[7]
581
+ # Physical tag
582
+ phys_tag = volume_physical.get(vol_id, 0)
583
+ num_phys = 1 if phys_tag else 0
584
+ phys_str = f' {phys_tag}' if phys_tag else ''
585
+ # Get bounding surfaces
586
+ try:
587
+ vol = cubit.volume(vol_id)
588
+ surf_ids = [s.id() for s in vol.surfaces()]
589
+ num_bnd = len(surf_ids)
590
+ surf_str = ' '.join(str(s) for s in surf_ids)
591
+ except:
592
+ num_bnd = 0
593
+ surf_str = ''
594
+ fid.write(f'{vol_id} {minx} {miny} {minz} {maxx} {maxy} {maxz} {num_phys}{phys_str} {num_bnd} {surf_str}\n')
595
+ except:
596
+ fid.write(f'{vol_id} 0 0 0 0 0 0 0 0\n')
597
+ else:
598
+ # Fallback: use blocks as volumes
599
+ for block_id in cubit.get_block_id_list():
600
+ if len(_get_block_elements(cubit, block_id, "tet")) + len(_get_block_elements(cubit, block_id, "hex")) + \
601
+ len(_get_block_elements(cubit, block_id, "wedge")) + len(_get_block_elements(cubit, block_id, "pyramid")) > 0:
602
+ fid.write(f'{block_id} 0 0 0 0 0 0 1 {block_id} 0\n')
603
+
604
+ fid.write('$EndEntities\n')
605
+
606
+ # ------------------------------------------------------------
607
+ # $Nodes (v4 format: entity blocks)
608
+ # ------------------------------------------------------------
609
+ # For simplicity, put all nodes in one entity block
610
+ fid.write('$Nodes\n')
611
+ # numEntityBlocks numNodes minNodeTag maxNodeTag
612
+ fid.write(f'1 {num_nodes} {min_node_tag} {max_node_tag}\n')
613
+ # entityDim entityTag parametric numNodesInBlock
614
+ entity_dim = 3 if has_3d_elements else 2
615
+ entity_tag = 1
616
+ fid.write(f'{entity_dim} {entity_tag} 0 {num_nodes}\n')
617
+ # Node tags
618
+ for node_id in sorted_nodes:
619
+ fid.write(f'{node_id}\n')
620
+ # Node coordinates
621
+ for node_id in sorted_nodes:
622
+ coord = cubit.get_nodal_coordinates(node_id)
623
+ if actual_dim == "2D":
624
+ fid.write(f'{coord[0]} {coord[1]} 0\n')
625
+ else:
626
+ fid.write(f'{coord[0]} {coord[1]} {coord[2]}\n')
627
+ fid.write('$EndNodes\n')
628
+
629
+ # ------------------------------------------------------------
630
+ # $Elements (v4 format: entity blocks)
631
+ # ------------------------------------------------------------
632
+
633
+ # Helper function: get element nodes with optional normal flip for 2D
634
+ def get_element_nodes(elem_type, elem_id, flip_normal=False):
635
+ nodes = list(cubit.get_expanded_connectivity(elem_type, elem_id))
636
+ if flip_normal:
637
+ if elem_type == "tri":
638
+ if len(nodes) == 3:
639
+ nodes = [nodes[0], nodes[2], nodes[1]]
640
+ elif len(nodes) == 6:
641
+ nodes = [nodes[0], nodes[2], nodes[1], nodes[5], nodes[4], nodes[3]]
642
+ elif elem_type in ["quad", "face"]:
643
+ if len(nodes) == 4:
644
+ nodes = [nodes[0], nodes[3], nodes[2], nodes[1]]
645
+ elif len(nodes) == 8:
646
+ nodes = [nodes[0], nodes[3], nodes[2], nodes[1], nodes[7], nodes[6], nodes[5], nodes[4]]
647
+ return nodes
648
+
649
+ # Check if normal flip is needed for 2D elements
650
+ def needs_normal_flip(elem_type, elem_id):
651
+ if actual_dim != "2D":
652
+ return False
653
+ try:
654
+ owner = cubit.get_geometry_owner(elem_type, elem_id)
655
+ if owner and "surface" in owner.lower():
656
+ surf_id = int(owner.split()[1])
657
+ normal = cubit.get_surface_normal(surf_id)
658
+ return normal[2] < 0
659
+ except:
660
+ pass
661
+ return False
662
+
663
+ # Count elements per block for entity blocks
664
+ block_elements = {} # block_id -> {'tet': [...], 'hex': [...], ...}
665
+ for block_id in cubit.get_block_id_list():
666
+ block_elements[block_id] = {
667
+ 'tet': list(_get_block_elements(cubit, block_id, "tet")),
668
+ 'hex': list(_get_block_elements(cubit, block_id, "hex")),
669
+ 'wedge': list(_get_block_elements(cubit, block_id, "wedge")),
670
+ 'pyramid': list(_get_block_elements(cubit, block_id, "pyramid")),
671
+ 'tri': list(_get_block_elements(cubit, block_id, "tri")),
672
+ 'quad': list(_get_block_elements(cubit, block_id, "face")),
673
+ 'edge': list(_get_block_elements(cubit, block_id, "edge")),
674
+ 'node': list(_get_block_elements(cubit, block_id, "node")),
675
+ }
676
+
677
+ # Count total elements and entity blocks
678
+ total_elements = len(all_tets) + len(all_hexes) + len(all_wedges) + len(all_pyramids) + \
679
+ len(all_tris) + len(all_quads) + len(all_edges) + len(all_points)
680
+
681
+ # Count entity blocks (one per element type per block)
682
+ num_entity_blocks = 0
683
+ for block_id, elems in block_elements.items():
684
+ for elem_type, elem_list in elems.items():
685
+ if len(elem_list) > 0:
686
+ num_entity_blocks += 1
687
+
688
+ if total_elements == 0:
689
+ min_elem_tag = 0
690
+ max_elem_tag = 0
691
+ else:
692
+ min_elem_tag = 1
693
+ max_elem_tag = total_elements
694
+
695
+ fid.write('$Elements\n')
696
+ fid.write(f'{num_entity_blocks} {total_elements} {min_elem_tag} {max_elem_tag}\n')
697
+
698
+ element_tag = 0
699
+
700
+ for block_id in cubit.get_block_id_list():
701
+ elems = block_elements[block_id]
702
+
703
+ # Tetrahedra
704
+ if len(elems['tet']) > 0:
705
+ tet_list = elems['tet']
706
+ # Determine element type from first element
707
+ sample_nodes = cubit.get_expanded_connectivity("tet", tet_list[0])
708
+ if len(sample_nodes) == 4:
709
+ gmsh_type = GMSH_TET4
710
+ elif len(sample_nodes) == 10:
711
+ gmsh_type = GMSH_TET10
712
+ else:
713
+ gmsh_type = GMSH_TET11
714
+
715
+ fid.write(f'3 {block_id} {gmsh_type} {len(tet_list)}\n')
716
+ for tet_id in tet_list:
717
+ element_tag += 1
718
+ nodes = cubit.get_expanded_connectivity("tet", tet_id)
719
+ if len(nodes) == 4:
720
+ node_str = ' '.join(str(n) for n in nodes)
721
+ elif len(nodes) == 10:
722
+ # Cubit -> Gmsh node order for TET10
723
+ reordered = [nodes[i] for i in [0, 1, 2, 3, 4, 5, 6, 7, 9, 8]]
724
+ node_str = ' '.join(str(n) for n in reordered)
725
+ else: # TET11
726
+ reordered = [nodes[i] for i in [0, 1, 2, 3, 4, 5, 6, 7, 9, 8, 10]]
727
+ node_str = ' '.join(str(n) for n in reordered)
728
+ fid.write(f'{element_tag} {node_str}\n')
729
+
730
+ # Hexahedra
731
+ if len(elems['hex']) > 0:
732
+ hex_list = elems['hex']
733
+ sample_nodes = cubit.get_expanded_connectivity("hex", hex_list[0])
734
+ gmsh_type = GMSH_HEX20 if len(sample_nodes) == 20 else GMSH_HEX8
735
+
736
+ fid.write(f'3 {block_id} {gmsh_type} {len(hex_list)}\n')
737
+ for hex_id in hex_list:
738
+ element_tag += 1
739
+ nodes = cubit.get_expanded_connectivity("hex", hex_id)
740
+ if len(nodes) == 8:
741
+ node_str = ' '.join(str(n) for n in nodes)
742
+ else: # HEX20
743
+ # Cubit -> Gmsh node order for HEX20
744
+ reordered = [nodes[i] for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 9, 13, 10, 14, 15, 16, 19, 17, 18]]
745
+ node_str = ' '.join(str(n) for n in reordered)
746
+ fid.write(f'{element_tag} {node_str}\n')
747
+
748
+ # Wedges
749
+ if len(elems['wedge']) > 0:
750
+ wedge_list = elems['wedge']
751
+ sample_nodes = cubit.get_expanded_connectivity("wedge", wedge_list[0])
752
+ gmsh_type = GMSH_WEDGE15 if len(sample_nodes) == 15 else GMSH_WEDGE6
753
+
754
+ fid.write(f'3 {block_id} {gmsh_type} {len(wedge_list)}\n')
755
+ for wedge_id in wedge_list:
756
+ element_tag += 1
757
+ nodes = cubit.get_expanded_connectivity("wedge", wedge_id)
758
+ if len(nodes) == 6:
759
+ node_str = ' '.join(str(n) for n in nodes)
760
+ else: # WEDGE15
761
+ reordered = [nodes[i] for i in [0, 1, 2, 3, 4, 5, 6, 8, 9, 7, 10, 11, 12, 14, 13]]
762
+ node_str = ' '.join(str(n) for n in reordered)
763
+ fid.write(f'{element_tag} {node_str}\n')
764
+
765
+ # Pyramids
766
+ if len(elems['pyramid']) > 0:
767
+ pyramid_list = elems['pyramid']
768
+ sample_nodes = cubit.get_expanded_connectivity("pyramid", pyramid_list[0])
769
+ gmsh_type = GMSH_PYRAMID13 if len(sample_nodes) == 13 else GMSH_PYRAMID5
770
+
771
+ fid.write(f'3 {block_id} {gmsh_type} {len(pyramid_list)}\n')
772
+ for pyramid_id in pyramid_list:
773
+ element_tag += 1
774
+ nodes = cubit.get_expanded_connectivity("pyramid", pyramid_id)
775
+ if len(nodes) == 5:
776
+ node_str = ' '.join(str(n) for n in nodes)
777
+ else: # PYRAMID13
778
+ reordered = [nodes[i] for i in [0, 1, 2, 3, 4, 5, 8, 9, 6, 10, 7, 11, 12]]
779
+ node_str = ' '.join(str(n) for n in reordered)
780
+ fid.write(f'{element_tag} {node_str}\n')
781
+
782
+ # Triangles
783
+ if len(elems['tri']) > 0:
784
+ tri_list = elems['tri']
785
+ sample_nodes = cubit.get_expanded_connectivity("tri", tri_list[0])
786
+ if len(sample_nodes) == 3:
787
+ gmsh_type = GMSH_TRI3
788
+ elif len(sample_nodes) == 6:
789
+ gmsh_type = GMSH_TRI6
790
+ else:
791
+ gmsh_type = GMSH_TRI7
792
+
793
+ fid.write(f'2 {block_id} {gmsh_type} {len(tri_list)}\n')
794
+ for tri_id in tri_list:
795
+ element_tag += 1
796
+ flip = needs_normal_flip("tri", tri_id)
797
+ nodes = get_element_nodes("tri", tri_id, flip)
798
+ node_str = ' '.join(str(n) for n in nodes)
799
+ fid.write(f'{element_tag} {node_str}\n')
800
+
801
+ # Quads
802
+ if len(elems['quad']) > 0:
803
+ quad_list = elems['quad']
804
+ sample_nodes = cubit.get_expanded_connectivity("quad", quad_list[0])
805
+ if len(sample_nodes) == 4:
806
+ gmsh_type = GMSH_QUAD4
807
+ elif len(sample_nodes) == 8:
808
+ gmsh_type = GMSH_QUAD8
809
+ else:
810
+ gmsh_type = GMSH_QUAD9
811
+
812
+ fid.write(f'2 {block_id} {gmsh_type} {len(quad_list)}\n')
813
+ for quad_id in quad_list:
814
+ element_tag += 1
815
+ flip = needs_normal_flip("quad", quad_id)
816
+ nodes = get_element_nodes("quad", quad_id, flip)
817
+ node_str = ' '.join(str(n) for n in nodes)
818
+ fid.write(f'{element_tag} {node_str}\n')
819
+
820
+ # Edges
821
+ if len(elems['edge']) > 0:
822
+ edge_list = elems['edge']
823
+ sample_nodes = cubit.get_expanded_connectivity("edge", edge_list[0])
824
+ gmsh_type = GMSH_LINE3 if len(sample_nodes) == 3 else GMSH_LINE2
825
+
826
+ fid.write(f'1 {block_id} {gmsh_type} {len(edge_list)}\n')
827
+ for edge_id in edge_list:
828
+ element_tag += 1
829
+ nodes = cubit.get_expanded_connectivity("edge", edge_id)
830
+ node_str = ' '.join(str(n) for n in nodes)
831
+ fid.write(f'{element_tag} {node_str}\n')
832
+
833
+ # Points (nodes as elements)
834
+ if len(elems['node']) > 0:
835
+ node_list = elems['node']
836
+ fid.write(f'0 {block_id} {GMSH_POINT} {len(node_list)}\n')
837
+ for node_id in node_list:
838
+ element_tag += 1
839
+ fid.write(f'{element_tag} {node_id}\n')
840
+
841
+ fid.write('$EndElements\n')
842
+
843
+ return cubit
844
+
845
+ ########################################################################
846
+ ### Nastran file
847
+ ########################################################################
848
+
849
+ def export_Nastran(cubit: Any, FileName: str, DIM: str = "3D", PYRAM: bool = True) -> Any:
850
+ """Export mesh to Nastran format.
851
+
852
+ Exports mesh elements from Cubit to NX Nastran bulk data format.
853
+ Supports 2D and 3D meshes with first-order elements only.
854
+
855
+ Args:
856
+ cubit: Cubit Python interface object
857
+ FileName: Output file path for the .nas or .bdf file
858
+ DIM: Dimension mode - "2D" for 2D meshes, "3D" for 3D meshes (default: "3D")
859
+ PYRAM: If True, export pyramids as CPYRAM; if False, convert to degenerate hex (default: True)
860
+
861
+ Returns:
862
+ cubit: The cubit object (for method chaining)
863
+
864
+ Supported elements:
865
+ - 3D: CTETRA (tet4), CHEXA (hex8), CPENTA (wedge6), CPYRAM (pyramid5)
866
+ - 2D: CTRIA3 (tri3), CQUAD4 (quad4)
867
+ - 1D: CROD (edge/bar)
868
+ - 0D: CMASS (point mass)
869
+
870
+ Note:
871
+ Only first-order elements are supported. Second-order elements are not exported.
872
+ """
873
+ _warn_mixed_element_types_in_blocks(cubit)
874
+
875
+ import datetime
876
+ formatted_date_time = datetime.datetime.now().strftime("%d-%b-%y at %H:%M:%S")
877
+ with open(FileName,'w',encoding='UTF-8') as fid:
878
+ fid.write("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\n")
879
+ fid.write("$\n")
880
+ fid.write("$ CUBIT NX Nastran Translator\n")
881
+ fid.write("$\n")
882
+ fid.write(f"$ File: {FileName}\n")
883
+ fid.write(f"$ Time Stamp: {formatted_date_time}\n")
884
+ fid.write("$\n")
885
+ fid.write("$\n")
886
+ fid.write("$ PLEASE CHECK YOUR MODEL FOR UNITS CONSISTENCY.\n")
887
+ fid.write("$\n")
888
+ fid.write("$ It should be noted that load ID's from CUBIT may NOT correspond to Nastran SID's\n")
889
+ fid.write("$ The SID's for the load and restraint sets start at one and increment by one:i.e.,1,2,3,4...\n")
890
+ fid.write("$\n")
891
+ fid.write("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\n")
892
+ fid.write("$\n")
893
+ fid.write("$\n")
894
+ fid.write("$ -------------------------\n")
895
+ fid.write("$ Executive Control Section\n")
896
+ fid.write("$ -------------------------\n")
897
+ fid.write("$\n")
898
+ fid.write("SOL 101\n")
899
+ fid.write("CEND\n")
900
+ fid.write("$\n")
901
+ fid.write("$\n")
902
+ fid.write("$ --------------------\n")
903
+ fid.write("$ Case Control Section\n")
904
+ fid.write("$ --------------------\n")
905
+ fid.write("$\n")
906
+ fid.write("ECHO = SORT\n")
907
+ fid.write("$\n")
908
+ fid.write("$\n")
909
+ fid.write("$ Name: Initial\n")
910
+ fid.write("$\n")
911
+ fid.write("$\n")
912
+ fid.write("$ Name: Default Set\n")
913
+ fid.write("$\n")
914
+ fid.write("SUBCASE = 1\n")
915
+ fid.write("$\n")
916
+ fid.write("LABEL = Default Set\n")
917
+ fid.write("$\n")
918
+ fid.write("$ -----------------\n")
919
+ fid.write("$ Bulk Data Section\n")
920
+ fid.write("$ -----------------\n")
921
+ fid.write("$\n")
922
+ fid.write("BEGIN BULK\n")
923
+ fid.write("$\n")
924
+ fid.write("$ Params\n")
925
+ fid.write("$\n")
926
+ fid.write("$\n")
927
+ fid.write("$ Node cards\n")
928
+ fid.write("$\n")
929
+
930
+ node_list = set()
931
+ for block_id in cubit.get_block_id_list():
932
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
933
+ for elem_type in elem_types:
934
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
935
+ node_ids = cubit.get_connectivity(elem_type, element_id)
936
+ node_list.update(node_ids)
937
+ for node_id in node_list:
938
+ coord = cubit.get_nodal_coordinates(node_id)
939
+ if DIM == "3D":
940
+ fid.write(f"GRID* {node_id:>16}{0:>16}{coord[0]:>16.5f}{coord[1]:>16.5f}\n* {coord[2]:>16.5f}\n")
941
+ else:
942
+ fid.write(f"GRID* {node_id:>16}{0:>16}{coord[0]:>16.5f}{coord[1]:>16.5f}\n* {0}\n")
943
+
944
+ element_id = 0
945
+ fid.write("$\n")
946
+ fid.write("$ Element cards\n")
947
+ fid.write("$\n")
948
+ for block_id in cubit.get_block_id_list():
949
+ name = cubit.get_exodus_entity_name("block",block_id)
950
+ fid.write("$\n")
951
+ fid.write(f"$ Name: {name}\n")
952
+ fid.write("$\n")
953
+ tet_list = _get_block_elements(cubit, block_id, "tet")
954
+
955
+ if DIM=="3D":
956
+ for tet_id in tet_list:
957
+ node_list = cubit.get_connectivity('tet',tet_id)
958
+ element_id += 1
959
+ fid.write(f"CTETRA {element_id:>8}{block_id:>8}{node_list[0]:>8}{node_list[1]:>8}{node_list[2]:>8}{node_list[3]:>8}\n")
960
+ hex_list = _get_block_elements(cubit, block_id, "hex")
961
+ for hex_id in hex_list:
962
+ node_list = cubit.get_connectivity('hex',hex_id)
963
+ element_id += 1
964
+ fid.write(f"CHEXA {element_id:>8}{block_id:>8}{node_list[0]:>8}{node_list[1]:>8}{node_list[2]:>8}{node_list[3]:>8}{node_list[4]:>8}{node_list[5]:>8}+\n+ {node_list[6]:>8}{node_list[7]:>8}\n")
965
+ wedge_list = _get_block_elements(cubit, block_id, "wedge")
966
+ for wedge_id in wedge_list:
967
+ node_list = cubit.get_connectivity('wedge',wedge_id)
968
+ element_id += 1
969
+ fid.write(f"CPENTA {element_id:>8}{block_id:>8}{node_list[0]:>8}{node_list[1]:>8}{node_list[2]:>8}{node_list[3]:>8}{node_list[4]:>8}{node_list[5]:>8}\n")
970
+ pyramid_list = _get_block_elements(cubit, block_id, "pyramid")
971
+ for pyramid_id in pyramid_list:
972
+ node_list = cubit.get_connectivity('pyramid',pyramid_id)
973
+ if PYRAM:
974
+ element_id += 1
975
+ fid.write(f"CPYRAM {element_id:>8}{block_id:>8}{node_list[0]:>8}{node_list[1]:>8}{node_list[2]:>8}{node_list[3]:>8}{node_list[4]:>8}\n")
976
+ else:
977
+ element_id += 1
978
+ fid.write(f"CHEXA {element_id:>8}{block_id:>8}{node_list[0]:>8}{node_list[1]:>8}{node_list[2]:>8}{node_list[3]:>8}{node_list[4]:>8}{node_list[4]:>8}+\n+ {node_list[4]:>8}{node_list[4]:>8}\n")
979
+
980
+ tri_list = _get_block_elements(cubit, block_id, "tri")
981
+ for tri_id in tri_list:
982
+ node_list = cubit.get_connectivity('tri',tri_id)
983
+ element_id += 1
984
+ if DIM=="3D":
985
+ fid.write(f"CTRIA3 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[1]:<8}{node_list[2]:<8}\n")
986
+ else:
987
+ surface_id = int(cubit.get_geometry_owner("tri", tri_id).split()[1])
988
+ normal = cubit.get_surface_normal(surface_id)
989
+ if normal[2] > 0:
990
+ fid.write(f"CTRIA3 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[1]:<8}{node_list[2]:<8}\n")
991
+ else:
992
+ fid.write(f"CTRIA3 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[2]:<8}{node_list[1]:<8}\n")
993
+ quad_list = _get_block_elements(cubit, block_id, "face")
994
+ for quad_id in quad_list:
995
+ node_list = cubit.get_connectivity('quad',quad_id)
996
+ element_id += 1
997
+ if DIM=="3D":
998
+ fid.write(f"CQUAD4 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[1]:<8}{node_list[2]:<8}{node_list[3]:<8}\n")
999
+ else:
1000
+ surface_id = int(cubit.get_geometry_owner("quad", quad_id).split()[1])
1001
+ normal = cubit.get_surface_normal(surface_id)
1002
+ node_list = cubit.get_connectivity('quad',quad_id)
1003
+ if normal[2] > 0:
1004
+ fid.write(f"CQUAD4 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[1]:<8}{node_list[2]:<8}{node_list[3]:<8}\n")
1005
+ else:
1006
+ fid.write(f"CQUAD4 {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[3]:<8}{node_list[2]:<8}{node_list[1]:<8}\n")
1007
+ edge_list = _get_block_elements(cubit, block_id, "edge")
1008
+ for edge_id in edge_list:
1009
+ element_id += 1
1010
+ node_list = cubit.get_connectivity('edge', edge_id)
1011
+ fid.write(f"CROD {element_id:<8}{block_id:<8}{node_list[0]:<8}{node_list[1]:<8}\n")
1012
+ node_list = _get_block_elements(cubit, block_id, "node")
1013
+ for node_id in node_list:
1014
+ element_id += 1
1015
+ fid.write(f"CMASS {element_id:<8}{block_id:<8}{node_id:<8}\n")
1016
+ fid.write("$\n")
1017
+ fid.write("$ Property cards\n")
1018
+ fid.write("$\n")
1019
+
1020
+ for block_id in cubit.get_block_id_list():
1021
+ name = cubit.get_exodus_entity_name("block",block_id)
1022
+ fid.write("$\n")
1023
+ fid.write(f"$ Name: {name}\n")
1024
+ if len(_get_block_elements(cubit, block_id, "node")) > 0:
1025
+ fid.write(f"PMASS {block_id:< 8}{block_id:< 8}\n")
1026
+ elif len(_get_block_elements(cubit, block_id, "edge")) > 0:
1027
+ fid.write(f"PROD {block_id:< 8}{block_id:< 8}\n")
1028
+ elif len(_get_block_elements(cubit, block_id, "tri")) + len(_get_block_elements(cubit, block_id, "face")) > 0:
1029
+ fid.write(f"PSHELL {block_id:< 8}{block_id:< 8}\n")
1030
+ else:
1031
+ fid.write(f"PSOLID {block_id:< 8}{block_id:< 8}\n")
1032
+ fid.write("$\n")
1033
+
1034
+ fid.write("ENDDATA\n")
1035
+ return cubit
1036
+
1037
+ ########################################################################
1038
+ ### ELF meg file
1039
+ ########################################################################
1040
+
1041
+ def export_meg(cubit: Any, FileName: str, DIM: str = 'T', MGR2: Optional[List[List[float]]] = None) -> Any:
1042
+ """Export mesh to ELF/MESH MEG format.
1043
+
1044
+ Exports mesh elements from Cubit to ELF/MAGIC MEG format for finite element analysis.
1045
+
1046
+ Args:
1047
+ cubit: Cubit Python interface object
1048
+ FileName: Output file path for the .meg file
1049
+ DIM: Dimension mode - 'T' for 3D, 'R' for axisymmetric, 'K' for 2D (default: 'T')
1050
+ MGR2: Optional list of spatial nodes [[x1,y1,z1], [x2,y2,z2], ...] (default: None)
1051
+
1052
+ Returns:
1053
+ cubit: The cubit object (for method chaining)
1054
+
1055
+ Supported elements:
1056
+ - 3D: Tetrahedron, Hexahedron, Wedge, Pyramid
1057
+ - 2D: Triangle, Quadrilateral
1058
+ - 1D: Edge
1059
+ - 0D: Node
1060
+ """
1061
+ _warn_mixed_element_types_in_blocks(cubit)
1062
+
1063
+ if MGR2 is None:
1064
+ MGR2 = []
1065
+
1066
+ with open(FileName,'w',encoding='UTF-8') as fid:
1067
+ fid.write("BOOK MEP 3.50\n")
1068
+ fid.write("* ELF/MESH VERSION 7.3.0\n")
1069
+ fid.write("* SOLVER = ELF/MAGIC\n")
1070
+ fid.write("MGSC 0.001\n")
1071
+ fid.write("* NODE\n")
1072
+
1073
+ node_list = set()
1074
+ for block_id in cubit.get_block_id_list():
1075
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge"]
1076
+ for elem_type in elem_types:
1077
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
1078
+ node_ids = cubit.get_connectivity(elem_type, element_id)
1079
+ node_list.update(node_ids)
1080
+ for node_id in node_list:
1081
+ coord = cubit.get_nodal_coordinates(node_id)
1082
+ if DIM=='T':
1083
+ fid.write(f"MGR1 {node_id} 0 {coord[0]} {coord[1]} {coord[2]}\n")
1084
+ if DIM=='K':
1085
+ fid.write(f"MGR1 {node_id} 0 {coord[0]} {coord[1]} {0}\n")
1086
+ if DIM=='R':
1087
+ fid.write(f"MGR1 {node_id} 0 {coord[0]} {0} {coord[2]}\n")
1088
+
1089
+ element_id = 0
1090
+ fid.write("* ELEMENT K\n")
1091
+ for block_id in cubit.get_block_id_list():
1092
+ name = cubit.get_exodus_entity_name("block",block_id)
1093
+
1094
+ if DIM=='T':
1095
+ tet_list = _get_block_elements(cubit, block_id, "tet")
1096
+ for tet_id in tet_list:
1097
+ node_list = cubit.get_connectivity('tet',tet_id)
1098
+ element_id += 1
1099
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]}\n")
1100
+
1101
+ hex_list = _get_block_elements(cubit, block_id, "hex")
1102
+ for hex_id in hex_list:
1103
+ node_list = cubit.get_connectivity('hex',hex_id)
1104
+ element_id += 1
1105
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]} {node_list[6]} {node_list[7]}\n")
1106
+
1107
+ wedge_list = _get_block_elements(cubit, block_id, "wedge")
1108
+ for wedge_id in wedge_list:
1109
+ node_list = cubit.get_connectivity('wedge',wedge_id)
1110
+ element_id += 1
1111
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[5]}\n")
1112
+
1113
+ pyramid_list = _get_block_elements(cubit, block_id, "pyramid")
1114
+ for pyramid_id in pyramid_list:
1115
+ node_list = cubit.get_connectivity('pyramid',pyramid_id)
1116
+ element_id += 1
1117
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]} {node_list[4]} {node_list[4]} {node_list[4]} {node_list[4]}\n")
1118
+
1119
+ tri_list = _get_block_elements(cubit, block_id, "tri")
1120
+ for tri_id in tri_list:
1121
+ node_list = cubit.get_connectivity('tri',tri_id)
1122
+ element_id += 1
1123
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]}\n")
1124
+
1125
+ quad_list = _get_block_elements(cubit, block_id, "face")
1126
+ for quad_id in quad_list:
1127
+ node_list = cubit.get_connectivity('quad',quad_id)
1128
+ element_id += 1
1129
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]} {node_list[2]} {node_list[3]}\n")
1130
+
1131
+ edge_list = _get_block_elements(cubit, block_id, "edge")
1132
+ for edge_id in edge_list:
1133
+ node_list = cubit.get_connectivity('edge',edge_id)
1134
+ element_id += 1
1135
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_list[0]} {node_list[1]}\n")
1136
+
1137
+ node_list = _get_block_elements(cubit, block_id, "node")
1138
+ for node_id in node_list:
1139
+ element_id += 1
1140
+ fid.write(f"{name[0:4]}{DIM} {element_id} 0 {block_id} {node_id}\n")
1141
+
1142
+ fid.write("* NODE\n")
1143
+ for node_id in range(len(MGR2)):
1144
+ fid.write(f"MGR2 {node_id+1} 0 {MGR2[node_id][0]} {MGR2[node_id][1]} {MGR2[node_id][2]}\n")
1145
+ fid.write("BOOK END\n")
1146
+ return cubit
1147
+
1148
+ ########################################################################
1149
+ ### vtk format
1150
+ ########################################################################
1151
+
1152
+ def export_vtk(cubit: Any, FileName: str) -> Any:
1153
+ """Export mesh to Legacy VTK format.
1154
+
1155
+ Exports mesh elements from Cubit to VTK (Visualization Toolkit) Legacy format.
1156
+ Automatically detects element order (1st or 2nd) based on node count.
1157
+ Supports mixed-order meshes (1st and 2nd order elements in same file).
1158
+
1159
+ Args:
1160
+ cubit: Cubit Python interface object
1161
+ FileName: Output file path for the .vtk file
1162
+
1163
+ Returns:
1164
+ cubit: The cubit object (for method chaining)
1165
+
1166
+ Supported elements:
1167
+ - 1st order: Tet4, Hex8, Wedge6, Pyramid5, Triangle3, Quad4, Line2, Point
1168
+ - 2nd order: Tet10, Hex20, Wedge15, Pyramid13, Triangle6, Quad8, Line3
1169
+
1170
+ Note:
1171
+ VTK cell data includes scalar values to distinguish element types.
1172
+ Element order is automatically detected from node count per element.
1173
+ """
1174
+ _warn_mixed_element_types_in_blocks(cubit)
1175
+
1176
+ with open(FileName,'w') as fid:
1177
+ fid.write('# vtk DataFile Version 3.0\n')
1178
+ fid.write(f'Unstructured Grid {FileName}\n')
1179
+ fid.write('ASCII\n')
1180
+ fid.write('DATASET UNSTRUCTURED_GRID\n')
1181
+ # First, collect all unique node IDs from all elements
1182
+ node_list = set()
1183
+ hex_list = set()
1184
+ tet_list = set()
1185
+ wedge_list = set()
1186
+ pyramid_list = set()
1187
+ tri_list = set()
1188
+ quad_list = set()
1189
+ edge_list = set()
1190
+ nodes_list = set()
1191
+
1192
+ for block_id in cubit.get_block_id_list():
1193
+ tet_list.update(_get_block_elements(cubit, block_id, "tet"))
1194
+ hex_list.update(_get_block_elements(cubit, block_id, "hex"))
1195
+ wedge_list.update(_get_block_elements(cubit, block_id, "wedge"))
1196
+ pyramid_list.update(_get_block_elements(cubit, block_id, "pyramid"))
1197
+ tri_list.update(_get_block_elements(cubit, block_id, "tri"))
1198
+ quad_list.update(_get_block_elements(cubit, block_id, "face"))
1199
+ edge_list.update(_get_block_elements(cubit, block_id, "edge"))
1200
+ nodes_list.update(_get_block_elements(cubit, block_id, "node"))
1201
+
1202
+ # Collect all node IDs from all element types
1203
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
1204
+ for block_id in cubit.get_block_id_list():
1205
+ for elem_type in elem_types:
1206
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
1207
+ node_ids = cubit.get_expanded_connectivity(elem_type, element_id)
1208
+ node_list.update(node_ids)
1209
+
1210
+ # Write POINTS header
1211
+ fid.write(f'POINTS {len(node_list)} float\n')
1212
+
1213
+ # Create mapping from Cubit node ID to VTK index (0-indexed)
1214
+ # Write coordinates in sorted order for consistency
1215
+ node_id_to_vtk_index = {}
1216
+ vtk_index = 0
1217
+ for node_id in sorted(node_list):
1218
+ coord = cubit.get_nodal_coordinates(node_id)
1219
+ fid.write(f'{coord[0]} {coord[1]} {coord[2]}\n')
1220
+ node_id_to_vtk_index[node_id] = vtk_index
1221
+ vtk_index += 1
1222
+
1223
+ # Pre-scan all elements to get node counts for each element
1224
+ # This enables auto-detection of element order and mixed-order support
1225
+ tet_node_counts = {tet_id: len(cubit.get_expanded_connectivity("tet", tet_id)) for tet_id in tet_list}
1226
+ hex_node_counts = {hex_id: len(cubit.get_expanded_connectivity("hex", hex_id)) for hex_id in hex_list}
1227
+ wedge_node_counts = {wedge_id: len(cubit.get_expanded_connectivity("wedge", wedge_id)) for wedge_id in wedge_list}
1228
+ pyramid_node_counts = {pyramid_id: len(cubit.get_expanded_connectivity("pyramid", pyramid_id)) for pyramid_id in pyramid_list}
1229
+ tri_node_counts = {tri_id: len(cubit.get_expanded_connectivity("tri", tri_id)) for tri_id in tri_list}
1230
+ quad_node_counts = {quad_id: len(cubit.get_expanded_connectivity("quad", quad_id)) for quad_id in quad_list}
1231
+ edge_node_counts = {edge_id: len(cubit.get_expanded_connectivity("edge", edge_id)) for edge_id in edge_list}
1232
+
1233
+ # Calculate total CELLS size: sum of (1 + node_count) for each element
1234
+ num_cells = len(tet_list) + len(hex_list) + len(wedge_list) + len(pyramid_list) + len(tri_list) + len(quad_list) + len(edge_list) + len(nodes_list)
1235
+ cells_size = (
1236
+ sum(1 + n for n in tet_node_counts.values()) +
1237
+ sum(1 + n for n in hex_node_counts.values()) +
1238
+ sum(1 + n for n in wedge_node_counts.values()) +
1239
+ sum(1 + n for n in pyramid_node_counts.values()) +
1240
+ sum(1 + n for n in tri_node_counts.values()) +
1241
+ sum(1 + n for n in quad_node_counts.values()) +
1242
+ sum(1 + n for n in edge_node_counts.values()) +
1243
+ 2 * len(nodes_list) # Each node: 1 (count) + 1 (node_id)
1244
+ )
1245
+ fid.write(f'CELLS {num_cells} {cells_size}\n')
1246
+
1247
+ for tet_id in tet_list:
1248
+ node_list = cubit.get_expanded_connectivity("tet", tet_id)
1249
+ if len(node_list)==4:
1250
+ fid.write(f'4 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]}\n')
1251
+ elif len(node_list)==10:
1252
+ fid.write(f'10 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]} {node_id_to_vtk_index[node_list[8]]} {node_id_to_vtk_index[node_list[9]]}\n')
1253
+ for hex_id in hex_list:
1254
+ node_list = cubit.get_expanded_connectivity("hex", hex_id)
1255
+ if len(node_list)==8:
1256
+ fid.write(f'8 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]}\n')
1257
+ elif len(node_list)==20:
1258
+ fid.write(f'20 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]} {node_id_to_vtk_index[node_list[8]]} {node_id_to_vtk_index[node_list[9]]} {node_id_to_vtk_index[node_list[10]]} {node_id_to_vtk_index[node_list[11]]} {node_id_to_vtk_index[node_list[16]]} {node_id_to_vtk_index[node_list[17]]} {node_id_to_vtk_index[node_list[18]]} {node_id_to_vtk_index[node_list[19]]} {node_id_to_vtk_index[node_list[12]]} {node_id_to_vtk_index[node_list[13]]} {node_id_to_vtk_index[node_list[14]]} {node_id_to_vtk_index[node_list[15]]}\n')
1259
+ for wedge_id in wedge_list:
1260
+ node_list = cubit.get_expanded_connectivity("wedge", wedge_id)
1261
+ if len(node_list)==6:
1262
+ fid.write(f'6 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} \n')
1263
+ elif len(node_list)==15:
1264
+ fid.write(f'15 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]} {node_id_to_vtk_index[node_list[8]]} {node_id_to_vtk_index[node_list[12]]} {node_id_to_vtk_index[node_list[13]]} {node_id_to_vtk_index[node_list[14]]} {node_id_to_vtk_index[node_list[9]]} {node_id_to_vtk_index[node_list[10]]} {node_id_to_vtk_index[node_list[11]]} \n')
1265
+
1266
+ for pyramid_id in pyramid_list:
1267
+ node_list = cubit.get_expanded_connectivity("pyramid", pyramid_id)
1268
+ if len(node_list)==5:
1269
+ fid.write(f'5 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} \n')
1270
+ elif len(node_list)==13:
1271
+ fid.write(f'13 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]} {node_id_to_vtk_index[node_list[8]]} {node_id_to_vtk_index[node_list[9]]} {node_id_to_vtk_index[node_list[10]]} {node_id_to_vtk_index[node_list[11]]} {node_id_to_vtk_index[node_list[12]]} \n')
1272
+ for tri_id in tri_list:
1273
+ node_list = cubit.get_expanded_connectivity("tri", tri_id)
1274
+ if len(node_list)==3:
1275
+ fid.write(f'3 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} \n')
1276
+ elif len(node_list)==6:
1277
+ fid.write(f'6 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} \n')
1278
+ for quad_id in quad_list:
1279
+ node_list = cubit.get_expanded_connectivity("quad", quad_id)
1280
+ if len(node_list)==4:
1281
+ fid.write(f'4 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} \n')
1282
+ elif len(node_list)==8:
1283
+ fid.write(f'8 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} {node_id_to_vtk_index[node_list[3]]} {node_id_to_vtk_index[node_list[4]]} {node_id_to_vtk_index[node_list[5]]} {node_id_to_vtk_index[node_list[6]]} {node_id_to_vtk_index[node_list[7]]}\n')
1284
+ for edge_id in edge_list:
1285
+ node_list = cubit.get_expanded_connectivity("edge", edge_id)
1286
+ if len(node_list)==2:
1287
+ fid.write(f'2 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} \n')
1288
+ elif len(node_list)==3:
1289
+ fid.write(f'3 {node_id_to_vtk_index[node_list[0]]} {node_id_to_vtk_index[node_list[1]]} {node_id_to_vtk_index[node_list[2]]} \n')
1290
+ for node_id in nodes_list:
1291
+ fid.write(f'1 {node_id_to_vtk_index[node_id]} \n')
1292
+
1293
+ fid.write(f'CELL_TYPES {num_cells}\n')
1294
+ # VTK cell types based on node count (auto-detection)
1295
+ # 1st order: TET=10, HEX=12, WEDGE=13, PYRAMID=14, TRI=5, QUAD=9, LINE=3, POINT=1
1296
+ # 2nd order: TET=24, HEX=25, WEDGE=26, PYRAMID=27, TRI=22, QUAD=23, LINE=21
1297
+ for tet_id in tet_list:
1298
+ fid.write('24\n' if tet_node_counts[tet_id] == 10 else '10\n')
1299
+ for hex_id in hex_list:
1300
+ fid.write('25\n' if hex_node_counts[hex_id] == 20 else '12\n')
1301
+ for wedge_id in wedge_list:
1302
+ fid.write('26\n' if wedge_node_counts[wedge_id] == 15 else '13\n')
1303
+ for pyramid_id in pyramid_list:
1304
+ fid.write('27\n' if pyramid_node_counts[pyramid_id] == 13 else '14\n')
1305
+ for tri_id in tri_list:
1306
+ fid.write('22\n' if tri_node_counts[tri_id] == 6 else '5\n')
1307
+ for quad_id in quad_list:
1308
+ fid.write('23\n' if quad_node_counts[quad_id] == 8 else '9\n')
1309
+ for edge_id in edge_list:
1310
+ fid.write('21\n' if edge_node_counts[edge_id] == 3 else '3\n')
1311
+ for node_id in nodes_list:
1312
+ fid.write('1\n')
1313
+ fid.write(f'CELL_DATA {num_cells}\n')
1314
+ fid.write('SCALARS scalars float\n')
1315
+ fid.write('LOOKUP_TABLE default\n')
1316
+ for tet_id in tet_list:
1317
+ fid.write('1\n')
1318
+ for hex_id in hex_list:
1319
+ fid.write('2\n')
1320
+ for wedge_id in wedge_list:
1321
+ fid.write('3\n')
1322
+ for pyramid_id in pyramid_list:
1323
+ fid.write('4\n')
1324
+ for tri_id in tri_list:
1325
+ fid.write('5\n')
1326
+ for quad_id in quad_list:
1327
+ fid.write('6\n')
1328
+ for edge_id in edge_list:
1329
+ fid.write('0\n')
1330
+ for node_id in nodes_list:
1331
+ fid.write('-1\n')
1332
+ return cubit
1333
+
1334
+ ########################################################################
1335
+ ### NGSolve/Netgen format
1336
+ ########################################################################
1337
+
1338
+ def export_NetgenMesh(cubit: Any, geometry_file: Optional[str] = None, geometry: Any = None) -> Any:
1339
+ """Export Cubit mesh to Netgen mesh format.
1340
+
1341
+ Creates a netgen.meshing.Mesh object directly from Cubit mesh data.
1342
+ When geometry is provided (file or OCC object), the mesh can be curved
1343
+ using ngsolve.Mesh.Curve(order) for high-order geometry approximation.
1344
+
1345
+ Note: netgen.meshing.Mesh is the core mesh data structure.
1346
+ ngsolve.Mesh is a wrapper/view for FEM analysis.
1347
+
1348
+ Args:
1349
+ cubit: Cubit Python interface object
1350
+ geometry_file: Path to geometry file (.step, .stp, .brep, .iges) for
1351
+ mesh.Curve() support. If None, mesh is created without
1352
+ geometry reference (Curve() will not work).
1353
+ geometry: An netgen.occ.OCCGeometry object. If provided, this takes
1354
+ precedence over geometry_file. Useful for avoiding seam
1355
+ issues with cylindrical surfaces (see note below).
1356
+
1357
+ Returns:
1358
+ netgen.meshing.Mesh: Netgen mesh object ready for use with NGSolve
1359
+
1360
+ Supported elements:
1361
+ - 3D: Tetrahedron (4-node), Hexahedron (8-node), Wedge (6-node), Pyramid (5-node)
1362
+ - 2D boundary: Triangle (3-node), Quadrilateral (4-node)
1363
+ - 1D boundary: Edge (2-node)
1364
+
1365
+ Usage:
1366
+ # Basic usage with STEP file
1367
+ ngmesh = export_NetgenMesh(cubit, geometry_file="geometry.step")
1368
+ mesh = ngsolve.Mesh(ngmesh)
1369
+ mesh.Curve(3)
1370
+
1371
+ # Using OCC geometry directly (recommended for cylinders)
1372
+ from netgen import occ
1373
+ from netgen.occ import OCCGeometry
1374
+ cyl = occ.Cylinder(occ.Pnt(0,0,-1), occ.Vec(0,0,1), radius=0.5, height=2)
1375
+ geo = OCCGeometry(cyl)
1376
+ ngmesh = export_NetgenMesh(cubit, geometry=geo)
1377
+ mesh = ngsolve.Mesh(ngmesh)
1378
+ mesh.Curve(3)
1379
+
1380
+ # Without geometry (no Curve support)
1381
+ ngmesh = export_NetgenMesh(cubit)
1382
+ mesh = ngsolve.Mesh(ngmesh)
1383
+
1384
+ Note:
1385
+ - Only 1st order elements are transferred; use mesh.Curve(order) for
1386
+ high-order geometry approximation
1387
+ - The geometry should match the geometry used to create the mesh
1388
+ - Block names are used as Materials/Boundaries in NGSolve
1389
+
1390
+ Cylindrical Surface Note:
1391
+ When using STEP files exported from Cubit with cylindrical surfaces,
1392
+ OCC may split the surface at the seam line (y=0), creating 2 faces
1393
+ where Cubit has 1 surface. Mesh elements crossing this seam may not
1394
+ curve correctly. To avoid this:
1395
+ 1. Use OCC primitives (occ.Cylinder) via the geometry parameter, or
1396
+ 2. Ensure mesh elements don't cross y=0 on cylindrical surfaces
1397
+ """
1398
+ _warn_mixed_element_types_in_blocks(cubit)
1399
+
1400
+ from netgen.meshing import Mesh, MeshPoint, Element3D, Element2D, Element1D, FaceDescriptor
1401
+ from netgen.csg import Pnt
1402
+
1403
+ # ============================================================
1404
+ # Node ordering conversion tables: Cubit -> Netgen
1405
+ # ============================================================
1406
+
1407
+ # Tetrahedron: Cubit [0,1,2,3] -> Netgen [0,1,2,3]
1408
+ TET_ORDERING = [0, 1, 2, 3]
1409
+
1410
+ # Hexahedron: Cubit [0,1,2,3,4,5,6,7] -> Netgen [0,1,5,4,3,2,6,7]
1411
+ HEX_ORDERING = [0, 1, 5, 4, 3, 2, 6, 7]
1412
+
1413
+ # Wedge/Prism: Cubit [0,1,2,3,4,5] -> Netgen [0,2,1,3,5,4]
1414
+ WEDGE_ORDERING = [0, 2, 1, 3, 5, 4]
1415
+
1416
+ # Pyramid: Cubit [0,1,2,3,4] -> Netgen [3,2,1,0,4]
1417
+ PYRAMID_ORDERING = [3, 2, 1, 0, 4]
1418
+
1419
+ # Triangle: Cubit [0,1,2] -> Netgen [0,1,2]
1420
+ TRI_ORDERING = [0, 1, 2]
1421
+
1422
+ # Quadrilateral: Cubit [0,1,2,3] -> Netgen [0,1,2,3]
1423
+ QUAD_ORDERING = [0, 1, 2, 3]
1424
+
1425
+ # ============================================================
1426
+ # Create netgen mesh
1427
+ # ============================================================
1428
+
1429
+ ngmesh = Mesh(dim=3)
1430
+
1431
+ # Load and attach geometry if provided
1432
+ # Priority: geometry parameter > geometry_file parameter
1433
+ geo = None
1434
+ if geometry is not None:
1435
+ # Use provided OCC geometry object directly
1436
+ geo = geometry
1437
+ ngmesh.SetGeometry(geo)
1438
+ elif geometry_file is not None:
1439
+ from netgen.occ import OCCGeometry
1440
+ geo = OCCGeometry(geometry_file)
1441
+ ngmesh.SetGeometry(geo)
1442
+
1443
+ # ============================================================
1444
+ # Collect all nodes from blocks
1445
+ # ============================================================
1446
+
1447
+ node_map = {} # Cubit node ID -> netgen point index
1448
+ all_nodes = set()
1449
+
1450
+ for block_id in cubit.get_block_id_list():
1451
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
1452
+ for elem_type in elem_types:
1453
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
1454
+ # Use get_connectivity for 1st order nodes only
1455
+ node_ids = cubit.get_connectivity(elem_type, element_id)
1456
+ all_nodes.update(node_ids)
1457
+
1458
+ # Add nodes to netgen mesh
1459
+ for node_id in sorted(all_nodes):
1460
+ coord = cubit.get_nodal_coordinates(node_id)
1461
+ pnt_idx = ngmesh.Add(MeshPoint(Pnt(coord[0], coord[1], coord[2])))
1462
+ node_map[node_id] = pnt_idx
1463
+
1464
+ # ============================================================
1465
+ # Add 3D volume elements
1466
+ # ============================================================
1467
+
1468
+ material_index = 0
1469
+
1470
+ for block_id in cubit.get_block_id_list():
1471
+ block_name = cubit.get_exodus_entity_name("block", block_id)
1472
+
1473
+ tet_list = _get_block_elements(cubit, block_id, "tet")
1474
+ hex_list = _get_block_elements(cubit, block_id, "hex")
1475
+ wedge_list = _get_block_elements(cubit, block_id, "wedge")
1476
+ pyramid_list = _get_block_elements(cubit, block_id, "pyramid")
1477
+
1478
+ # Only process blocks with 3D elements
1479
+ if len(tet_list) + len(hex_list) + len(wedge_list) + len(pyramid_list) > 0:
1480
+ material_index += 1
1481
+ ngmesh.SetMaterial(material_index, block_name)
1482
+
1483
+ # Add tetrahedra
1484
+ for tet_id in tet_list:
1485
+ nodes = cubit.get_connectivity("tet", tet_id)
1486
+ ng_nodes = [node_map[nodes[i]] for i in TET_ORDERING]
1487
+ ngmesh.Add(Element3D(material_index, ng_nodes))
1488
+
1489
+ # Add hexahedra
1490
+ for hex_id in hex_list:
1491
+ nodes = cubit.get_connectivity("hex", hex_id)
1492
+ ng_nodes = [node_map[nodes[i]] for i in HEX_ORDERING]
1493
+ ngmesh.Add(Element3D(material_index, ng_nodes))
1494
+
1495
+ # Add wedges/prisms
1496
+ for wedge_id in wedge_list:
1497
+ nodes = cubit.get_connectivity("wedge", wedge_id)
1498
+ ng_nodes = [node_map[nodes[i]] for i in WEDGE_ORDERING]
1499
+ ngmesh.Add(Element3D(material_index, ng_nodes))
1500
+
1501
+ # Add pyramids
1502
+ for pyramid_id in pyramid_list:
1503
+ nodes = cubit.get_connectivity("pyramid", pyramid_id)
1504
+ ng_nodes = [node_map[nodes[i]] for i in PYRAMID_ORDERING]
1505
+ ngmesh.Add(Element3D(material_index, ng_nodes))
1506
+
1507
+ # ============================================================
1508
+ # Add 2D surface elements (boundary faces)
1509
+ # ============================================================
1510
+
1511
+ fd_index = 0
1512
+
1513
+ # Build OCC face mapping when geometry is provided (for Curve() support)
1514
+ occ_face_info = None
1515
+ elem_to_occ_face = {} # (elem_type, elem_id) -> OCC face index
1516
+
1517
+ if geo is not None:
1518
+ # Analyze OCC faces to build mapping
1519
+ occ_faces = list(geo.shape.faces)
1520
+ occ_face_info = []
1521
+ for i, face in enumerate(occ_faces):
1522
+ center = face.center
1523
+ bb = face.bounding_box
1524
+ # bb is ((xmin, ymin, zmin), (xmax, ymax, zmax))
1525
+ z_range = bb[1][2] - bb[0][2]
1526
+ y_min, y_max = bb[0][1], bb[1][1]
1527
+ occ_face_info.append({
1528
+ 'index': i,
1529
+ 'center': (center.x, center.y, center.z),
1530
+ 'bbox': bb,
1531
+ 'z_range': z_range,
1532
+ 'y_min': y_min,
1533
+ 'y_max': y_max,
1534
+ 'is_planar': z_range < 0.01,
1535
+ })
1536
+
1537
+ # Build tri_id -> Cubit surface_id mapping
1538
+ tri_to_surface = {}
1539
+ surface_ids = cubit.get_entities("surface")
1540
+ for sid in surface_ids:
1541
+ for tri_id in cubit.get_surface_tris(sid):
1542
+ tri_to_surface[tri_id] = sid
1543
+
1544
+ # Build quad_id -> Cubit surface_id mapping
1545
+ quad_to_surface = {}
1546
+ for sid in surface_ids:
1547
+ for quad_id in cubit.get_surface_quads(sid):
1548
+ quad_to_surface[quad_id] = sid
1549
+
1550
+ # Helper function to get element centroid
1551
+ def get_elem_centroid(elem_type, elem_id):
1552
+ nodes = cubit.get_connectivity(elem_type, elem_id)
1553
+ coords = [cubit.get_nodal_coordinates(n) for n in nodes]
1554
+ return [sum(c[i] for c in coords) / len(coords) for i in range(3)]
1555
+
1556
+ # Helper function to check if element crosses y=0 seam
1557
+ def crosses_seam(elem_type, elem_id, tolerance=0.001):
1558
+ nodes = cubit.get_connectivity(elem_type, elem_id)
1559
+ coords = [cubit.get_nodal_coordinates(n) for n in nodes]
1560
+ y_vals = [c[1] for c in coords]
1561
+ y_min, y_max = min(y_vals), max(y_vals)
1562
+ # Element crosses seam if vertices are on both sides of y=0
1563
+ return y_min < -tolerance and y_max > tolerance
1564
+
1565
+ # Map Cubit surfaces to OCC faces based on centroid
1566
+ surface_to_occ = {}
1567
+ for sid in surface_ids:
1568
+ surf_centroid = cubit.get_surface_centroid(sid)
1569
+ surf_type = cubit.get_surface_type(sid).lower()
1570
+
1571
+ # For planar surfaces, match by z-coordinate
1572
+ if "plane" in surf_type:
1573
+ for info in occ_face_info:
1574
+ if info['is_planar']:
1575
+ if abs(info['center'][2] - surf_centroid[2]) < 0.1:
1576
+ surface_to_occ[sid] = [info['index']]
1577
+ break
1578
+ else:
1579
+ # For curved surfaces (cone, cylinder, etc.), find all matching OCC faces
1580
+ # OCC may split curved surfaces into multiple faces
1581
+ matching = []
1582
+ for info in occ_face_info:
1583
+ if not info['is_planar']:
1584
+ # Check if centroid z-range overlaps
1585
+ if (info['bbox'][0][2] <= surf_centroid[2] <= info['bbox'][1][2]):
1586
+ matching.append(info['index'])
1587
+ if matching:
1588
+ surface_to_occ[sid] = matching
1589
+
1590
+ # Map each triangle to its OCC face
1591
+ seam_crossing_count = 0
1592
+ for tri_id, sid in tri_to_surface.items():
1593
+ if sid not in surface_to_occ:
1594
+ continue
1595
+ occ_faces_for_surface = surface_to_occ[sid]
1596
+ if len(occ_faces_for_surface) == 1:
1597
+ elem_to_occ_face[('tri', tri_id)] = occ_faces_for_surface[0]
1598
+ else:
1599
+ # Multiple OCC faces for this surface (e.g., cylinder split)
1600
+ # Check if element crosses the seam (y=0)
1601
+ if crosses_seam("tri", tri_id):
1602
+ # Element crosses seam - mark as None to exclude from Curve()
1603
+ elem_to_occ_face[('tri', tri_id)] = None
1604
+ seam_crossing_count += 1
1605
+ else:
1606
+ # Determine by y-coordinate of triangle centroid
1607
+ centroid = get_elem_centroid("tri", tri_id)
1608
+ for occ_idx in occ_faces_for_surface:
1609
+ info = occ_face_info[occ_idx]
1610
+ # Check if centroid y is within OCC face bbox
1611
+ if info['y_min'] - 0.01 <= centroid[1] <= info['y_max'] + 0.01:
1612
+ elem_to_occ_face[('tri', tri_id)] = occ_idx
1613
+ break
1614
+
1615
+ # Map each quad to its OCC face
1616
+ for quad_id, sid in quad_to_surface.items():
1617
+ if sid not in surface_to_occ:
1618
+ continue
1619
+ occ_faces_for_surface = surface_to_occ[sid]
1620
+ if len(occ_faces_for_surface) == 1:
1621
+ elem_to_occ_face[('quad', quad_id)] = occ_faces_for_surface[0]
1622
+ else:
1623
+ # Check if element crosses the seam (y=0)
1624
+ if crosses_seam("face", quad_id):
1625
+ # Element crosses seam - mark as None to exclude from Curve()
1626
+ elem_to_occ_face[('quad', quad_id)] = None
1627
+ seam_crossing_count += 1
1628
+ else:
1629
+ centroid = get_elem_centroid("face", quad_id)
1630
+ for occ_idx in occ_faces_for_surface:
1631
+ info = occ_face_info[occ_idx]
1632
+ if info['y_min'] - 0.01 <= centroid[1] <= info['y_max'] + 0.01:
1633
+ elem_to_occ_face[('quad', quad_id)] = occ_idx
1634
+ break
1635
+
1636
+ # Print warning if seam-crossing elements found
1637
+ if seam_crossing_count > 0:
1638
+ print(f" Note: {seam_crossing_count} boundary element(s) cross y=0 seam - excluded from Curve()")
1639
+ print(f" For better results, use OCC geometry primitives (geometry= parameter)")
1640
+
1641
+ # Create FaceDescriptors and add boundary elements
1642
+ if occ_face_info is not None:
1643
+ # Create one FaceDescriptor per OCC face
1644
+ # surfnr is 1-indexed in Netgen (OCC Face i -> surfnr=i+1)
1645
+ occ_to_fd_index = {}
1646
+ for info in occ_face_info:
1647
+ fd_index += 1
1648
+ fd = FaceDescriptor(bc=fd_index, surfnr=info['index'] + 1)
1649
+ fd.bcname = f"face_{info['index']}"
1650
+ ngmesh.Add(fd)
1651
+ ngmesh.SetBCName(fd_index - 1, f"face_{info['index']}")
1652
+ occ_to_fd_index[info['index']] = fd_index
1653
+
1654
+ # Create a special FaceDescriptor for seam-crossing elements (surfnr=0)
1655
+ fd_index += 1
1656
+ fd_seam = FaceDescriptor(bc=fd_index, surfnr=0) # surfnr=0 -> no geometry curving
1657
+ fd_seam.bcname = "seam_crossing"
1658
+ ngmesh.Add(fd_seam)
1659
+ ngmesh.SetBCName(fd_index - 1, "seam_crossing")
1660
+ fd_seam_index = fd_index
1661
+
1662
+ # Add boundary elements with correct FaceDescriptor
1663
+ for block_id in cubit.get_block_id_list():
1664
+ tri_list = _get_block_elements(cubit, block_id, "tri")
1665
+ quad_list = _get_block_elements(cubit, block_id, "face")
1666
+
1667
+ for tri_id in tri_list:
1668
+ nodes = cubit.get_connectivity("tri", tri_id)
1669
+ if all(n in node_map for n in nodes):
1670
+ ng_nodes = [node_map[nodes[i]] for i in TRI_ORDERING]
1671
+ key = ('tri', tri_id)
1672
+ if key in elem_to_occ_face:
1673
+ occ_idx = elem_to_occ_face[key]
1674
+ if occ_idx is None:
1675
+ bc_idx = fd_seam_index # Seam-crossing element
1676
+ else:
1677
+ bc_idx = occ_to_fd_index[occ_idx]
1678
+ else:
1679
+ bc_idx = 1 # Fallback
1680
+ ngmesh.Add(Element2D(bc_idx, ng_nodes))
1681
+
1682
+ for quad_id in quad_list:
1683
+ nodes = cubit.get_connectivity("quad", quad_id)
1684
+ if all(n in node_map for n in nodes):
1685
+ ng_nodes = [node_map[nodes[i]] for i in QUAD_ORDERING]
1686
+ key = ('quad', quad_id)
1687
+ if key in elem_to_occ_face:
1688
+ occ_idx = elem_to_occ_face[key]
1689
+ if occ_idx is None:
1690
+ bc_idx = fd_seam_index # Seam-crossing element
1691
+ else:
1692
+ bc_idx = occ_to_fd_index[occ_idx]
1693
+ else:
1694
+ bc_idx = 1 # Fallback
1695
+ ngmesh.Add(Element2D(bc_idx, ng_nodes))
1696
+ else:
1697
+ # No geometry file - use simple block-based FaceDescriptors
1698
+ for block_id in cubit.get_block_id_list():
1699
+ block_name = cubit.get_exodus_entity_name("block", block_id)
1700
+
1701
+ tri_list = _get_block_elements(cubit, block_id, "tri")
1702
+ quad_list = _get_block_elements(cubit, block_id, "face")
1703
+
1704
+ # Only process blocks with 2D elements
1705
+ if len(tri_list) + len(quad_list) > 0:
1706
+ fd_index += 1
1707
+ fd = FaceDescriptor(bc=fd_index, surfnr=fd_index)
1708
+ fd.bcname = block_name
1709
+ ngmesh.Add(fd)
1710
+ ngmesh.SetBCName(fd_index - 1, block_name)
1711
+
1712
+ # Add triangles
1713
+ for tri_id in tri_list:
1714
+ nodes = cubit.get_connectivity("tri", tri_id)
1715
+ if all(n in node_map for n in nodes):
1716
+ ng_nodes = [node_map[nodes[i]] for i in TRI_ORDERING]
1717
+ ngmesh.Add(Element2D(fd_index, ng_nodes))
1718
+
1719
+ # Add quadrilaterals
1720
+ for quad_id in quad_list:
1721
+ nodes = cubit.get_connectivity("quad", quad_id)
1722
+ if all(n in node_map for n in nodes):
1723
+ ng_nodes = [node_map[nodes[i]] for i in QUAD_ORDERING]
1724
+ ngmesh.Add(Element2D(fd_index, ng_nodes))
1725
+
1726
+ # ============================================================
1727
+ # Add 1D edge elements (if any)
1728
+ # ============================================================
1729
+
1730
+ for block_id in cubit.get_block_id_list():
1731
+ block_name = cubit.get_exodus_entity_name("block", block_id)
1732
+ edge_list = _get_block_elements(cubit, block_id, "edge")
1733
+
1734
+ if len(edge_list) > 0:
1735
+ fd_index += 1
1736
+ fd = FaceDescriptor(bc=fd_index, surfnr=fd_index)
1737
+ fd.bcname = block_name
1738
+ ngmesh.Add(fd)
1739
+
1740
+ for edge_id in edge_list:
1741
+ nodes = cubit.get_connectivity("edge", edge_id)
1742
+ if all(n in node_map for n in nodes):
1743
+ ng_nodes = [node_map[n] for n in nodes]
1744
+ ngmesh.Add(Element1D(ng_nodes, index=fd_index))
1745
+
1746
+ return ngmesh
1747
+
1748
+ ########################################################################
1749
+ ### VTK XML format (VTU)
1750
+ ########################################################################
1751
+
1752
+ def export_vtu(cubit: Any, FileName: str, binary: bool = False) -> Any:
1753
+ """Export mesh to VTK XML format (VTU - Unstructured Grid).
1754
+
1755
+ Exports mesh elements from Cubit to VTK XML format (.vtu).
1756
+ This is the modern VTK format recommended for ParaView and other tools.
1757
+ Automatically detects element order (1st or 2nd) based on node count.
1758
+
1759
+ Args:
1760
+ cubit: Cubit Python interface object
1761
+ FileName: Output file path for the .vtu file
1762
+ binary: If True, write binary data (appended format). Default: False (ASCII)
1763
+
1764
+ Returns:
1765
+ cubit: The cubit object (for method chaining)
1766
+
1767
+ Supported elements:
1768
+ - 1st order: Tet4, Hex8, Wedge6, Pyramid5, Triangle3, Quad4, Line2, Point
1769
+ - 2nd order: Tet10, Hex20, Wedge15, Pyramid13, Triangle6, Quad8, Line3
1770
+
1771
+ VTK XML vs Legacy:
1772
+ - XML format is more efficient and extensible
1773
+ - Supports compression (when binary=True)
1774
+ - Better metadata support
1775
+ - Recommended format for modern workflows
1776
+
1777
+ Example:
1778
+ cubit_mesh_export.export_vtu(cubit, "mesh.vtu")
1779
+ cubit_mesh_export.export_vtu(cubit, "mesh.vtu", binary=True) # Binary mode
1780
+ """
1781
+ _warn_mixed_element_types_in_blocks(cubit)
1782
+
1783
+ import base64
1784
+ import struct
1785
+
1786
+ # VTK cell type codes
1787
+ VTK_VERTEX = 1
1788
+ VTK_LINE = 3
1789
+ VTK_QUADRATIC_EDGE = 21
1790
+ VTK_TRIANGLE = 5
1791
+ VTK_QUADRATIC_TRIANGLE = 22
1792
+ VTK_QUAD = 9
1793
+ VTK_QUADRATIC_QUAD = 23
1794
+ VTK_TETRA = 10
1795
+ VTK_QUADRATIC_TETRA = 24
1796
+ VTK_HEXAHEDRON = 12
1797
+ VTK_QUADRATIC_HEXAHEDRON = 25
1798
+ VTK_WEDGE = 13
1799
+ VTK_QUADRATIC_WEDGE = 26
1800
+ VTK_PYRAMID = 14
1801
+ VTK_QUADRATIC_PYRAMID = 27
1802
+
1803
+ # Collect all elements from blocks
1804
+ hex_list = set()
1805
+ tet_list = set()
1806
+ wedge_list = set()
1807
+ pyramid_list = set()
1808
+ tri_list = set()
1809
+ quad_list = set()
1810
+ edge_list = set()
1811
+ nodes_list = set()
1812
+ node_set = set()
1813
+
1814
+ for block_id in cubit.get_block_id_list():
1815
+ tet_list.update(_get_block_elements(cubit, block_id, "tet"))
1816
+ hex_list.update(_get_block_elements(cubit, block_id, "hex"))
1817
+ wedge_list.update(_get_block_elements(cubit, block_id, "wedge"))
1818
+ pyramid_list.update(_get_block_elements(cubit, block_id, "pyramid"))
1819
+ tri_list.update(_get_block_elements(cubit, block_id, "tri"))
1820
+ quad_list.update(_get_block_elements(cubit, block_id, "face"))
1821
+ edge_list.update(_get_block_elements(cubit, block_id, "edge"))
1822
+ nodes_list.update(_get_block_elements(cubit, block_id, "node"))
1823
+
1824
+ # Collect all node IDs
1825
+ elem_types = ["hex", "tet", "wedge", "pyramid", "tri", "face", "edge", "node"]
1826
+ for block_id in cubit.get_block_id_list():
1827
+ for elem_type in elem_types:
1828
+ for element_id in _get_block_elements(cubit, block_id, elem_type):
1829
+ node_ids = cubit.get_expanded_connectivity(elem_type, element_id)
1830
+ node_set.update(node_ids)
1831
+
1832
+ # Create node ID to VTK index mapping
1833
+ node_id_to_vtk_index = {}
1834
+ vtk_index = 0
1835
+ sorted_nodes = sorted(node_set)
1836
+ for node_id in sorted_nodes:
1837
+ node_id_to_vtk_index[node_id] = vtk_index
1838
+ vtk_index += 1
1839
+
1840
+ # Pre-scan elements for node counts (for auto order detection)
1841
+ tet_node_counts = {tet_id: len(cubit.get_expanded_connectivity("tet", tet_id)) for tet_id in tet_list}
1842
+ hex_node_counts = {hex_id: len(cubit.get_expanded_connectivity("hex", hex_id)) for hex_id in hex_list}
1843
+ wedge_node_counts = {wedge_id: len(cubit.get_expanded_connectivity("wedge", wedge_id)) for wedge_id in wedge_list}
1844
+ pyramid_node_counts = {pyramid_id: len(cubit.get_expanded_connectivity("pyramid", pyramid_id)) for pyramid_id in pyramid_list}
1845
+ tri_node_counts = {tri_id: len(cubit.get_expanded_connectivity("tri", tri_id)) for tri_id in tri_list}
1846
+ quad_node_counts = {quad_id: len(cubit.get_expanded_connectivity("quad", quad_id)) for quad_id in quad_list}
1847
+ edge_node_counts = {edge_id: len(cubit.get_expanded_connectivity("edge", edge_id)) for edge_id in edge_list}
1848
+
1849
+ num_points = len(sorted_nodes)
1850
+ num_cells = len(tet_list) + len(hex_list) + len(wedge_list) + len(pyramid_list) + len(tri_list) + len(quad_list) + len(edge_list) + len(nodes_list)
1851
+
1852
+ # Build connectivity and offsets arrays
1853
+ connectivity = []
1854
+ offsets = []
1855
+ cell_types = []
1856
+ block_ids = []
1857
+ current_offset = 0
1858
+
1859
+ # Helper to get VTK type based on element type and node count
1860
+ def get_vtk_type(elem_type, node_count):
1861
+ if elem_type == "tet":
1862
+ return VTK_QUADRATIC_TETRA if node_count == 10 else VTK_TETRA
1863
+ elif elem_type == "hex":
1864
+ return VTK_QUADRATIC_HEXAHEDRON if node_count == 20 else VTK_HEXAHEDRON
1865
+ elif elem_type == "wedge":
1866
+ return VTK_QUADRATIC_WEDGE if node_count == 15 else VTK_WEDGE
1867
+ elif elem_type == "pyramid":
1868
+ return VTK_QUADRATIC_PYRAMID if node_count == 13 else VTK_PYRAMID
1869
+ elif elem_type == "tri":
1870
+ return VTK_QUADRATIC_TRIANGLE if node_count == 6 else VTK_TRIANGLE
1871
+ elif elem_type == "quad":
1872
+ return VTK_QUADRATIC_QUAD if node_count == 8 else VTK_QUAD
1873
+ elif elem_type == "edge":
1874
+ return VTK_QUADRATIC_EDGE if node_count == 3 else VTK_LINE
1875
+ else:
1876
+ return VTK_VERTEX
1877
+
1878
+ # Process tetrahedra
1879
+ for tet_id in tet_list:
1880
+ nodes = cubit.get_expanded_connectivity("tet", tet_id)
1881
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1882
+ connectivity.extend(vtk_nodes)
1883
+ current_offset += len(nodes)
1884
+ offsets.append(current_offset)
1885
+ cell_types.append(get_vtk_type("tet", len(nodes)))
1886
+ # Find block ID for this element
1887
+ for block_id in cubit.get_block_id_list():
1888
+ if tet_id in _get_block_elements(cubit, block_id, "tet"):
1889
+ block_ids.append(block_id)
1890
+ break
1891
+
1892
+ # Process hexahedra
1893
+ for hex_id in hex_list:
1894
+ nodes = cubit.get_expanded_connectivity("hex", hex_id)
1895
+ # HEX20 node reordering: Cubit -> VTK
1896
+ if len(nodes) == 20:
1897
+ vtk_nodes = [node_id_to_vtk_index[nodes[i]] for i in [0,1,2,3,4,5,6,7,8,9,10,11,16,17,18,19,12,13,14,15]]
1898
+ else:
1899
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1900
+ connectivity.extend(vtk_nodes)
1901
+ current_offset += len(nodes)
1902
+ offsets.append(current_offset)
1903
+ cell_types.append(get_vtk_type("hex", len(nodes)))
1904
+ for block_id in cubit.get_block_id_list():
1905
+ if hex_id in _get_block_elements(cubit, block_id, "hex"):
1906
+ block_ids.append(block_id)
1907
+ break
1908
+
1909
+ # Process wedges
1910
+ for wedge_id in wedge_list:
1911
+ nodes = cubit.get_expanded_connectivity("wedge", wedge_id)
1912
+ # WEDGE15 node reordering: Cubit -> VTK
1913
+ if len(nodes) == 15:
1914
+ vtk_nodes = [node_id_to_vtk_index[nodes[i]] for i in [0,1,2,3,4,5,6,7,8,12,13,14,9,10,11]]
1915
+ else:
1916
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1917
+ connectivity.extend(vtk_nodes)
1918
+ current_offset += len(nodes)
1919
+ offsets.append(current_offset)
1920
+ cell_types.append(get_vtk_type("wedge", len(nodes)))
1921
+ for block_id in cubit.get_block_id_list():
1922
+ if wedge_id in _get_block_elements(cubit, block_id, "wedge"):
1923
+ block_ids.append(block_id)
1924
+ break
1925
+
1926
+ # Process pyramids
1927
+ for pyramid_id in pyramid_list:
1928
+ nodes = cubit.get_expanded_connectivity("pyramid", pyramid_id)
1929
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1930
+ connectivity.extend(vtk_nodes)
1931
+ current_offset += len(nodes)
1932
+ offsets.append(current_offset)
1933
+ cell_types.append(get_vtk_type("pyramid", len(nodes)))
1934
+ for block_id in cubit.get_block_id_list():
1935
+ if pyramid_id in _get_block_elements(cubit, block_id, "pyramid"):
1936
+ block_ids.append(block_id)
1937
+ break
1938
+
1939
+ # Process triangles
1940
+ for tri_id in tri_list:
1941
+ nodes = cubit.get_expanded_connectivity("tri", tri_id)
1942
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1943
+ connectivity.extend(vtk_nodes)
1944
+ current_offset += len(nodes)
1945
+ offsets.append(current_offset)
1946
+ cell_types.append(get_vtk_type("tri", len(nodes)))
1947
+ for block_id in cubit.get_block_id_list():
1948
+ if tri_id in _get_block_elements(cubit, block_id, "tri"):
1949
+ block_ids.append(block_id)
1950
+ break
1951
+
1952
+ # Process quads
1953
+ for quad_id in quad_list:
1954
+ nodes = cubit.get_expanded_connectivity("quad", quad_id)
1955
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1956
+ connectivity.extend(vtk_nodes)
1957
+ current_offset += len(nodes)
1958
+ offsets.append(current_offset)
1959
+ cell_types.append(get_vtk_type("quad", len(nodes)))
1960
+ for block_id in cubit.get_block_id_list():
1961
+ if quad_id in _get_block_elements(cubit, block_id, "face"):
1962
+ block_ids.append(block_id)
1963
+ break
1964
+
1965
+ # Process edges
1966
+ for edge_id in edge_list:
1967
+ nodes = cubit.get_expanded_connectivity("edge", edge_id)
1968
+ vtk_nodes = [node_id_to_vtk_index[n] for n in nodes]
1969
+ connectivity.extend(vtk_nodes)
1970
+ current_offset += len(nodes)
1971
+ offsets.append(current_offset)
1972
+ cell_types.append(get_vtk_type("edge", len(nodes)))
1973
+ for block_id in cubit.get_block_id_list():
1974
+ if edge_id in _get_block_elements(cubit, block_id, "edge"):
1975
+ block_ids.append(block_id)
1976
+ break
1977
+
1978
+ # Process point elements
1979
+ for node_id in nodes_list:
1980
+ connectivity.append(node_id_to_vtk_index[node_id])
1981
+ current_offset += 1
1982
+ offsets.append(current_offset)
1983
+ cell_types.append(VTK_VERTEX)
1984
+ for block_id in cubit.get_block_id_list():
1985
+ if node_id in _get_block_elements(cubit, block_id, "node"):
1986
+ block_ids.append(block_id)
1987
+ break
1988
+
1989
+ # Write VTU file
1990
+ with open(FileName, 'w', encoding='UTF-8') as fid:
1991
+ fid.write('<?xml version="1.0"?>\n')
1992
+ fid.write('<VTKFile type="UnstructuredGrid" version="1.0" byte_order="LittleEndian">\n')
1993
+ fid.write(' <UnstructuredGrid>\n')
1994
+ fid.write(f' <Piece NumberOfPoints="{num_points}" NumberOfCells="{num_cells}">\n')
1995
+
1996
+ # Points
1997
+ fid.write(' <Points>\n')
1998
+ fid.write(' <DataArray type="Float64" NumberOfComponents="3" format="ascii">\n')
1999
+ for node_id in sorted_nodes:
2000
+ coord = cubit.get_nodal_coordinates(node_id)
2001
+ fid.write(f' {coord[0]} {coord[1]} {coord[2]}\n')
2002
+ fid.write(' </DataArray>\n')
2003
+ fid.write(' </Points>\n')
2004
+
2005
+ # Cells
2006
+ fid.write(' <Cells>\n')
2007
+
2008
+ # Connectivity
2009
+ fid.write(' <DataArray type="Int64" Name="connectivity" format="ascii">\n')
2010
+ fid.write(' ')
2011
+ for i, c in enumerate(connectivity):
2012
+ fid.write(f'{c} ')
2013
+ if (i + 1) % 20 == 0:
2014
+ fid.write('\n ')
2015
+ fid.write('\n')
2016
+ fid.write(' </DataArray>\n')
2017
+
2018
+ # Offsets
2019
+ fid.write(' <DataArray type="Int64" Name="offsets" format="ascii">\n')
2020
+ fid.write(' ')
2021
+ for i, o in enumerate(offsets):
2022
+ fid.write(f'{o} ')
2023
+ if (i + 1) % 20 == 0:
2024
+ fid.write('\n ')
2025
+ fid.write('\n')
2026
+ fid.write(' </DataArray>\n')
2027
+
2028
+ # Cell types
2029
+ fid.write(' <DataArray type="UInt8" Name="types" format="ascii">\n')
2030
+ fid.write(' ')
2031
+ for i, t in enumerate(cell_types):
2032
+ fid.write(f'{t} ')
2033
+ if (i + 1) % 20 == 0:
2034
+ fid.write('\n ')
2035
+ fid.write('\n')
2036
+ fid.write(' </DataArray>\n')
2037
+
2038
+ fid.write(' </Cells>\n')
2039
+
2040
+ # Cell Data (Block IDs)
2041
+ fid.write(' <CellData Scalars="BlockID">\n')
2042
+ fid.write(' <DataArray type="Int32" Name="BlockID" format="ascii">\n')
2043
+ fid.write(' ')
2044
+ for i, b in enumerate(block_ids):
2045
+ fid.write(f'{b} ')
2046
+ if (i + 1) % 20 == 0:
2047
+ fid.write('\n ')
2048
+ fid.write('\n')
2049
+ fid.write(' </DataArray>\n')
2050
+ fid.write(' </CellData>\n')
2051
+
2052
+ # Point Data (Node IDs)
2053
+ fid.write(' <PointData Scalars="NodeID">\n')
2054
+ fid.write(' <DataArray type="Int32" Name="NodeID" format="ascii">\n')
2055
+ fid.write(' ')
2056
+ for i, node_id in enumerate(sorted_nodes):
2057
+ fid.write(f'{node_id} ')
2058
+ if (i + 1) % 20 == 0:
2059
+ fid.write('\n ')
2060
+ fid.write('\n')
2061
+ fid.write(' </DataArray>\n')
2062
+ fid.write(' </PointData>\n')
2063
+
2064
+ fid.write(' </Piece>\n')
2065
+ fid.write(' </UnstructuredGrid>\n')
2066
+ fid.write('</VTKFile>\n')
2067
+
2068
+ return cubit
2069
+
2070
+
2071
+ ########################################################################
2072
+ ### Exodus II format (Cubit's native format)
2073
+ ########################################################################
2074
+
2075
+ def export_exodus(
2076
+ cubit: Any,
2077
+ filename: str,
2078
+ overwrite: bool = True,
2079
+ large_model: bool = False
2080
+ ) -> Any:
2081
+ """Export mesh to Exodus II format.
2082
+
2083
+ Exports the current Cubit mesh to Exodus II format (.exo, .e, .g).
2084
+ This is Cubit's native format and supports all element types and orders.
2085
+
2086
+ Args:
2087
+ cubit: Cubit Python interface object
2088
+ filename: Output file path (typically .exo, .e, or .g extension)
2089
+ overwrite: Whether to overwrite existing file (default: True)
2090
+ large_model: Use 64-bit integers for large models (default: False)
2091
+
2092
+ Returns:
2093
+ cubit: The cubit object (for method chaining)
2094
+
2095
+ Supported elements:
2096
+ - 0D: NODE
2097
+ - 1D: BAR, BAR2, BAR3
2098
+ - 2D: TRI, TRI6, TRI7, QUAD, QUAD8, QUAD9
2099
+ - 3D: TET, TET10, TET11, HEX, HEX20, HEX27, WEDGE, WEDGE15, PYRAMID, PYRAMID13
2100
+
2101
+ Example:
2102
+ >>> cubit.cmd("create brick x 1 y 1 z 1")
2103
+ >>> cubit.cmd("volume 1 scheme tetmesh")
2104
+ >>> cubit.cmd("mesh volume 1")
2105
+ >>> cubit.cmd("block 1 add tet all")
2106
+ >>> cubit.cmd("block 1 name 'solid'")
2107
+ >>> export_exodus(cubit, "cube.exo")
2108
+
2109
+ Note:
2110
+ Exodus format natively supports nodesets and sidesets.
2111
+ For element order control (e.g., converting to TET10),
2112
+ blocks must contain mesh elements rather than geometry.
2113
+ """
2114
+ _warn_mixed_element_types_in_blocks(cubit)
2115
+
2116
+
2117
+ # Build export command
2118
+ cmd_parts = ['export mesh']
2119
+ cmd_parts.append(f'"{filename}"')
2120
+
2121
+ # Add options
2122
+ if overwrite:
2123
+ cmd_parts.append('overwrite')
2124
+
2125
+ if large_model:
2126
+ cmd_parts.append('large')
2127
+
2128
+ # Execute export
2129
+ cmd = ' '.join(cmd_parts)
2130
+ cubit.cmd(cmd)
2131
+
2132
+ # Print summary
2133
+ _print_exodus_summary(cubit, filename)
2134
+
2135
+ return cubit
2136
+
2137
+
2138
+ def _print_exodus_summary(cubit: Any, filename: str) -> None:
2139
+ """Print summary of exported Exodus mesh."""
2140
+ print(f"\nExodus export: {filename}")
2141
+ print("-" * 50)
2142
+
2143
+ # Count elements by type
2144
+ element_counts = {}
2145
+
2146
+ # 3D elements
2147
+ try:
2148
+ tet_count = cubit.get_tet_count()
2149
+ if tet_count > 0:
2150
+ element_counts['Tetrahedra'] = tet_count
2151
+ except:
2152
+ pass
2153
+
2154
+ try:
2155
+ hex_count = cubit.get_hex_count()
2156
+ if hex_count > 0:
2157
+ element_counts['Hexahedra'] = hex_count
2158
+ except:
2159
+ pass
2160
+
2161
+ try:
2162
+ wedge_count = cubit.get_wedge_count()
2163
+ if wedge_count > 0:
2164
+ element_counts['Wedges'] = wedge_count
2165
+ except:
2166
+ pass
2167
+
2168
+ try:
2169
+ pyramid_count = cubit.get_pyramid_count()
2170
+ if pyramid_count > 0:
2171
+ element_counts['Pyramids'] = pyramid_count
2172
+ except:
2173
+ pass
2174
+
2175
+ # 2D elements
2176
+ try:
2177
+ tri_count = cubit.get_tri_count()
2178
+ if tri_count > 0:
2179
+ element_counts['Triangles'] = tri_count
2180
+ except:
2181
+ pass
2182
+
2183
+ try:
2184
+ quad_count = cubit.get_quad_count()
2185
+ if quad_count > 0:
2186
+ element_counts['Quads'] = quad_count
2187
+ except:
2188
+ pass
2189
+
2190
+ # Print counts
2191
+ print(f"Nodes: {cubit.get_node_count()}")
2192
+ for elem_type, count in element_counts.items():
2193
+ print(f"{elem_type}: {count}")
2194
+
2195
+ # Blocks
2196
+ block_ids = cubit.get_block_id_list()
2197
+ print(f"\nBlocks: {len(block_ids)}")
2198
+ for block_id in block_ids:
2199
+ name = cubit.get_exodus_entity_name("block", block_id)
2200
+ if name:
2201
+ print(f" Block {block_id}: \"{name}\"")
2202
+ else:
2203
+ print(f" Block {block_id}")
2204
+
2205
+ # Nodesets
2206
+ nodeset_ids = cubit.get_nodeset_id_list()
2207
+ if len(nodeset_ids) > 0:
2208
+ print(f"\nNodesets: {len(nodeset_ids)}")
2209
+ for ns_id in nodeset_ids:
2210
+ name = cubit.get_exodus_entity_name("nodeset", ns_id)
2211
+ if name:
2212
+ print(f" Nodeset {ns_id}: \"{name}\"")
2213
+ else:
2214
+ print(f" Nodeset {ns_id}")
2215
+
2216
+ # Sidesets
2217
+ sideset_ids = cubit.get_sideset_id_list()
2218
+ if len(sideset_ids) > 0:
2219
+ print(f"\nSidesets: {len(sideset_ids)}")
2220
+ for ss_id in sideset_ids:
2221
+ name = cubit.get_exodus_entity_name("sideset", ss_id)
2222
+ if name:
2223
+ print(f" Sideset {ss_id}: \"{name}\"")
2224
+ else:
2225
+ print(f" Sideset {ss_id}")
2226
+
2227
+ print("-" * 50)
2228
+
2229
+
2230
+ ########################################################################
2231
+ ### Function Aliases (snake_case naming convention)
2232
+ ### For backward compatibility, original names are preserved.
2233
+ ### New code should prefer snake_case names.
2234
+ ########################################################################
2235
+
2236
+ # Gmsh format aliases
2237
+ export_gmsh_v2 = export_Gmsh_ver2
2238
+ export_gmsh_v4 = export_Gmsh_ver4
2239
+
2240
+ # Nastran format alias
2241
+ export_nastran = export_Nastran
2242
+
2243
+ # Netgen format alias
2244
+ export_netgen = export_NetgenMesh
2245
+
2246
+
2247
+ ########################################################################
2248
+ ### SetDeformation functions for high-order curving in NGSolve
2249
+ ### These functions provide an alternative to mesh.Curve() when
2250
+ ### geominfo (UV parameters) are not available from imported meshes.
2251
+ ########################################################################
2252
+
2253
+ def apply_cylinder_deformation(mesh: Any, radius: float, boundary_names: Any,
2254
+ order: int = 2, axis: str = 'z',
2255
+ center: tuple = (0, 0, 0), scale: float = 2.0) -> Any:
2256
+ """Apply SetDeformation to project boundary elements onto a cylinder surface.
2257
+
2258
+ This function provides high-order curving for cylindrical surfaces when
2259
+ mesh.Curve() fails due to missing geominfo (UV parameters).
2260
+
2261
+ Args:
2262
+ mesh: NGSolve Mesh object
2263
+ radius: Cylinder radius
2264
+ boundary_names: Single boundary name (str) or list of boundary names
2265
+ order: Polynomial order for deformation field (2 is sufficient)
2266
+ axis: Cylinder axis ('x', 'y', or 'z')
2267
+ center: Center point of cylinder axis (x, y, z)
2268
+
2269
+ Returns:
2270
+ GridFunction: The deformation field (already applied to mesh)
2271
+
2272
+ Example:
2273
+ >>> mesh = Mesh(ngmesh)
2274
+ >>> mesh.Curve(1) # Linear mesh only
2275
+ >>> deform = apply_cylinder_deformation(mesh, R=0.5,
2276
+ ... boundary_names=['face_0', 'face_1'], order=4)
2277
+ >>> # mesh.SetDeformation is called automatically
2278
+ """
2279
+ from ngsolve import VectorH1, GridFunction, CF, sqrt, IfPos
2280
+ from ngsolve import x as ng_x, y as ng_y, z as ng_z
2281
+
2282
+ # Import coordinate functions
2283
+ x, y, z = ng_x, ng_y, ng_z
2284
+
2285
+ # Shift to cylinder center
2286
+ cx, cy, cz = center
2287
+ x_shifted = x - cx
2288
+ y_shifted = y - cy
2289
+ z_shifted = z - cz
2290
+
2291
+ # Create deformation field
2292
+ fes = VectorH1(mesh, order=order)
2293
+ deform = GridFunction(fes)
2294
+
2295
+ # Calculate radial distance and deformation based on axis
2296
+ epsilon = 0.01 # Avoid division by zero near axis
2297
+
2298
+ if axis.lower() == 'z':
2299
+ r = sqrt(x_shifted*x_shifted + y_shifted*y_shifted)
2300
+ scale_factor = IfPos(r - epsilon, scale * (radius/r - 1), 0)
2301
+ deform_cf = CF((scale_factor * x_shifted, scale_factor * y_shifted, 0))
2302
+ elif axis.lower() == 'x':
2303
+ r = sqrt(y_shifted*y_shifted + z_shifted*z_shifted)
2304
+ scale_factor = IfPos(r - epsilon, scale * (radius/r - 1), 0)
2305
+ deform_cf = CF((0, scale_factor * y_shifted, scale_factor * z_shifted))
2306
+ elif axis.lower() == 'y':
2307
+ r = sqrt(x_shifted*x_shifted + z_shifted*z_shifted)
2308
+ scale_factor = IfPos(r - epsilon, scale * (radius/r - 1), 0)
2309
+ deform_cf = CF((scale_factor * x_shifted, 0, scale_factor * z_shifted))
2310
+ else:
2311
+ raise ValueError(f"Invalid axis '{axis}'. Must be 'x', 'y', or 'z'.")
2312
+
2313
+ # Handle single boundary or list
2314
+ if isinstance(boundary_names, str):
2315
+ boundary_names = [boundary_names]
2316
+
2317
+ # Apply deformation to specified boundaries
2318
+ for bnd_name in boundary_names:
2319
+ region = mesh.Boundaries(bnd_name)
2320
+ deform.Set(deform_cf, definedon=region)
2321
+
2322
+ # Apply to mesh
2323
+ mesh.SetDeformation(deform)
2324
+
2325
+ return deform
2326
+
2327
+
2328
+ def apply_sphere_deformation(mesh: Any, radius: float, boundary_names: Any,
2329
+ order: int = 2, center: tuple = (0, 0, 0), scale: float = 2.0) -> Any:
2330
+ """Apply SetDeformation to project boundary elements onto a sphere surface.
2331
+
2332
+ This function provides high-order curving for spherical surfaces when
2333
+ mesh.Curve() fails due to missing geominfo (UV parameters).
2334
+
2335
+ Args:
2336
+ mesh: NGSolve Mesh object
2337
+ radius: Sphere radius
2338
+ boundary_names: Single boundary name (str) or list of boundary names
2339
+ order: Polynomial order for deformation field (2 is sufficient)
2340
+ center: Center point of sphere (x, y, z)
2341
+
2342
+ Returns:
2343
+ GridFunction: The deformation field (already applied to mesh)
2344
+
2345
+ Example:
2346
+ >>> mesh = Mesh(ngmesh)
2347
+ >>> mesh.Curve(1)
2348
+ >>> deform = apply_sphere_deformation(mesh, R=1.0,
2349
+ ... boundary_names='face_0', order=4)
2350
+ """
2351
+ from ngsolve import VectorH1, GridFunction, CF, sqrt, IfPos
2352
+ from ngsolve import x as ng_x, y as ng_y, z as ng_z
2353
+
2354
+ x, y, z = ng_x, ng_y, ng_z
2355
+ cx, cy, cz = center
2356
+ x_shifted = x - cx
2357
+ y_shifted = y - cy
2358
+ z_shifted = z - cz
2359
+
2360
+ fes = VectorH1(mesh, order=order)
2361
+ deform = GridFunction(fes)
2362
+
2363
+ # Distance from center
2364
+ r = sqrt(x_shifted*x_shifted + y_shifted*y_shifted + z_shifted*z_shifted)
2365
+ epsilon = 0.01
2366
+ scale_factor = IfPos(r - epsilon, scale * (radius/r - 1), 0)
2367
+ deform_cf = CF((scale_factor * x_shifted, scale_factor * y_shifted, scale_factor * z_shifted))
2368
+
2369
+ if isinstance(boundary_names, str):
2370
+ boundary_names = [boundary_names]
2371
+
2372
+ for bnd_name in boundary_names:
2373
+ region = mesh.Boundaries(bnd_name)
2374
+ deform.Set(deform_cf, definedon=region)
2375
+
2376
+ mesh.SetDeformation(deform)
2377
+
2378
+ return deform
2379
+
2380
+
2381
+ def apply_cone_deformation(mesh: Any, apex: tuple, base_center: tuple,
2382
+ base_radius: float, boundary_names: Any,
2383
+ order: int = 2, scale: float = 2.0) -> Any:
2384
+ """Apply SetDeformation to project boundary elements onto a cone surface.
2385
+
2386
+ Projects points onto a cone surface defined by apex, base center, and base radius.
2387
+
2388
+ Args:
2389
+ mesh: NGSolve Mesh object
2390
+ apex: Apex point of cone (x, y, z)
2391
+ base_center: Center of cone base (x, y, z)
2392
+ base_radius: Radius at base
2393
+ boundary_names: Single boundary name (str) or list of boundary names
2394
+ order: Polynomial order for deformation field (2 is sufficient)
2395
+
2396
+ Returns:
2397
+ GridFunction: The deformation field (already applied to mesh)
2398
+
2399
+ Example:
2400
+ >>> # Cone with apex at (0,0,2), base at (0,0,0), radius 1
2401
+ >>> deform = apply_cone_deformation(mesh, apex=(0,0,2),
2402
+ ... base_center=(0,0,0), base_radius=1.0, boundary_names='face_0')
2403
+ """
2404
+ from ngsolve import VectorH1, GridFunction, CF, sqrt, IfPos
2405
+ from ngsolve import x as ng_x, y as ng_y, z as ng_z
2406
+ import math
2407
+
2408
+ x, y, z = ng_x, ng_y, ng_z
2409
+ ax, ay, az = apex
2410
+ bx, by, bz = base_center
2411
+
2412
+ # Cone axis direction (from apex to base)
2413
+ dx = bx - ax
2414
+ dy = by - ay
2415
+ dz = bz - az
2416
+ h = math.sqrt(dx*dx + dy*dy + dz*dz) # Cone height
2417
+
2418
+ # Normalize axis
2419
+ dx, dy, dz = dx/h, dy/h, dz/h
2420
+
2421
+ fes = VectorH1(mesh, order=order)
2422
+ deform = GridFunction(fes)
2423
+
2424
+ # For z-axis aligned cone (apex at top, base at bottom)
2425
+ # Correct formula: r_expected = base_radius * (az - z) / h
2426
+ # At base (z=bz): r = R, At apex (z=az): r = 0
2427
+ if abs(dz) > 0.99: # Nearly z-aligned
2428
+ # r_expected at height z
2429
+ r_expected = base_radius * (az - z) / h
2430
+
2431
+ r_xy = sqrt((x - ax)*(x - ax) + (y - ay)*(y - ay))
2432
+ epsilon = 0.01
2433
+ # scale = r_expected/r_xy - 1, so new_r = r_xy * (1 + scale) = r_expected
2434
+ scale_factor = IfPos(r_xy - epsilon, scale * (r_expected/r_xy - 1), 0)
2435
+ deform_cf = CF((scale_factor * (x - ax), scale_factor * (y - ay), 0))
2436
+ else:
2437
+ # General case - more complex projection
2438
+ # For simplicity, assume y-axis aligned
2439
+ if abs(dy) > 0.99:
2440
+ r_expected = base_radius * (ay - y) / h
2441
+ r_xz = sqrt((x - ax)*(x - ax) + (z - az)*(z - az))
2442
+ epsilon = 0.01
2443
+ scale_factor = IfPos(r_xz - epsilon, scale * (r_expected/r_xz - 1), 0)
2444
+ deform_cf = CF((scale_factor * (x - ax), 0, scale_factor * (z - az)))
2445
+ else: # x-axis aligned
2446
+ r_expected = base_radius * (ax - x) / h
2447
+ r_yz = sqrt((y - ay)*(y - ay) + (z - az)*(z - az))
2448
+ epsilon = 0.01
2449
+ scale_factor = IfPos(r_yz - epsilon, scale * (r_expected/r_yz - 1), 0)
2450
+ deform_cf = CF((0, scale_factor * (y - ay), scale_factor * (z - az)))
2451
+
2452
+ if isinstance(boundary_names, str):
2453
+ boundary_names = [boundary_names]
2454
+
2455
+ for bnd_name in boundary_names:
2456
+ region = mesh.Boundaries(bnd_name)
2457
+ deform.Set(deform_cf, definedon=region)
2458
+
2459
+ mesh.SetDeformation(deform)
2460
+
2461
+ return deform
2462
+
2463
+
2464
+ def apply_torus_deformation(mesh: Any, major_radius: float, minor_radius: float,
2465
+ boundary_names: Any, order: int = 2,
2466
+ axis: str = 'z', center: tuple = (0, 0, 0), scale: float = 2.0) -> Any:
2467
+ """Apply SetDeformation to project boundary elements onto a torus surface.
2468
+
2469
+ Projects points onto a torus (donut shape) defined by major and minor radii.
2470
+
2471
+ Args:
2472
+ mesh: NGSolve Mesh object
2473
+ major_radius: Distance from center of torus to center of tube (R)
2474
+ minor_radius: Radius of the tube (r)
2475
+ boundary_names: Single boundary name (str) or list of boundary names
2476
+ order: Polynomial order for deformation field (4+ recommended for torus)
2477
+ axis: Axis of revolution ('x', 'y', or 'z')
2478
+ center: Center point of torus (x, y, z)
2479
+
2480
+ Returns:
2481
+ GridFunction: The deformation field (already applied to mesh)
2482
+
2483
+ Example:
2484
+ >>> # Torus with R=1.0, r=0.3, centered at origin
2485
+ >>> deform = apply_torus_deformation(mesh, major_radius=1.0,
2486
+ ... minor_radius=0.3, boundary_names='face_0', order=4)
2487
+ """
2488
+ from ngsolve import VectorH1, GridFunction, CF, sqrt, IfPos
2489
+ from ngsolve import x as ng_x, y as ng_y, z as ng_z
2490
+
2491
+ x, y, z = ng_x, ng_y, ng_z
2492
+ cx, cy, cz = center
2493
+ x_shifted = x - cx
2494
+ y_shifted = y - cy
2495
+ z_shifted = z - cz
2496
+
2497
+ R = major_radius
2498
+ r = minor_radius
2499
+
2500
+ fes = VectorH1(mesh, order=order)
2501
+ deform = GridFunction(fes)
2502
+
2503
+ epsilon = 0.01
2504
+
2505
+ if axis.lower() == 'z':
2506
+ # Torus around z-axis
2507
+ # Distance from z-axis in xy-plane
2508
+ rho = sqrt(x_shifted*x_shifted + y_shifted*y_shifted)
2509
+
2510
+ # Distance from the tube center circle
2511
+ # tube_center is at (R*x/rho, R*y/rho, 0) from origin
2512
+ # We need to find vector from tube center to point
2513
+ tube_dist = sqrt((rho - R)*(rho - R) + z_shifted*z_shifted)
2514
+
2515
+ # Scale factor to project to tube surface
2516
+ scale_factor = IfPos(tube_dist - epsilon, scale * (r/tube_dist - 1), 0)
2517
+
2518
+ # Direction from tube center to point
2519
+ # In xy: (x/rho - R/rho*x/rho, y/rho - R/rho*y/rho) = ((1-R/rho)*x/rho, ...)
2520
+ # Simplified: the radial component is (rho-R)/tube_dist, z component is z/tube_dist
2521
+ dx_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (x_shifted / rho), 0)
2522
+ dy_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (y_shifted / rho), 0)
2523
+ dz_tube = z_shifted / IfPos(tube_dist - epsilon, tube_dist, 1)
2524
+
2525
+ deform_cf = CF((scale_factor * dx_tube * tube_dist,
2526
+ scale_factor * dy_tube * tube_dist,
2527
+ scale_factor * dz_tube * tube_dist))
2528
+
2529
+ elif axis.lower() == 'y':
2530
+ rho = sqrt(x_shifted*x_shifted + z_shifted*z_shifted)
2531
+ tube_dist = sqrt((rho - R)*(rho - R) + y_shifted*y_shifted)
2532
+ scale_factor = IfPos(tube_dist - epsilon, scale * (r/tube_dist - 1), 0)
2533
+
2534
+ dx_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (x_shifted / rho), 0)
2535
+ dy_tube = y_shifted / IfPos(tube_dist - epsilon, tube_dist, 1)
2536
+ dz_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (z_shifted / rho), 0)
2537
+
2538
+ deform_cf = CF((scale_factor * dx_tube * tube_dist,
2539
+ scale_factor * dy_tube * tube_dist,
2540
+ scale_factor * dz_tube * tube_dist))
2541
+
2542
+ else: # x-axis
2543
+ rho = sqrt(y_shifted*y_shifted + z_shifted*z_shifted)
2544
+ tube_dist = sqrt((rho - R)*(rho - R) + x_shifted*x_shifted)
2545
+ scale_factor = IfPos(tube_dist - epsilon, scale * (r/tube_dist - 1), 0)
2546
+
2547
+ dx_tube = x_shifted / IfPos(tube_dist - epsilon, tube_dist, 1)
2548
+ dy_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (y_shifted / rho), 0)
2549
+ dz_tube = IfPos(rho - epsilon, (rho - R) / tube_dist * (z_shifted / rho), 0)
2550
+
2551
+ deform_cf = CF((scale_factor * dx_tube * tube_dist,
2552
+ scale_factor * dy_tube * tube_dist,
2553
+ scale_factor * dz_tube * tube_dist))
2554
+
2555
+ if isinstance(boundary_names, str):
2556
+ boundary_names = [boundary_names]
2557
+
2558
+ for bnd_name in boundary_names:
2559
+ region = mesh.Boundaries(bnd_name)
2560
+ deform.Set(deform_cf, definedon=region)
2561
+
2562
+ mesh.SetDeformation(deform)
2563
+
2564
+ return deform
2565
+
2566
+
2567
+ def detect_cylinder_boundaries(mesh: Any, radius: float,
2568
+ axis: str = 'z', tol: float = 0.05) -> list:
2569
+ """Detect which boundaries belong to a cylinder surface (not caps).
2570
+
2571
+ Automatically identifies boundary regions that are part of the cylinder
2572
+ surface based on vertex positions.
2573
+
2574
+ Args:
2575
+ mesh: NGSolve Mesh object
2576
+ radius: Expected cylinder radius
2577
+ axis: Cylinder axis ('x', 'y', or 'z')
2578
+ tol: Tolerance for radius matching
2579
+
2580
+ Returns:
2581
+ list: List of boundary names that are on the cylinder surface
2582
+
2583
+ Example:
2584
+ >>> boundaries = detect_cylinder_boundaries(mesh, radius=0.5, axis='z')
2585
+ >>> deform = apply_cylinder_deformation(mesh, 0.5, boundaries)
2586
+ """
2587
+ import math
2588
+ from ngsolve import BND
2589
+
2590
+ boundaries = mesh.GetBoundaries()
2591
+ cylinder_boundaries = []
2592
+
2593
+ for bnd_name in boundaries:
2594
+ count = 0
2595
+ on_surface_count = 0
2596
+
2597
+ for el in mesh.Elements(BND):
2598
+ if mesh.GetBoundaries()[el.index] != bnd_name:
2599
+ continue
2600
+ count += 1
2601
+
2602
+ all_on_surface = True
2603
+ for v in el.vertices:
2604
+ p = mesh.vertices[v.nr].point
2605
+ if axis.lower() == 'z':
2606
+ r = math.sqrt(p[0]**2 + p[1]**2)
2607
+ elif axis.lower() == 'y':
2608
+ r = math.sqrt(p[0]**2 + p[2]**2)
2609
+ else: # x
2610
+ r = math.sqrt(p[1]**2 + p[2]**2)
2611
+
2612
+ if abs(r - radius) > tol:
2613
+ all_on_surface = False
2614
+ break
2615
+
2616
+ if all_on_surface:
2617
+ on_surface_count += 1
2618
+
2619
+ # Consider it cylinder surface if > 50% elements are on surface
2620
+ if count > 0 and on_surface_count / count > 0.5:
2621
+ cylinder_boundaries.append(bnd_name)
2622
+
2623
+ return cylinder_boundaries
2624
+
2625
+
2626
+ def detect_sphere_boundaries(mesh: Any, radius: float,
2627
+ center: tuple = (0, 0, 0), tol: float = 0.05) -> list:
2628
+ """Detect which boundaries belong to a sphere surface.
2629
+
2630
+ Automatically identifies boundary regions that are part of the sphere
2631
+ surface based on vertex positions.
2632
+
2633
+ Args:
2634
+ mesh: NGSolve Mesh object
2635
+ radius: Expected sphere radius
2636
+ center: Center of sphere (x, y, z)
2637
+ tol: Tolerance for radius matching
2638
+
2639
+ Returns:
2640
+ list: List of boundary names that are on the sphere surface
2641
+ """
2642
+ import math
2643
+ from ngsolve import BND
2644
+
2645
+ cx, cy, cz = center
2646
+ boundaries = mesh.GetBoundaries()
2647
+ sphere_boundaries = []
2648
+
2649
+ for bnd_name in boundaries:
2650
+ count = 0
2651
+ on_surface_count = 0
2652
+
2653
+ for el in mesh.Elements(BND):
2654
+ if mesh.GetBoundaries()[el.index] != bnd_name:
2655
+ continue
2656
+ count += 1
2657
+
2658
+ all_on_surface = True
2659
+ for v in el.vertices:
2660
+ p = mesh.vertices[v.nr].point
2661
+ r = math.sqrt((p[0]-cx)**2 + (p[1]-cy)**2 + (p[2]-cz)**2)
2662
+ if abs(r - radius) > tol:
2663
+ all_on_surface = False
2664
+ break
2665
+
2666
+ if all_on_surface:
2667
+ on_surface_count += 1
2668
+
2669
+ if count > 0 and on_surface_count / count > 0.5:
2670
+ sphere_boundaries.append(bnd_name)
2671
+
2672
+ return sphere_boundaries
2673
+
2674
+
2675
+ def detect_torus_boundaries(mesh: Any, major_radius: float, minor_radius: float,
2676
+ axis: str = 'z', center: tuple = (0, 0, 0),
2677
+ tol: float = 0.05) -> list:
2678
+ """Detect which boundaries belong to a torus surface.
2679
+
2680
+ Automatically identifies boundary regions that are part of the torus
2681
+ surface based on vertex positions.
2682
+
2683
+ Args:
2684
+ mesh: NGSolve Mesh object
2685
+ major_radius: Distance from center of torus to center of tube (R)
2686
+ minor_radius: Radius of the tube (r)
2687
+ axis: Axis of revolution ('x', 'y', or 'z')
2688
+ center: Center of torus (x, y, z)
2689
+ tol: Tolerance for radius matching
2690
+
2691
+ Returns:
2692
+ list: List of boundary names that are on the torus surface
2693
+ """
2694
+ import math
2695
+ from ngsolve import BND
2696
+
2697
+ cx, cy, cz = center
2698
+ R = major_radius
2699
+ r = minor_radius
2700
+ boundaries = mesh.GetBoundaries()
2701
+ torus_boundaries = []
2702
+
2703
+ for bnd_name in boundaries:
2704
+ count = 0
2705
+ on_surface_count = 0
2706
+
2707
+ for el in mesh.Elements(BND):
2708
+ if mesh.GetBoundaries()[el.index] != bnd_name:
2709
+ continue
2710
+ count += 1
2711
+
2712
+ all_on_surface = True
2713
+ for v in el.vertices:
2714
+ p = mesh.vertices[v.nr].point
2715
+ px, py, pz = p[0] - cx, p[1] - cy, p[2] - cz
2716
+
2717
+ # Calculate distance from torus surface
2718
+ if axis == 'z':
2719
+ r_major = math.sqrt(px*px + py*py)
2720
+ dist = math.sqrt((r_major - R)**2 + pz*pz)
2721
+ elif axis == 'y':
2722
+ r_major = math.sqrt(px*px + pz*pz)
2723
+ dist = math.sqrt((r_major - R)**2 + py*py)
2724
+ else: # x-axis
2725
+ r_major = math.sqrt(py*py + pz*pz)
2726
+ dist = math.sqrt((r_major - R)**2 + px*px)
2727
+
2728
+ if abs(dist - r) > tol:
2729
+ all_on_surface = False
2730
+ break
2731
+
2732
+ if all_on_surface:
2733
+ on_surface_count += 1
2734
+
2735
+ if count > 0 and on_surface_count / count > 0.5:
2736
+ torus_boundaries.append(bnd_name)
2737
+
2738
+ return torus_boundaries