radia 1.3.5__py3-none-any.whl → 1.3.7__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.
- python/__init__.py +2 -0
- python/radia.pyd +0 -0
- {radia → python}/radia_ngsolve.pyd +0 -0
- {radia-1.3.5.dist-info → radia-1.3.7.dist-info}/METADATA +79 -3
- radia-1.3.7.dist-info/RECORD +17 -0
- radia-1.3.7.dist-info/top_level.txt +1 -0
- radia/__init__.py +0 -2
- radia/rad_ngsolve.pyd +0 -0
- radia/radia_tetra_mesh.py +0 -657
- radia-1.3.5.dist-info/RECORD +0 -18
- radia-1.3.5.dist-info/top_level.txt +0 -1
- {radia → python}/nastran_mesh_import.py +0 -0
- {radia → python}/netgen_mesh_import.py +0 -0
- {radia → python}/rad_ngsolve_fast.py +0 -0
- {radia → python}/radia_coil_builder.py +0 -0
- {radia → python}/radia_field_cached.py +0 -0
- {radia → python}/radia_ngsolve_field.py +0 -0
- {radia → python}/radia_ngsolve_utils.py +0 -0
- {radia → python}/radia_pyvista_viewer.py +0 -0
- {radia → python}/radia_vtk_export.py +0 -0
- {radia-1.3.5.dist-info → radia-1.3.7.dist-info}/WHEEL +0 -0
- {radia-1.3.5.dist-info → radia-1.3.7.dist-info}/licenses/LICENSE +0 -0
python/__init__.py
ADDED
python/radia.pyd
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: radia
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.7
|
|
4
4
|
Summary: Radia 3D Magnetostatics with NGSolve Integration and OpenMP Parallelization
|
|
5
5
|
Home-page: https://github.com/ksugahar/Radia_NGSolve
|
|
6
6
|
Author: Pascal Elleaume
|
|
@@ -296,6 +296,82 @@ export_geometry_to_vtk(mag, 'geometry.vtk')
|
|
|
296
296
|
- NGSolve (optional, for FEM coupling via radia_ngsolve)
|
|
297
297
|
- PyVista (optional, for 3D visualization)
|
|
298
298
|
|
|
299
|
+
## Linear Material with External Field (ObjBckgCF)
|
|
300
|
+
|
|
301
|
+
Radia supports soft magnetic materials (linear materials) in external fields using `ObjBckgCF`.
|
|
302
|
+
|
|
303
|
+
### Working Example
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
import radia as rad
|
|
307
|
+
import numpy as np
|
|
308
|
+
|
|
309
|
+
rad.UtiDelAll()
|
|
310
|
+
rad.FldUnits('m') # Use SI units
|
|
311
|
+
|
|
312
|
+
# Create soft iron cube (10cm side)
|
|
313
|
+
cube = rad.ObjRecMag([0, 0, 0], [0.1, 0.1, 0.1], [0, 0, 0])
|
|
314
|
+
|
|
315
|
+
# Apply isotropic linear material (mu_r = 100)
|
|
316
|
+
chi = 99.0 # chi = mu_r - 1
|
|
317
|
+
mat = rad.MatLin(chi) # IMPORTANT: Use single argument for isotropic
|
|
318
|
+
rad.MatApl(cube, mat)
|
|
319
|
+
|
|
320
|
+
# Create external field (1 Tesla in z-direction)
|
|
321
|
+
def uniform_B(pos):
|
|
322
|
+
return [0, 0, 1.0] # Returns B in Tesla
|
|
323
|
+
bg = rad.ObjBckgCF(uniform_B)
|
|
324
|
+
|
|
325
|
+
# Create system and solve
|
|
326
|
+
system = rad.ObjCnt([cube, bg])
|
|
327
|
+
result = rad.Solve(system, 0.0001, 1000)
|
|
328
|
+
|
|
329
|
+
# Result: M ~ 2.3 MA/m (correct for cube with mu_r=100 in 1T field)
|
|
330
|
+
M = rad.ObjM(cube)
|
|
331
|
+
print(f"Magnetization: {M[1]} A/m")
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Important Notes
|
|
335
|
+
|
|
336
|
+
1. **Isotropic materials**: Always use `MatLin(chi)` (single argument)
|
|
337
|
+
- Do NOT use `MatLin(chi, [0, 0, 1e-10])` - this creates a poorly defined anisotropic tensor
|
|
338
|
+
|
|
339
|
+
2. **Callback returns B in Tesla**: The callback function for `ObjBckgCF` should return magnetic flux density B in Tesla
|
|
340
|
+
|
|
341
|
+
3. **Units**: When using `rad.FldUnits('m')`, all coordinates are in meters
|
|
342
|
+
|
|
343
|
+
4. **Tetrahedral elements**: Also works with `ObjPolyhdr` tetrahedral meshes
|
|
344
|
+
|
|
345
|
+
### Supported Element Types
|
|
346
|
+
|
|
347
|
+
| Element | API | External Field Support |
|
|
348
|
+
|---------|-----|----------------------|
|
|
349
|
+
| Hexahedron | `ObjRecMag` | Yes |
|
|
350
|
+
| Tetrahedron | `ObjPolyhdr` | Yes |
|
|
351
|
+
| Extruded Polygon | `ObjThckPgn` | Yes |
|
|
352
|
+
|
|
353
|
+
## Tetrahedral Mesh Import from NGSolve/Netgen
|
|
354
|
+
|
|
355
|
+
Radia can import tetrahedral meshes from NGSolve/Netgen for complex geometries.
|
|
356
|
+
|
|
357
|
+
**Features:**
|
|
358
|
+
- Direct Netgen/OCC mesh import to Radia MMM
|
|
359
|
+
- Independent mesh optimization for FEM vs MMM
|
|
360
|
+
- Nastran CTETRA face connectivity standard
|
|
361
|
+
|
|
362
|
+
**Usage:** See `examples/cube_uniform_field/linear/` for examples.
|
|
363
|
+
|
|
364
|
+
**Benchmark** (mu_r=100, B0=1T, 100mm cube):
|
|
365
|
+
|
|
366
|
+
| Mesh | Elements | M_z (MA/m) | Error |
|
|
367
|
+
|------|----------|------------|-------|
|
|
368
|
+
| 1x1x1 hex | 1 | 2.3171 | 0.00% |
|
|
369
|
+
| 2x2x2 hex | 8 | 2.4180 | 4.36% |
|
|
370
|
+
| 5x5x5 hex | 125 | 2.6652 | 15.02% |
|
|
371
|
+
|
|
372
|
+
Analytical: M = 2.3171 MA/m (N = 1/3 for cube)
|
|
373
|
+
|
|
374
|
+
|
|
299
375
|
## Changes from Original Radia
|
|
300
376
|
|
|
301
377
|
### Removed Components
|
|
@@ -379,5 +455,5 @@ See [examples/NGSolve_CoefficientFunction_to_Radia_BackgroundField/](examples/NG
|
|
|
379
455
|
|
|
380
456
|
---
|
|
381
457
|
|
|
382
|
-
**Version**: 1.
|
|
383
|
-
**Last Updated**: 2025-11-
|
|
458
|
+
**Version**: 1.3.4 (OpenMP + NGSolve Edition)
|
|
459
|
+
**Last Updated**: 2025-11-30
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
python/__init__.py,sha256=oUOAjf_vY8DNy5HRU7oArAMic8urvHCR9yHSi4HFkkQ,47
|
|
2
|
+
python/nastran_mesh_import.py,sha256=CSoVhZCXa85lPiTF2hlspE2clBKOD7-_sCp1bxu_IK0,18147
|
|
3
|
+
python/netgen_mesh_import.py,sha256=UopXk-5bbfj1j9_hyiq8jbjb4SQXnWaeuaC7TDf17wA,19872
|
|
4
|
+
python/rad_ngsolve_fast.py,sha256=GkC7ruKy3MESHsO0iRSWsrgLU4-DPPgctOi6pPpsWg4,5675
|
|
5
|
+
python/radia.pyd,sha256=6CNhfoZk5c07LIfeqR6-OT-vT22Oxnc_K3PkTADfa90,1735680
|
|
6
|
+
python/radia_coil_builder.py,sha256=nQkiAbfhueNvvxUARHdPD0C68ImidHmUQv_q4RsImeY,11253
|
|
7
|
+
python/radia_field_cached.py,sha256=Bjw3ecNe3u7AAXnLob5m_tjYIY7HwB9DpgFg9a-aP_8,9509
|
|
8
|
+
python/radia_ngsolve.pyd,sha256=hmPQal8FEqt9Ia5NQ05Vw35RUW5jbWFGQ5PzwXJiLww,566272
|
|
9
|
+
python/radia_ngsolve_field.py,sha256=suJr4wacfYFKOkyV-5AQuHWnW5rtUMb0gSSjq8VRSXc,10166
|
|
10
|
+
python/radia_ngsolve_utils.py,sha256=xZCR9DOIKMwdEjmC28rOXVZiWFY5BQYH2VfopfuVBps,8406
|
|
11
|
+
python/radia_pyvista_viewer.py,sha256=JS33Mx4azGI7hUX0bzefc6zJfhv6qfRjM3Kl1bE9Mjs,4275
|
|
12
|
+
python/radia_vtk_export.py,sha256=I8Vyyt9tky78Qw1xPru9f0Rii6QEmdEgTFjQtamyooc,6540
|
|
13
|
+
radia-1.3.7.dist-info/licenses/LICENSE,sha256=yaWxyzG9DpJ44dDNdGni4nukwiZ8pU-r_aW-1tYNAjk,4374
|
|
14
|
+
radia-1.3.7.dist-info/METADATA,sha256=IpwhuG1ehOREVYTnjk8MQjl3ERnO7Y859KO8ChypMLQ,15518
|
|
15
|
+
radia-1.3.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
radia-1.3.7.dist-info/top_level.txt,sha256=J-z0poNcsv31IHB413--iOY8LoHBKiTHeybHX3abokI,7
|
|
17
|
+
radia-1.3.7.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python
|
radia/__init__.py
DELETED
radia/rad_ngsolve.pyd
DELETED
|
Binary file
|
radia/radia_tetra_mesh.py
DELETED
|
@@ -1,657 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
"""
|
|
3
|
-
radia_tetra_mesh.py - Volume-based tetrahedral mesh solver for Radia
|
|
4
|
-
|
|
5
|
-
This module provides a specialized solver for tetrahedral meshes with linear
|
|
6
|
-
magnetic materials and background fields. It uses surface-based field computation
|
|
7
|
-
to correctly handle interior face cancellation.
|
|
8
|
-
|
|
9
|
-
Key features:
|
|
10
|
-
- Surface extraction: Only external faces are used for field computation
|
|
11
|
-
- Interior faces automatically cancel (volume-based approach)
|
|
12
|
-
- Relaxation method for linear material problems
|
|
13
|
-
- Compatible with NGSolve/Netgen tetrahedral meshes
|
|
14
|
-
|
|
15
|
-
Author: Radia Development Team
|
|
16
|
-
Created: 2025-11-27
|
|
17
|
-
Version: 0.1.0
|
|
18
|
-
|
|
19
|
-
Example
|
|
20
|
-
-------
|
|
21
|
-
>>> from ngsolve import Mesh
|
|
22
|
-
>>> from netgen.occ import Box, OCCGeometry
|
|
23
|
-
>>> import radia as rad
|
|
24
|
-
>>> import radia_tetra_mesh as rtm
|
|
25
|
-
>>>
|
|
26
|
-
>>> rad.FldUnits('m')
|
|
27
|
-
>>> geo = OCCGeometry(Box((-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)))
|
|
28
|
-
>>> mesh = Mesh(geo.GenerateMesh(maxh=0.2))
|
|
29
|
-
>>>
|
|
30
|
-
>>> # Solve with linear material in background field
|
|
31
|
-
>>> mu_0 = 4 * np.pi * 1e-7
|
|
32
|
-
>>> result = rtm.solve_linear_material(
|
|
33
|
-
... mesh,
|
|
34
|
-
... chi=99.0, # mu_r - 1
|
|
35
|
-
... background_B=[0, 0, mu_0 * 1.0], # H0 = 1 A/m
|
|
36
|
-
... precision=0.0001,
|
|
37
|
-
... max_iter=10000
|
|
38
|
-
... )
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
import numpy as np
|
|
42
|
-
import radia as rad
|
|
43
|
-
from ngsolve import VOL, ET
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Tetrahedral face topology (0-indexed vertices)
|
|
47
|
-
# v0, v1, v2, v3 are the 4 vertices of the tetrahedron
|
|
48
|
-
TETRA_FACE_VERTICES = [
|
|
49
|
-
(0, 2, 1), # Face 0: opposite to v3
|
|
50
|
-
(0, 1, 3), # Face 1: opposite to v2
|
|
51
|
-
(1, 2, 3), # Face 2: opposite to v0
|
|
52
|
-
(2, 0, 3) # Face 3: opposite to v1
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def extract_surface_faces(mesh, material_filter=None, verbose=True):
|
|
57
|
-
"""
|
|
58
|
-
Extract external surface faces from tetrahedral mesh.
|
|
59
|
-
|
|
60
|
-
Interior faces (shared by two tetrahedra) are identified and excluded.
|
|
61
|
-
Only external (boundary) faces are returned.
|
|
62
|
-
|
|
63
|
-
Parameters
|
|
64
|
-
----------
|
|
65
|
-
mesh : ngsolve.Mesh
|
|
66
|
-
Input tetrahedral mesh from Netgen/NGSolve
|
|
67
|
-
material_filter : str or list, optional
|
|
68
|
-
Filter elements by material name(s)
|
|
69
|
-
verbose : bool, default=True
|
|
70
|
-
Print progress information
|
|
71
|
-
|
|
72
|
-
Returns
|
|
73
|
-
-------
|
|
74
|
-
dict
|
|
75
|
-
{
|
|
76
|
-
'surface_faces': list of dict
|
|
77
|
-
Each face: {
|
|
78
|
-
'vertices': [[x1,y1,z1], [x2,y2,z2], [x3,y3,z3]],
|
|
79
|
-
'normal': [nx, ny, nz],
|
|
80
|
-
'area': float,
|
|
81
|
-
'centroid': [cx, cy, cz],
|
|
82
|
-
'element_idx': int, # Parent element index
|
|
83
|
-
'local_face_idx': int # Face index within tetrahedron (0-3)
|
|
84
|
-
}
|
|
85
|
-
'elements': list of dict
|
|
86
|
-
Each element: {
|
|
87
|
-
'vertices': [[x1,y1,z1], ..., [x4,y4,z4]],
|
|
88
|
-
'centroid': [cx, cy, cz],
|
|
89
|
-
'volume': float,
|
|
90
|
-
'surface_face_indices': list # Indices into surface_faces
|
|
91
|
-
}
|
|
92
|
-
'interior_face_count': int
|
|
93
|
-
'surface_face_count': int
|
|
94
|
-
}
|
|
95
|
-
"""
|
|
96
|
-
# Normalize material_filter
|
|
97
|
-
if material_filter is None:
|
|
98
|
-
allowed_materials = None
|
|
99
|
-
elif isinstance(material_filter, str):
|
|
100
|
-
allowed_materials = {material_filter}
|
|
101
|
-
else:
|
|
102
|
-
allowed_materials = set(material_filter)
|
|
103
|
-
|
|
104
|
-
# First pass: collect all elements and their faces
|
|
105
|
-
elements = []
|
|
106
|
-
face_dict = {} # Key: sorted vertex indices, Value: list of (element_idx, local_face_idx)
|
|
107
|
-
|
|
108
|
-
for el_idx, el in enumerate(mesh.Elements(VOL)):
|
|
109
|
-
if el.type != ET.TET:
|
|
110
|
-
raise ValueError(f"Element {el_idx} is not tetrahedral (type={el.type})")
|
|
111
|
-
|
|
112
|
-
if allowed_materials is not None:
|
|
113
|
-
if el.mat not in allowed_materials:
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
# Get vertex coordinates
|
|
117
|
-
vert_node_ids = el.vertices
|
|
118
|
-
vertices = []
|
|
119
|
-
for v_node in vert_node_ids:
|
|
120
|
-
v = mesh.vertices[v_node.nr]
|
|
121
|
-
coord = v.point
|
|
122
|
-
vertices.append([coord[0], coord[1], coord[2]])
|
|
123
|
-
|
|
124
|
-
# Compute centroid
|
|
125
|
-
centroid = [
|
|
126
|
-
sum(v[0] for v in vertices) / 4.0,
|
|
127
|
-
sum(v[1] for v in vertices) / 4.0,
|
|
128
|
-
sum(v[2] for v in vertices) / 4.0
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
# Compute volume (1/6 * |det([v1-v0, v2-v0, v3-v0])|)
|
|
132
|
-
v0, v1, v2, v3 = [np.array(v) for v in vertices]
|
|
133
|
-
vol = abs(np.dot(v1 - v0, np.cross(v2 - v0, v3 - v0))) / 6.0
|
|
134
|
-
|
|
135
|
-
el_data = {
|
|
136
|
-
'vertices': vertices,
|
|
137
|
-
'centroid': centroid,
|
|
138
|
-
'volume': vol,
|
|
139
|
-
'vertex_indices': [v_node.nr for v_node in vert_node_ids],
|
|
140
|
-
'surface_face_indices': []
|
|
141
|
-
}
|
|
142
|
-
elements.append(el_data)
|
|
143
|
-
|
|
144
|
-
current_el_idx = len(elements) - 1
|
|
145
|
-
|
|
146
|
-
# Process each face
|
|
147
|
-
for local_face_idx, face_verts in enumerate(TETRA_FACE_VERTICES):
|
|
148
|
-
# Get global vertex indices for this face
|
|
149
|
-
global_verts = tuple(sorted([el_data['vertex_indices'][i] for i in face_verts]))
|
|
150
|
-
|
|
151
|
-
if global_verts not in face_dict:
|
|
152
|
-
face_dict[global_verts] = []
|
|
153
|
-
face_dict[global_verts].append((current_el_idx, local_face_idx))
|
|
154
|
-
|
|
155
|
-
# Second pass: identify surface faces (appear only once)
|
|
156
|
-
surface_faces = []
|
|
157
|
-
interior_face_count = 0
|
|
158
|
-
|
|
159
|
-
for global_verts, occurrences in face_dict.items():
|
|
160
|
-
if len(occurrences) == 1:
|
|
161
|
-
# Surface face
|
|
162
|
-
el_idx, local_face_idx = occurrences[0]
|
|
163
|
-
el_data = elements[el_idx]
|
|
164
|
-
|
|
165
|
-
# Get face vertex coordinates
|
|
166
|
-
face_vert_indices = TETRA_FACE_VERTICES[local_face_idx]
|
|
167
|
-
face_vertices = [el_data['vertices'][i] for i in face_vert_indices]
|
|
168
|
-
|
|
169
|
-
# Compute face normal and area
|
|
170
|
-
v0, v1, v2 = [np.array(v) for v in face_vertices]
|
|
171
|
-
edge1 = v1 - v0
|
|
172
|
-
edge2 = v2 - v0
|
|
173
|
-
normal_vec = np.cross(edge1, edge2)
|
|
174
|
-
area = np.linalg.norm(normal_vec) / 2.0
|
|
175
|
-
|
|
176
|
-
if area > 1e-15:
|
|
177
|
-
normal_unit = normal_vec / (2.0 * area)
|
|
178
|
-
else:
|
|
179
|
-
normal_unit = np.array([0.0, 0.0, 1.0])
|
|
180
|
-
|
|
181
|
-
# Ensure normal points outward (away from element centroid)
|
|
182
|
-
face_centroid = (v0 + v1 + v2) / 3.0
|
|
183
|
-
el_centroid = np.array(el_data['centroid'])
|
|
184
|
-
outward = face_centroid - el_centroid
|
|
185
|
-
if np.dot(normal_unit, outward) < 0:
|
|
186
|
-
normal_unit = -normal_unit
|
|
187
|
-
|
|
188
|
-
face_data = {
|
|
189
|
-
'vertices': face_vertices,
|
|
190
|
-
'normal': normal_unit.tolist(),
|
|
191
|
-
'area': area,
|
|
192
|
-
'centroid': face_centroid.tolist(),
|
|
193
|
-
'element_idx': el_idx,
|
|
194
|
-
'local_face_idx': local_face_idx
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
face_idx = len(surface_faces)
|
|
198
|
-
surface_faces.append(face_data)
|
|
199
|
-
el_data['surface_face_indices'].append(face_idx)
|
|
200
|
-
|
|
201
|
-
elif len(occurrences) == 2:
|
|
202
|
-
# Interior face - skip
|
|
203
|
-
interior_face_count += 1
|
|
204
|
-
else:
|
|
205
|
-
# Should not happen in valid mesh
|
|
206
|
-
raise ValueError(f"Face with {len(occurrences)} occurrences (expected 1 or 2)")
|
|
207
|
-
|
|
208
|
-
if verbose:
|
|
209
|
-
print(f"[Surface Extraction]")
|
|
210
|
-
print(f" Total elements: {len(elements)}")
|
|
211
|
-
print(f" Total faces: {len(face_dict)}")
|
|
212
|
-
print(f" Surface faces: {len(surface_faces)}")
|
|
213
|
-
print(f" Interior faces: {interior_face_count} (excluded)")
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
'surface_faces': surface_faces,
|
|
217
|
-
'elements': elements,
|
|
218
|
-
'interior_face_count': interior_face_count,
|
|
219
|
-
'surface_face_count': len(surface_faces)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def compute_h_field_from_surface_charge(obs_point, faces, magnetizations, element_map):
|
|
224
|
-
"""
|
|
225
|
-
Compute H field at observation point from surface magnetic charges.
|
|
226
|
-
|
|
227
|
-
The surface charge density on each face is sigma = M . n, where M is the
|
|
228
|
-
magnetization of the parent element and n is the outward face normal.
|
|
229
|
-
|
|
230
|
-
Parameters
|
|
231
|
-
----------
|
|
232
|
-
obs_point : array-like
|
|
233
|
-
Observation point [x, y, z]
|
|
234
|
-
faces : list
|
|
235
|
-
List of surface face dictionaries
|
|
236
|
-
magnetizations : ndarray
|
|
237
|
-
Magnetization vectors for each element, shape (n_elements, 3)
|
|
238
|
-
element_map : list
|
|
239
|
-
Maps face index to parent element index
|
|
240
|
-
|
|
241
|
-
Returns
|
|
242
|
-
-------
|
|
243
|
-
ndarray
|
|
244
|
-
H field vector [Hx, Hy, Hz] in A/m
|
|
245
|
-
"""
|
|
246
|
-
obs = np.array(obs_point)
|
|
247
|
-
H = np.zeros(3)
|
|
248
|
-
|
|
249
|
-
for face_idx, face in enumerate(faces):
|
|
250
|
-
el_idx = face['element_idx']
|
|
251
|
-
M = magnetizations[el_idx]
|
|
252
|
-
n = np.array(face['normal'])
|
|
253
|
-
|
|
254
|
-
# Surface charge density: sigma = M . n
|
|
255
|
-
sigma = np.dot(M, n)
|
|
256
|
-
|
|
257
|
-
if abs(sigma) < 1e-20:
|
|
258
|
-
continue
|
|
259
|
-
|
|
260
|
-
# Face centroid
|
|
261
|
-
face_center = np.array(face['centroid'])
|
|
262
|
-
|
|
263
|
-
# Vector from face to observation point
|
|
264
|
-
r_vec = obs - face_center
|
|
265
|
-
r_mag = np.linalg.norm(r_vec)
|
|
266
|
-
|
|
267
|
-
if r_mag < 1e-15:
|
|
268
|
-
# Observation point at face centroid - use simplified formula
|
|
269
|
-
# H_local = -sigma / 2 * n (for field inside material)
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
# H = sigma * Area / (4 * pi * r^2) * r_hat
|
|
273
|
-
# This is the monopole approximation for far field
|
|
274
|
-
area = face['area']
|
|
275
|
-
factor = sigma * area / (4.0 * np.pi * r_mag**3)
|
|
276
|
-
H += factor * r_vec
|
|
277
|
-
|
|
278
|
-
return H
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def compute_interaction_matrix(mesh_data, verbose=True):
|
|
282
|
-
"""
|
|
283
|
-
Compute interaction matrix for relaxation solver.
|
|
284
|
-
|
|
285
|
-
The interaction matrix A[i,j] gives the H field at element i's centroid
|
|
286
|
-
due to a unit magnetization in element j (through surface charges).
|
|
287
|
-
|
|
288
|
-
For linear materials: H_total = A @ M + H_ext
|
|
289
|
-
|
|
290
|
-
Parameters
|
|
291
|
-
----------
|
|
292
|
-
mesh_data : dict
|
|
293
|
-
Output from extract_surface_faces()
|
|
294
|
-
verbose : bool, default=True
|
|
295
|
-
Print progress
|
|
296
|
-
|
|
297
|
-
Returns
|
|
298
|
-
-------
|
|
299
|
-
ndarray
|
|
300
|
-
Interaction matrix, shape (3*n_elements, 3*n_elements)
|
|
301
|
-
Organized as [Hx_0, Hy_0, Hz_0, Hx_1, Hy_1, Hz_1, ...]
|
|
302
|
-
"""
|
|
303
|
-
elements = mesh_data['elements']
|
|
304
|
-
faces = mesh_data['surface_faces']
|
|
305
|
-
n_elements = len(elements)
|
|
306
|
-
|
|
307
|
-
if verbose:
|
|
308
|
-
print(f"[Interaction Matrix]")
|
|
309
|
-
print(f" Building {3*n_elements} x {3*n_elements} matrix...")
|
|
310
|
-
|
|
311
|
-
# For each source element, set unit M in x, y, z directions
|
|
312
|
-
# and compute H at all observation points
|
|
313
|
-
A = np.zeros((3 * n_elements, 3 * n_elements))
|
|
314
|
-
|
|
315
|
-
# Progress tracking
|
|
316
|
-
total_ops = n_elements * 3
|
|
317
|
-
progress_interval = max(1, total_ops // 10)
|
|
318
|
-
|
|
319
|
-
for src_el in range(n_elements):
|
|
320
|
-
for src_comp in range(3): # Mx, My, Mz
|
|
321
|
-
col_idx = 3 * src_el + src_comp
|
|
322
|
-
|
|
323
|
-
# Unit magnetization in direction src_comp
|
|
324
|
-
M_test = np.zeros((n_elements, 3))
|
|
325
|
-
M_test[src_el, src_comp] = 1.0
|
|
326
|
-
|
|
327
|
-
# Compute H at all element centroids
|
|
328
|
-
for obs_el in range(n_elements):
|
|
329
|
-
obs_point = elements[obs_el]['centroid']
|
|
330
|
-
H = compute_h_field_from_surface_charge(obs_point, faces, M_test, None)
|
|
331
|
-
|
|
332
|
-
for obs_comp in range(3):
|
|
333
|
-
row_idx = 3 * obs_el + obs_comp
|
|
334
|
-
A[row_idx, col_idx] = H[obs_comp]
|
|
335
|
-
|
|
336
|
-
if verbose and (col_idx + 1) % progress_interval == 0:
|
|
337
|
-
print(f" Progress: {100*(col_idx+1)/total_ops:.0f}%")
|
|
338
|
-
|
|
339
|
-
if verbose:
|
|
340
|
-
print(f" Matrix construction complete")
|
|
341
|
-
|
|
342
|
-
return A
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def solve_linear_material(mesh, chi, background_B, precision=0.0001, max_iter=10000,
|
|
346
|
-
material_filter=None, relax_param=0.5, verbose=True):
|
|
347
|
-
"""
|
|
348
|
-
Solve for magnetization in linear magnetic material with background field.
|
|
349
|
-
|
|
350
|
-
Uses relaxation method to solve: M = chi * H_total
|
|
351
|
-
where H_total = H_background + H_demagnetizing(M)
|
|
352
|
-
|
|
353
|
-
Parameters
|
|
354
|
-
----------
|
|
355
|
-
mesh : ngsolve.Mesh
|
|
356
|
-
Input tetrahedral mesh
|
|
357
|
-
chi : float
|
|
358
|
-
Magnetic susceptibility (chi = mu_r - 1)
|
|
359
|
-
background_B : array-like
|
|
360
|
-
Background magnetic field [Bx, By, Bz] in Tesla
|
|
361
|
-
precision : float, default=0.0001
|
|
362
|
-
Convergence precision (relative change in M)
|
|
363
|
-
max_iter : int, default=10000
|
|
364
|
-
Maximum iterations
|
|
365
|
-
material_filter : str or list, optional
|
|
366
|
-
Filter elements by material name
|
|
367
|
-
relax_param : float, default=0.5
|
|
368
|
-
Relaxation parameter (0 < relax_param <= 1)
|
|
369
|
-
Smaller values = more stable but slower convergence
|
|
370
|
-
verbose : bool, default=True
|
|
371
|
-
Print progress information
|
|
372
|
-
|
|
373
|
-
Returns
|
|
374
|
-
-------
|
|
375
|
-
dict
|
|
376
|
-
{
|
|
377
|
-
'magnetizations': ndarray, shape (n_elements, 3)
|
|
378
|
-
Final magnetization vectors for each element
|
|
379
|
-
'H_fields': ndarray, shape (n_elements, 3)
|
|
380
|
-
H field at each element centroid
|
|
381
|
-
'iterations': int
|
|
382
|
-
Number of iterations performed
|
|
383
|
-
'converged': bool
|
|
384
|
-
Whether solution converged
|
|
385
|
-
'mesh_data': dict
|
|
386
|
-
Surface extraction data
|
|
387
|
-
'final_residual': float
|
|
388
|
-
Final relative residual
|
|
389
|
-
}
|
|
390
|
-
"""
|
|
391
|
-
mu_0 = 4.0 * np.pi * 1e-7
|
|
392
|
-
|
|
393
|
-
# Convert background B to H
|
|
394
|
-
B0 = np.array(background_B)
|
|
395
|
-
H0 = B0 / mu_0
|
|
396
|
-
|
|
397
|
-
if verbose:
|
|
398
|
-
print("=" * 60)
|
|
399
|
-
print("Volume-based Tetrahedral Mesh Solver")
|
|
400
|
-
print("=" * 60)
|
|
401
|
-
print(f"Parameters:")
|
|
402
|
-
print(f" chi = {chi} (mu_r = {chi + 1})")
|
|
403
|
-
print(f" Background H0 = {H0} A/m")
|
|
404
|
-
print(f" Precision = {precision}")
|
|
405
|
-
print(f" Max iterations = {max_iter}")
|
|
406
|
-
print()
|
|
407
|
-
|
|
408
|
-
# Extract surface faces
|
|
409
|
-
mesh_data = extract_surface_faces(mesh, material_filter=material_filter, verbose=verbose)
|
|
410
|
-
elements = mesh_data['elements']
|
|
411
|
-
faces = mesh_data['surface_faces']
|
|
412
|
-
n_elements = len(elements)
|
|
413
|
-
|
|
414
|
-
if n_elements == 0:
|
|
415
|
-
raise ValueError("No elements found in mesh")
|
|
416
|
-
|
|
417
|
-
if verbose:
|
|
418
|
-
print()
|
|
419
|
-
|
|
420
|
-
# Compute interaction matrix
|
|
421
|
-
A = compute_interaction_matrix(mesh_data, verbose=verbose)
|
|
422
|
-
|
|
423
|
-
if verbose:
|
|
424
|
-
print()
|
|
425
|
-
print("[Relaxation Solver]")
|
|
426
|
-
print(f" Starting iteration...")
|
|
427
|
-
|
|
428
|
-
# Initialize magnetizations to chi * H0
|
|
429
|
-
M = np.zeros((n_elements, 3))
|
|
430
|
-
for i in range(n_elements):
|
|
431
|
-
M[i] = chi * H0
|
|
432
|
-
|
|
433
|
-
# External field array (same for all elements)
|
|
434
|
-
H_ext = np.tile(H0, n_elements) # Shape: (3*n_elements,)
|
|
435
|
-
|
|
436
|
-
# Relaxation iteration
|
|
437
|
-
converged = False
|
|
438
|
-
final_residual = np.inf
|
|
439
|
-
|
|
440
|
-
for iteration in range(max_iter):
|
|
441
|
-
# Flatten M for matrix multiplication
|
|
442
|
-
M_flat = M.flatten()
|
|
443
|
-
|
|
444
|
-
# Compute H_total = A @ M + H_ext
|
|
445
|
-
H_demag = A @ M_flat
|
|
446
|
-
H_total = H_demag + H_ext
|
|
447
|
-
|
|
448
|
-
# Compute new M from material relation: M_new = chi * H_total
|
|
449
|
-
M_new_flat = chi * H_total
|
|
450
|
-
M_new = M_new_flat.reshape((n_elements, 3))
|
|
451
|
-
|
|
452
|
-
# Apply relaxation
|
|
453
|
-
M_update = (1.0 - relax_param) * M + relax_param * M_new
|
|
454
|
-
|
|
455
|
-
# Check convergence
|
|
456
|
-
dM = M_update - M
|
|
457
|
-
residual = np.sqrt(np.sum(dM**2) / n_elements)
|
|
458
|
-
M_norm = np.sqrt(np.sum(M**2) / n_elements)
|
|
459
|
-
|
|
460
|
-
if M_norm > 1e-15:
|
|
461
|
-
rel_residual = residual / M_norm
|
|
462
|
-
else:
|
|
463
|
-
rel_residual = residual
|
|
464
|
-
|
|
465
|
-
M = M_update
|
|
466
|
-
final_residual = rel_residual
|
|
467
|
-
|
|
468
|
-
if verbose and (iteration + 1) % 100 == 0:
|
|
469
|
-
print(f" Iter {iteration+1}: residual = {rel_residual:.6e}")
|
|
470
|
-
|
|
471
|
-
if rel_residual < precision:
|
|
472
|
-
converged = True
|
|
473
|
-
if verbose:
|
|
474
|
-
print(f" Converged at iteration {iteration+1}")
|
|
475
|
-
break
|
|
476
|
-
|
|
477
|
-
if not converged and verbose:
|
|
478
|
-
print(f" [WARNING] Did not converge after {max_iter} iterations")
|
|
479
|
-
print(f" Final residual: {final_residual:.6e}")
|
|
480
|
-
|
|
481
|
-
# Compute final H fields
|
|
482
|
-
H_fields = np.zeros((n_elements, 3))
|
|
483
|
-
M_flat = M.flatten()
|
|
484
|
-
H_demag = A @ M_flat
|
|
485
|
-
for i in range(n_elements):
|
|
486
|
-
H_fields[i] = H_demag[3*i:3*i+3] + H0
|
|
487
|
-
|
|
488
|
-
if verbose:
|
|
489
|
-
print()
|
|
490
|
-
print("[Results]")
|
|
491
|
-
# Average M and H
|
|
492
|
-
M_avg = np.mean(M, axis=0)
|
|
493
|
-
H_avg = np.mean(H_fields, axis=0)
|
|
494
|
-
print(f" Average M: ({M_avg[0]:.6f}, {M_avg[1]:.6f}, {M_avg[2]:.6f}) A/m")
|
|
495
|
-
print(f" Average H: ({H_avg[0]:.6f}, {H_avg[1]:.6f}, {H_avg[2]:.6f}) A/m")
|
|
496
|
-
print(f" H at center element: ({H_fields[0][0]:.6f}, {H_fields[0][1]:.6f}, {H_fields[0][2]:.6f}) A/m")
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
'magnetizations': M,
|
|
500
|
-
'H_fields': H_fields,
|
|
501
|
-
'iterations': iteration + 1,
|
|
502
|
-
'converged': converged,
|
|
503
|
-
'mesh_data': mesh_data,
|
|
504
|
-
'final_residual': final_residual,
|
|
505
|
-
'elements': elements
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def evaluate_field(result, point, include_background=True):
|
|
510
|
-
"""
|
|
511
|
-
Evaluate H field at arbitrary point using solved magnetization.
|
|
512
|
-
|
|
513
|
-
Parameters
|
|
514
|
-
----------
|
|
515
|
-
result : dict
|
|
516
|
-
Output from solve_linear_material()
|
|
517
|
-
point : array-like
|
|
518
|
-
Evaluation point [x, y, z]
|
|
519
|
-
include_background : bool, default=True
|
|
520
|
-
Whether to include background field in result
|
|
521
|
-
|
|
522
|
-
Returns
|
|
523
|
-
-------
|
|
524
|
-
ndarray
|
|
525
|
-
H field vector [Hx, Hy, Hz] in A/m
|
|
526
|
-
"""
|
|
527
|
-
mesh_data = result['mesh_data']
|
|
528
|
-
faces = mesh_data['surface_faces']
|
|
529
|
-
M = result['magnetizations']
|
|
530
|
-
|
|
531
|
-
H = compute_h_field_from_surface_charge(point, faces, M, None)
|
|
532
|
-
|
|
533
|
-
# Background field is implicit in the solved M, but for external evaluation
|
|
534
|
-
# we need to add it back if requested
|
|
535
|
-
# Actually, the H_fields already include background, so we don't need to do anything
|
|
536
|
-
|
|
537
|
-
return H
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def create_radia_container(result, apply_magnetization=True):
|
|
541
|
-
"""
|
|
542
|
-
Create Radia container from solved result for field evaluation.
|
|
543
|
-
|
|
544
|
-
Parameters
|
|
545
|
-
----------
|
|
546
|
-
result : dict
|
|
547
|
-
Output from solve_linear_material()
|
|
548
|
-
apply_magnetization : bool, default=True
|
|
549
|
-
If True, apply solved magnetizations to Radia objects
|
|
550
|
-
|
|
551
|
-
Returns
|
|
552
|
-
-------
|
|
553
|
-
int
|
|
554
|
-
Radia container object ID
|
|
555
|
-
"""
|
|
556
|
-
from netgen_mesh_import import TETRA_FACES
|
|
557
|
-
|
|
558
|
-
elements = result['elements']
|
|
559
|
-
M = result['magnetizations']
|
|
560
|
-
|
|
561
|
-
obj_ids = []
|
|
562
|
-
for el_idx, el in enumerate(elements):
|
|
563
|
-
vertices = el['vertices']
|
|
564
|
-
mag = M[el_idx].tolist() if apply_magnetization else [0, 0, 0]
|
|
565
|
-
|
|
566
|
-
obj_id = rad.ObjPolyhdr(vertices, TETRA_FACES, mag)
|
|
567
|
-
obj_ids.append(obj_id)
|
|
568
|
-
|
|
569
|
-
container = rad.ObjCnt(obj_ids)
|
|
570
|
-
return container
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
# Convenience function for common use case
|
|
574
|
-
def solve_cube_in_field(cube_size, chi, H0_z, mesh_size=None, **kwargs):
|
|
575
|
-
"""
|
|
576
|
-
Convenience function to solve a cube of linear material in uniform H field.
|
|
577
|
-
|
|
578
|
-
Parameters
|
|
579
|
-
----------
|
|
580
|
-
cube_size : float
|
|
581
|
-
Full size of cube (centered at origin)
|
|
582
|
-
chi : float
|
|
583
|
-
Magnetic susceptibility (chi = mu_r - 1)
|
|
584
|
-
H0_z : float
|
|
585
|
-
z-component of background H field in A/m
|
|
586
|
-
mesh_size : float, optional
|
|
587
|
-
Maximum element size. Default: cube_size / 5
|
|
588
|
-
**kwargs
|
|
589
|
-
Additional arguments passed to solve_linear_material()
|
|
590
|
-
|
|
591
|
-
Returns
|
|
592
|
-
-------
|
|
593
|
-
dict
|
|
594
|
-
Result from solve_linear_material()
|
|
595
|
-
"""
|
|
596
|
-
from ngsolve import Mesh
|
|
597
|
-
from netgen.occ import Box, Pnt, OCCGeometry
|
|
598
|
-
|
|
599
|
-
half = cube_size / 2.0
|
|
600
|
-
if mesh_size is None:
|
|
601
|
-
mesh_size = cube_size / 5.0
|
|
602
|
-
|
|
603
|
-
mu_0 = 4.0 * np.pi * 1e-7
|
|
604
|
-
B0 = [0, 0, mu_0 * H0_z]
|
|
605
|
-
|
|
606
|
-
cube_geo = Box(Pnt(-half, -half, -half), Pnt(half, half, half))
|
|
607
|
-
geo = OCCGeometry(cube_geo)
|
|
608
|
-
ngmesh = geo.GenerateMesh(maxh=mesh_size)
|
|
609
|
-
mesh = Mesh(ngmesh)
|
|
610
|
-
|
|
611
|
-
return solve_linear_material(mesh, chi, B0, **kwargs)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if __name__ == "__main__":
|
|
615
|
-
# Test with cube in uniform field (same as NGSolve reference)
|
|
616
|
-
print("Testing volume-based tetrahedral solver")
|
|
617
|
-
print("=" * 60)
|
|
618
|
-
print()
|
|
619
|
-
|
|
620
|
-
# Parameters matching NGSolve reference
|
|
621
|
-
cube_size = 1.0 # 1m cube
|
|
622
|
-
mu_r = 100
|
|
623
|
-
chi = mu_r - 1.0
|
|
624
|
-
H0 = 1.0 # A/m
|
|
625
|
-
|
|
626
|
-
rad.FldUnits('m')
|
|
627
|
-
|
|
628
|
-
result = solve_cube_in_field(
|
|
629
|
-
cube_size=cube_size,
|
|
630
|
-
chi=chi,
|
|
631
|
-
H0_z=H0,
|
|
632
|
-
mesh_size=0.25,
|
|
633
|
-
precision=0.0001,
|
|
634
|
-
max_iter=1000,
|
|
635
|
-
relax_param=0.3 # Slower but more stable
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
print()
|
|
639
|
-
print("=" * 60)
|
|
640
|
-
print("Comparison with NGSolve reference:")
|
|
641
|
-
print("=" * 60)
|
|
642
|
-
print(f"NGSolve Hz_total at origin: 0.034702 A/m")
|
|
643
|
-
|
|
644
|
-
# Find element closest to origin
|
|
645
|
-
min_dist = np.inf
|
|
646
|
-
center_el_idx = 0
|
|
647
|
-
for i, el in enumerate(result['elements']):
|
|
648
|
-
dist = np.linalg.norm(el['centroid'])
|
|
649
|
-
if dist < min_dist:
|
|
650
|
-
min_dist = dist
|
|
651
|
-
center_el_idx = i
|
|
652
|
-
|
|
653
|
-
Hz_center = result['H_fields'][center_el_idx][2]
|
|
654
|
-
print(f"Our Hz at center element: {Hz_center:.6f} A/m")
|
|
655
|
-
print(f"Difference: {abs(Hz_center - 0.034702):.6f} A/m")
|
|
656
|
-
if Hz_center > 1e-6:
|
|
657
|
-
print(f"Relative error: {abs(Hz_center - 0.034702) / 0.034702 * 100:.2f}%")
|
radia-1.3.5.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
radia/__init__.py,sha256=edxD1jM_-2LddwoxqtgATa_vTaruXk5WZsntpFJ9qtM,47
|
|
2
|
-
radia/nastran_mesh_import.py,sha256=CSoVhZCXa85lPiTF2hlspE2clBKOD7-_sCp1bxu_IK0,18147
|
|
3
|
-
radia/netgen_mesh_import.py,sha256=UopXk-5bbfj1j9_hyiq8jbjb4SQXnWaeuaC7TDf17wA,19872
|
|
4
|
-
radia/rad_ngsolve.pyd,sha256=s5ZvpTIrjYeV_B3vQv5HrtP4u_RaPhvxuZf5aKv3oC0,566272
|
|
5
|
-
radia/rad_ngsolve_fast.py,sha256=GkC7ruKy3MESHsO0iRSWsrgLU4-DPPgctOi6pPpsWg4,5675
|
|
6
|
-
radia/radia_coil_builder.py,sha256=nQkiAbfhueNvvxUARHdPD0C68ImidHmUQv_q4RsImeY,11253
|
|
7
|
-
radia/radia_field_cached.py,sha256=Bjw3ecNe3u7AAXnLob5m_tjYIY7HwB9DpgFg9a-aP_8,9509
|
|
8
|
-
radia/radia_ngsolve.pyd,sha256=s5ZvpTIrjYeV_B3vQv5HrtP4u_RaPhvxuZf5aKv3oC0,566272
|
|
9
|
-
radia/radia_ngsolve_field.py,sha256=suJr4wacfYFKOkyV-5AQuHWnW5rtUMb0gSSjq8VRSXc,10166
|
|
10
|
-
radia/radia_ngsolve_utils.py,sha256=xZCR9DOIKMwdEjmC28rOXVZiWFY5BQYH2VfopfuVBps,8406
|
|
11
|
-
radia/radia_pyvista_viewer.py,sha256=JS33Mx4azGI7hUX0bzefc6zJfhv6qfRjM3Kl1bE9Mjs,4275
|
|
12
|
-
radia/radia_tetra_mesh.py,sha256=nEpNpOBdJ-0DjS7WUx_bRHskrDKSKH3rSEWr-CwyQRM,21147
|
|
13
|
-
radia/radia_vtk_export.py,sha256=I8Vyyt9tky78Qw1xPru9f0Rii6QEmdEgTFjQtamyooc,6540
|
|
14
|
-
radia-1.3.5.dist-info/licenses/LICENSE,sha256=yaWxyzG9DpJ44dDNdGni4nukwiZ8pU-r_aW-1tYNAjk,4374
|
|
15
|
-
radia-1.3.5.dist-info/METADATA,sha256=LuqDACqJI2J9ucBkZe1UQFfh77ZuYnZvZIwI9JwRpX0,13202
|
|
16
|
-
radia-1.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
-
radia-1.3.5.dist-info/top_level.txt,sha256=kNDonE5X3Q2xnmOsWleQnGKQobuFsKM6Px5_Ta1I6x8,6
|
|
18
|
-
radia-1.3.5.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
radia
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|