cfsem 2.3.1__cp313-cp313-win_amd64.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.
cfsem/__init__.py ADDED
@@ -0,0 +1,577 @@
1
+ """Quasi-steady electromagnetics calcs"""
2
+
3
+ from typing import Tuple
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+ from interpn import MulticubicRectilinear
7
+
8
+ from cfsem.types import Array3xN
9
+
10
+ from cfsem.bindings import (
11
+ flux_circular_filament,
12
+ flux_density_linear_filament,
13
+ flux_density_biot_savart,
14
+ flux_density_circular_filament,
15
+ gs_operator_order2,
16
+ gs_operator_order4,
17
+ inductance_piecewise_linear_filaments,
18
+ filament_helix_path,
19
+ rotate_filaments_about_path,
20
+ vector_potential_linear_filament,
21
+ vector_potential_circular_filament,
22
+ flux_density_circular_filament_cartesian,
23
+ mutual_inductance_circular_to_linear,
24
+ flux_density_dipole,
25
+ body_force_density_circular_filament_cartesian,
26
+ body_force_density_linear_filament,
27
+ )
28
+
29
+ from ._cfsem import ellipe, ellipk
30
+
31
+ MU_0 = 4.0 * np.pi * 1e-7 * (1.0 + 5.5e-10)
32
+ """
33
+ Vacuum permeability with slight correction from latest measurements per NIST CODATA 2019 (SP-961),
34
+ https://www.physics.nist.gov/cuu/pdf/wall_2018.pdf .
35
+ """
36
+
37
+ __all__ = [
38
+ "flux_circular_filament",
39
+ "flux_density_biot_savart",
40
+ "flux_density_linear_filament",
41
+ "flux_density_circular_filament",
42
+ "gs_operator_order2",
43
+ "gs_operator_order4",
44
+ "filament_helix_path",
45
+ "inductance_piecewise_linear_filaments",
46
+ "self_inductance_piecewise_linear_filaments",
47
+ "mutual_inductance_piecewise_linear_filaments",
48
+ "flux_density_ideal_solenoid",
49
+ "self_inductance_lyle6",
50
+ "mutual_inductance_of_circular_filaments",
51
+ "mutual_inductance_of_cylindrical_coils",
52
+ "filament_coil",
53
+ "self_inductance_circular_ring_wien",
54
+ "self_inductance_annular_ring",
55
+ "self_inductance_distributed_axisymmetric_conductor",
56
+ "ellipe",
57
+ "ellipk",
58
+ "rotate_filaments_about_path",
59
+ "vector_potential_linear_filament",
60
+ "vector_potential_circular_filament",
61
+ "flux_density_circular_filament_cartesian",
62
+ "mutual_inductance_circular_to_linear",
63
+ "flux_density_dipole",
64
+ "body_force_density_circular_filament_cartesian",
65
+ "body_force_density_linear_filament",
66
+ ]
67
+
68
+
69
+ def self_inductance_piecewise_linear_filaments(xyzp: Array3xN) -> float:
70
+ """
71
+ Estimate the self-inductance of one piecewise-linear current filament.
72
+
73
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops
74
+ for non-self-pairings, zeroes-out the contributions from self-pairings
75
+ to resolve the thin-filament self-inductance singularity, and replaces the
76
+ segment self-inductance term with an analytic value from [3].
77
+
78
+ Assumes:
79
+
80
+ * Thin, well-behaved filaments
81
+ * Uniform current distribution within segments
82
+ * Low frequency operation; no skin effect
83
+ (which would reduce the segment self-field term)
84
+ * Vacuum permeability everywhere
85
+ * Each filament has a constant current in all segments
86
+ (otherwise we need an inductance matrix)
87
+
88
+ References:
89
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
90
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
91
+
92
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
93
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
94
+
95
+ [3] R. Dengler, “Self inductance of a wire loop as a curve integral,”
96
+ AEM, vol. 5, no. 1, p. 1, Jan. 2016, doi: [10.7716/aem.v5i1.331](https://doi.org/10.7716/aem.v5i1.331)
97
+
98
+ Args:
99
+ xyzp: [m] 3xN point series describing the filament
100
+
101
+ Returns:
102
+ [H] Scalar self-inductance
103
+ """
104
+ # Indexing numpy arrays here produces some `Any`-type hints and strips the element type
105
+ # erroneously in the pyright output as of pyright 1.1.393.
106
+ x, y, z = xyzp # type: ignore
107
+ xyzfil = (x[:-1], y[:-1], z[:-1]) # type: ignore
108
+ dlxyzfil = (x[1:] - x[:-1], y[1:] - y[:-1], z[1:] - z[:-1]) # type: ignore
109
+
110
+ self_inductance = inductance_piecewise_linear_filaments(
111
+ xyzfil, # type: ignore
112
+ dlxyzfil, # type: ignore
113
+ xyzfil, # type: ignore
114
+ dlxyzfil, # type: ignore
115
+ True,
116
+ )
117
+
118
+ return self_inductance # [H]
119
+
120
+
121
+ def mutual_inductance_piecewise_linear_filaments(
122
+ xyz0: Array3xN,
123
+ xyz1: Array3xN,
124
+ ) -> float:
125
+ """
126
+ Estimate the mutual inductance between two piecewise-linear current filaments.
127
+
128
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops, which is
129
+ originally from [2] and can be found in a more friendly format on wikipedia.
130
+
131
+ Assumes:
132
+
133
+ * Thin, well-behaved filaments
134
+ * Vacuum permeability everywhere
135
+ * Each filament has a constant current in all segments
136
+ (otherwise we need an inductance matrix)
137
+ * All segments between the two filaments are distinct; no identical pairs
138
+
139
+ References:
140
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
141
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
142
+
143
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
144
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
145
+
146
+ Args:
147
+ xyz0: [m] 3xN point series describing the first filament
148
+ xyz1: [m] 3xM point series describing the second filament
149
+
150
+ Returns:
151
+ [H] Scalar mutual inductance between the two filaments
152
+ """
153
+ # Indexing numpy arrays here produces some `Any`-type hints and strips the element type
154
+ # erroneously in the pyright output as of pyright 1.1.393.
155
+ x0, y0, z0 = xyz0 # type: ignore
156
+ xyzfil0 = (x0[:-1], y0[:-1], z0[:-1]) # type: ignore
157
+ dlxyzfil0 = (x0[1:] - x0[:-1], y0[1:] - y0[:-1], z0[1:] - z0[:-1]) # type: ignore
158
+
159
+ x1, y1, z1 = xyz1 # type: ignore
160
+ xyzfil1 = (x1[:-1], y1[:-1], z1[:-1]) # type: ignore
161
+ dlxyzfil1 = (x1[1:] - x1[:-1], y1[1:] - y1[:-1], z1[1:] - z1[:-1]) # type: ignore
162
+
163
+ inductance = inductance_piecewise_linear_filaments(
164
+ xyzfil0, # type: ignore
165
+ dlxyzfil0, # type: ignore
166
+ xyzfil1, # type: ignore
167
+ dlxyzfil1, # type: ignore
168
+ False,
169
+ )
170
+
171
+ return inductance # [H]
172
+
173
+
174
+ def flux_density_ideal_solenoid(
175
+ current: float, num_turns: float, length: float
176
+ ) -> float:
177
+ """
178
+ Axial B-field on centerline of an ideal (infinitely long) solenoid.
179
+
180
+ This calc converges reasonably well for coil L/D > 20.
181
+
182
+ Args:
183
+ current: [A] solenoid current
184
+ num_turns: [#] number of conductor turns
185
+ length: [m] length of winding pack
186
+
187
+ Returns:
188
+ [T] B-field on axis (in the direction aligned with the axis)
189
+ """
190
+ b_on_axis = MU_0 * num_turns * current / length
191
+ return b_on_axis # [T]
192
+
193
+
194
+ def self_inductance_lyle6(r: float, dr: float, dz: float, n: float) -> float:
195
+ """
196
+ Self-inductance of a cylindrically-symmetric coil of rectangular
197
+ cross-section, estimated to 6th order.
198
+
199
+ This estimate is viable up to an L/D of about 1.5, above which it
200
+ rapidly accumulates error and eventually produces negative values.
201
+
202
+ References:
203
+ [1] T. R. Lyle,
204
+ “IX. On the self-inductance of circular coils of rectangular section,”
205
+ Philosophical Transactions of the Royal Society of London.
206
+ Series A, Containing Papers of a Mathematical or Physical Character,
207
+ vol. 213, no. 497-508, pp. 421-435, Jan. 1914, doi: [10.1098/rsta.1914.0009](https://doi.org/10.1098/rsta.1914.0009)
208
+
209
+ Args:
210
+ r: [m] radius, coil center
211
+ dr: [m] radial width of coil
212
+ dz: [m] cylindrical height of coil
213
+ n: number of turns
214
+
215
+ Returns:
216
+ [H] self-inductance
217
+ """
218
+
219
+ assert dr < 2.0 * r, "Axisymmetric coil edges can't extend to negative R"
220
+ assert dz / r <= 3.5, "Coil geometry outside domain of validity of Lyle's formula"
221
+
222
+ # Guarantee 64-bit floats needed for 6th-order shape term
223
+ a = np.float64(r)
224
+ b = np.float64(dz)
225
+ c = np.float64(dr)
226
+
227
+ # Build up reusable terms for calculation of shape parameter
228
+ d = np.sqrt(b**2 + c**2) # [m] diagonal length
229
+ u = ((b / c) ** 2) * 2 * np.log(d / b)
230
+ v = ((c / b) ** 2) * 2 * np.log(d / c)
231
+ w = (b / c) * np.arctan(c / b)
232
+ ww = (c / b) * np.arctan(b / c)
233
+
234
+ bd2 = (b / d) ** 2
235
+ cd2 = (c / d) ** 2
236
+ da2 = (d / a) ** 2
237
+ ml = np.log(8 * a / d)
238
+
239
+ f = (
240
+ ml
241
+ + (1 + u + v - 8 * (w + ww)) / 12.0 # 0th order in d/a
242
+ + (
243
+ da2
244
+ * (
245
+ cd2 * (221 + 60 * ml - 6 * v)
246
+ + 3 * bd2 * (69 + 60 * ml + 10 * u - 64 * w)
247
+ )
248
+ )
249
+ / 5760.0 # 2nd order
250
+ + (
251
+ da2**2
252
+ * (
253
+ 2 * cd2**2 * (5721 + 3080 * ml - 345 * v)
254
+ + 5 * bd2 * cd2 * (407 + 5880 * ml + 6720 * u - 14336 * w)
255
+ - 10 * bd2**2 * (3659 + 2520 * ml + 805 * u - 6144 * w)
256
+ )
257
+ )
258
+ / 2.58048e7 # 4th order
259
+ + (
260
+ da2**3
261
+ * (
262
+ 3 * cd2**3 * (4308631 + 86520 * ml - 10052 * v)
263
+ - 14 * bd2**2 * cd2 * (617423 + 289800 * ml + 579600 * u - 1474560 * w)
264
+ + 21 * bd2**3 * (308779 + 63000 * ml + 43596 * u - 409600 * w)
265
+ + 42 * bd2 * cd2**2 * (-8329 + 46200 * ml + 134400 * u - 172032 * w)
266
+ )
267
+ )
268
+ / 1.73408256e10 # 6th order
269
+ ) # [nondim] shape parameter
270
+
271
+ self_inductance = MU_0 * (n**2) * a * f
272
+
273
+ assert self_inductance >= 0.0, "Coil geometry outside domain of validity of Lyle's formula"
274
+
275
+ return self_inductance # [H]
276
+
277
+
278
+ def mutual_inductance_of_circular_filaments(
279
+ rzn1: NDArray, rzn2: NDArray, par: bool = True
280
+ ) -> float:
281
+ """
282
+ Analytic mutual inductance between a pair of ideal cylindrically-symmetric coaxial filaments.
283
+
284
+ This is equivalent to taking the flux produced by each circular filament from
285
+ either collection of filaments to the other. Mutual inductance is reflexive,
286
+ so the order of the inputs is not important.
287
+
288
+ Args:
289
+ rzn1 (array): 3x1 array (r [m], z [m], n []) coordinates and number of turns
290
+ rzn2 (array): 3x1 array (r [m], z [m], n []) coordinates and number of turns
291
+ par: Whether to use CPU parallelism
292
+
293
+ Returns:
294
+ float: [H] mutual inductance
295
+ """
296
+
297
+ m = mutual_inductance_of_cylindrical_coils(
298
+ rzn1.reshape((3, 1)), rzn2.reshape((3, 1)), par
299
+ )
300
+
301
+ return m # [H]
302
+
303
+
304
+ def mutual_inductance_of_cylindrical_coils(
305
+ f1: NDArray, f2: NDArray, par: bool = True
306
+ ) -> float:
307
+ """
308
+ Analytical mutual inductance between two coaxial collections of ideal filaments.
309
+
310
+ Each collection typically represents a discretized "real" cylindrically-symmetric
311
+ coil of rectangular cross-section, but could have any cross-section as long as it
312
+ maintains cylindrical symmetry.
313
+
314
+ Args:
315
+ f1: 3 x N array of filament definitions like (r [m], z [m], n [])
316
+ f2: 3 x N array of filament definitions like (r [m], z [m], n [])
317
+ par: Whether to use CPU parallelism
318
+
319
+ Returns:
320
+ [H] mutual inductance of the two discretized coils
321
+ """
322
+ r1, z1, n1 = f1
323
+ r2, z2, n2 = f2
324
+
325
+ # Using n2 as the current per filament is equivalent to examining a 1A reference current,
326
+ # which gives us the flux per amp (inductance)
327
+ m = np.sum(
328
+ n1 * flux_circular_filament(n2, r2, z2, r1, z1, par)
329
+ ) # [H] total mutual inductance
330
+
331
+ return m # [H]
332
+
333
+
334
+ def filament_coil(
335
+ r: float, z: float, w: float, h: float, nt: float, nr: int, nz: int
336
+ ) -> NDArray:
337
+ """
338
+ Create an array of filaments from coil cross-section, evenly spaced
339
+ _inside_ the winding pack. No filaments are coincident with the coil surface.
340
+
341
+ Args:
342
+ r: [m] radius, coil center
343
+ z: [m] axial position, coil center
344
+ w: [m] width of coil pack
345
+ h: [m] height of coil pack
346
+ nt: turns
347
+ nr: radial discretizations
348
+ nz: axial discretizations
349
+
350
+ Returns:
351
+ (nr*nz) x 3, (r,z,n) of each filament
352
+ """
353
+
354
+ # Build a 2D mesh of points evenly spaced in the interior of the bounding rectangle
355
+ rs = np.linspace(r - w * (nr - 1) / nr / 2, r + w * (nr - 1) / nr / 2, nr) # [m]
356
+ zs = np.linspace(z - h * (nz - 1) / nz / 2, z + h * (nz - 1) / nz / 2, nz) # [m]
357
+ rmesh, zmesh = np.meshgrid(rs, zs, indexing="ij") # [m]
358
+
359
+ # Number of turns attributed to each point is not necessarily an integer
360
+ n = np.full_like(rmesh.flatten(), float(nt) / (nr * nz))
361
+
362
+ # Pack filament locations and number of turns into an array together
363
+ filaments = np.dstack([rmesh.flatten(), zmesh.flatten(), n]).reshape(nr * nz, 3)
364
+
365
+ return filaments # [m], [m], [dimensionless]
366
+
367
+
368
+ def self_inductance_circular_ring_wien(
369
+ major_radius: NDArray, minor_radius: NDArray
370
+ ) -> NDArray:
371
+ """
372
+ Wien's formula for the self-inductance of a circular ring
373
+ with thin circular cross section.
374
+
375
+ Uses equation 7 from reference [1].
376
+
377
+ References:
378
+ [1] E Rosa and L Cohen, "On the Self-Inductance of Circles,"
379
+ Bulletin of the Bureau of Standards, 1908.
380
+ [Online]. Available:
381
+ <https://nvlpubs.nist.gov/nistpubs/bulletin/04/nbsbulletinv4n1p149_A2b.pdf>
382
+
383
+ Args:
384
+ major_radius: [m] Major radius of the ring.
385
+ minor_radius: [m] Radius of the ring's cross section.
386
+
387
+ Returns:
388
+ [H] self-inductance
389
+ """
390
+ ar = major_radius / minor_radius # [], a / rho, dimensionless
391
+ ra2 = (minor_radius / major_radius) ** 2 # [], (rho / a)^2, dimensionless
392
+ # Equation 7 in Rosa & Cohen lacks the factor of 1e-7
393
+ # because it is not in SI base units.
394
+ self_inductance = (
395
+ MU_0 * major_radius * ((1 + 0.125 * ra2) * np.log(8 * ar) - 0.0083 * ra2 - 1.75)
396
+ ) # [H]
397
+ return self_inductance # [H]
398
+
399
+
400
+ def self_inductance_distributed_axisymmetric_conductor(
401
+ current: float,
402
+ grid: Tuple[NDArray, NDArray],
403
+ mesh: Tuple[NDArray, NDArray],
404
+ b_part: Tuple[NDArray, NDArray],
405
+ psi_part: NDArray,
406
+ mask: NDArray,
407
+ edge_path: Tuple[NDArray, NDArray],
408
+ ) -> Tuple[float, float, float]:
409
+ """
410
+ Calculation of a distributed conductor's self-inductance from two components:
411
+
412
+ * External inductance: the portion related to the poloidal flux exactly at the conductor edge
413
+ * Internal inductance: the portion related to the poloidal magnetic field inside the conductor,
414
+ where the filamentized method used for coils does not apply due to the parallel arrangement
415
+ and variable current density.
416
+
417
+ Note: the B-field and flux inputs are _not_ the total from all sources - they are only the
418
+ contribution from the distributed conductor under examination.
419
+
420
+ This calculation was developed for use with tokamak plasmas, but applies similarly to
421
+ other kinds of distributed axisymmetric conductor.
422
+
423
+ Assumptions:
424
+
425
+ * Cylindrically-symmetric, distributed, single-winding, contiguous conductor
426
+ * No high-magnetic-permeability materials in the vicinity
427
+ * Isopotential on the edge contour
428
+ * This is slightly less restrictive than isopotential on the section,
429
+ but notably does _not_ allow the calc to be used with, for example,
430
+ large, shell conductors where different regions are meaningfully
431
+ independent of each other.
432
+ * Conductor interior does not touch the edge of the computational domain
433
+ * At least one grid cell of padding is needed to support finite differences
434
+
435
+ References:
436
+ [1] S. Ejima, R. W. Callis, J. L. Luxon, R. D. Stambaugh, T. S. Taylor, and J. C. Wesley,
437
+ “Volt-second analysis and consumption in Doublet III plasmas,”
438
+ Nucl. Fusion, vol. 22, no. 10, pp. 1313-1319, Oct. 1982,
439
+ doi: [10.1088/0029-5515/22/10/006](https://doi.org/10.1088/0029-5515/22/10/006)
440
+
441
+ [2] J. A. Romero and J.-E. Contributors, “Plasma internal inductance dynamics in a tokamak,”
442
+ arXiv.org. Accessed: Dec. 21, 2023. [Online]. Available: https://arxiv.org/abs/1009.1984v1
443
+ doi: [10.1088/0029-5515/50/11/115002](https://doi.org/10.1088/0029-5515/50/11/115002)
444
+
445
+ [3] J. T. Wai and E. Kolemen, “GSPD: An algorithm for time-dependent tokamak equilibria design.”
446
+ arXiv, Jun. 22, 2023. Accessed: Sep. 15, 2023. [Online]. Available: https://arxiv.org/abs/2306.13163
447
+ doi: [10.48550/arXiv.2306.13163](https://doi.org/10.48550/arXiv.2306.13163)
448
+
449
+ Args:
450
+ current: [A] total toroidal current in this conductor
451
+ grid: [m] (1 X nr) grids of (R coords, Z coords)
452
+ mesh: [m] (nr X nz) meshgrids of (R coords, Z coords)
453
+ b_part: [T] (nr X nz) Flux density (R-component, Z-component) due to this conductor
454
+ psi_part: [V-s] or [T-m^2] (nr X nz) this conductor's poloidal flux field
455
+ mask: (nr X nz) positive mask of the conductor's interior region
456
+ edge_path: [m] (2 x N) closed (r, z) path along conductor edge
457
+
458
+ Returns:
459
+ (Lt, Li, Le) Total, internal, and external self-inductance components
460
+ """
461
+ # Unpack
462
+ rgrid, zgrid = grid # [m]
463
+ rmesh, zmesh = mesh # [m]
464
+ br, bz = b_part # [T]
465
+
466
+ # Set up
467
+ nr = rgrid.size
468
+ nz = zgrid.size
469
+ psi_interpolator = MulticubicRectilinear.new([rgrid, zgrid], psi_part.flatten())
470
+
471
+ # Same-length diffs assuming last grid cell is the same size
472
+ # as the previous one. In general the conductor can't touch the
473
+ # edge of the computational domain without breaking the solver,
474
+ # so it's ok to allow some potential error at the last index.
475
+ drmesh = np.zeros_like(rmesh) # [m]
476
+ drmesh[: nr - 1, :] = np.diff(rmesh, axis=0)
477
+ drmesh[-1, :] = drmesh[-2, :]
478
+
479
+ dzmesh = np.zeros_like(zmesh) # [m]
480
+ dzmesh[:, : nz - 1] = np.diff(zmesh, axis=1)
481
+ dzmesh[:, -1] = dzmesh[:, -2]
482
+
483
+ # Toroidal volume of each cell in the mesh.
484
+ #
485
+ # Ideally we'd use a two-sided difference for
486
+ # calculating dr and dz in order to properly handle cell volume
487
+ # for nonuniform grids, but (1) that's a lot of extra array handling
488
+ # for minimal real benefit and (2) the grid is almost always uniform
489
+ volmesh = 2.0 * np.pi * rmesh * drmesh * dzmesh # [m^3]
490
+
491
+ # Stored poloidal magnetic energy inside the conductor volume.
492
+ #
493
+ # Because E ~ B^2 and B = sqrt(Br^2 + Bz^2 + Btor^2) -> B^2 = Br^2 + Bz^2 + Btor^2,
494
+ # the components of magnetic energy (and induction) due to a current on different axes
495
+ # are separable, and we can ignore the toroidal field entirely when treating the
496
+ # poloidal inductance.
497
+ #
498
+ # That doesn't mean there isn't store energy related to the conductor's toroidal field,
499
+ # only that it can be separated from the poloidal inductance.
500
+ wmag_pol = (1.0 / (2.0 * MU_0)) * np.sum((br**2 + bz**2) * volmesh * mask) # [J]
501
+
502
+ # Internal inductance.
503
+ #
504
+ # Because E = (1/2) * L * I^2, we can superpose multiple inductance terms
505
+ # for a given current-carrying element, and say E = (1/2) * (Li + Le) * I^2 .
506
+ #
507
+ # Now that we have the internal stored magnetic energy, we can calculate
508
+ # the part of the self-inductance related to that energy directly.
509
+ internal_inductance = 2.0 * wmag_pol / current**2 # [H]
510
+
511
+ # External inductance.
512
+ #
513
+ # This is a hack based on Poynting's Theorem relating surface conditions to
514
+ # magnetic energy.
515
+ #
516
+ # Do weighted average of flux along the edge contour
517
+ # to adjust for the length of individual segments
518
+ edge_dr = np.diff(edge_path[0]) # [m]
519
+ edge_dz = np.diff(edge_path[1]) # [m]
520
+ edge_dl = (edge_dr**2 + edge_dz**2) ** 0.5 # [m] length of each segment
521
+ edge_length = np.sum(edge_dl) # [m] total length of conductor limit contour
522
+ edge_psi = psi_interpolator.eval([x[:-1] for x in edge_path]) # [V-s]
523
+ edge_psi_mean = np.sum(edge_psi * edge_dl) / edge_length # [V-s]
524
+ # Take the inductance
525
+ external_inductance = edge_psi_mean / current # [H]
526
+
527
+ # Total inductance.
528
+ total_inductance = internal_inductance + external_inductance # [H]
529
+
530
+ return (
531
+ float(total_inductance),
532
+ float(internal_inductance),
533
+ float(external_inductance),
534
+ )
535
+
536
+
537
+ def self_inductance_annular_ring(r: float, a: float, b: float) -> float:
538
+ """
539
+ Low-frequency self-inductance of a thick-walled tube bent in a circle.
540
+
541
+ Uses Wien's method, per the 1912 NIST handbook [1], Eqn. 64 on pg. 112,
542
+ with a correction to a misprint in term 5 and a unit conversion factor
543
+ in the final expression.
544
+
545
+ This is an approximation that drops terms of order higher than `(a/r)^2`
546
+ and `(b/r)^2`.
547
+
548
+ References:
549
+ [1] E. B. Rosa and F. W. Grover, “Formulas and tables for the calculation of mutual and self-inductance (Revised),”
550
+ BULL. NATL. BUR. STAND., vol. 8, no. 1, p. 1, Jan. 1912,
551
+ doi: [10.6028/bulletin.185](https://doi.org/10.6028/bulletin.185)
552
+
553
+ Args:
554
+ r: [m] major radius of loop
555
+ a: [m] inner minor radius (tube inside radius)
556
+ b: [m] outer minor radius (tube outside radius)
557
+
558
+ Returns:
559
+ [H] self-inductance [H]
560
+ """
561
+
562
+ if not ((r > 0.0) and (a > 0.0) and (b > 0.0)):
563
+ raise ValueError("All radii must be positive and nonzero.")
564
+ if not (a < b):
565
+ raise ValueError("Inner minor radius must be less than outer minor radius.")
566
+ if not (b < r):
567
+ raise ValueError("Outer minor radius must be less than major radius.")
568
+
569
+ term1 = (1.0 + (a**2 + b**2) / (8.0 * r**2)) * np.log(8.0 * r / b)
570
+ term2 = -1.75 + (2.0 * b**2 + a**2) / (32.0 * r**2)
571
+ term3 = -0.5 * a**2 / (b**2 - a**2)
572
+ term4 = (a**4 / ((b**2 - a**2) ** 2)) * (1.0 + a**2 / (8.0 * r**2)) * np.log(b / a)
573
+ term5 = -(a**4 + a**2 * b**2 + b**4) / (48.0 * r**2 * (b**2 - a**2))
574
+
575
+ self_inductance = MU_0 * r * (term1 + term2 + term3 + term4 + term5) # [H]
576
+
577
+ return self_inductance # [H]
Binary file
cfsem/bindings.py ADDED
@@ -0,0 +1,587 @@
1
+ """
2
+ Symmetric bindings for backend calcs.
3
+
4
+ This fulfills the function of typing stubs, while also guaranteeing arrays are
5
+ passed as contiguous and reallocating into contiguous inputs if necessary.
6
+ """
7
+
8
+ from cfsem.types import Array3xN
9
+
10
+ from numpy import ascontiguousarray, float64, zeros_like
11
+ from numpy.typing import NDArray
12
+
13
+ from ._cfsem import flux_circular_filament as em_flux_circular_filament
14
+ from ._cfsem import flux_density_linear_filament as em_flux_density_linear_filament
15
+ from ._cfsem import flux_density_circular_filament as em_flux_density_circular_filament
16
+ from ._cfsem import (
17
+ flux_density_circular_filament_cartesian as em_flux_density_circular_filament_cartesian,
18
+ )
19
+ from ._cfsem import (
20
+ mutual_inductance_circular_to_linear as em_mutual_inductance_circular_to_linear,
21
+ )
22
+ from ._cfsem import (
23
+ body_force_density_circular_filament_cartesian as em_body_force_density_circular_filament_cartesian,
24
+ )
25
+
26
+ from ._cfsem import gs_operator_order2 as em_gs_operator_order2
27
+ from ._cfsem import gs_operator_order4 as em_gs_operator_order4
28
+ from ._cfsem import (
29
+ inductance_piecewise_linear_filaments as em_inductance_piecewise_linear_filaments,
30
+ )
31
+ from ._cfsem import filament_helix_path as em_filament_helix_path
32
+ from ._cfsem import rotate_filaments_about_path as em_rotate_filaments_about_path
33
+ from ._cfsem import (
34
+ vector_potential_circular_filament as em_vector_potential_circular_filament,
35
+ )
36
+ from ._cfsem import (
37
+ vector_potential_linear_filament as em_vector_potential_linear_filament,
38
+ )
39
+ from ._cfsem import (
40
+ body_force_density_linear_filament as em_body_force_density_linear_filament,
41
+ )
42
+
43
+ from ._cfsem import flux_density_dipole as em_flux_density_dipole
44
+
45
+
46
+ def flux_circular_filament(
47
+ ifil: NDArray[float64],
48
+ rfil: NDArray[float64],
49
+ zfil: NDArray[float64],
50
+ rprime: NDArray[float64],
51
+ zprime: NDArray[float64],
52
+ par: bool = True,
53
+ ) -> NDArray[float64]:
54
+ """
55
+ Flux contributions from some circular filaments to some observation points,
56
+ which happens to be the Green's function for the Grad-Shafranov solve.
57
+
58
+ This represents the integral of $\\vec{B} \\cdot \\hat{n} \\, dA$ from the z-axis to each
59
+ (`rprime`, `zprime`) observation location with $\\hat{n}$ oriented parallel to the z-axis.
60
+
61
+ A convenient interpretation of the flux is as the mutual inductance
62
+ between a circular filament at (`rfil`, `zfil`) and a second circular
63
+ filament at (`rprime`, `zprime`); this can be used to get the mutual inductance
64
+ between two filamentized coils as the sum of flux contributions between each coil's filaments.
65
+ Because mutual inductance is reflexive, the order of the coils can be reversed and
66
+ the same result is obtained.
67
+
68
+ Args:
69
+ ifil: [A] filament current
70
+ rfil: [m] filament R-coord
71
+ zfil: [m] filament Z-coord
72
+ rprime: [m] Observation point R-coord
73
+ zprime: [m] Observation point Z-coord
74
+ par: Whether to use CPU parallelism
75
+
76
+ Returns:
77
+ [Wb] or [T-m^2] or [V-s] psi, poloidal flux at each observation point
78
+ """
79
+ ifil, rfil, zfil = _3tup_contig((ifil, rfil, zfil))
80
+ rprime, zprime = _2tup_contig((rprime, zprime))
81
+ psi = em_flux_circular_filament(ifil, rfil, zfil, rprime, zprime, par)
82
+ return psi # [Wb] or [T-m^2] or [V-s]
83
+
84
+
85
+ def vector_potential_circular_filament(
86
+ ifil: NDArray[float64],
87
+ rfil: NDArray[float64],
88
+ zfil: NDArray[float64],
89
+ rprime: NDArray[float64],
90
+ zprime: NDArray[float64],
91
+ par: bool = True,
92
+ ) -> NDArray[float64]:
93
+ """
94
+ Vector potential contributions from some circular filaments to some observation points.
95
+ Off-axis A_phi component for a circular current filament in vacuum.
96
+
97
+ The vector potential of a loop has zero r- and z- components due to symmetry,
98
+ and does not vary in the phi-direction.
99
+
100
+ Note that to recover the B-field as the curl of A, the curl operator for cylindrical
101
+ coordinates must be used with the output of this function incorporated into a full
102
+ 3D A-field like [A_r, A_phi, A_z].
103
+
104
+ References:
105
+ [1] J. C. Simpson, J. E. Lane, C. D. Immer, R. C. Youngquist, and T. Steinrock,
106
+ “Simple Analytic Expressions for the Magnetic Field of a Circular Current Loop,”
107
+ Jan. 01, 2001. Accessed: Sep. 06, 2022. [Online]. Available: <https://ntrs.nasa.gov/citations/20010038494>
108
+
109
+ Args:
110
+ ifil: [A] filament current
111
+ rfil: [m] filament R-coord
112
+ zfil: [m] filament Z-coord
113
+ rprime: [m] Observation point R-coord
114
+ zprime: [m] Observation point Z-coord
115
+ par: Whether to use CPU parallelism
116
+
117
+ Returns:
118
+ [Wb/m] or [V-s/m] a_phi, vector potential in the toroidal direction
119
+ """
120
+ ifil, rfil, zfil = _3tup_contig((ifil, rfil, zfil))
121
+ rprime, zprime = _2tup_contig((rprime, zprime))
122
+ a_phi = em_vector_potential_circular_filament(ifil, rfil, zfil, rprime, zprime, par)
123
+ return a_phi # [Wb/m] or [V-s/m]
124
+
125
+
126
+ def flux_density_circular_filament(
127
+ ifil: NDArray[float64],
128
+ rfil: NDArray[float64],
129
+ zfil: NDArray[float64],
130
+ rprime: NDArray[float64],
131
+ zprime: NDArray[float64],
132
+ par: bool = True,
133
+ ) -> tuple[NDArray[float64], NDArray[float64]]:
134
+ """
135
+ Off-axis Br,Bz components for a circular current filament in vacuum.
136
+
137
+ Near-exact formula (except numerically-evaluated elliptic integrals)
138
+ See eqns. 12, 13 pg. 34 in [1], eqn 9.8.7 in [2], and all of [3].
139
+
140
+ Note the formula for Br as given by [1] is incorrect and does not satisfy the
141
+ constraints of the calculation without correcting by a factor of ($z / r$).
142
+
143
+ References:
144
+ [1] D. B. Montgomery and J. Terrell,
145
+ “Some Useful Information For The Design Of Aircore Solenoids,
146
+ Part I. Relationships Between Magnetic Field, Power, Ampere-Turns
147
+ And Current Density. Part II. Homogeneous Magnetic Fields,”
148
+ Massachusetts Inst. Of Tech. Francis Bitter National Magnet Lab, Cambridge, MA,
149
+ Nov. 1961. Accessed: May 18, 2021. [Online].
150
+ Available: <https://apps.dtic.mil/sti/citations/tr/AD0269073>
151
+
152
+ [2] 8.02 Course Notes. Available:
153
+ <https://web.mit.edu/8.02t/www/802TEAL3D/visualizations/coursenotes/modules/guide09.pdf>
154
+
155
+ [3] Eric Dennyson, "Magnet Formulas". Available:
156
+ <https://tiggerntatie.github.io/emagnet-py/offaxis/off_axis_loop.html>
157
+
158
+ Args:
159
+ ifil: [A] filament current
160
+ rfil: [m] filament R-coord
161
+ zfil: [m] filament Z-coord
162
+ rprime: [m] Observation point R-coord
163
+ zprime: [m] Observation point Z-coord
164
+ par: Whether to use CPU parallelism
165
+
166
+ Returns:
167
+ [T] (Br, Bz) flux density components
168
+ """
169
+ ifil, rfil, zfil = _3tup_contig((ifil, rfil, zfil))
170
+ rprime, zprime = _2tup_contig((rprime, zprime))
171
+ br, bz = em_flux_density_circular_filament(ifil, rfil, zfil, rprime, zprime, par)
172
+ return br, bz # [T]
173
+
174
+
175
+ def flux_density_linear_filament(
176
+ xyzp: Array3xN,
177
+ xyzfil: Array3xN,
178
+ dlxyzfil: Array3xN,
179
+ ifil: NDArray[float64],
180
+ par: bool = True,
181
+ ) -> Array3xN:
182
+ """
183
+ Biot-Savart law calculation for B-field contributions from many filament segments
184
+ to many observation points.
185
+
186
+ Args:
187
+ xyzp: [m] x,y,z coords of observation points
188
+ xyzfil: [m] x,y,z coords of current filament origins (start of segment)
189
+ dlxyzfil: [m] x,y,z length delta of current filaments
190
+ ifil: [A] current in each filament segment
191
+ par: Whether to use CPU parallelism
192
+
193
+ Returns:
194
+ [T] (Bx, By, Bz) magnetic flux density at observation points
195
+ """
196
+ xyzp = _3tup_contig(xyzp)
197
+ xyzfil = _3tup_contig(xyzfil)
198
+ dlxyzfil = _3tup_contig(dlxyzfil)
199
+ ifil = ascontiguousarray(ifil).flatten()
200
+ return em_flux_density_linear_filament(xyzp, xyzfil, dlxyzfil, ifil, par)
201
+
202
+
203
+ flux_density_biot_savart = flux_density_linear_filament # For backwards-compatibility
204
+
205
+
206
+ def vector_potential_linear_filament(
207
+ xyzp: Array3xN,
208
+ xyzfil: Array3xN,
209
+ dlxyzfil: Array3xN,
210
+ ifil: NDArray[float64],
211
+ par: bool = True,
212
+ ) -> Array3xN:
213
+ """
214
+ Vector potential calculation for A-field contribution from many current filament
215
+ segments to many observation points.
216
+
217
+ Args:
218
+ xyzp: [m] x,y,z coords of observation points
219
+ xyzfil: [m] x,y,z coords of current filament origins (start of segment)
220
+ dlxyzfil: [m] x,y,z length delta of current filaments
221
+ ifil: [A] current in each filament segment
222
+ par: Whether to use CPU parallelism
223
+
224
+ Returns:
225
+ [Wb/m] or [V-s/m] (Ax, Ay, Az) magnetic vector potential at observation points
226
+ """
227
+ xyzp = _3tup_contig(xyzp)
228
+ xyzfil = _3tup_contig(xyzfil)
229
+ dlxyzfil = _3tup_contig(dlxyzfil)
230
+ ifil = ascontiguousarray(ifil).flatten()
231
+ return em_vector_potential_linear_filament(xyzp, xyzfil, dlxyzfil, ifil, par)
232
+
233
+
234
+ def inductance_piecewise_linear_filaments(
235
+ xyzfil0: Array3xN,
236
+ dlxyzfil0: Array3xN,
237
+ xyzfil1: Array3xN,
238
+ dlxyzfil1: Array3xN,
239
+ self_inductance: bool = False,
240
+ ) -> float:
241
+ """
242
+ Estimate the mutual inductance between two piecewise-linear current filaments,
243
+ or estimate self-inductance by passing the same filaments twice and setting
244
+ `self_inductance = True`.
245
+
246
+ It may be easier to use wrappers of this function that are specialized for self- and mutual-inductance
247
+ calculations:
248
+ [`self_inductance_piecewise_linear_filaments`][cfsem.self_inductance_piecewise_linear_filaments]
249
+ and [`mutual_inductance_piecewise_linear_filaments`][cfsem.mutual_inductance_piecewise_linear_filaments].
250
+
251
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops, which is
252
+ originally from [2] and can be found in a more friendly format on wikipedia.
253
+
254
+ When self_inductance flag is set, zeroes-out the contributions from self-pairings
255
+ to resolve the thin-filament self-inductance singularity and replaces the
256
+ segment self-inductance term with an analytic value from [3].
257
+
258
+ Assumes:
259
+
260
+ * Thin, well-behaved filaments
261
+ * Uniform current distribution within segments
262
+ * Low frequency operation; no skin effect
263
+ (which would reduce the segment self-field term)
264
+ * Vacuum permeability everywhere
265
+ * Each filament has a constant current in all segments
266
+ (otherwise we need an inductance matrix)
267
+
268
+ References:
269
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
270
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
271
+
272
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
273
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
274
+
275
+ [3] R. Dengler, “Self inductance of a wire loop as a curve integral,”
276
+ AEM, vol. 5, no. 1, p. 1, Jan. 2016, doi: [10.7716/aem.v5i1.331](https://doi.org/10.7716/aem.v5i1.331)
277
+
278
+ Args:
279
+ xyzfil0: [m] Nx3 point series describing the filament origins
280
+ dlxyzfil0: [m] Nx3 length vector of each filament
281
+ xyzfil1: [m] Nx3 point series describing the filament origins
282
+ dlxyzfil1: [m] Nx3 length vector of each filament
283
+ self_inductance: Whether this is being used as a self-inductance calc
284
+
285
+ Returns:
286
+ [H] Scalar inductance
287
+ """
288
+ xyzfil0 = _3tup_contig(xyzfil0)
289
+ dlxyzfil0 = _3tup_contig(dlxyzfil0)
290
+ xyzfil1 = _3tup_contig(xyzfil1)
291
+ dlxyzfil1 = _3tup_contig(dlxyzfil1)
292
+
293
+ return em_inductance_piecewise_linear_filaments(
294
+ xyzfil0, dlxyzfil0, xyzfil1, dlxyzfil1, self_inductance
295
+ )
296
+
297
+
298
+ def gs_operator_order2(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
299
+ """Build second-order Grad-Shafranov operator in triplet format.
300
+ Assumes regular grid spacing.
301
+
302
+ Args:
303
+ rs: [m] r-coordinates of finite difference grid
304
+ zs: [m] z-coordinates of finite difference grid
305
+
306
+ Returns:
307
+ Differential operator as triplet format sparse matrix
308
+ """
309
+ rs, zs = _2tup_contig((rs, zs))
310
+ return em_gs_operator_order2(rs, zs)
311
+
312
+
313
+ def gs_operator_order4(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
314
+ """
315
+ Build fourth-order Grad-Shafranov operator in triplet format.
316
+ Assumes regular grid spacing.
317
+
318
+ Args:
319
+ rs: [m] r-coordinates of finite difference grid
320
+ zs: [m] z-coordinates of finite difference grid
321
+
322
+ Returns:
323
+ Differential operator as triplet format sparse matrix
324
+ """
325
+ rs, zs = _2tup_contig((rs, zs))
326
+ return em_gs_operator_order4(rs, zs)
327
+
328
+
329
+ def filament_helix_path(
330
+ path: Array3xN,
331
+ helix_start_offset: tuple[float, float, float],
332
+ twist_pitch: float,
333
+ angle_offset: float,
334
+ ) -> NDArray[float64]:
335
+ """
336
+ Filamentize a helix about an arbitrary piecewise-linear path.
337
+
338
+ Assumes angle between sequential path segments is small and will fail
339
+ if that angle approaches or exceeds 90 degrees.
340
+
341
+ The helix initial position vector, helix_start_offset, must be in a plane normal to
342
+ the first path segment in order to produce good results. If it is not in-plane,
343
+ it will be projected on to that plane and then scaled to the magnitude of its
344
+ original length s.t. the distance from the helix to the path center is preserved
345
+ but its orientation is not.
346
+
347
+ Description of the method:
348
+
349
+ 1. Translate [filament segment n-1] to the base of [path segment n]
350
+ and call it [filament segment n]
351
+ 2. Take cross product of [path segment n] with [path segment n-1]
352
+ 3. Rotate [filament segment n] segment about the axis of that cross product
353
+ to bring it into the plane defined by [path segment n] as a normal vector
354
+ 4. Rotate [filament seg. n] about [path seg. n] to continue the helix orbit
355
+
356
+ Args:
357
+ path: [m] 3xN Centerline points
358
+ helix_start_offset: [m] (3x1) Initial position of helix rel. to centerline path
359
+ twist_pitch: [m] (scalar) Centerline length per helix orbit
360
+ angle_offset: [rad] (scalar) Initial rotation offset about centerline
361
+
362
+ Returns:
363
+ [m] 3xN array of points on the helix that twists around the path
364
+ """
365
+
366
+ # Make sure input is contiguous, reallocating only if necessary
367
+ path = ascontiguousarray(path)
368
+
369
+ # Allocate output
370
+ helix = zeros_like(path) # [m]
371
+
372
+ # Calculate, mutating output
373
+ em_filament_helix_path(
374
+ (*path,),
375
+ helix_start_offset,
376
+ twist_pitch,
377
+ angle_offset,
378
+ (*helix,),
379
+ )
380
+
381
+ return helix # [m]
382
+
383
+
384
+ def rotate_filaments_about_path(
385
+ path: Array3xN, angle_offset: float, fils: Array3xN
386
+ ) -> NDArray[float64]:
387
+ """
388
+ Rotate a path of point about another path.
389
+
390
+ Intended for rotating a helix generated by [`filament_helix_path`][cfsem.filament_helix_path]
391
+ about the centerline that was used to generate it.
392
+
393
+ Args:
394
+ path: [m] x,y,z Centerline points
395
+ angle_offset: [rad] (scalar) Initial rotation offset about centerline
396
+ fils: [m] x,y,z Filaments to rotate around centerline
397
+
398
+ Returns:
399
+ [m] 3xN array of points on the helix that twists around the path
400
+ """
401
+
402
+ # Make sure input is contiguous, reallocating only if necessary
403
+ path = ascontiguousarray(path)
404
+
405
+ new_fils = ascontiguousarray(fils).copy()
406
+
407
+ em_rotate_filaments_about_path(
408
+ (*path,),
409
+ angle_offset,
410
+ (*new_fils,),
411
+ )
412
+
413
+ return new_fils # [m]
414
+
415
+
416
+ def flux_density_circular_filament_cartesian(
417
+ ifil: NDArray[float64],
418
+ rfil: NDArray[float64],
419
+ zfil: NDArray[float64],
420
+ xyzp: Array3xN,
421
+ par: bool = True,
422
+ ) -> NDArray[float64]:
423
+ """
424
+ Flux density of a circular filament in cartesian form
425
+ at a set of locations given in cartesian coordinates.
426
+
427
+ Args:
428
+ ifil: [A] filament current
429
+ rfil: [m] filament R-coord
430
+ zfil: [m] filament Z-coord
431
+ xyzp: [m] x,y,z coords of observation points
432
+ par: Whether to use CPU parallelism
433
+
434
+ Returns:
435
+ [T] flux density
436
+ """
437
+ ifil, rfil, zfil = _3tup_contig((ifil, rfil, zfil))
438
+ xyzp = _3tup_contig(xyzp)
439
+ bx, by, bz = em_flux_density_circular_filament_cartesian(
440
+ ifil, rfil, zfil, xyzp, par
441
+ ) # [T]
442
+
443
+ return bx, by, bz # type: ignore
444
+
445
+
446
+ def mutual_inductance_circular_to_linear(
447
+ rfil: NDArray[float64],
448
+ zfil: NDArray[float64],
449
+ nfil: NDArray[float64],
450
+ xyzfil: Array3xN,
451
+ dlxyzfil: Array3xN,
452
+ par: bool = True,
453
+ ) -> NDArray[float64]:
454
+ """
455
+ Mutual inductance between a collection of circular filaments and a piecewise-linear filament.
456
+ This method is much faster (~100x typically) than discretizing the circular loop
457
+ into linear segments and using Neumann's formula.
458
+
459
+ Args:
460
+ rfil: [m] filament R-coord
461
+ zfil: [m] filament Z-coord
462
+ nfil: [dimensionless] filament number of turns
463
+ xyzfil: [m] x,y,z coords of current filament origins (start of segment)
464
+ dlxyzfil: [m] x,y,z length delta of current filaments
465
+ par: Whether to use CPU parallelism
466
+
467
+ Returns:
468
+ [H] mutual inductance
469
+ """
470
+ rfil, zfil, nfil = _3tup_contig((rfil, zfil, nfil))
471
+ xyzfil = _3tup_contig(xyzfil)
472
+ dlxyzfil = _3tup_contig(dlxyzfil)
473
+ m = em_mutual_inductance_circular_to_linear(rfil, zfil, nfil, xyzfil, dlxyzfil, par)
474
+
475
+ return m # [H]
476
+
477
+
478
+ def flux_density_dipole(
479
+ loc: Array3xN,
480
+ moment: Array3xN,
481
+ xyzp: Array3xN,
482
+ par: bool = True,
483
+ ) -> NDArray[float64]:
484
+ """
485
+ Magnetic flux density of a dipole in cartesian coordiantes.
486
+
487
+ Args:
488
+ loc: [m] x,y,z coordinates of dipole
489
+ moment: [A-m^2] dipole magnetic moment vector
490
+ xyzp: [m] x,y,z coords of observation points
491
+ par: Whether to use CPU parallelism
492
+
493
+ Returns:
494
+ [T] flux density
495
+ """
496
+ loc = _3tup_contig(loc)
497
+ moment = _3tup_contig(moment)
498
+ xyzp = _3tup_contig(xyzp)
499
+ bx, by, bz = em_flux_density_dipole(loc, moment, xyzp, par) # [T]
500
+
501
+ return bx, by, bz # type: ignore
502
+
503
+
504
+ def body_force_density_circular_filament_cartesian(
505
+ ifil: NDArray[float64],
506
+ rfil: NDArray[float64],
507
+ zfil: NDArray[float64],
508
+ obs: Array3xN,
509
+ j: Array3xN,
510
+ par: bool = True,
511
+ ) -> NDArray[float64]:
512
+ """
513
+ JxB (Lorentz) body force density (per volume) in cartesian form due to a circular current
514
+ filament segment at an observation point in cartesian form with some current density (per area).
515
+
516
+ Args:
517
+ ifil: [A] filament current
518
+ rfil: [m] filament R-coord
519
+ zfil: [m] filament Z-coord
520
+ obs: [m] x,y,z coords of observation locations
521
+ j: [A/m^2] current density vector at observation locations
522
+ par: Whether to use CPU parallelism
523
+
524
+ Returns:
525
+ [N/m^3] body force density
526
+ """
527
+ ifil, rfil, zfil = _3tup_contig((ifil, rfil, zfil))
528
+ obs = _3tup_contig(obs)
529
+ j = _3tup_contig(j)
530
+ jxbx, jxby, jxbz = em_body_force_density_circular_filament_cartesian(
531
+ ifil, rfil, zfil, obs, j, par
532
+ ) # [N/m^3]
533
+
534
+ return jxbx, jxby, jxbz # type: ignore
535
+
536
+
537
+ def body_force_density_linear_filament(
538
+ xyzfil: Array3xN,
539
+ dlxyzfil: Array3xN,
540
+ ifil: NDArray[float64],
541
+ obs: Array3xN,
542
+ j: Array3xN,
543
+ par: bool = True,
544
+ ) -> NDArray[float64]:
545
+ """
546
+ JxB (Lorentz) body force density (per volume) due to a linear current
547
+ filament segment at an observation point with some current density (per area).
548
+
549
+ Args:
550
+ xyzfil: [m] x,y,z coords of current filament origins (start of segment)
551
+ dlxyzfil: [m] x,y,z length delta of current filaments
552
+ ifil: [A] filament current
553
+ obs: [m] x,y,z coords of observation locations
554
+ j: [A/m^2] current density vector at observation locations
555
+ par: Whether to use CPU parallelism
556
+
557
+ Returns:
558
+ [N/m^3] body force density
559
+ """
560
+ xyzfil = _3tup_contig(xyzfil)
561
+ dlxyzfil = _3tup_contig(dlxyzfil)
562
+ ifil = ascontiguousarray(ifil).flatten()
563
+ obs = _3tup_contig(obs)
564
+ j = _3tup_contig(j)
565
+ jxbx, jxby, jxbz = em_body_force_density_linear_filament(
566
+ xyzfil, dlxyzfil, ifil, obs, j, par
567
+ ) # [N/m^3]
568
+
569
+ return jxbx, jxby, jxbz # type: ignore
570
+
571
+
572
+ def _3tup_contig(
573
+ t: Array3xN,
574
+ ) -> tuple[NDArray[float64], NDArray[float64], NDArray[float64]]:
575
+ """Make contiguous references or copies to arrays in a 3-tuple. Only copies data if it is not already contiguous."""
576
+ return (
577
+ ascontiguousarray(t[0]).flatten(),
578
+ ascontiguousarray(t[1]).flatten(),
579
+ ascontiguousarray(t[2]).flatten(),
580
+ )
581
+
582
+
583
+ def _2tup_contig(
584
+ t: tuple[NDArray[float64], NDArray[float64]],
585
+ ) -> tuple[NDArray[float64], NDArray[float64]]:
586
+ """Make contiguous references or copies to arrays in a 2-tuple. Only copies data if it is not already contiguous."""
587
+ return (ascontiguousarray(t[0]).flatten(), ascontiguousarray(t[1]).flatten())
cfsem/py.typed ADDED
File without changes
cfsem/types.py ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Tuple, Union
2
+ from numpy.typing import NDArray
3
+ from numpy import float64
4
+
5
+ Array3xN = Union[
6
+ NDArray[float64], Tuple[NDArray[float64], NDArray[float64], NDArray[float64]]
7
+ ]
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: cfsem
3
+ Version: 2.3.1
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Requires-Dist: numpy>=2
8
+ Requires-Dist: interpn>=0.2.5
9
+ Requires-Dist: pytest>=7.1.3 ; extra == 'dev'
10
+ Requires-Dist: coverage>=6.5.0 ; extra == 'dev'
11
+ Requires-Dist: ruff>=0.6.2 ; extra == 'dev'
12
+ Requires-Dist: pyright==1.1.393 ; extra == 'dev'
13
+ Requires-Dist: scipy>=1.8 ; extra == 'dev'
14
+ Requires-Dist: mkdocs>=1.6.0 ; extra == 'dev'
15
+ Requires-Dist: mkdocs-material>=9.5.25 ; extra == 'dev'
16
+ Requires-Dist: mkdocstrings[python]>=0.25.1 ; extra == 'dev'
17
+ Requires-Dist: matplotlib>=3.9 ; extra == 'dev'
18
+ Provides-Extra: dev
19
+ License-File: LICENSE
20
+ Summary: Quasi-steady electromagnetics including filamentized approximations, Biot-Savart, and Grad-Shafranov.
21
+ Author-email: Commonwealth Fusion Systems <jlogan@cfs.energy>
22
+ License: MIT
23
+ Requires-Python: >=3.9, <3.14
24
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
25
+
26
+ # cfsem
27
+
28
+ [Docs - Rust](https://docs.rs/cfsem) | [Docs - Python](https://cfsem-py.readthedocs.io/)
29
+
30
+ Quasi-steady electromagnetics including filamentized approximations, Biot-Savart, and Grad-Shafranov.
31
+
32
+ ## Installation
33
+
34
+ Requirements
35
+
36
+ * Python 3.9-3.12 and pip
37
+ * Don't worry about this:
38
+ * This info provided for troubleshooting purposes:
39
+ * If on an x86 processor, you will need a CPU that supports SSE through 4.1, AVX, and FMA.
40
+ * This should be true on any modern machine.
41
+
42
+ ```bash
43
+ pip install cfsem
44
+ ```
45
+
46
+ ## Development
47
+
48
+ Requirements
49
+
50
+ * [Rust](https://www.rust-lang.org/tools/install)
51
+
52
+ To install in the active python environment, do
53
+
54
+ ```bash
55
+ pip install -e .[dev]
56
+ ```
57
+
58
+ To build the Rust bindings only, do
59
+
60
+ ```bash
61
+ maturin develop --release
62
+ ```
63
+
64
+ No part of installation requires root. If access issues are encountered, this can likely be resolved by using a virtual environment.
65
+
66
+ Some computationally-expensive calculations are written in Rust. These calculations and their python bindings are installed from pre-built binaries when installing from pypi or compiled during local development installation, with no intervention from the user in either case. Symmetric bindings with docstrings are available in the `bindings.py` module and re-exported at the library level.
67
+
68
+ To build with all of the optimizations available on your local machine, you can do:
69
+
70
+ ```bash
71
+ RUSTCFLAGS="-Ctarget-cpu=native" maturin develop --release
72
+ pip install -e .[dev]
73
+ ```
74
+
75
+ ## Contributing
76
+
77
+ Contributions consistent with the goals and anti-goals of the package are welcome.
78
+
79
+ Please make an issue ticket to discuss changes before investing significant time into a branch.
80
+
81
+ Goals
82
+
83
+ * Library-level functions and formulas
84
+ * Comprehensive documentation including literature references, assumptions, and units-of-measure
85
+ * Quantitative unit-testing of formulas
86
+ * Performance (both speed and memory-efficiency)
87
+ * Guide development of performance-sensitive functions with structured benchmarking
88
+ * Cross-platform compatibility
89
+ * Minimization of long-term maintenance overhead (both for the library, and for users of the library)
90
+ * Semantic versioning
91
+ * Automated linting and formatting tools
92
+ * Centralized CI and toolchain configuration in as few files as possible
93
+
94
+ Anti-Goals
95
+
96
+ * Fanciness that increases environment complexity, obfuscates reasoning, or introduces platform restrictions
97
+ * Brittle CI or toolchain processes that drive increased maintenance overhead
98
+ * Application-level functionality (graphical interfaces, simulation frameworks, etc)
99
+
100
+ ## License
101
+
102
+ Licensed under the MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT) .
103
+
@@ -0,0 +1,9 @@
1
+ cfsem-2.3.1.dist-info/METADATA,sha256=FURrdp6mYIsLb1Z7obQ5AxFgN41vrBqt98qd5bS2Zxw,3768
2
+ cfsem-2.3.1.dist-info/WHEEL,sha256=6gg8M7kMeWmDzkhJuJzWLhBYdt7OGDF81wlSJjN85_E,96
3
+ cfsem-2.3.1.dist-info/licenses/LICENSE,sha256=Z0UP8fLqfO3bA_i2g9fzRBRjwB7lHUsnAB3jmtIW2PU,1105
4
+ cfsem/bindings.py,sha256=wFrs9Aj4E38Tjht0kRPYjMB_szgX4rho0IesYvDmdQQ,21345
5
+ cfsem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ cfsem/types.py,sha256=rr8VSLL4VkEZVmV4lSh9e6SO_MLBPTUcKME5nrIiS1Q,201
7
+ cfsem/__init__.py,sha256=yIzSHtKvfSiEkLeFj6fKFTj82WjY9VvWtnCWD-mfkwo,22394
8
+ cfsem/_cfsem.cp313-win_amd64.pyd,sha256=NFTIERDH0yN4s_JaysiCEAcLNXAPlX2Xf_H3yHDGGZk,724480
9
+ cfsem-2.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.8.3)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Commonwealth Fusion Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.