-
Notifications
You must be signed in to change notification settings - Fork 12
/
biot_savart_v4_3.py
472 lines (367 loc) · 17.4 KB
/
biot_savart_v4_3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
'''
Biot-Savart Magnetic Field Calculator v4.3
Mingde Yin
Ryan Zazo
All lengths are in cm, B-field is in G
'''
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm
import matplotlib.ticker as ticker
'''
Feature Wishlist:
improve plot_coil with different colors for different values of current
accelerate integrator to use meshgrids directly instead of a layer of for loop
get parse_coil to use vectorized function instead of for loop
'''
def parse_coil(filename):
'''
Parses 4 column CSV into x,y,z,I slices for coil.
Each (x,y,z,I) entry defines a vertex on the coil.
The current I of the vertex, defines the amount of current running through the next segment of coil, in amperes.
i.e. (0, 0, 1, 2), (0, 1, 1, 3), (1, 1, 1, 4) means that:
- There are 2 amps of current running between points 1 and 2
- There are 3 amps of current running between points 2 and 3
- The last bit of current is functionally useless.
'''
with open(filename, "r") as f: return np.array([[eval(i) for i in line.split(",")] for line in f.read().splitlines()]).T
def slice_coil(coil, steplength):
'''
Slices a coil into pieces of size steplength.
If the coil is already sliced into pieces smaller than that, this does nothing.
'''
def interpolate_points(p1, p2, parts):
'''
Produces a series of linearly spaced points between two given points in R3+I
Linearly interpolates X,Y,Z; but keeps I the SAME
i.e. (0, 2, 1, 3), (3, 4, 2, 5), parts = 2:
(0, 2, 1, 3), (1.5, 3, 1.5, 3), (3, 4, 2, 5)
'''
return np.column_stack((np.linspace(p1[0], p2[0], parts+1), np.linspace(p1[1], p2[1], parts+1),
np.linspace(p1[2], p2[2], parts+1), p1[3] * np.ones((parts+1))))
newcoil = np.zeros((1, 4)) # fill column with dummy data, we will remove this later.
segment_starts = coil[:,:-1]
segment_ends = coil[:,1:]
# determine start and end of each segment
segments = segment_ends-segment_starts
segment_lengths = np.apply_along_axis(np.linalg.norm, 0, segments)
# create segments; determine start and end of each segment, as well as segment lengths
# chop up into smaller bits (elements)
stepnumbers = (segment_lengths/steplength).astype(int)
# determine how many steps we must chop each segment into
for i in range(segments.shape[1]):
newrows = interpolate_points(segment_starts[:,i], segment_ends[:,i], stepnumbers[i])
# set of new interpolated points to feed in
newcoil = np.vstack((newcoil, newrows))
if newcoil.shape[0] %2 != 0: newcoil = np.vstack((newcoil, newcoil[-1,:]))
## Force the coil to have an even number of segments, for Richardson Extrapolation to work
return newcoil[1:,:].T # return non-dummy columns
def calculate_field(coil, x, y, z):
'''
Calculates magnetic field vector as a result of some position and current x, y, z, I
[In the same coordinate system as the coil]
Coil: Input Coil Positions, already sub-divided into small pieces using slice_coil
x, y, z: position in cm
Output B-field is a 3-D vector in units of G
'''
FACTOR = 0.1 # = mu_0 / 4pi when lengths are in cm, and B-field is in G
def bs_integrate(start, end):
'''
Produces tiny segment of magnetic field vector (dB) using the midpoint approximation over some interval
TODO for future optimization: Get this to work with meshgrids directly
'''
dl = (end-start).T
mid = (start+end)/2
position = np.array((x-mid[0], y-mid[1], z-mid[2])).T
# relative position vector
mag = np.sqrt((x-mid[0])**2 + (y-mid[1])**2 + (z-mid[2])**2)
# magnitude of the relative position vector
return start[3] * np.cross(dl[:3], position) / np.array((mag ** 3, mag ** 3, mag ** 3)).T
# Apply the Biot-Savart Law to get the differential magnetic field
# current flowing in this segment is represented by start[3]
B = 0
# midpoint integration with 1 layer of Richardson Extrapolation
starts, mids, ends = coil[:,:-1:2], coil[:,1::2], coil[:,2::2]
for start, mid, end in np.nditer([starts, mids, ends], flags=['external_loop'], order='F'):
# use numpy fast indexing
fullpart = bs_integrate(start, end) # stage 1 richardson
halfpart = bs_integrate(start, mid) + bs_integrate(mid, end) # stage 2 richardson
B += 4/3 * halfpart - 1/3 * fullpart # richardson extrapolated midpoint rule
return B * FACTOR # return SUM of all components as 3 (x,y,z) meshgrids for (Bx, By, Bz) component when evaluated using produce_target_volume
def produce_target_volume(coil, box_size, start_point, vol_resolution):
'''
Generates a set of field vector values for each tuple (x, y, z) in the box.
Coil: Input Coil Positions in format specified above, already sub-divided into small pieces
box_size: (x, y, z) dimensions of the box in cm
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box
vol_resolution: Spatial resolution (in cm)
'''
x = np.linspace(start_point[0], box_size[0] + start_point[0],int(box_size[0]/vol_resolution)+1)
y = np.linspace(start_point[1], box_size[1] + start_point[1],int(box_size[1]/vol_resolution)+1)
z = np.linspace(start_point[2], box_size[2] + start_point[2],int(box_size[2]/vol_resolution)+1)
# Generate points at regular spacing, incl. end points
Z, Y, X = np.meshgrid(z, y, x, indexing='ij')
# NOTE: Requires axes to be flipped in order for meshgrid to have the correct dimensional order
return calculate_field(coil, X,Y,Z)
def get_field_vector(targetVolume, position, start_point, volume_resolution):
'''
Returns the B vector [Bx, By, Bz] components in a generated Target Volume at a given position tuple (x, y, z) in a coordinate system
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box
volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm)
'''
relativePosition = ((np.array(position) - np.array(start_point)) / volume_resolution).astype(int)
# adjust to the meshgrid's system
if (relativePosition < 0).any(): return ("ERROR: Out of bounds! (negative indices)")
try: return targetVolume[relativePosition[0], relativePosition[1], relativePosition[2], :]
except: return ("ERROR: Out of bounds!")
# basic error checking to see if you actually got a correct input/output
'''
- If you are indexing a targetvolume meshgrid on your own, remember to account for the offset (starting point), and spatial resolution
- You will need an index like <relativePosition = ((np.array(position) - np.array(start_point)) / volume_resolution).astype(int)>
'''
def write_target_volume(input_filename,output_filename, box_size, start_point,
coil_resolution=1, volume_resolution=1):
'''
Takes a coil specified in input_filename, generates a target volume, and saves the generated target volume to output_filename.
box_size: (x, y, z) dimensions of the box in cm
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box AKA the offset
coil_resolution: How long each coil subsegment should be
volume_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm)
'''
coil = parse_coil(input_filename)
chopped = slice_coil(coil, coil_resolution)
targetVolume = produce_target_volume(chopped, box_size, start_point, volume_resolution)
with open(output_filename, "wb") as f: np.save(f, targetVolume)
# stored in standard numpy pickle form
def read_target_volume(filename):
'''
Takes the name of a saved target volume and loads the B vector meshgrid.
Returns None if not found.
'''
targetVolume = None
try:
with open(filename, "rb") as f:
targetVolume = np.load(f)
return targetVolume
except:
pass
## plotting routines
def plot_fields(Bfields,box_size,start_point,vol_resolution,which_plane='z',level=0,num_contours=50):
'''
Plots the set of Bfields in the given region, at the specified resolutions.
Bfields: A 4D array of the Bfield.
box_size: (x, y, z) dimensions of the box in cm
start_point: (x, y, z) = (0, 0, 0) = bottom left corner position of the box AKA the offset
vol_resolution: Division of volumetric meshgrid (generate a point every volume_resolution cm)
which_plane: Plane to plot on, can be "x", "y" or "z"
level : The "height" of the plane. For instance the Z = 5 plane would have a level of 5
num_contours: THe amount of contours on the contour plot.
'''
# filled contour plot of Bx, By, and Bz on a chosen slice plane
X = np.linspace(start_point[0], box_size[0] + start_point[0],int(box_size[0]/vol_resolution)+1)
Y = np.linspace(start_point[1], box_size[1] + start_point[1],int(box_size[1]/vol_resolution)+1)
Z = np.linspace(start_point[2], box_size[2] + start_point[2],int(box_size[2]/vol_resolution)+1)
if which_plane=='x':
converted_level = np.where(X >= level)
B_sliced = [Bfields[converted_level[0][0],:,:,i].T for i in range(3)]
x_label,y_label = "y","z"
x_array,y_array = Y,Z
elif which_plane=='y':
converted_level = np.where(Y >= level)
B_sliced = [Bfields[:,converted_level[0][0],:,i].T for i in range(3)]
x_label,y_label = "x","z"
x_array,y_array = X,Z
else:
converted_level = np.where(Z >= level)
B_sliced = [Bfields[:,:,converted_level[0][0],i].T for i in range(3)]
x_label,y_label = "x","y"
x_array,y_array = X,Y
Bmin,Bmax = np.amin(B_sliced),np.amax(B_sliced)
component_labels = ['x','y','z']
fig,axes = plt.subplots(nrows=1,ncols=4,figsize=(10,5))
axes[0].set_ylabel(y_label + " (cm)")
for i in range(3):
contours = axes[i].contourf(x_array,y_array,B_sliced[i],
vmin=Bmin,vmax=Bmax,
cmap=cm.magma,levels=num_contours)
axes[i].set_xlabel(x_label + " (cm)")
axes[i].set_title("$\mathcal{B}$"+"$_{}$".format(component_labels[i]))
axes[3].set_aspect(20)
fig.colorbar(contours,cax=axes[3],extend='both')
plt.tight_layout()
plt.show()
def plot_coil(*input_filenames):
'''
Plots one or more coils in space.
input_filenames: Name of the files containing the coils.
Should be formatted appropriately.
'''
fig = plt.figure()
tick_spacing = 2
ax = fig.add_subplot(111, projection='3d')
ax.set_xlabel("$x$ (cm)")
ax.set_ylabel("$y$ (cm)")
ax.set_zlabel("$z$ (cm)")
for input_filename in input_filenames:
coil_points = np.array(parse_coil(input_filename))
ax.plot3D(coil_points[0,:],coil_points[1,:],coil_points[2,:],lw=2)
for axis in [ax.xaxis,ax.yaxis,ax.zaxis]:
axis.set_major_locator(ticker.MultipleLocator(tick_spacing))
plt.tight_layout()
plt.show()
def create_B_x_rectangle(name,p0=[-21.59,-38.1,-21.59,1],L = 76.20,W= 43.18):
'''
Creates a rectangle of the Y-Z plane that produces a B_x field.
name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle.
L: Length (on Z)
W: Width (on y)
'''
f = open(name,"w")
p1 = [p0[0],p0[1]+W,p0[2],p0[3]]
p2 = [p0[0],p0[1]+W,p0[2]+L ,p0[3]]
p3 = [p0[0],p0[1],p0[2]+L,p0[3]]
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p1)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p2)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p3)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
f.close()
def create_B_y_rectangle(name,p0=[-21.59,-38.1,-21.59,1], L = 76.20, D= 43.18):
'''
Creates a rectangle of the X-Z plane that produces a B_y field.
name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle.
L: Length (on Z)
D: Depth (on X)
'''
f = open(name,"w")
p1 = [p0[0], p0[1] , p0[2]+L, p0[3]]
p2 = [p0[0] + D , p0[1] , p0[2]+L, p0[3]]
p3 = [p0[0] + D , p0[1], p0[2], p0[3]]
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p1)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p2)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p3)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
f.close()
def create_B_z_rectangle(name,p0=[-26.67,-26.67,-26.67,1], H= 53.340, DD = 53.340):
'''
Creates a rectangle of the X-Y plane that produces a B_z field.
name: filename to output to. Should be a .txt file.
p0: [x0,y0,z0,Current] Starting point of the rectangle.
H: Height (on Y)
DD: Depth (on X)
'''
f = open(name,"w")
p1 = [p0[0] + DD, p0[1], p0[2], p0[3]]
p2 = [p0[0] + DD, p0[1]+H, p0[2], p0[3]]
p3 = [p0[0], p0[1]+H, p0[2], p0[3]]
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p1)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p2)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p3)
line = line[1:len(line)-1] + "\n"
f.write(line)
line = str(p0)
line = line[1:len(line)-1] + "\n"
f.write(line)
f.close()
def helmholtz_coils(fname1, fname2, numSegments, radius, spacing, current):
'''
Creates a pair of Helmholtz Coils that are parallel to the X-Y plane.
fname1: Name of the file where the first coil will be saved.
fname2: Name of the file where the second coil will be saved.
numSegments: Number of segments per coil
radius: Radius of the coils
spacing: Spacing between the coils. The first coil will be located at -spacing/2 and the 2nd coil will be located at spacing/2 on the Z plane
current: The current that goest through each coil.
'''
f = open(fname1,"w")
line = ""
for i in range(0,numSegments,1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(-spacing/2.0) + "," + str(current) + "\n"
f.write(line)
f.close()
f = open(fname2,"w")
line = ""
for i in range(0,numSegments,1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius) + "," + str(spacing/2.0) + "," + str(current) + "\n"
f.write(line)
f.close()
def create_Bx_circle(fname, numSegments, radius, spacing, current, center):
'''
Creates a coil on the Y-Z plane that produces a B_x field.
fname: Name of the file where the first coil will be saved.
numSegments: Number of segments per coil
radius: Radius of the coil
spacing: Spacing between the coil and the Y-Z plane
current: The current that goest through the coil.
center: (y,z) The center of the coil on the Y-Z plane
'''
f = open(fname,"w")
line = ""
for i in range(0,numSegments,1):
line = str(spacing) + "," + str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(current) + "\n"
f.write(line)
f.close()
def create_By_circle(fname, numSegments, radius, spacing, current, center):
'''
Creates a coil on the X-Z plane that produces a B_y field.
fname: Name of the file where the first coil will be saved.
numSegments: Number of segments per coil
radius: Radius of the coil
spacing: Spacing between the coil and the X-Z plane
current: The current that goest through the coil.
center: (x,z) The center of the coil on the X-Z plane
'''
f = open(fname,"w")
line = ""
for i in range(0,numSegments,1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(spacing) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(current) + "\n"
f.write(line)
f.close()
def create_Bz_circle(fname, numSegments, radius, spacing, current, center):
'''
Creates a coil on the X-Y plane that produces a B_z field.
fname: Name of the file where the first coil will be saved.
numSegments: Number of segments per coil
radius: Radius of the coil
spacing: Spacing between the coil and the X-Y plane
current: The current that goest through the coil.
center: (x,y) The center of the coil on the X-Y plane
'''
f = open(fname,"w")
line = ""
for i in range(0,numSegments,1):
line = str(np.cos(2*np.pi*(i)/(numSegments-1))*radius + center[0]) + "," + str(np.sin(2*np.pi*(i)/(numSegments-1))*radius + center[1]) + "," + str(spacing) + "," + str(current) + "\n"
f.write(line)
f.close()