radia 1.0.8__py3-none-any.whl → 1.0.9__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/nastran_reader.py +295 -0
- python/rad_ngsolve.pyd +0 -0
- python/radia.pyd +0 -0
- python/radia_coil_builder.py +383 -0
- python/radia_ngsolve_field.py +374 -0
- python/radia_pyvista_viewer.py +172 -0
- python/radia_vtk_export.py +134 -0
- {radia-1.0.8.dist-info → radia-1.0.9.dist-info}/METADATA +1 -1
- radia-1.0.9.dist-info/RECORD +13 -0
- {radia-1.0.8.dist-info → radia-1.0.9.dist-info}/licenses/LICENSE +564 -564
- radia-1.0.9.dist-info/top_level.txt +1 -0
- radia-1.0.8.dist-info/RECORD +0 -5
- radia-1.0.8.dist-info/top_level.txt +0 -1
- {radia-1.0.8.dist-info → radia-1.0.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radia-NGSolve Field Integration
|
|
3
|
+
|
|
4
|
+
Provides arbitrary background field from NGSolve CoefficientFunction or analytical functions.
|
|
5
|
+
|
|
6
|
+
Date: 2025-10-31
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import radia as rad
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_cf_field_source(cf, unit='m'):
|
|
14
|
+
"""
|
|
15
|
+
Create Radia background field source from NGSolve CoefficientFunction.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
cf : ngsolve.CoefficientFunction or callable
|
|
20
|
+
NGSolve CF defining the field [Bx, By, Bz]
|
|
21
|
+
CF should be evaluable at (x, y, z) and return 3-vector
|
|
22
|
+
Can also be a Python function (x, y, z) -> [Bx, By, Bz]
|
|
23
|
+
unit : str, optional
|
|
24
|
+
Unit of CF coordinates: 'm' (meters) or 'mm' (millimeters)
|
|
25
|
+
Default: 'm' (NGSolve standard)
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
int
|
|
30
|
+
Radia object key for the field source
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> from ngsolve import CF, x, y, z
|
|
35
|
+
>>> # Create spatially varying field
|
|
36
|
+
>>> Bx = CF(0)
|
|
37
|
+
>>> By = CF(0)
|
|
38
|
+
>>> Bz = 1.0 + 0.01 * x # Linear gradient in x
|
|
39
|
+
>>> B_cf = CF((Bx, By, Bz))
|
|
40
|
+
>>>
|
|
41
|
+
>>> # Register as Radia background field
|
|
42
|
+
>>> field_obj = create_cf_field_source(B_cf, unit='m')
|
|
43
|
+
>>> magnet = rad.ObjRecMag([0,0,0], [5,5,5], [0,0,1])
|
|
44
|
+
>>> container = rad.ObjCnt([magnet, field_obj])
|
|
45
|
+
>>> rad.Solve(container, 0.0001, 10000)
|
|
46
|
+
|
|
47
|
+
>>> # Check total field
|
|
48
|
+
>>> B_total = rad.Fld(container, 'b', [0, 0, 0])
|
|
49
|
+
|
|
50
|
+
Notes
|
|
51
|
+
-----
|
|
52
|
+
- Radia uses millimeters internally
|
|
53
|
+
- NGSolve typically uses meters
|
|
54
|
+
- This function handles unit conversion automatically
|
|
55
|
+
- For best performance with NGSolve CF, evaluate on a mesh with specialcf
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Check if cf is NGSolve CF or Python callable
|
|
59
|
+
has_ngsolve = False
|
|
60
|
+
try:
|
|
61
|
+
import ngsolve
|
|
62
|
+
has_ngsolve = True
|
|
63
|
+
is_ngsolve_cf = isinstance(cf, ngsolve.CoefficientFunction)
|
|
64
|
+
except ImportError:
|
|
65
|
+
is_ngsolve_cf = False
|
|
66
|
+
|
|
67
|
+
if is_ngsolve_cf:
|
|
68
|
+
# NGSolve CoefficientFunction: need special handling
|
|
69
|
+
print("Warning: Direct NGSolve CF evaluation without mesh may be limited.")
|
|
70
|
+
print("For best results, use create_analytical_field() with a function that")
|
|
71
|
+
print("evaluates the CF on a GridFunction.")
|
|
72
|
+
|
|
73
|
+
# Create wrapper for NGSolve CF
|
|
74
|
+
def cf_wrapper(coords):
|
|
75
|
+
"""
|
|
76
|
+
Wrapper to evaluate NGSolve CF at given point.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
coords: [x, y, z] in mm (Radia units)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
[Bx, By, Bz] in Tesla
|
|
83
|
+
"""
|
|
84
|
+
# Convert Radia mm to NGSolve units
|
|
85
|
+
if unit == 'm':
|
|
86
|
+
x_ngs = coords[0] / 1000.0 # mm → m
|
|
87
|
+
y_ngs = coords[1] / 1000.0
|
|
88
|
+
z_ngs = coords[2] / 1000.0
|
|
89
|
+
else: # unit == 'mm'
|
|
90
|
+
x_ngs = coords[0]
|
|
91
|
+
y_ngs = coords[1]
|
|
92
|
+
z_ngs = coords[2]
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Try to evaluate CF
|
|
96
|
+
# Note: This may not work for all CFs without a mesh context
|
|
97
|
+
B_value = cf(ngsolve.mesh.MakeIntegrationPoint(x_ngs, y_ngs, z_ngs))
|
|
98
|
+
|
|
99
|
+
if hasattr(B_value, '__len__') and len(B_value) >= 3:
|
|
100
|
+
return [float(B_value[0]), float(B_value[1]), float(B_value[2])]
|
|
101
|
+
elif hasattr(B_value, '__len__'):
|
|
102
|
+
# Scalar or less than 3 components
|
|
103
|
+
return [0.0, 0.0, float(B_value[0]) if len(B_value) > 0 else 0.0]
|
|
104
|
+
else:
|
|
105
|
+
# Scalar CF
|
|
106
|
+
return [0.0, 0.0, float(B_value)]
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Warning: CF evaluation failed at ({x_ngs}, {y_ngs}, {z_ngs}): {e}")
|
|
110
|
+
print("Returning zero field. Consider using create_analytical_field() instead.")
|
|
111
|
+
return [0.0, 0.0, 0.0]
|
|
112
|
+
|
|
113
|
+
return rad.ObjBckgCF(cf_wrapper)
|
|
114
|
+
|
|
115
|
+
elif callable(cf):
|
|
116
|
+
# Python callable
|
|
117
|
+
def wrapper(coords):
|
|
118
|
+
"""
|
|
119
|
+
Wrapper for Python function with unit conversion.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
coords: [x, y, z] in mm (Radia units)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
[Bx, By, Bz] in Tesla
|
|
126
|
+
"""
|
|
127
|
+
if unit == 'm':
|
|
128
|
+
x_m = coords[0] / 1000.0
|
|
129
|
+
y_m = coords[1] / 1000.0
|
|
130
|
+
z_m = coords[2] / 1000.0
|
|
131
|
+
return cf(x_m, y_m, z_m)
|
|
132
|
+
else: # unit == 'mm'
|
|
133
|
+
return cf(coords[0], coords[1], coords[2])
|
|
134
|
+
|
|
135
|
+
return rad.ObjBckgCF(wrapper)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
raise TypeError("cf must be NGSolve CoefficientFunction or Python callable")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_analytical_field(field_func, unit='mm'):
|
|
142
|
+
"""
|
|
143
|
+
Create background field from analytical Python function.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
field_func : callable
|
|
148
|
+
Python function: [Bx, By, Bz] = field_func(x, y, z)
|
|
149
|
+
Coordinates in specified units
|
|
150
|
+
unit : str, optional
|
|
151
|
+
Unit of coordinates: 'm' or 'mm'
|
|
152
|
+
Default: 'mm' (Radia internal units)
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
int
|
|
157
|
+
Radia object key for the field source
|
|
158
|
+
|
|
159
|
+
Examples
|
|
160
|
+
--------
|
|
161
|
+
>>> # Quadrupole field
|
|
162
|
+
>>> def quadrupole_field(x, y, z):
|
|
163
|
+
... gradient = 10.0 # T/m
|
|
164
|
+
... Bx = gradient * y / 1000 # Convert mm to m for gradient
|
|
165
|
+
... By = gradient * x / 1000
|
|
166
|
+
... Bz = 0.0
|
|
167
|
+
... return [Bx, By, Bz]
|
|
168
|
+
>>>
|
|
169
|
+
>>> field_obj = create_analytical_field(quadrupole_field, unit='mm')
|
|
170
|
+
>>>
|
|
171
|
+
>>> # Use in Radia
|
|
172
|
+
>>> magnet = rad.ObjRecMag([0,0,0], [10,10,10], [0,0,1])
|
|
173
|
+
>>> container = rad.ObjCnt([magnet, field_obj])
|
|
174
|
+
>>> rad.Solve(container, 0.0001, 10000)
|
|
175
|
+
|
|
176
|
+
>>> # Linear gradient in z
|
|
177
|
+
>>> def gradient_field(x, y, z):
|
|
178
|
+
... B0 = 1.0 # T
|
|
179
|
+
... grad = 0.01 # T/mm
|
|
180
|
+
... return [0, 0, B0 + grad * z]
|
|
181
|
+
>>>
|
|
182
|
+
>>> field_obj = create_analytical_field(gradient_field, unit='mm')
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def wrapper(coords):
|
|
186
|
+
"""
|
|
187
|
+
Wrapper with unit conversion.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
coords: [x, y, z] in mm (Radia units)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
[Bx, By, Bz] in Tesla
|
|
194
|
+
"""
|
|
195
|
+
if unit == 'mm':
|
|
196
|
+
return field_func(coords[0], coords[1], coords[2])
|
|
197
|
+
else: # unit == 'm'
|
|
198
|
+
x_m = coords[0] / 1000.0
|
|
199
|
+
y_m = coords[1] / 1000.0
|
|
200
|
+
z_m = coords[2] / 1000.0
|
|
201
|
+
return field_func(x_m, y_m, z_m)
|
|
202
|
+
|
|
203
|
+
return rad.ObjBckgCF(wrapper)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_uniform_field(Bx, By, Bz):
|
|
207
|
+
"""
|
|
208
|
+
Create uniform background field (alternative to rad.ObjBckg).
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
Bx, By, Bz : float
|
|
213
|
+
Uniform field components in Tesla
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
int
|
|
218
|
+
Radia object key
|
|
219
|
+
|
|
220
|
+
Examples
|
|
221
|
+
--------
|
|
222
|
+
>>> # Equivalent to rad.ObjBckg([0, 0, 1.0])
|
|
223
|
+
>>> field_obj = create_uniform_field(0, 0, 1.0)
|
|
224
|
+
|
|
225
|
+
Notes
|
|
226
|
+
-----
|
|
227
|
+
This is provided for completeness. For uniform fields,
|
|
228
|
+
rad.ObjBckg() is more efficient.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def uniform(x, y, z):
|
|
232
|
+
return [Bx, By, Bz]
|
|
233
|
+
|
|
234
|
+
return create_analytical_field(uniform, unit='mm')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Predefined field templates
|
|
238
|
+
|
|
239
|
+
def dipole_field(moment, position=(0, 0, 0), unit='m'):
|
|
240
|
+
"""
|
|
241
|
+
Create magnetic dipole field.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
moment : array_like
|
|
246
|
+
Magnetic dipole moment [mx, my, mz] in A·m²
|
|
247
|
+
position : array_like, optional
|
|
248
|
+
Dipole position [x0, y0, z0] in specified units
|
|
249
|
+
Default: (0, 0, 0)
|
|
250
|
+
unit : str, optional
|
|
251
|
+
Unit of coordinates: 'm' or 'mm'
|
|
252
|
+
Default: 'm'
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
int
|
|
257
|
+
Radia object key
|
|
258
|
+
|
|
259
|
+
Examples
|
|
260
|
+
--------
|
|
261
|
+
>>> # Dipole at origin with moment in z
|
|
262
|
+
>>> field_obj = dipole_field([0, 0, 1.0], unit='m')
|
|
263
|
+
|
|
264
|
+
Notes
|
|
265
|
+
-----
|
|
266
|
+
B(r) = (μ₀/4π) * (3(m·r)r/r⁵ - m/r³)
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
m = np.array(moment, dtype=float)
|
|
270
|
+
r0 = np.array(position, dtype=float)
|
|
271
|
+
mu0_4pi = 1e-7 # T·m/A
|
|
272
|
+
|
|
273
|
+
def dipole(x, y, z):
|
|
274
|
+
# Position relative to dipole
|
|
275
|
+
if unit == 'm':
|
|
276
|
+
r = np.array([x - r0[0], y - r0[1], z - r0[2]])
|
|
277
|
+
else: # mm
|
|
278
|
+
r = np.array([(x - r0[0])/1000, (y - r0[1])/1000, (z - r0[2])/1000])
|
|
279
|
+
|
|
280
|
+
r_mag = np.linalg.norm(r)
|
|
281
|
+
|
|
282
|
+
if r_mag < 1e-10:
|
|
283
|
+
# Avoid singularity at dipole location
|
|
284
|
+
return [0.0, 0.0, 0.0]
|
|
285
|
+
|
|
286
|
+
r3 = r_mag**3
|
|
287
|
+
r5 = r_mag**5
|
|
288
|
+
|
|
289
|
+
m_dot_r = np.dot(m, r)
|
|
290
|
+
|
|
291
|
+
B = mu0_4pi * (3 * m_dot_r * r / r5 - m / r3)
|
|
292
|
+
|
|
293
|
+
return [float(B[0]), float(B[1]), float(B[2])]
|
|
294
|
+
|
|
295
|
+
return create_analytical_field(dipole, unit=unit)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def solenoid_field(I, turns_per_m, radius, length, center=(0, 0, 0), unit='m'):
|
|
299
|
+
"""
|
|
300
|
+
Create ideal solenoid field (uniform inside, zero outside).
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
I : float
|
|
305
|
+
Current in Amperes
|
|
306
|
+
turns_per_m : float
|
|
307
|
+
Number of turns per meter
|
|
308
|
+
radius : float
|
|
309
|
+
Solenoid radius in specified units
|
|
310
|
+
length : float
|
|
311
|
+
Solenoid length in specified units
|
|
312
|
+
center : array_like, optional
|
|
313
|
+
Solenoid center [x0, y0, z0]
|
|
314
|
+
unit : str, optional
|
|
315
|
+
Unit: 'm' or 'mm'
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
int
|
|
320
|
+
Radia object key
|
|
321
|
+
|
|
322
|
+
Examples
|
|
323
|
+
--------
|
|
324
|
+
>>> # 1000 A·turns/m solenoid, 0.1m radius, 1m long
|
|
325
|
+
>>> field_obj = solenoid_field(10.0, 100, 0.1, 1.0, unit='m')
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
mu0 = 4*np.pi*1e-7 # H/m
|
|
329
|
+
B_inside = mu0 * I * turns_per_m # T
|
|
330
|
+
|
|
331
|
+
c = np.array(center, dtype=float)
|
|
332
|
+
|
|
333
|
+
def solenoid(x, y, z):
|
|
334
|
+
# Check if inside solenoid
|
|
335
|
+
if unit == 'm':
|
|
336
|
+
r_perp = np.sqrt((x - c[0])**2 + (y - c[1])**2)
|
|
337
|
+
z_rel = z - c[2]
|
|
338
|
+
else: # mm
|
|
339
|
+
r_perp = np.sqrt((x - c[0])**2 + (y - c[1])**2) / 1000
|
|
340
|
+
z_rel = (z - c[2]) / 1000
|
|
341
|
+
radius_m = radius / 1000
|
|
342
|
+
length_m = length / 1000
|
|
343
|
+
|
|
344
|
+
if r_perp < radius and abs(z_rel) < length/2:
|
|
345
|
+
return [0.0, 0.0, B_inside]
|
|
346
|
+
else:
|
|
347
|
+
return [0.0, 0.0, 0.0]
|
|
348
|
+
|
|
349
|
+
return create_analytical_field(solenoid, unit=unit)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
# Test examples
|
|
354
|
+
print("Radia-NGSolve Field Integration Module")
|
|
355
|
+
print("=======================================")
|
|
356
|
+
print()
|
|
357
|
+
print("Example usage:")
|
|
358
|
+
print()
|
|
359
|
+
print("1. Uniform field:")
|
|
360
|
+
print(" field = create_uniform_field(0, 0, 1.0)")
|
|
361
|
+
print()
|
|
362
|
+
print("2. Quadrupole field:")
|
|
363
|
+
print(" def quad(x, y, z):")
|
|
364
|
+
print(" g = 10.0 # T/m")
|
|
365
|
+
print(" return [g*y, g*x, 0]")
|
|
366
|
+
print(" field = create_analytical_field(quad, unit='m')")
|
|
367
|
+
print()
|
|
368
|
+
print("3. Dipole field:")
|
|
369
|
+
print(" field = dipole_field([0, 0, 1.0], unit='m')")
|
|
370
|
+
print()
|
|
371
|
+
print("4. With NGSolve CF:")
|
|
372
|
+
print(" from ngsolve import CF, x, y, z")
|
|
373
|
+
print(" B_cf = CF((0, 0, 1 + 0.01*x))")
|
|
374
|
+
print(" field = create_cf_field_source(B_cf, unit='m')")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
PyVista-based viewer for Radia objects
|
|
4
|
+
This is a modern alternative to rad.ObjDrwOpenGL()
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys, os
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'dist'))
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'build', 'lib', 'Release'))
|
|
10
|
+
|
|
11
|
+
import radia as rad
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
def view_radia_object(obj, plotter=None, show=True, color=None, opacity=1.0):
|
|
15
|
+
"""
|
|
16
|
+
View a Radia object using PyVista
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
-----------
|
|
20
|
+
obj : int
|
|
21
|
+
Radia object ID
|
|
22
|
+
plotter : pyvista.Plotter, optional
|
|
23
|
+
Existing plotter to add to (for combining multiple objects)
|
|
24
|
+
show : bool, default True
|
|
25
|
+
Whether to show the plotter immediately
|
|
26
|
+
color : str or list, optional
|
|
27
|
+
Color override (e.g., 'red', [1,0,0])
|
|
28
|
+
opacity : float, default 1.0
|
|
29
|
+
Opacity (0=transparent, 1=opaque)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
--------
|
|
33
|
+
plotter : pyvista.Plotter
|
|
34
|
+
The plotter object (can be reused)
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
import pyvista as pv
|
|
38
|
+
except ImportError:
|
|
39
|
+
print("ERROR: PyVista is not installed.")
|
|
40
|
+
print("Install it with: pip install pyvista")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Get VTK data from Radia
|
|
44
|
+
vtk_data = rad.ObjDrwVTK(obj, 'Axes->False')
|
|
45
|
+
|
|
46
|
+
if 'polygons' not in vtk_data:
|
|
47
|
+
print("ERROR: No polygon data found in Radia object")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
poly = vtk_data['polygons']
|
|
51
|
+
|
|
52
|
+
# Extract vertices (flatten x,y,z triplets)
|
|
53
|
+
vertices = np.array(poly['vertices']).reshape(-1, 3)
|
|
54
|
+
|
|
55
|
+
# Extract polygon connectivity
|
|
56
|
+
lengths = poly['lengths']
|
|
57
|
+
n_polys = len(lengths)
|
|
58
|
+
|
|
59
|
+
# Build faces array for PyVista
|
|
60
|
+
# PyVista format: [n_points, p0, p1, p2, ..., n_points, p0, p1, ...]
|
|
61
|
+
faces = []
|
|
62
|
+
offset = 0
|
|
63
|
+
for length in lengths:
|
|
64
|
+
faces.append(length)
|
|
65
|
+
for i in range(length):
|
|
66
|
+
faces.append(offset + i)
|
|
67
|
+
offset += length
|
|
68
|
+
|
|
69
|
+
faces = np.array(faces)
|
|
70
|
+
|
|
71
|
+
# Create PyVista mesh
|
|
72
|
+
mesh = pv.PolyData(vertices, faces)
|
|
73
|
+
|
|
74
|
+
# Create or use existing plotter
|
|
75
|
+
if plotter is None:
|
|
76
|
+
plotter = pv.Plotter()
|
|
77
|
+
plotter.add_axes()
|
|
78
|
+
plotter.add_text("Radia Object Viewer (PyVista)", position='upper_left', font_size=10)
|
|
79
|
+
|
|
80
|
+
# Determine color
|
|
81
|
+
if color is None:
|
|
82
|
+
# Use Radia's color if available
|
|
83
|
+
colors = poly.get('colors', None)
|
|
84
|
+
if colors is not None and len(colors) > 0:
|
|
85
|
+
# Use first color (all polygons typically same color)
|
|
86
|
+
rgb = colors[0:3]
|
|
87
|
+
# Radia uses -1 for default, convert to actual color
|
|
88
|
+
if rgb[0] < 0:
|
|
89
|
+
color = 'lightblue'
|
|
90
|
+
else:
|
|
91
|
+
color = rgb
|
|
92
|
+
|
|
93
|
+
# Add mesh to plotter
|
|
94
|
+
plotter.add_mesh(mesh, color=color, opacity=opacity, show_edges=True,
|
|
95
|
+
edge_color='black', line_width=0.5)
|
|
96
|
+
|
|
97
|
+
if show:
|
|
98
|
+
plotter.show()
|
|
99
|
+
|
|
100
|
+
return plotter
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def view_radia_vtk_file(filename):
|
|
104
|
+
"""
|
|
105
|
+
View a VTK file generated by exportGeometryToVTK
|
|
106
|
+
|
|
107
|
+
Parameters:
|
|
108
|
+
-----------
|
|
109
|
+
filename : str
|
|
110
|
+
Path to .vtk file
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
import pyvista as pv
|
|
114
|
+
except ImportError:
|
|
115
|
+
print("ERROR: PyVista is not installed.")
|
|
116
|
+
print("Install it with: pip install pyvista")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Read VTK file
|
|
120
|
+
mesh = pv.read(filename)
|
|
121
|
+
|
|
122
|
+
# Create plotter
|
|
123
|
+
plotter = pv.Plotter()
|
|
124
|
+
plotter.add_axes()
|
|
125
|
+
plotter.add_text(f"Viewing: {os.path.basename(filename)}",
|
|
126
|
+
position='upper_left', font_size=10)
|
|
127
|
+
|
|
128
|
+
# Add mesh
|
|
129
|
+
plotter.add_mesh(mesh, show_edges=True, edge_color='black', line_width=0.5)
|
|
130
|
+
|
|
131
|
+
# Show
|
|
132
|
+
plotter.show()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == '__main__':
|
|
136
|
+
print("="*60)
|
|
137
|
+
print("Radia PyVista Viewer Demo")
|
|
138
|
+
print("="*60)
|
|
139
|
+
|
|
140
|
+
# Check if PyVista is available
|
|
141
|
+
try:
|
|
142
|
+
import pyvista as pv
|
|
143
|
+
print(f"\nPyVista version: {pv.__version__}")
|
|
144
|
+
except ImportError:
|
|
145
|
+
print("\nERROR: PyVista is not installed.")
|
|
146
|
+
print("Install it with:")
|
|
147
|
+
print(" pip install pyvista")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
# Create a simple test object
|
|
151
|
+
print("\nCreating test objects...")
|
|
152
|
+
|
|
153
|
+
# Arc current
|
|
154
|
+
g1 = rad.ObjArcCur([0,0,0], [95, 105], [0, 2*np.pi], 20, 36, 1e6/20/10, "man")
|
|
155
|
+
|
|
156
|
+
# Rectangular magnet
|
|
157
|
+
g2 = rad.ObjRecMag([0,0,0], [30,30,10], [0,0,1])
|
|
158
|
+
|
|
159
|
+
# Container
|
|
160
|
+
g = rad.ObjCnt([g1, g2])
|
|
161
|
+
|
|
162
|
+
print("Objects created. Opening PyVista viewer...")
|
|
163
|
+
print("\nControls:")
|
|
164
|
+
print(" - Left click + drag: Rotate")
|
|
165
|
+
print(" - Right click + drag: Pan")
|
|
166
|
+
print(" - Scroll wheel: Zoom")
|
|
167
|
+
print(" - 'q': Quit")
|
|
168
|
+
|
|
169
|
+
# View the object
|
|
170
|
+
view_radia_object(g)
|
|
171
|
+
|
|
172
|
+
print("\nViewer closed.")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Radia VTK Export Utilities
|
|
5
|
+
|
|
6
|
+
Functions for exporting Radia geometry to VTK format for visualization
|
|
7
|
+
in ParaView and other VTK-compatible tools.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import radia as rad
|
|
11
|
+
import csv
|
|
12
|
+
from itertools import accumulate
|
|
13
|
+
|
|
14
|
+
def chunks(lst, n):
|
|
15
|
+
"""
|
|
16
|
+
Yield successive n-sized chunks from a list.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
lst: List to be chunked
|
|
20
|
+
n: Chunk size
|
|
21
|
+
|
|
22
|
+
Yields:
|
|
23
|
+
Chunks of size n from the input list
|
|
24
|
+
"""
|
|
25
|
+
for i in range(0, len(lst), n):
|
|
26
|
+
yield lst[i:i + n]
|
|
27
|
+
|
|
28
|
+
def exportGeometryToVTK(obj, fileName='radia_Geometry'):
|
|
29
|
+
"""
|
|
30
|
+
Export Radia object geometry to VTK Legacy format file.
|
|
31
|
+
|
|
32
|
+
Writes the geometry of a Radia object to a .vtk file for visualization
|
|
33
|
+
in ParaView. The format is VTK Legacy (ASCII), consisting of polygons only.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
obj: Radia object ID (integer)
|
|
37
|
+
fileName: Output filename without extension (default: 'radia_Geometry')
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
Creates fileName.vtk in the current directory
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> import radia as rad
|
|
44
|
+
>>> from radia_vtk_export import exportGeometryToVTK
|
|
45
|
+
>>> mag = rad.ObjRecMag([0,0,0], [10,10,10], [0,0,1])
|
|
46
|
+
>>> exportGeometryToVTK(mag, 'my_magnet')
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
vtkData = rad.ObjDrwVTK(obj, 'Axes->False')
|
|
50
|
+
|
|
51
|
+
lengths = vtkData['polygons']['lengths']
|
|
52
|
+
nPoly = len(lengths)
|
|
53
|
+
offsets = list(accumulate(lengths))
|
|
54
|
+
offsets.insert(0, 0) # prepend list with a zero
|
|
55
|
+
points = vtkData['polygons']['vertices']
|
|
56
|
+
nPnts = int(len(points)/3)
|
|
57
|
+
|
|
58
|
+
# format the points array to be floats rather than double
|
|
59
|
+
points = [round(num/1000.0, 8) for num in points]
|
|
60
|
+
# Note: Converted from mm (Radia units) to m
|
|
61
|
+
# define the connectivity list
|
|
62
|
+
conn = list(range(nPnts))
|
|
63
|
+
# define colours array
|
|
64
|
+
colors = vtkData['polygons']['colors']
|
|
65
|
+
|
|
66
|
+
# pre-process the output lists to have chunkLength items per line
|
|
67
|
+
chunkLength = 9 # this writes 9 numbers per line (9 is the number used in Paraview if data is saved as the VTK Legacy format)
|
|
68
|
+
offsets = list(chunks(offsets, chunkLength))
|
|
69
|
+
points = list(chunks(points, chunkLength))
|
|
70
|
+
conn = list(chunks(conn, chunkLength))
|
|
71
|
+
colors = list(chunks(colors, chunkLength))
|
|
72
|
+
|
|
73
|
+
# write the data to file
|
|
74
|
+
with open(fileName + ".vtk", "w", newline="") as f:
|
|
75
|
+
f.write('# vtk DataFile Version 5.1\n')
|
|
76
|
+
f.write('vtk output\nASCII\nDATASET POLYDATA\n')
|
|
77
|
+
f.write('POINTS ' + str(nPnts) + ' float\n')
|
|
78
|
+
writer = csv.writer(f, delimiter=" ")
|
|
79
|
+
writer.writerows(points)
|
|
80
|
+
f.write('\n')
|
|
81
|
+
f.write('POLYGONS ' + str(nPoly+1) + ' ' + str(nPnts) + '\n')
|
|
82
|
+
f.write('OFFSETS vtktypeint64\n')
|
|
83
|
+
writer.writerows(offsets)
|
|
84
|
+
f.write('CONNECTIVITY vtktypeint64\n')
|
|
85
|
+
writer.writerows(conn)
|
|
86
|
+
f.write('\n')
|
|
87
|
+
f.write('CELL_DATA ' + str(nPoly) + '\n')
|
|
88
|
+
f.write('COLOR_SCALARS Radia_colours 3\n')
|
|
89
|
+
writer.writerows(colors)
|
|
90
|
+
|
|
91
|
+
print(f"VTK file exported: {fileName}.vtk")
|
|
92
|
+
print(f" Polygons: {nPoly}")
|
|
93
|
+
print(f" Points: {nPnts}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == '__main__':
|
|
97
|
+
"""
|
|
98
|
+
Demo: Export a simple Radia geometry to VTK format
|
|
99
|
+
"""
|
|
100
|
+
import sys
|
|
101
|
+
import os
|
|
102
|
+
|
|
103
|
+
# Add build directory to path
|
|
104
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'build', 'lib', 'Release'))
|
|
105
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'dist'))
|
|
106
|
+
|
|
107
|
+
print("=" * 60)
|
|
108
|
+
print("Radia VTK Export Demo")
|
|
109
|
+
print("=" * 60)
|
|
110
|
+
|
|
111
|
+
# Create a simple test geometry
|
|
112
|
+
print("\nCreating test geometry...")
|
|
113
|
+
|
|
114
|
+
# Rectangular magnet
|
|
115
|
+
mag = rad.ObjRecMag([0, 0, 0], [30, 30, 10], [0, 0, 1])
|
|
116
|
+
|
|
117
|
+
# Cylindrical magnet
|
|
118
|
+
cyl = rad.ObjCylMag([50, 0, 0], 15, 20, 16, 'z', [0, 0, 1])
|
|
119
|
+
|
|
120
|
+
# Container
|
|
121
|
+
container = rad.ObjCnt([mag, cyl])
|
|
122
|
+
|
|
123
|
+
# Export to VTK
|
|
124
|
+
output_file = 'radia_demo_geometry'
|
|
125
|
+
print(f"\nExporting geometry to {output_file}.vtk...")
|
|
126
|
+
exportGeometryToVTK(container, output_file)
|
|
127
|
+
|
|
128
|
+
print("\n" + "=" * 60)
|
|
129
|
+
print("Export complete!")
|
|
130
|
+
print("\nTo view in ParaView:")
|
|
131
|
+
print(f" 1. Open ParaView")
|
|
132
|
+
print(f" 2. File → Open → {output_file}.vtk")
|
|
133
|
+
print(f" 3. Click 'Apply' in the Properties panel")
|
|
134
|
+
print("=" * 60)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
python/__init__.py,sha256=o4JtQw0g5WcRpQAUi97FgL5BIUAlfMBrn4442GGdGuc,47
|
|
2
|
+
python/nastran_reader.py,sha256=r4K2LJvHBtvBumbinbiQeyPA42iyvxNyAd6Y05lxeZs,10035
|
|
3
|
+
python/rad_ngsolve.pyd,sha256=pfLmNoFQtTqS_Uzey4v3WWwD_KpqrOjuZeXuK1_8LEs,576512
|
|
4
|
+
python/radia.pyd,sha256=aSCAxppUbllAM0AJPOKvHxBQ_Iio61U0uiosZevirOk,1820672
|
|
5
|
+
python/radia_coil_builder.py,sha256=nQkiAbfhueNvvxUARHdPD0C68ImidHmUQv_q4RsImeY,11253
|
|
6
|
+
python/radia_ngsolve_field.py,sha256=suJr4wacfYFKOkyV-5AQuHWnW5rtUMb0gSSjq8VRSXc,10166
|
|
7
|
+
python/radia_pyvista_viewer.py,sha256=JS33Mx4azGI7hUX0bzefc6zJfhv6qfRjM3Kl1bE9Mjs,4275
|
|
8
|
+
python/radia_vtk_export.py,sha256=UbPvo7ftHYLREz6TSpWrpLw7JesFhMA58-22R63HHH4,3997
|
|
9
|
+
radia-1.0.9.dist-info/licenses/LICENSE,sha256=Db6_UKUk2lyGv0e4MMLNG74LJ-aep64N2FEYQ8eA9DU,29796
|
|
10
|
+
radia-1.0.9.dist-info/METADATA,sha256=iN0Z0S9aCOvMKldmXLbpDseSBciwPOarHz84E01fLgE,12323
|
|
11
|
+
radia-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
radia-1.0.9.dist-info/top_level.txt,sha256=J-z0poNcsv31IHB413--iOY8LoHBKiTHeybHX3abokI,7
|
|
13
|
+
radia-1.0.9.dist-info/RECORD,,
|