radia 1.3.3__py3-none-any.whl → 1.3.5__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.
radia/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # Radia Python package
2
+ __version__ = "1.3.5"
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ nastran_mesh_import.py - Import Nastran mesh files to Radia
4
+
5
+ Supports fixed-width and long-format Nastran (.bdf, .nas, .dat)
6
+ for hexahedral (CHEXA) and tetrahedral (CTETRA) elements.
7
+
8
+ Handles continuation lines marked with '+' for multi-line cards.
9
+
10
+ Author: Radia Development Team
11
+ Created: 2025-11-22
12
+ Version: 0.2.0
13
+ """
14
+
15
+
16
+ def parse_nastran_fixed_width(line, card_type):
17
+ """
18
+ Parse Nastran fixed-width format card.
19
+
20
+ Fixed-width format:
21
+ - Field width: 8 characters
22
+ - Fields: 0-7, 8-15, 16-23, 24-31, ...
23
+ """
24
+ fields = []
25
+ for i in range(0, len(line), 8):
26
+ field = line[i:i+8].strip()
27
+ if field and field != '+': # Ignore continuation markers
28
+ fields.append(field)
29
+ return fields
30
+
31
+
32
+ def import_nastran_mesh(nas_file, units='m', verbose=True):
33
+ """
34
+ Import Nastran mesh file to Radia.
35
+
36
+ Parameters
37
+ ----------
38
+ nas_file : str
39
+ Path to Nastran file (.bdf, .nas, .dat)
40
+ units : str, default='m'
41
+ Unit system: 'm' (meters) or 'mm' (millimeters)
42
+ Must match rad.FldUnits() setting
43
+ verbose : bool, default=True
44
+ Print progress information
45
+
46
+ Returns
47
+ -------
48
+ dict
49
+ {
50
+ 'vertices': list of [x, y, z],
51
+ 'hex_elements': list of element vertex indices,
52
+ 'tet_elements': list of element vertex indices,
53
+ 'wedge_elements': list of element vertex indices,
54
+ 'pyramid_elements': list of element vertex indices
55
+ }
56
+
57
+ Notes
58
+ -----
59
+ Supports fixed-width and long-format Nastran:
60
+ - GRID/GRID* cards: Node definitions (with continuation)
61
+ - CHEXA cards: 8-node hexahedral elements (with continuation)
62
+ - CTETRA cards: 4-node tetrahedral elements (with continuation)
63
+ - CPENTA cards: 6-node wedge/pentahedron elements (with continuation)
64
+ - CPYRAM cards: 5-node pyramid elements (with continuation)
65
+ """
66
+ if verbose:
67
+ print(f"[Nastran Import] Reading file: {nas_file}")
68
+
69
+ vertices = []
70
+ hex_elements = []
71
+ tet_elements = []
72
+ wedge_elements = []
73
+ pyramid_elements = []
74
+ tria_groups = {} # material_id -> {'faces': [[n1,n2,n3], ...], 'node_ids': set()}
75
+ vertex_map = {} # node_id -> vertex_index
76
+
77
+ with open(nas_file, 'r') as f:
78
+ lines = f.readlines()
79
+
80
+ i = 0
81
+ while i < len(lines):
82
+ line = lines[i]
83
+
84
+ # Skip comments and empty lines
85
+ if line.startswith('$') or not line.strip():
86
+ i += 1
87
+ continue
88
+
89
+ # End of bulk data
90
+ if line.strip().startswith('ENDDATA'):
91
+ break
92
+
93
+ # Parse card type (first 8 characters)
94
+ card_type = line[:8].strip()
95
+
96
+ # GRID/GRID* card: Node definition with continuation support
97
+ if card_type == 'GRID' or card_type.startswith('GRID'):
98
+ # Check for long format (GRID*)
99
+ is_long = '*' in card_type
100
+
101
+ if is_long:
102
+ # GRID* long format: 16-character fields
103
+ # First line: GRID* (0-7), node_id (8-23), coord_sys (24-39), X (40-55), Y (56-71)
104
+ # Second line: * (0-7), Z (8-23)
105
+ try:
106
+ node_id = int(line[8:24].strip())
107
+ coord_sys = int(line[24:40].strip()) if len(line) > 24 else 0
108
+ x = float(line[40:56].strip())
109
+ y = float(line[56:72].strip()) if len(line) > 56 else 0.0
110
+
111
+ # Get continuation line for Z coordinate
112
+ z = 0.0
113
+ if i + 1 < len(lines) and lines[i + 1].strip().startswith('*'):
114
+ i += 1
115
+ cont_line = lines[i]
116
+ z = float(cont_line[8:24].strip())
117
+
118
+ vertex_idx = len(vertices)
119
+ vertices.append([x, y, z])
120
+ vertex_map[node_id] = vertex_idx
121
+ except (ValueError, IndexError) as e:
122
+ if verbose:
123
+ print(f" Warning: Failed to parse GRID* at line {i+1}: {e}")
124
+ else:
125
+ # Standard GRID format: 8-character fields
126
+ # GRID (0-7), node_id (8-15), coord_sys (16-23), X (24-31), Y (32-39), Z (40-47)
127
+ try:
128
+ node_id = int(line[8:16].strip())
129
+ coord_sys = int(line[16:24].strip()) if len(line) > 16 and line[16:24].strip() else 0
130
+ x = float(line[24:32].strip())
131
+ y = float(line[32:40].strip()) if len(line) > 32 else 0.0
132
+ z = float(line[40:48].strip()) if len(line) > 40 else 0.0
133
+
134
+ vertex_idx = len(vertices)
135
+ vertices.append([x, y, z])
136
+ vertex_map[node_id] = vertex_idx
137
+ except (ValueError, IndexError) as e:
138
+ if verbose:
139
+ print(f" Warning: Failed to parse GRID at line {i+1}: {e}")
140
+
141
+ # CHEXA card: 8-node hexahedral element with continuation support
142
+ elif card_type == 'CHEXA' or card_type.startswith('CHEXA'):
143
+ # Collect continuation lines (marked with + at end)
144
+ full_line = line
145
+ while i + 1 < len(lines) and full_line.rstrip().endswith('+'):
146
+ i += 1
147
+ full_line = full_line.rstrip()[:-1] # Remove trailing '+'
148
+ cont_line = lines[i]
149
+ if cont_line.startswith('+'):
150
+ cont_line = cont_line[1:] # Remove leading '+'
151
+ full_line += cont_line
152
+
153
+ fields = parse_nastran_fixed_width(full_line, card_type)
154
+ if len(fields) >= 10:
155
+ try:
156
+ # Element ID (field 1), Property ID (field 2), then 8 nodes
157
+ node_ids = [int(fields[j]) for j in range(3, 11)]
158
+ element_verts = [vertex_map[nid] for nid in node_ids]
159
+ hex_elements.append(element_verts)
160
+ except (ValueError, KeyError, IndexError) as e:
161
+ if verbose:
162
+ print(f" Warning: Failed to parse CHEXA at line {i+1}: {e}")
163
+
164
+ # CTETRA card: 4-node tetrahedral element with continuation support
165
+ elif card_type == 'CTETRA' or card_type.startswith('CTETRA'):
166
+ # Collect continuation lines
167
+ full_line = line
168
+ while i + 1 < len(lines) and full_line.rstrip().endswith('+'):
169
+ i += 1
170
+ full_line = full_line.rstrip()[:-1] # Remove trailing '+'
171
+ cont_line = lines[i]
172
+ if cont_line.startswith('+'):
173
+ cont_line = cont_line[1:] # Remove leading '+'
174
+ full_line += cont_line
175
+
176
+ fields = parse_nastran_fixed_width(full_line, card_type)
177
+ if len(fields) >= 6:
178
+ try:
179
+ node_ids = [int(fields[j]) for j in range(3, 7)]
180
+ element_verts = [vertex_map[nid] for nid in node_ids]
181
+ tet_elements.append(element_verts)
182
+ except (ValueError, KeyError, IndexError) as e:
183
+ if verbose:
184
+ print(f" Warning: Failed to parse CTETRA at line {i+1}: {e}")
185
+
186
+ # CPENTA card: 6-node wedge/pentahedron element with continuation support
187
+ elif card_type == 'CPENTA' or card_type.startswith('CPENTA'):
188
+ # Collect continuation lines
189
+ full_line = line
190
+ while i + 1 < len(lines) and full_line.rstrip().endswith('+'):
191
+ i += 1
192
+ full_line = full_line.rstrip()[:-1] # Remove trailing '+'
193
+ cont_line = lines[i]
194
+ if cont_line.startswith('+'):
195
+ cont_line = cont_line[1:] # Remove leading '+'
196
+ full_line += cont_line
197
+
198
+ fields = parse_nastran_fixed_width(full_line, card_type)
199
+ if len(fields) >= 8:
200
+ try:
201
+ # Element ID (field 1), Property ID (field 2), then 6 nodes
202
+ node_ids = [int(fields[j]) for j in range(3, 9)]
203
+ element_verts = [vertex_map[nid] for nid in node_ids]
204
+ wedge_elements.append(element_verts)
205
+ except (ValueError, KeyError, IndexError) as e:
206
+ if verbose:
207
+ print(f" Warning: Failed to parse CPENTA at line {i+1}: {e}")
208
+
209
+ # CPYRAM card: 5-node pyramid element with continuation support
210
+ elif card_type == 'CPYRAM' or card_type.startswith('CPYRAM'):
211
+ # Collect continuation lines
212
+ full_line = line
213
+ while i + 1 < len(lines) and full_line.rstrip().endswith('+'):
214
+ i += 1
215
+ full_line = full_line.rstrip()[:-1] # Remove trailing '+'
216
+ cont_line = lines[i]
217
+ if cont_line.startswith('+'):
218
+ cont_line = cont_line[1:] # Remove leading '+'
219
+ full_line += cont_line
220
+
221
+ fields = parse_nastran_fixed_width(full_line, card_type)
222
+ if len(fields) >= 7:
223
+ try:
224
+ # Element ID (field 1), Property ID (field 2), then 5 nodes
225
+ node_ids = [int(fields[j]) for j in range(3, 8)]
226
+ element_verts = [vertex_map[nid] for nid in node_ids]
227
+ pyramid_elements.append(element_verts)
228
+ except (ValueError, KeyError, IndexError) as e:
229
+ if verbose:
230
+ print(f" Warning: Failed to parse CPYRAM at line {i+1}: {e}")
231
+
232
+ # CTRIA3 card: 3-node triangle surface element
233
+ elif card_type == 'CTRIA3' or card_type.startswith('CTRIA3'):
234
+ # CTRIA3 format: CTRIA3, elem_id, prop_id, n1, n2, n3
235
+ # Fixed format: 8-character fields
236
+ try:
237
+ elem_id = int(line[8:16].strip())
238
+ prop_id = int(line[16:24].strip()) # Material/Property ID
239
+ n1 = int(line[24:32].strip())
240
+ n2 = int(line[32:40].strip())
241
+ n3 = int(line[40:48].strip())
242
+
243
+ # Group by material ID (prop_id)
244
+ if prop_id not in tria_groups:
245
+ tria_groups[prop_id] = {'faces': [], 'node_ids': set()}
246
+
247
+ # Store face (using original node IDs, will convert later)
248
+ tria_groups[prop_id]['faces'].append([n1, n2, n3])
249
+ tria_groups[prop_id]['node_ids'].update([n1, n2, n3])
250
+
251
+ except (ValueError, IndexError) as e:
252
+ if verbose:
253
+ print(f" Warning: Failed to parse CTRIA3 at line {i+1}: {e}")
254
+
255
+ i += 1
256
+
257
+ # Calculate total triangle faces across all material groups
258
+ total_tria_faces = sum(len(group['faces']) for group in tria_groups.values())
259
+
260
+ if verbose:
261
+ print(f"[Nastran Import] Parsing complete")
262
+ print(f" Vertices: {len(vertices)}")
263
+ print(f" Hexahedral elements: {len(hex_elements)}")
264
+ print(f" Wedge elements: {len(wedge_elements)}")
265
+ print(f" Pyramid elements: {len(pyramid_elements)}")
266
+ print(f" Tetrahedral elements: {len(tet_elements)}")
267
+ print(f" Triangle surface groups: {len(tria_groups)} (total {total_tria_faces} faces)")
268
+
269
+ return {
270
+ 'vertices': vertices,
271
+ 'hex_elements': hex_elements,
272
+ 'wedge_elements': wedge_elements,
273
+ 'pyramid_elements': pyramid_elements,
274
+ 'tet_elements': tet_elements,
275
+ 'tria_groups': tria_groups,
276
+ 'vertex_map': vertex_map
277
+ }
278
+
279
+
280
+ def create_radia_from_nastran(nas_file, material=None, units='m',
281
+ combine=True, verbose=True):
282
+ """
283
+ Create Radia geometry from Nastran mesh file.
284
+
285
+ Parameters
286
+ ----------
287
+ nas_file : str
288
+ Path to Nastran file
289
+ material : dict or None
290
+ Material specification: {'magnetization': [Mx, My, Mz]}
291
+ units : str, default='m'
292
+ 'm' (meters) or 'mm' (millimeters)
293
+ combine : bool, default=True
294
+ If True, return rad.ObjCnt() container
295
+ If False, return list of object IDs
296
+ verbose : bool, default=True
297
+ Print progress
298
+
299
+ Returns
300
+ -------
301
+ int or list
302
+ Radia object ID (if combine=True) or list of IDs (if combine=False)
303
+ """
304
+ import radia as rad
305
+
306
+ # Import mesh data
307
+ mesh_data = import_nastran_mesh(nas_file, units=units, verbose=verbose)
308
+
309
+ vertices = mesh_data['vertices']
310
+ hex_elements = mesh_data['hex_elements']
311
+ wedge_elements = mesh_data['wedge_elements']
312
+ pyramid_elements = mesh_data['pyramid_elements']
313
+ tet_elements = mesh_data['tet_elements']
314
+ tria_groups = mesh_data['tria_groups']
315
+ vertex_map = mesh_data['vertex_map']
316
+
317
+ total_elements = len(hex_elements) + len(wedge_elements) + len(pyramid_elements) + len(tet_elements)
318
+ total_tria_groups = len(tria_groups)
319
+ if total_elements == 0 and total_tria_groups == 0:
320
+ raise ValueError("No valid elements found in Nastran file")
321
+
322
+ # Material specification
323
+ if material is None:
324
+ magnetization = [0, 0, 0]
325
+ elif isinstance(material, dict) and 'magnetization' in material:
326
+ magnetization = material['magnetization']
327
+ else:
328
+ raise ValueError("material must be dict with 'magnetization' key")
329
+
330
+ # Import face topologies
331
+ from netgen_mesh_import import HEX_FACES, WEDGE_FACES, PYRAMID_FACES, TETRA_FACES
332
+
333
+ # Create Radia objects
334
+ radia_objects = []
335
+
336
+ # Create hexahedral elements
337
+ if len(hex_elements) > 0 and verbose:
338
+ print(f"[Nastran Import] Creating {len(hex_elements)} hexahedral elements...")
339
+
340
+ for i, elem_verts in enumerate(hex_elements):
341
+ elem_coords = [vertices[vi] for vi in elem_verts]
342
+ obj_id = rad.ObjPolyhdr(elem_coords, HEX_FACES, magnetization)
343
+ radia_objects.append(obj_id)
344
+
345
+ if verbose and (i + 1) % 25 == 0:
346
+ print(f" Progress: {i+1}/{len(hex_elements)}", end='\r')
347
+
348
+ if len(hex_elements) > 0 and verbose:
349
+ print(f" Progress: {len(hex_elements)}/{len(hex_elements)}")
350
+
351
+ # Create wedge elements
352
+ if len(wedge_elements) > 0 and verbose:
353
+ print(f"[Nastran Import] Creating {len(wedge_elements)} wedge elements...")
354
+
355
+ for i, elem_verts in enumerate(wedge_elements):
356
+ elem_coords = [vertices[vi] for vi in elem_verts]
357
+ obj_id = rad.ObjPolyhdr(elem_coords, WEDGE_FACES, magnetization)
358
+ radia_objects.append(obj_id)
359
+
360
+ if verbose and (i + 1) % 50 == 0:
361
+ print(f" Progress: {i+1}/{len(wedge_elements)}", end='\r')
362
+
363
+ if len(wedge_elements) > 0 and verbose:
364
+ print(f" Progress: {len(wedge_elements)}/{len(wedge_elements)}")
365
+
366
+ # Create pyramid elements
367
+ if len(pyramid_elements) > 0 and verbose:
368
+ print(f"[Nastran Import] Creating {len(pyramid_elements)} pyramid elements...")
369
+
370
+ for i, elem_verts in enumerate(pyramid_elements):
371
+ elem_coords = [vertices[vi] for vi in elem_verts]
372
+ obj_id = rad.ObjPolyhdr(elem_coords, PYRAMID_FACES, magnetization)
373
+ radia_objects.append(obj_id)
374
+
375
+ if verbose and (i + 1) % 50 == 0:
376
+ print(f" Progress: {i+1}/{len(pyramid_elements)}", end='\r')
377
+
378
+ if len(pyramid_elements) > 0 and verbose:
379
+ print(f" Progress: {len(pyramid_elements)}/{len(pyramid_elements)}")
380
+
381
+ # Create tetrahedral elements
382
+ if len(tet_elements) > 0 and verbose:
383
+ print(f"[Nastran Import] Creating {len(tet_elements)} tetrahedral elements...")
384
+
385
+ for i, elem_verts in enumerate(tet_elements):
386
+ elem_coords = [vertices[vi] for vi in elem_verts]
387
+ obj_id = rad.ObjPolyhdr(elem_coords, TETRA_FACES, magnetization)
388
+ radia_objects.append(obj_id)
389
+
390
+ if verbose and (i + 1) % 100 == 0:
391
+ print(f" Progress: {i+1}/{len(tet_elements)}", end='\r')
392
+
393
+ if len(tet_elements) > 0 and verbose:
394
+ print(f" Progress: {len(tet_elements)}/{len(tet_elements)}")
395
+
396
+ # Create surface polyhedra from triangle groups
397
+ if len(tria_groups) > 0 and verbose:
398
+ print(f"[Nastran Import] Creating {len(tria_groups)} surface polyhedra from triangle groups...")
399
+
400
+ for mat_id, group in tria_groups.items():
401
+ faces_global = group['faces'] # Faces with global node IDs
402
+ node_ids = sorted(group['node_ids']) # All unique node IDs for this material
403
+
404
+ # Create local node ID mapping (global node ID -> local index 1-based for Radia)
405
+ node_id_to_local = {nid: idx + 1 for idx, nid in enumerate(node_ids)}
406
+
407
+ # Get vertex coordinates for this group
408
+ group_vertices = [vertices[vertex_map[nid]] for nid in node_ids]
409
+
410
+ # Convert faces from global node IDs to local 1-based indices
411
+ faces_local = []
412
+ for face in faces_global:
413
+ local_face = [node_id_to_local[nid] for nid in face]
414
+ faces_local.append(local_face)
415
+
416
+ # Create polyhedron from surface mesh
417
+ obj_id = rad.ObjPolyhdr(group_vertices, faces_local, magnetization)
418
+ radia_objects.append(obj_id)
419
+
420
+ if verbose:
421
+ print(f" Material {mat_id}: {len(faces_local)} faces, {len(group_vertices)} vertices")
422
+
423
+ # Return result
424
+ if combine:
425
+ if verbose:
426
+ print(f"[Nastran Import] Combining into container...")
427
+
428
+ container = rad.ObjCnt(radia_objects)
429
+
430
+ if verbose:
431
+ print(f" [OK] Container object ID: {container}")
432
+
433
+ return container
434
+ else:
435
+ return radia_objects
436
+
437
+
438
+ __all__ = [
439
+ 'import_nastran_mesh',
440
+ 'create_radia_from_nastran',
441
+ ]