radia 1.0.8__py3-none-any.whl → 1.0.10__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.10.dist-info}/METADATA +2 -2
- radia-1.0.10.dist-info/RECORD +13 -0
- {radia-1.0.8.dist-info → radia-1.0.10.dist-info}/licenses/LICENSE +564 -564
- radia-1.0.10.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.10.dist-info}/WHEEL +0 -0
python/__init__.py
ADDED
python/nastran_reader.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Nastran Mesh Reader for Radia
|
|
5
|
+
|
|
6
|
+
Reads Nastran .nas/.bdf files and converts to Python data structures.
|
|
7
|
+
Supports GRID (nodes), CHEXA (hexahedron), CPENTA (pentahedron), CTETRA (tetrahedron), and CTRIA3 (triangle) elements.
|
|
8
|
+
|
|
9
|
+
For surface meshes (CTRIA3), triangles with the same material ID (PID) are grouped into a single polyhedron,
|
|
10
|
+
which is useful for linear magnetic analysis where only the surface representation is needed.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from nastran_reader import read_nastran_mesh
|
|
14
|
+
|
|
15
|
+
mesh = read_nastran_mesh('sphere.bdf')
|
|
16
|
+
nodes = mesh['nodes'] # numpy array (N, 3)
|
|
17
|
+
hex_elements = mesh['hex_elements'] # list of [n1, ..., n8]
|
|
18
|
+
penta_elements = mesh['penta_elements'] # list of [n1, ..., n6]
|
|
19
|
+
tetra_elements = mesh['tetra_elements'] # list of [n1, ..., n4]
|
|
20
|
+
tria_groups = mesh['tria_groups'] # dict {material_id: {'faces': [[n1,n2,n3], ...], 'nodes': set(...)}}
|
|
21
|
+
|
|
22
|
+
Date: 2025-11-01
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_nastran_mesh(filename, verbose=True):
|
|
30
|
+
"""
|
|
31
|
+
Read Nastran mesh file (.nas/.bdf format).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
filename: Path to .nas or .bdf file
|
|
35
|
+
verbose: Print progress messages (default: True)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
dict: Dictionary with mesh data
|
|
39
|
+
- nodes: numpy array (N, 3) with node coordinates [mm]
|
|
40
|
+
- hex_elements: list of hexahedra [n1, n2, n3, n4, n5, n6, n7, n8]
|
|
41
|
+
- penta_elements: list of pentahedra [n1, n2, n3, n4, n5, n6]
|
|
42
|
+
- tetra_elements: list of tetrahedra [n1, n2, n3, n4]
|
|
43
|
+
- tria_groups: dict {material_id: {'faces': [[n1,n2,n3], ...], 'nodes': set(node_ids)}}
|
|
44
|
+
- node_ids: dict mapping node_id to array index
|
|
45
|
+
- node_id_list: list of node IDs in order
|
|
46
|
+
"""
|
|
47
|
+
if verbose:
|
|
48
|
+
print(f"Reading Nastran mesh: {filename}")
|
|
49
|
+
|
|
50
|
+
with open(filename, 'r') as f:
|
|
51
|
+
lines = f.readlines()
|
|
52
|
+
|
|
53
|
+
# Parse GRID entries
|
|
54
|
+
nodes = {} # node_id -> [x, y, z]
|
|
55
|
+
hex_elements = [] # List of [n1, n2, n3, n4, n5, n6, n7, n8]
|
|
56
|
+
penta_elements = [] # List of [n1, n2, n3, n4, n5, n6]
|
|
57
|
+
tetra_elements = [] # List of [n1, n2, n3, n4]
|
|
58
|
+
tria_groups = {} # material_id -> {'faces': [[n1,n2,n3], ...], 'nodes': set(node_ids)}
|
|
59
|
+
|
|
60
|
+
i = 0
|
|
61
|
+
while i < len(lines):
|
|
62
|
+
line = lines[i].rstrip('\n')
|
|
63
|
+
|
|
64
|
+
# Parse GRID* lines (Extended format with continuation)
|
|
65
|
+
if line.startswith('GRID*'):
|
|
66
|
+
try:
|
|
67
|
+
# GRID* ID CP X1 X2
|
|
68
|
+
# * X3
|
|
69
|
+
# Field width: 16 characters
|
|
70
|
+
node_id = int(line[8:24].strip())
|
|
71
|
+
# Skip CP field (line[24:40])
|
|
72
|
+
x = float(line[40:56].strip())
|
|
73
|
+
y = float(line[56:72].strip())
|
|
74
|
+
|
|
75
|
+
# Get continuation line for Z coordinate
|
|
76
|
+
i += 1
|
|
77
|
+
cont_line = lines[i].rstrip('\n')
|
|
78
|
+
z = float(cont_line[8:24].strip())
|
|
79
|
+
|
|
80
|
+
nodes[node_id] = [x, y, z]
|
|
81
|
+
except (ValueError, IndexError) as e:
|
|
82
|
+
if verbose:
|
|
83
|
+
print(f"Warning: Failed to parse GRID* line {i}: {e}")
|
|
84
|
+
|
|
85
|
+
# Parse GRID lines (Fixed format: 8 characters per field)
|
|
86
|
+
elif line.startswith('GRID') and not line.startswith('GRID*'):
|
|
87
|
+
try:
|
|
88
|
+
# GRID ID CP X1 X2 X3
|
|
89
|
+
# 01234567890123456789012345678901234567890123456789
|
|
90
|
+
node_id = int(line[8:16])
|
|
91
|
+
x = float(line[24:32])
|
|
92
|
+
y = float(line[32:40])
|
|
93
|
+
z = float(line[40:48])
|
|
94
|
+
nodes[node_id] = [x, y, z]
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
if verbose:
|
|
97
|
+
print(f"Warning: Failed to parse GRID line {i+1}: {e}")
|
|
98
|
+
|
|
99
|
+
# Parse CHEXA lines (Hexahedron with continuation)
|
|
100
|
+
elif line.startswith('CHEXA'):
|
|
101
|
+
try:
|
|
102
|
+
# CHEXA EID PID G1 G2 G3 G4 G5 G6 +
|
|
103
|
+
# + G7 G8
|
|
104
|
+
# Get nodes from first line (6 nodes)
|
|
105
|
+
n1 = int(line[24:32].strip())
|
|
106
|
+
n2 = int(line[32:40].strip())
|
|
107
|
+
n3 = int(line[40:48].strip())
|
|
108
|
+
n4 = int(line[48:56].strip())
|
|
109
|
+
n5 = int(line[56:64].strip())
|
|
110
|
+
n6 = int(line[64:72].strip())
|
|
111
|
+
|
|
112
|
+
# Get continuation line
|
|
113
|
+
i += 1
|
|
114
|
+
cont_line = lines[i].rstrip('\n')
|
|
115
|
+
# + G7 G8
|
|
116
|
+
n7 = int(cont_line[14:22].strip())
|
|
117
|
+
n8 = int(cont_line[22:30].strip())
|
|
118
|
+
|
|
119
|
+
hex_elements.append([n1, n2, n3, n4, n5, n6, n7, n8])
|
|
120
|
+
except (ValueError, IndexError) as e:
|
|
121
|
+
if verbose:
|
|
122
|
+
print(f"Warning: Failed to parse CHEXA at line {i}: {e}")
|
|
123
|
+
|
|
124
|
+
# Parse CPENTA lines (Pentahedron, single line)
|
|
125
|
+
elif line.startswith('CPENTA'):
|
|
126
|
+
try:
|
|
127
|
+
# CPENTA EID PID G1 G2 G3 G4 G5 G6
|
|
128
|
+
# All 6 nodes on one line
|
|
129
|
+
n1 = int(line[24:32])
|
|
130
|
+
n2 = int(line[32:40])
|
|
131
|
+
n3 = int(line[40:48])
|
|
132
|
+
n4 = int(line[48:56])
|
|
133
|
+
n5 = int(line[56:64])
|
|
134
|
+
n6 = int(line[64:72])
|
|
135
|
+
|
|
136
|
+
penta_elements.append([n1, n2, n3, n4, n5, n6])
|
|
137
|
+
except (ValueError, IndexError) as e:
|
|
138
|
+
if verbose:
|
|
139
|
+
print(f"Warning: Failed to parse CPENTA at line {i}: {e}")
|
|
140
|
+
|
|
141
|
+
# Parse CTETRA lines (Tetrahedron, single line)
|
|
142
|
+
elif line.startswith('CTETRA'):
|
|
143
|
+
try:
|
|
144
|
+
# CTETRA EID PID G1 G2 G3 G4
|
|
145
|
+
# All 4 nodes on one line (10-node tetra has continuation, but we only use first 4)
|
|
146
|
+
n1 = int(line[24:32])
|
|
147
|
+
n2 = int(line[32:40])
|
|
148
|
+
n3 = int(line[40:48])
|
|
149
|
+
n4 = int(line[48:56])
|
|
150
|
+
|
|
151
|
+
tetra_elements.append([n1, n2, n3, n4])
|
|
152
|
+
except (ValueError, IndexError) as e:
|
|
153
|
+
if verbose:
|
|
154
|
+
print(f"Warning: Failed to parse CTETRA at line {i}: {e}")
|
|
155
|
+
|
|
156
|
+
# Parse CTRIA3 lines (Triangle, single line)
|
|
157
|
+
elif line.startswith('CTRIA3'):
|
|
158
|
+
try:
|
|
159
|
+
# CTRIA3 EID PID G1 G2 G3
|
|
160
|
+
# Element ID, Property ID (material), and 3 node IDs
|
|
161
|
+
element_id = int(line[8:16].strip())
|
|
162
|
+
material_id = int(line[16:24].strip())
|
|
163
|
+
n1 = int(line[24:32].strip())
|
|
164
|
+
n2 = int(line[32:40].strip())
|
|
165
|
+
n3 = int(line[40:48].strip())
|
|
166
|
+
|
|
167
|
+
# Group triangles by material ID
|
|
168
|
+
if material_id not in tria_groups:
|
|
169
|
+
tria_groups[material_id] = {'faces': [], 'nodes': set()}
|
|
170
|
+
|
|
171
|
+
tria_groups[material_id]['faces'].append([n1, n2, n3])
|
|
172
|
+
tria_groups[material_id]['nodes'].update([n1, n2, n3])
|
|
173
|
+
except (ValueError, IndexError) as e:
|
|
174
|
+
if verbose:
|
|
175
|
+
print(f"Warning: Failed to parse CTRIA3 at line {i}: {e}")
|
|
176
|
+
|
|
177
|
+
i += 1
|
|
178
|
+
|
|
179
|
+
# Convert to numpy arrays
|
|
180
|
+
node_ids = sorted(nodes.keys())
|
|
181
|
+
node_id_to_idx = {nid: idx for idx, nid in enumerate(node_ids)}
|
|
182
|
+
|
|
183
|
+
nodes_array = np.array([nodes[nid] for nid in node_ids])
|
|
184
|
+
hex_array = np.array(hex_elements, dtype=int) if hex_elements else np.array([])
|
|
185
|
+
penta_array = np.array(penta_elements, dtype=int) if penta_elements else np.array([])
|
|
186
|
+
tetra_array = np.array(tetra_elements, dtype=int) if tetra_elements else np.array([])
|
|
187
|
+
|
|
188
|
+
if verbose:
|
|
189
|
+
print(f" Nodes: {len(nodes_array)}")
|
|
190
|
+
print(f" Elements (CHEXA): {len(hex_elements)}")
|
|
191
|
+
print(f" Elements (CPENTA): {len(penta_elements)}")
|
|
192
|
+
print(f" Elements (CTETRA): {len(tetra_elements)}")
|
|
193
|
+
if tria_groups:
|
|
194
|
+
total_trias = sum(len(group['faces']) for group in tria_groups.values())
|
|
195
|
+
print(f" Elements (CTRIA3): {total_trias} triangles in {len(tria_groups)} material group(s)")
|
|
196
|
+
for mat_id, group in tria_groups.items():
|
|
197
|
+
print(f" Material {mat_id}: {len(group['faces'])} triangles, {len(group['nodes'])} unique nodes")
|
|
198
|
+
total_elements = len(hex_elements) + len(penta_elements) + len(tetra_elements)
|
|
199
|
+
if not tria_groups:
|
|
200
|
+
print(f" Total elements: {total_elements}")
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
'nodes': nodes_array,
|
|
204
|
+
'hex_elements': hex_array,
|
|
205
|
+
'penta_elements': penta_array,
|
|
206
|
+
'tetra_elements': tetra_array,
|
|
207
|
+
'tria_groups': tria_groups,
|
|
208
|
+
'node_ids': node_id_to_idx,
|
|
209
|
+
'node_id_list': node_ids
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Element face connectivity for Radia ObjPolyhdr
|
|
214
|
+
# Node numbering is 1-indexed (as used in connectivity arrays)
|
|
215
|
+
|
|
216
|
+
# Hexahedron face connectivity (1-indexed)
|
|
217
|
+
# Nastran CHEXA node numbering:
|
|
218
|
+
# Nodes: G1, G2, G3, G4 (bottom), G5, G6, G7, G8 (top)
|
|
219
|
+
HEX_FACES = [
|
|
220
|
+
[1, 2, 3, 4], # Bottom face
|
|
221
|
+
[5, 6, 7, 8], # Top face
|
|
222
|
+
[1, 2, 6, 5], # Side face 1
|
|
223
|
+
[2, 3, 7, 6], # Side face 2
|
|
224
|
+
[3, 4, 8, 7], # Side face 3
|
|
225
|
+
[4, 1, 5, 8], # Side face 4
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
# Pentahedron face connectivity (1-indexed)
|
|
229
|
+
# Nastran CPENTA node numbering:
|
|
230
|
+
# Nodes: G1, G2, G3 (bottom triangle), G4, G5, G6 (top triangle)
|
|
231
|
+
PENTA_FACES = [
|
|
232
|
+
[1, 2, 3], # Bottom triangle
|
|
233
|
+
[4, 5, 6], # Top triangle
|
|
234
|
+
[1, 2, 5, 4], # Side face 1 (quad)
|
|
235
|
+
[2, 3, 6, 5], # Side face 2 (quad)
|
|
236
|
+
[3, 1, 4, 6], # Side face 3 (quad)
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
# Tetrahedron face connectivity (1-indexed)
|
|
240
|
+
# Nastran CTETRA node numbering:
|
|
241
|
+
# Nodes: G1, G2, G3, G4
|
|
242
|
+
TETRA_FACES = [
|
|
243
|
+
[1, 2, 3], # Face 1
|
|
244
|
+
[1, 4, 2], # Face 2
|
|
245
|
+
[2, 4, 3], # Face 3
|
|
246
|
+
[3, 4, 1], # Face 4
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == '__main__':
|
|
251
|
+
"""Test the Nastran reader"""
|
|
252
|
+
import os
|
|
253
|
+
import sys
|
|
254
|
+
|
|
255
|
+
# Try to find a test .bdf file
|
|
256
|
+
test_files = [
|
|
257
|
+
'../../examples/electromagnet/York.bdf',
|
|
258
|
+
'../../examples/NGSolve_CoefficientFunction_to_Radia_BackgroundField/sphere.bdf',
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
nas_file = None
|
|
262
|
+
for f in test_files:
|
|
263
|
+
if os.path.exists(f):
|
|
264
|
+
nas_file = f
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
if nas_file:
|
|
268
|
+
mesh = read_nastran_mesh(nas_file)
|
|
269
|
+
|
|
270
|
+
print("\nMesh statistics:")
|
|
271
|
+
print(f" Total nodes: {len(mesh['nodes'])}")
|
|
272
|
+
print(f" Hexahedra (CHEXA): {len(mesh['hex_elements'])}")
|
|
273
|
+
print(f" Pentahedra (CPENTA): {len(mesh['penta_elements'])}")
|
|
274
|
+
print(f" Tetrahedra (CTETRA): {len(mesh['tetra_elements'])}")
|
|
275
|
+
print(f"\nFirst 5 nodes:")
|
|
276
|
+
for i in range(min(5, len(mesh['nodes']))):
|
|
277
|
+
print(f" Node {mesh['node_id_list'][i]}: {mesh['nodes'][i]}")
|
|
278
|
+
|
|
279
|
+
if len(mesh['hex_elements']) > 0:
|
|
280
|
+
print(f"\nFirst 3 hexahedra (node IDs):")
|
|
281
|
+
for i in range(min(3, len(mesh['hex_elements']))):
|
|
282
|
+
print(f" Hex {i+1}: {mesh['hex_elements'][i]}")
|
|
283
|
+
|
|
284
|
+
if len(mesh['penta_elements']) > 0:
|
|
285
|
+
print(f"\nFirst 3 pentahedra (node IDs):")
|
|
286
|
+
for i in range(min(3, len(mesh['penta_elements']))):
|
|
287
|
+
print(f" Penta {i+1}: {mesh['penta_elements'][i]}")
|
|
288
|
+
|
|
289
|
+
if len(mesh['tetra_elements']) > 0:
|
|
290
|
+
print(f"\nFirst 3 tetrahedra (node IDs):")
|
|
291
|
+
for i in range(min(3, len(mesh['tetra_elements']))):
|
|
292
|
+
print(f" Tetra {i+1}: {mesh['tetra_elements'][i]}")
|
|
293
|
+
else:
|
|
294
|
+
print(f"Error: No test .bdf files found")
|
|
295
|
+
print(f"Searched for: {test_files}")
|
python/rad_ngsolve.pyd
ADDED
|
Binary file
|
python/radia.pyd
ADDED
|
Binary file
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Radia Coil Builder - Elegant fluent interface for constructing complex coil geometries.
|
|
5
|
+
|
|
6
|
+
This module provides a modern object-oriented design for defining multi-segment
|
|
7
|
+
coil paths with automatic state tracking and seamless Radia integration.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from radia_coil_builder import CoilBuilder
|
|
11
|
+
>>>
|
|
12
|
+
>>> # Create a racetrack coil
|
|
13
|
+
>>> coil = (CoilBuilder(current=1000)
|
|
14
|
+
... .set_start([0, 0, 0])
|
|
15
|
+
... .set_cross_section(width=20, height=20)
|
|
16
|
+
... .add_straight(100)
|
|
17
|
+
... .add_arc(radius=50, arc_angle=180, tilt=90)
|
|
18
|
+
... .add_straight(100)
|
|
19
|
+
... .add_arc(radius=50, arc_angle=180, tilt=90)
|
|
20
|
+
... .to_radia())
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.spatial.transform import Rotation
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoilSegment(ABC):
|
|
29
|
+
"""
|
|
30
|
+
Abstract base class for coil segments.
|
|
31
|
+
|
|
32
|
+
All coil segments must implement end_pos and end_orientation properties
|
|
33
|
+
to enable automatic state tracking in the builder pattern.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, current, start_pos, orientation, width, height, tilt=0):
|
|
37
|
+
"""
|
|
38
|
+
Initialize coil segment.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
current (float): Current in Amperes
|
|
42
|
+
start_pos (array): Starting position [x, y, z] in mm
|
|
43
|
+
orientation (array): 3x3 orientation matrix (row vectors)
|
|
44
|
+
width (float): Cross-section width in mm
|
|
45
|
+
height (float): Cross-section height in mm
|
|
46
|
+
tilt (float): Tilt angle in degrees (not applied here, applied in subclass)
|
|
47
|
+
"""
|
|
48
|
+
self.current = current
|
|
49
|
+
self.start_pos = np.array(start_pos)
|
|
50
|
+
self.orientation = np.array(orientation)
|
|
51
|
+
self.width = width
|
|
52
|
+
self.height = height
|
|
53
|
+
|
|
54
|
+
# Extract Euler angles for Radia transformations
|
|
55
|
+
rot = Rotation.from_matrix(self.orientation)
|
|
56
|
+
self.euler_angles = rot.as_euler('ZXZ', degrees=True) * (-1)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def end_pos(self):
|
|
61
|
+
"""End position of the segment."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def end_orientation(self):
|
|
67
|
+
"""End orientation matrix of the segment."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def center(self):
|
|
72
|
+
"""Geometric center point (midpoint between start and end)."""
|
|
73
|
+
return (self.start_pos + self.end_pos) / 2
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def current_density(self):
|
|
77
|
+
"""Current density in A/mm²."""
|
|
78
|
+
return self.current / (self.width * self.height)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class StraightSegment(CoilSegment):
|
|
82
|
+
"""
|
|
83
|
+
Straight coil segment with optional tilt.
|
|
84
|
+
|
|
85
|
+
The tilt rotates the cross-section around the Y-axis before
|
|
86
|
+
extending in the local Y direction.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, current, start_pos, orientation, width, height, length, tilt=0):
|
|
90
|
+
"""
|
|
91
|
+
Initialize straight segment.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
current (float): Current in Amperes
|
|
95
|
+
start_pos (array): Starting position [x, y, z] in mm
|
|
96
|
+
orientation (array): 3x3 orientation matrix (row vectors)
|
|
97
|
+
width (float): Cross-section width in mm
|
|
98
|
+
height (float): Cross-section height in mm
|
|
99
|
+
length (float): Segment length in mm
|
|
100
|
+
tilt (float): Tilt angle in degrees (rotation around Y-axis)
|
|
101
|
+
"""
|
|
102
|
+
# Apply tilt transformation to orientation
|
|
103
|
+
tilt_rad = np.deg2rad(tilt)
|
|
104
|
+
tilt_matrix = np.array([
|
|
105
|
+
[np.cos(tilt_rad), 0, -np.sin(tilt_rad)],
|
|
106
|
+
[0, 1, 0],
|
|
107
|
+
[np.sin(tilt_rad), 0, np.cos(tilt_rad)]
|
|
108
|
+
])
|
|
109
|
+
tilted_orientation = tilt_matrix @ orientation
|
|
110
|
+
|
|
111
|
+
# Cross-section dimensions change with tilt
|
|
112
|
+
tilted_width = abs(np.cos(tilt_rad) * width + np.sin(tilt_rad) * height)
|
|
113
|
+
tilted_height = abs(-np.sin(tilt_rad) * width + np.cos(tilt_rad) * height)
|
|
114
|
+
|
|
115
|
+
super().__init__(current, start_pos, tilted_orientation, tilted_width, tilted_height, tilt)
|
|
116
|
+
self.length = length
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def end_pos(self):
|
|
120
|
+
"""End position: start + length * Y-direction."""
|
|
121
|
+
return self.start_pos + self.length * self.orientation[1, :]
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def end_orientation(self):
|
|
125
|
+
"""End orientation: same as start (no rotation)."""
|
|
126
|
+
return self.orientation
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ArcSegment(CoilSegment):
|
|
130
|
+
"""
|
|
131
|
+
Arc coil segment with optional tilt.
|
|
132
|
+
|
|
133
|
+
The arc rotates around a center point in the local XY plane.
|
|
134
|
+
Tilt is applied first, then the arc rotation.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, current, start_pos, orientation, width, height, radius, arc_angle, tilt=0):
|
|
138
|
+
"""
|
|
139
|
+
Initialize arc segment.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
current (float): Current in Amperes
|
|
143
|
+
start_pos (array): Starting position [x, y, z] in mm
|
|
144
|
+
orientation (array): 3x3 orientation matrix (row vectors)
|
|
145
|
+
width (float): Cross-section width in mm
|
|
146
|
+
height (float): Cross-section height in mm
|
|
147
|
+
radius (float): Arc radius in mm
|
|
148
|
+
arc_angle (float): Arc angle in degrees
|
|
149
|
+
tilt (float): Tilt angle in degrees (rotation around Y-axis)
|
|
150
|
+
"""
|
|
151
|
+
# Apply tilt transformation to orientation
|
|
152
|
+
tilt_rad = np.deg2rad(tilt)
|
|
153
|
+
tilt_matrix = np.array([
|
|
154
|
+
[np.cos(tilt_rad), 0, -np.sin(tilt_rad)],
|
|
155
|
+
[0, 1, 0],
|
|
156
|
+
[np.sin(tilt_rad), 0, np.cos(tilt_rad)]
|
|
157
|
+
])
|
|
158
|
+
tilted_orientation = tilt_matrix @ orientation
|
|
159
|
+
|
|
160
|
+
# Cross-section dimensions change with tilt
|
|
161
|
+
tilted_width = abs(np.cos(tilt_rad) * width + np.sin(tilt_rad) * height)
|
|
162
|
+
tilted_height = abs(-np.sin(tilt_rad) * width + np.cos(tilt_rad) * height)
|
|
163
|
+
|
|
164
|
+
super().__init__(current, start_pos, tilted_orientation, tilted_width, tilted_height, tilt)
|
|
165
|
+
self.radius = radius
|
|
166
|
+
self.arc_angle = arc_angle
|
|
167
|
+
|
|
168
|
+
# Arc center: start position minus radius in X-direction (row vector)
|
|
169
|
+
self.arc_center = self.start_pos - self.radius * self.orientation[0, :]
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def end_pos(self):
|
|
173
|
+
"""End position: arc_center + radius * rotated X-direction."""
|
|
174
|
+
phi_rad = np.deg2rad(self.arc_angle)
|
|
175
|
+
rotation_matrix = np.array([
|
|
176
|
+
[np.cos(phi_rad), np.sin(phi_rad), 0],
|
|
177
|
+
[-np.sin(phi_rad), np.cos(phi_rad), 0],
|
|
178
|
+
[0, 0, 1]
|
|
179
|
+
])
|
|
180
|
+
end_orientation = rotation_matrix @ self.orientation
|
|
181
|
+
return self.arc_center + self.radius * end_orientation[0, :]
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def end_orientation(self):
|
|
185
|
+
"""End orientation: rotated by arc_angle around Z-axis."""
|
|
186
|
+
phi_rad = np.deg2rad(self.arc_angle)
|
|
187
|
+
rotation_matrix = np.array([
|
|
188
|
+
[np.cos(phi_rad), np.sin(phi_rad), 0],
|
|
189
|
+
[-np.sin(phi_rad), np.cos(phi_rad), 0],
|
|
190
|
+
[0, 0, 1]
|
|
191
|
+
])
|
|
192
|
+
return rotation_matrix @ self.orientation
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class CoilBuilder:
|
|
196
|
+
"""
|
|
197
|
+
Fluent builder interface for creating multi-segment coil paths.
|
|
198
|
+
|
|
199
|
+
The builder maintains current state (position, orientation, cross-section)
|
|
200
|
+
and automatically updates it after each segment is added. This eliminates
|
|
201
|
+
manual state tracking and reduces boilerplate code by ~75%.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> builder = CoilBuilder(current=1265)
|
|
205
|
+
>>> coil_radia_objects = (builder
|
|
206
|
+
... .set_start([218, -16.4, -81])
|
|
207
|
+
... .set_cross_section(width=122, height=122)
|
|
208
|
+
... .add_straight(length=32.9, tilt=0)
|
|
209
|
+
... .add_arc(radius=121, arc_angle=64.6, tilt=90)
|
|
210
|
+
... .add_straight(length=1018.5, tilt=90)
|
|
211
|
+
... .to_radia())
|
|
212
|
+
>>>
|
|
213
|
+
>>> import radia as rad
|
|
214
|
+
>>> coils = rad.ObjCnt(coil_radia_objects)
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, current):
|
|
218
|
+
"""
|
|
219
|
+
Initialize coil builder.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
current (float): Current in Amperes (constant for all segments)
|
|
223
|
+
"""
|
|
224
|
+
self.current = current
|
|
225
|
+
self.segments = []
|
|
226
|
+
|
|
227
|
+
# Initial state (identity orientation at origin)
|
|
228
|
+
self._position = np.array([0.0, 0.0, 0.0])
|
|
229
|
+
self._orientation = np.eye(3)
|
|
230
|
+
self._width = 100.0
|
|
231
|
+
self._height = 100.0
|
|
232
|
+
|
|
233
|
+
def set_start(self, position, orientation=None):
|
|
234
|
+
"""
|
|
235
|
+
Set starting position and orientation.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
position (array): Starting position [x, y, z] in mm
|
|
239
|
+
orientation (array, optional): 3x3 orientation matrix (row vectors).
|
|
240
|
+
Defaults to identity (aligned with XYZ axes).
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
self (for method chaining)
|
|
244
|
+
"""
|
|
245
|
+
self._position = np.array(position)
|
|
246
|
+
if orientation is not None:
|
|
247
|
+
self._orientation = np.array(orientation)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def set_cross_section(self, width, height):
|
|
251
|
+
"""
|
|
252
|
+
Set cross-section dimensions for subsequent segments.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
width (float): Width in mm
|
|
256
|
+
height (float): Height in mm
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
self (for method chaining)
|
|
260
|
+
"""
|
|
261
|
+
self._width = width
|
|
262
|
+
self._height = height
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def add_straight(self, length, tilt=0):
|
|
266
|
+
"""
|
|
267
|
+
Add a straight segment.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
length (float): Length in mm
|
|
271
|
+
tilt (float): Tilt angle in degrees (rotation around Y-axis)
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
self (for method chaining)
|
|
275
|
+
"""
|
|
276
|
+
segment = StraightSegment(
|
|
277
|
+
self.current,
|
|
278
|
+
self._position,
|
|
279
|
+
self._orientation,
|
|
280
|
+
self._width,
|
|
281
|
+
self._height,
|
|
282
|
+
length,
|
|
283
|
+
tilt
|
|
284
|
+
)
|
|
285
|
+
self.segments.append(segment)
|
|
286
|
+
|
|
287
|
+
# Automatic state update
|
|
288
|
+
self._position = segment.end_pos
|
|
289
|
+
self._orientation = segment.end_orientation
|
|
290
|
+
self._width = segment.width
|
|
291
|
+
self._height = segment.height
|
|
292
|
+
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
def add_arc(self, radius, arc_angle, tilt=0):
|
|
296
|
+
"""
|
|
297
|
+
Add an arc segment.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
radius (float): Arc radius in mm
|
|
301
|
+
arc_angle (float): Arc angle in degrees
|
|
302
|
+
tilt (float): Tilt angle in degrees (rotation around Y-axis)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
self (for method chaining)
|
|
306
|
+
"""
|
|
307
|
+
segment = ArcSegment(
|
|
308
|
+
self.current,
|
|
309
|
+
self._position,
|
|
310
|
+
self._orientation,
|
|
311
|
+
self._width,
|
|
312
|
+
self._height,
|
|
313
|
+
radius,
|
|
314
|
+
arc_angle,
|
|
315
|
+
tilt
|
|
316
|
+
)
|
|
317
|
+
self.segments.append(segment)
|
|
318
|
+
|
|
319
|
+
# Automatic state update
|
|
320
|
+
self._position = segment.end_pos
|
|
321
|
+
self._orientation = segment.end_orientation
|
|
322
|
+
self._width = segment.width
|
|
323
|
+
self._height = segment.height
|
|
324
|
+
|
|
325
|
+
return self
|
|
326
|
+
|
|
327
|
+
def to_radia(self):
|
|
328
|
+
"""
|
|
329
|
+
Convert all segments to Radia objects.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
list: List of Radia object IDs (can be combined with rad.ObjCnt)
|
|
333
|
+
"""
|
|
334
|
+
import radia as rad
|
|
335
|
+
|
|
336
|
+
radia_objects = []
|
|
337
|
+
for seg in self.segments:
|
|
338
|
+
if isinstance(seg, StraightSegment):
|
|
339
|
+
# Create straight current segment
|
|
340
|
+
J = [0, seg.current_density, 0] # Current density in Y-direction
|
|
341
|
+
coil = rad.ObjRecCur([0, 0, 0], [seg.width, seg.length, seg.height], J)
|
|
342
|
+
|
|
343
|
+
# Build transformation (ZXZ Euler angles + translation)
|
|
344
|
+
trf = rad.TrfRot([0, 0, 0], [0, 0, 1], np.deg2rad(seg.euler_angles[2]))
|
|
345
|
+
trf = rad.TrfCmbR(trf, rad.TrfRot([0, 0, 0], [1, 0, 0], np.deg2rad(seg.euler_angles[1])))
|
|
346
|
+
trf = rad.TrfCmbR(trf, rad.TrfRot([0, 0, 0], [0, 0, 1], np.deg2rad(seg.euler_angles[0])))
|
|
347
|
+
trf = rad.TrfCmbL(trf, rad.TrfTrsl(seg.center.tolist()))
|
|
348
|
+
|
|
349
|
+
radia_objects.append(rad.TrfOrnt(coil, trf))
|
|
350
|
+
|
|
351
|
+
elif isinstance(seg, ArcSegment):
|
|
352
|
+
# Create arc current segment
|
|
353
|
+
phi1 = np.deg2rad(seg.euler_angles[0])
|
|
354
|
+
if phi1 <= 0:
|
|
355
|
+
phi1 += 2 * np.pi
|
|
356
|
+
|
|
357
|
+
phi2 = np.deg2rad(seg.euler_angles[0] + seg.arc_angle)
|
|
358
|
+
if phi1 > phi2 or phi2 <= 0:
|
|
359
|
+
phi2 += 2 * np.pi
|
|
360
|
+
|
|
361
|
+
r_range = [seg.radius - seg.width / 2, seg.radius + seg.width / 2]
|
|
362
|
+
coil = rad.ObjArcCur(
|
|
363
|
+
[0, 0, 0],
|
|
364
|
+
r_range,
|
|
365
|
+
[phi1, phi2],
|
|
366
|
+
seg.height,
|
|
367
|
+
10, # Number of segments
|
|
368
|
+
seg.current_density,
|
|
369
|
+
"auto"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Build transformation (ZX Euler angles + translation to arc center)
|
|
373
|
+
trf = rad.TrfRot([0, 0, 0], [0, 0, 1], np.deg2rad(seg.euler_angles[2]))
|
|
374
|
+
trf = rad.TrfCmbR(trf, rad.TrfRot([0, 0, 0], [1, 0, 0], np.deg2rad(seg.euler_angles[1])))
|
|
375
|
+
trf = rad.TrfCmbL(trf, rad.TrfTrsl(seg.arc_center.tolist()))
|
|
376
|
+
|
|
377
|
+
radia_objects.append(rad.TrfOrnt(coil, trf))
|
|
378
|
+
|
|
379
|
+
return radia_objects
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# Export public API
|
|
383
|
+
__all__ = ['CoilBuilder', 'CoilSegment', 'StraightSegment', 'ArcSegment']
|