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 ADDED
@@ -0,0 +1,2 @@
1
+ # Radia Python package
2
+ __version__ = "1.0.9"
@@ -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']