radia 1.3.2__py3-none-any.whl → 1.3.4__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.
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ netgen_mesh_import.py - Convert Netgen meshes to Radia geometry
4
+
5
+ This module provides functionality to import NGSolve/Netgen tetrahedral meshes
6
+ into Radia's magnetic field computation framework.
7
+
8
+ Author: Radia Development Team
9
+ Created: 2025-11-21
10
+ Version: 0.1.0
11
+
12
+ Functions
13
+ ---------
14
+ netgen_mesh_to_radia : Convert NGSolve mesh to Radia geometry
15
+ extract_tetrahedra : Extract tetrahedral elements from mesh
16
+ create_radia_tetrahedron : Create single tetrahedron in Radia
17
+
18
+ Example
19
+ -------
20
+ >>> from ngsolve import Mesh
21
+ >>> from netgen.occ import Box, OCCGeometry
22
+ >>> import radia as rad
23
+ >>> from netgen_mesh_import import netgen_mesh_to_radia
24
+ >>>
25
+ >>> rad.FldUnits('m')
26
+ >>> geo = OCCGeometry(Box((0, 0, 0), (0.01, 0.01, 0.01)))
27
+ >>> mesh = Mesh(geo.GenerateMesh(maxh=0.003))
28
+ >>> mag_obj = netgen_mesh_to_radia(mesh,
29
+ ... material={'magnetization': [0, 0, 1.2]},
30
+ ... units='m')
31
+ """
32
+
33
+ import sys
34
+ import radia as rad
35
+ from ngsolve import VOL, ET
36
+
37
+
38
+ # Standard tetrahedral face topology (1-indexed for Radia)
39
+ # Face winding REVERSED from Nastran CTETRA standard to ensure outward normals
40
+ # This correction was identified through test_face_winding.py (2025-11-22)
41
+ # Vertices: v0, v1, v2, v3 (0-indexed) -> 1, 2, 3, 4 (Radia 1-indexed)
42
+ TETRA_FACES = [
43
+ [1, 3, 2], # Face 0: v0-v2-v1
44
+ [1, 2, 4], # Face 1: v0-v1-v3
45
+ [2, 3, 4], # Face 2: v1-v2-v3
46
+ [3, 1, 4] # Face 3: v2-v0-v3
47
+ ]
48
+
49
+ # Hexahedral face topology (1-indexed for Radia)
50
+ # Standard brick/hexahedron with 8 vertices
51
+ # Vertices numbered as: v0-v7 (0-indexed) -> 1-8 (Radia 1-indexed)
52
+ #
53
+ # WARNING: Hexahedral elements have known issues in Radia MMM
54
+ # ============================================================
55
+ # 1. Non-convex (concave) meshes: Hexahedral meshes from Netgen may produce
56
+ # non-convex (concave) elements, which cause errors in Radia MMM.
57
+ # Radia requires convex polyhedra for correct field computation.
58
+ #
59
+ # 2. Netgen 3D hex meshing limitations: Netgen's 3D hexahedral meshing
60
+ # functionality is very limited and often produces poor quality meshes.
61
+ #
62
+ # EXCEPTION: Regular cubic hexahedral meshes (structured grids) are safe
63
+ # and do not produce concave elements. These work correctly in Radia.
64
+ #
65
+ # Recommended: Use tetrahedral meshes for general geometries
66
+ HEX_FACES = [
67
+ [1, 4, 3, 2], # Bottom face (z=0)
68
+ [5, 6, 7, 8], # Top face (z=1)
69
+ [1, 2, 6, 5], # Front face (y=0)
70
+ [3, 4, 8, 7], # Back face (y=1)
71
+ [1, 5, 8, 4], # Left face (x=0)
72
+ [2, 3, 7, 6] # Right face (x=1)
73
+ ]
74
+
75
+ # Wedge/Pentahedron face topology (1-indexed for Radia)
76
+ # Standard wedge/prism with 6 vertices (triangular prism)
77
+ # Vertices: v0-v5 (0-indexed) -> 1-6 (Radia 1-indexed)
78
+ # Bottom triangle: v0, v1, v2
79
+ # Top triangle: v3, v4, v5
80
+ WEDGE_FACES = [
81
+ [1, 3, 2], # Bottom triangle face (v0-v2-v1)
82
+ [4, 5, 6], # Top triangle face (v3-v4-v5)
83
+ [1, 2, 5, 4], # Quad face (v0-v1-v4-v3)
84
+ [2, 3, 6, 5], # Quad face (v1-v2-v5-v4)
85
+ [3, 1, 4, 6] # Quad face (v2-v0-v3-v5)
86
+ ]
87
+
88
+ # Pyramid face topology (1-indexed for Radia)
89
+ # Standard pyramid with 5 vertices
90
+ # Vertices: v0-v4 (0-indexed) -> 1-5 (Radia 1-indexed)
91
+ # Base quad: v0, v1, v2, v3
92
+ # Apex: v4
93
+ PYRAMID_FACES = [
94
+ [1, 4, 3, 2], # Base quad face (v0-v3-v2-v1)
95
+ [1, 2, 5], # Triangle face (v0-v1-v4)
96
+ [2, 3, 5], # Triangle face (v1-v2-v4)
97
+ [3, 4, 5], # Triangle face (v2-v3-v4)
98
+ [4, 1, 5] # Triangle face (v3-v0-v4)
99
+ ]
100
+
101
+
102
+ def create_radia_hexahedron(vertices, magnetization=None):
103
+ """
104
+ Create a single hexahedral (brick) polyhedron in Radia.
105
+
106
+ WARNING: Hexahedral elements may cause numerical issues in Radia MMM.
107
+ Tetrahedral meshes are recommended for better stability.
108
+
109
+ Parameters
110
+ ----------
111
+ vertices : list of list
112
+ 8 vertices: [[x1,y1,z1], ..., [x8,y8,z8]]
113
+ Vertices should follow standard hexahedron numbering
114
+ magnetization : list, optional
115
+ Magnetization vector [Mx, My, Mz] in Tesla
116
+ Default: [0, 0, 0]
117
+
118
+ Returns
119
+ -------
120
+ int
121
+ Radia object ID
122
+
123
+ Raises
124
+ ------
125
+ RuntimeError
126
+ If Radia polyhedron creation fails
127
+
128
+ Notes
129
+ -----
130
+ Face topology uses HEX_FACES constant (see module level definition)
131
+ """
132
+ if magnetization is None:
133
+ magnetization = [0, 0, 0]
134
+
135
+ try:
136
+ # Create polyhedron with hexahedral faces
137
+ poly_id = rad.ObjPolyhdr(vertices, HEX_FACES, magnetization)
138
+ return poly_id
139
+ except Exception as e:
140
+ raise RuntimeError(
141
+ f"Failed to create Radia hexahedron: {e}\n"
142
+ f"Vertices: {vertices}\n"
143
+ f"Faces: {HEX_FACES}"
144
+ )
145
+
146
+
147
+ def create_radia_tetrahedron(vertices, magnetization=None):
148
+ """
149
+ Create a single tetrahedral polyhedron in Radia.
150
+
151
+ Parameters
152
+ ----------
153
+ vertices : list of list
154
+ 4 vertices: [[x1,y1,z1], [x2,y2,z2], [x3,y3,z3], [x4,y4,z4]]
155
+ Coordinates should be in units matching rad.FldUnits() setting.
156
+ magnetization : list, optional
157
+ Magnetization vector [Mx, My, Mz] in Tesla.
158
+ Default: [0, 0, 0] (no magnetization)
159
+
160
+ Returns
161
+ -------
162
+ int
163
+ Radia object ID
164
+
165
+ Raises
166
+ ------
167
+ RuntimeError
168
+ If Radia polyhedron creation fails
169
+
170
+ Notes
171
+ -----
172
+ Face topology (1-indexed):
173
+ [1, 3, 2], # Bottom face
174
+ [1, 2, 4], # Front face
175
+ [1, 4, 3], # Left face
176
+ [2, 3, 4] # Back face
177
+
178
+ Tetrahedra are always convex, making them ideal for Radia polyhedra.
179
+ """
180
+ if magnetization is None:
181
+ magnetization = [0, 0, 0]
182
+
183
+ if len(vertices) != 4:
184
+ raise ValueError(f"Tetrahedron must have exactly 4 vertices, got {len(vertices)}")
185
+
186
+ try:
187
+ obj_id = rad.ObjPolyhdr(vertices, TETRA_FACES, magnetization)
188
+ return obj_id
189
+ except Exception as e:
190
+ raise RuntimeError(f"Failed to create Radia tetrahedron: {e}")
191
+
192
+
193
+ def extract_elements(mesh, material_filter=None, allow_hex=False):
194
+ """
195
+ Extract volume elements (tetrahedra and optionally hexahedra) from NGSolve mesh.
196
+
197
+ Parameters
198
+ ----------
199
+ mesh : ngsolve.Mesh
200
+ Input mesh from Netgen/NGSolve
201
+ material_filter : str, list of str, or None, optional
202
+ Filter elements by material name(s):
203
+ - str: Import only elements with this material name
204
+ - list of str: Import only elements with these material names
205
+ - None: Import all elements (default)
206
+ allow_hex : bool, optional
207
+ If True, allow hexahedral elements (ET.HEX)
208
+ If False, raise error on non-tetrahedral elements
209
+ Default: False
210
+ WARNING: Hexahedral elements may cause issues in Radia MMM
211
+
212
+ Returns
213
+ -------
214
+ tuple of (list of dict, int)
215
+ (elements, skipped_count) where elements is list of dicts:
216
+ {
217
+ 'vertices': [[x1,y1,z1], ...], # 4 for TET, 8 for HEX
218
+ 'element_index': int,
219
+ 'element_type': str, # 'TET' or 'HEX'
220
+ 'material': str # Material name
221
+ }
222
+
223
+ Raises
224
+ ------
225
+ ValueError
226
+ If mesh contains unsupported element types
227
+
228
+ Notes
229
+ -----
230
+ - Vertex coordinates extracted from mesh.vertices
231
+ - Tetrahedral elements (ET.TET): 4 vertices
232
+ - Hexahedral elements (ET.HEX): 8 vertices (if allow_hex=True)
233
+ - Element indices are 0-based
234
+ - Material filtering reduces import time for multi-material meshes
235
+ """
236
+ # Normalize material_filter to set
237
+ if material_filter is None:
238
+ allowed_materials = None
239
+ elif isinstance(material_filter, str):
240
+ allowed_materials = {material_filter}
241
+ elif isinstance(material_filter, (list, tuple)):
242
+ allowed_materials = set(material_filter)
243
+ else:
244
+ raise ValueError(
245
+ f"material_filter must be str, list, or None, got {type(material_filter)}"
246
+ )
247
+
248
+ elements = []
249
+ skipped_count = 0
250
+ hex_count = 0
251
+ tet_count = 0
252
+
253
+ for el_idx, el in enumerate(mesh.Elements(VOL)):
254
+ # Check material filter
255
+ if allowed_materials is not None:
256
+ if el.mat not in allowed_materials:
257
+ skipped_count += 1
258
+ continue
259
+
260
+ # Check element type
261
+ if el.type == ET.TET:
262
+ element_type = 'TET'
263
+ expected_vertices = 4
264
+ tet_count += 1
265
+ elif el.type == ET.HEX and allow_hex:
266
+ element_type = 'HEX'
267
+ expected_vertices = 8
268
+ hex_count += 1
269
+ else:
270
+ if el.type == ET.HEX:
271
+ raise ValueError(
272
+ f"Element {el_idx} is hexahedral (ET.HEX). "
273
+ f"Hexahedral elements are not allowed by default. "
274
+ f"Set allow_hex=True to enable (WARNING: may cause MMM issues)."
275
+ )
276
+ else:
277
+ raise ValueError(
278
+ f"Element {el_idx} has unsupported type {el.type}. "
279
+ f"Only ET.TET (tetrahedra) and optionally ET.HEX (hexahedra) supported."
280
+ )
281
+
282
+ # Extract vertex NodeId objects (NGSolve format: V0, V1, etc.)
283
+ vert_node_ids = el.vertices
284
+
285
+ if len(vert_node_ids) != expected_vertices:
286
+ raise ValueError(
287
+ f"Element {el_idx}: Expected {expected_vertices} vertices for {element_type}, "
288
+ f"got {len(vert_node_ids)}"
289
+ )
290
+
291
+ # Get vertex coordinates in original NGSolve order
292
+ vertices = []
293
+ for v_node in vert_node_ids:
294
+ v_idx = v_node.nr # Get integer index from NodeId
295
+ v = mesh.vertices[v_idx]
296
+ coord = v.point
297
+ vertices.append([coord[0], coord[1], coord[2]])
298
+
299
+ elements.append({
300
+ 'vertices': vertices,
301
+ 'element_index': el_idx,
302
+ 'element_type': element_type,
303
+ 'material': el.mat
304
+ })
305
+
306
+ if hex_count > 0:
307
+ print(f"[WARNING] Imported {hex_count} hexahedral elements. "
308
+ f"Hexahedra may cause numerical issues in Radia MMM.")
309
+
310
+ return elements, skipped_count
311
+
312
+
313
+ def netgen_mesh_to_radia(mesh, material=None, units='m', combine=True, verbose=True,
314
+ material_filter=None, allow_hex=False):
315
+ """
316
+ Convert NGSolve/Netgen mesh to Radia geometry.
317
+
318
+ IMPORTANT: Call rad.FldUnits() BEFORE using this function to ensure
319
+ unit consistency between Netgen (meters) and Radia.
320
+
321
+ Parameters
322
+ ----------
323
+ mesh : ngsolve.Mesh
324
+ Input mesh from Netgen/NGSolve
325
+ material : dict or callable, optional
326
+ Material specification:
327
+
328
+ - dict: {'magnetization': [Mx, My, Mz]}
329
+ Applies uniform magnetization to all elements
330
+
331
+ - callable: function(element_index) -> {'magnetization': [Mx, My, Mz]}
332
+ Per-element material specification
333
+
334
+ - None: Use default [0, 0, 0] (no magnetization)
335
+
336
+ units : str, default='m'
337
+ Unit system: 'm' (meters) or 'mm' (millimeters).
338
+ Must match rad.FldUnits() setting.
339
+
340
+ Note: As of v1.3.4, coordinate scaling is handled automatically by
341
+ rad.FldUnits(). This parameter is kept for API compatibility but
342
+ the actual scaling is performed by Radia's unit conversion system.
343
+
344
+ combine : bool, default=True
345
+ If True, return rad.ObjCnt() container of all elements.
346
+ If False, return list of individual polyhedra object IDs.
347
+
348
+ verbose : bool, default=True
349
+ If True, print progress information during conversion.
350
+
351
+ material_filter : str, list of str, or None, optional
352
+ Filter elements by mesh material name(s):
353
+ - str: Import only elements with this material name
354
+ - list of str: Import only elements with these material names
355
+ - None: Import all elements (default)
356
+
357
+ Example: material_filter='magnetic' imports only 'magnetic' material elements
358
+
359
+ allow_hex : bool, optional
360
+ If True, allow hexahedral elements (ET.HEX) in addition to tetrahedra
361
+ If False, raise error if mesh contains hexahedral elements
362
+ Default: False
363
+
364
+ WARNING: Hexahedral meshes may produce concave elements causing MMM errors.
365
+ Exception: Regular cubic grids (structured hex meshes) are safe.
366
+
367
+ Returns
368
+ -------
369
+ int or list
370
+ - If combine=True: Radia container object ID (int)
371
+ - If combine=False: List of individual polyhedron object IDs (list of int)
372
+
373
+ Raises
374
+ ------
375
+ ValueError
376
+ If mesh contains hexahedral elements and allow_hex=False
377
+ If mesh contains unsupported element types
378
+ If material specification is invalid
379
+ If units parameter is not 'm' or 'mm'
380
+ RuntimeError
381
+ If Radia polyhedron creation fails
382
+
383
+ Examples
384
+ --------
385
+ Basic usage with uniform magnetization:
386
+
387
+ >>> from ngsolve import Mesh
388
+ >>> from netgen.occ import Box, OCCGeometry
389
+ >>> import radia as rad
390
+ >>> from netgen_mesh_import import netgen_mesh_to_radia
391
+ >>>
392
+ >>> # IMPORTANT: Set Radia units first!
393
+ >>> rad.FldUnits('m')
394
+ >>>
395
+ >>> # Create Netgen mesh
396
+ >>> geo = OCCGeometry(Box((0, 0, 0), (0.01, 0.01, 0.01)))
397
+ >>> mesh = Mesh(geo.GenerateMesh(maxh=0.003))
398
+ >>>
399
+ >>> # Convert to Radia with uniform magnetization
400
+ >>> mag_obj = netgen_mesh_to_radia(mesh,
401
+ ... material={'magnetization': [0, 0, 1.2]},
402
+ ... units='m')
403
+ >>> print(f"Created Radia object: {mag_obj}")
404
+
405
+ Per-element material specification:
406
+
407
+ >>> def material_func(el_idx):
408
+ ... # Left half: magnetized, right half: air
409
+ ... if el_idx < 100:
410
+ ... return {'magnetization': [0, 0, 1.2]}
411
+ ... else:
412
+ ... return {'magnetization': [0, 0, 0]}
413
+ >>>
414
+ >>> mag_obj = netgen_mesh_to_radia(mesh, material=material_func)
415
+
416
+ Using millimeters (Radia default):
417
+
418
+ >>> rad.FldUnits('mm') # Set Radia to mm
419
+ >>> mag_obj = netgen_mesh_to_radia(mesh, units='mm') # Auto-scales coordinates
420
+
421
+ Notes
422
+ -----
423
+ - Supports tetrahedral (ET.TET) and optionally hexahedral (ET.HEX) elements
424
+ - Vertex coordinates are extracted in Netgen's native units (meters)
425
+ - Scaling applied automatically if units='mm'
426
+ - All tetrahedra are convex, suitable for rad.ObjPolyhdr()
427
+ - Hexahedra may be concave (avoid except for structured cubic grids)
428
+ - Progress printed every 100 elements if verbose=True
429
+ """
430
+ # Validate units parameter
431
+ if units not in ['m', 'mm']:
432
+ raise ValueError(f"units must be 'm' or 'mm', got '{units}'")
433
+
434
+ # Extract elements (tetrahedra and optionally hexahedra)
435
+ if verbose:
436
+ print(f"[Netgen Import] Extracting elements from mesh...")
437
+ print(f" Mesh: {mesh.ne} elements")
438
+ if material_filter is not None:
439
+ filter_str = material_filter if isinstance(material_filter, str) else ', '.join(material_filter)
440
+ print(f" Material filter: {filter_str}")
441
+ if allow_hex:
442
+ print(f" [WARNING] Hexahedral elements enabled (may cause MMM issues)")
443
+
444
+ try:
445
+ elements, skipped_count = extract_elements(mesh, material_filter=material_filter, allow_hex=allow_hex)
446
+ except ValueError as e:
447
+ print(f"[ERROR] {e}")
448
+ raise
449
+
450
+ num_elements = len(elements)
451
+ if verbose:
452
+ # Count element types
453
+ tet_count = sum(1 for el in elements if el['element_type'] == 'TET')
454
+ hex_count = sum(1 for el in elements if el['element_type'] == 'HEX')
455
+
456
+ if hex_count > 0:
457
+ print(f" Extracted: {num_elements} elements ({tet_count} TET, {hex_count} HEX)")
458
+ else:
459
+ print(f" Extracted: {num_elements} tetrahedra")
460
+
461
+ if skipped_count > 0:
462
+ print(f" Skipped: {skipped_count} elements (filtered by material)")
463
+
464
+ # Coordinate scaling is now handled by rad.FldUnits()
465
+ # No manual scaling needed - Radia automatically converts units
466
+ coord_scale = 1.0
467
+
468
+ if verbose:
469
+ print(f" Units: {units} (scaling handled by rad.FldUnits())")
470
+
471
+ # Process material specification
472
+ if material is None:
473
+ # Default: no magnetization
474
+ def get_material(el_idx):
475
+ return {'magnetization': [0, 0, 0]}
476
+ elif isinstance(material, dict):
477
+ # Uniform material
478
+ def get_material(el_idx):
479
+ return material
480
+ elif callable(material):
481
+ # Per-element material function
482
+ get_material = material
483
+ else:
484
+ raise ValueError(
485
+ f"material must be dict, callable, or None. Got {type(material)}"
486
+ )
487
+
488
+ # Create Radia polyhedra
489
+ polyhedra = []
490
+
491
+ if verbose:
492
+ print(f"[Netgen Import] Creating Radia polyhedra...")
493
+
494
+ for i, element in enumerate(elements):
495
+ # Scale coordinates if needed
496
+ vertices = element['vertices']
497
+ if coord_scale != 1.0:
498
+ vertices = [[x*coord_scale, y*coord_scale, z*coord_scale]
499
+ for x, y, z in vertices]
500
+
501
+ # Get material for this element
502
+ el_idx = element['element_index']
503
+ try:
504
+ mat = get_material(el_idx)
505
+ except Exception as e:
506
+ raise RuntimeError(
507
+ f"Material function failed for element {el_idx}: {e}"
508
+ )
509
+
510
+ # Validate material specification
511
+ if not isinstance(mat, dict) or 'magnetization' not in mat:
512
+ raise ValueError(
513
+ f"Material function for element {el_idx} must return "
514
+ f"dict with 'magnetization' key. Got: {mat}"
515
+ )
516
+
517
+ magnetization = mat['magnetization']
518
+
519
+ # Create polyhedron based on element type
520
+ element_type = element['element_type']
521
+ try:
522
+ if element_type == 'TET':
523
+ obj_id = create_radia_tetrahedron(vertices, magnetization)
524
+ elif element_type == 'HEX':
525
+ obj_id = create_radia_hexahedron(vertices, magnetization)
526
+ else:
527
+ raise ValueError(f"Unknown element type: {element_type}")
528
+
529
+ polyhedra.append(obj_id)
530
+ except RuntimeError as e:
531
+ raise RuntimeError(
532
+ f"Failed to create {element_type} polyhedron for element {el_idx}: {e}"
533
+ )
534
+
535
+ # Progress reporting
536
+ if verbose and (i + 1) % 100 == 0:
537
+ print(f" Progress: {i+1}/{num_elements}", end='\r')
538
+
539
+ if verbose:
540
+ print(f" Progress: {num_elements}/{num_elements}")
541
+ print(f" [OK] Created {len(polyhedra)} polyhedra")
542
+
543
+ # Return result
544
+ if combine:
545
+ if verbose:
546
+ print(f"[Netgen Import] Combining into container...")
547
+
548
+ container = rad.ObjCnt(polyhedra)
549
+
550
+ if verbose:
551
+ print(f" [OK] Container object ID: {container}")
552
+
553
+ return container
554
+ else:
555
+ if verbose:
556
+ print(f"[Netgen Import] Returning list of {len(polyhedra)} object IDs")
557
+
558
+ return polyhedra
559
+
560
+
561
+ # Module-level constants for external use
562
+ __version__ = '0.2.0'
563
+ __all__ = [
564
+ 'netgen_mesh_to_radia',
565
+ 'extract_elements',
566
+ 'create_radia_tetrahedron',
567
+ 'create_radia_hexahedron',
568
+ 'TETRA_FACES',
569
+ 'HEX_FACES',
570
+ 'WEDGE_FACES',
571
+ 'PYRAMID_FACES'
572
+ ]
python/rad_ngsolve.pyd CHANGED
Binary file