cfsem 2.0.4__cp39-none-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,536 @@
1
+ """Physics calculations shared between various magnet models"""
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_biot_savart,
13
+ flux_density_circular_filament,
14
+ gs_operator_order2,
15
+ gs_operator_order4,
16
+ inductance_piecewise_linear_filaments,
17
+ filament_helix_path,
18
+ rotate_filaments_about_path,
19
+ )
20
+
21
+ from ._cfsem import ellipe, ellipk
22
+
23
+ MU_0 = 4.0 * np.pi * 1e-7 * (1.0 + 5.5e-10)
24
+ """
25
+ Vacuum permeability with slight correction from latest measurements per NIST CODATA 2019 (SP-961),
26
+ https://www.physics.nist.gov/cuu/pdf/wall_2018.pdf .
27
+ """
28
+
29
+ __all__ = [
30
+ "flux_circular_filament",
31
+ "flux_density_biot_savart",
32
+ "flux_density_circular_filament",
33
+ "gs_operator_order2",
34
+ "gs_operator_order4",
35
+ "filament_helix_path",
36
+ "inductance_piecewise_linear_filaments",
37
+ "self_inductance_piecewise_linear_filaments",
38
+ "mutual_inductance_piecewise_linear_filaments",
39
+ "flux_density_ideal_solenoid",
40
+ "self_inductance_lyle6",
41
+ "mutual_inductance_of_circular_filaments",
42
+ "mutual_inductance_of_cylindrical_coils",
43
+ "filament_coil",
44
+ "self_inductance_circular_ring_wien",
45
+ "self_inductance_annular_ring",
46
+ "self_inductance_distributed_axisymmetric_conductor",
47
+ "ellipe",
48
+ "ellipk",
49
+ "rotate_filaments_about_path",
50
+ ]
51
+
52
+
53
+ def self_inductance_piecewise_linear_filaments(xyzp: Array3xN) -> float:
54
+ """
55
+ Estimate the self-inductance of one piecewise-linear current filament.
56
+
57
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops
58
+ for non-self-pairings, zeroes-out the contributions from self-pairings
59
+ to resolve the thin-filament self-inductance singularity, and replaces the
60
+ segment self-inductance term with an analytic value from [3].
61
+
62
+ Assumes:
63
+
64
+ * Thin, well-behaved filaments
65
+ * Uniform current distribution within segments
66
+ * Low frequency operation; no skin effect
67
+ (which would reduce the segment self-field term)
68
+ * Vacuum permeability everywhere
69
+ * Each filament has a constant current in all segments
70
+ (otherwise we need an inductance matrix)
71
+
72
+ References:
73
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
74
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
75
+
76
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
77
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
78
+
79
+ [3] R. Dengler, “Self inductance of a wire loop as a curve integral,”
80
+ AEM, vol. 5, no. 1, p. 1, Jan. 2016, doi: [10.7716/aem.v5i1.331](https://doi.org/10.7716/aem.v5i1.331)
81
+
82
+ Args:
83
+ xyzp: [m] 3xN point series describing the filament
84
+
85
+ Returns:
86
+ [H] Scalar self-inductance
87
+ """
88
+ x, y, z = xyzp
89
+ xyzfil = (x[:-1], y[:-1], z[:-1])
90
+ dlxyzfil = (x[1:] - x[:-1], y[1:] - y[:-1], z[1:] - z[:-1])
91
+
92
+ self_inductance = inductance_piecewise_linear_filaments(
93
+ xyzfil, dlxyzfil, xyzfil, dlxyzfil, True
94
+ ) # [H]
95
+
96
+ return self_inductance # [H]
97
+
98
+
99
+ def mutual_inductance_piecewise_linear_filaments(
100
+ xyz0: Array3xN,
101
+ xyz1: Array3xN,
102
+ ) -> float:
103
+ """
104
+ Estimate the mutual inductance between two piecewise-linear current filaments.
105
+
106
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops, which is
107
+ originally from [2] and can be found in a more friendly format on wikipedia.
108
+
109
+ Assumes:
110
+
111
+ * Thin, well-behaved filaments
112
+ * Vacuum permeability everywhere
113
+ * Each filament has a constant current in all segments
114
+ (otherwise we need an inductance matrix)
115
+ * All segments between the two filaments are distinct; no identical pairs
116
+
117
+ References:
118
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
119
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
120
+
121
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
122
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
123
+
124
+ Args:
125
+ xyz0: [m] 3xN point series describing the first filament
126
+ xyz1: [m] 3xM point series describing the second filament
127
+
128
+ Returns:
129
+ [H] Scalar mutual inductance between the two filaments
130
+ """
131
+
132
+ x0, y0, z0 = xyz0
133
+ xyzfil0 = (x0[:-1], y0[:-1], z0[:-1])
134
+ dlxyzfil0 = (x0[1:] - x0[:-1], y0[1:] - y0[:-1], z0[1:] - z0[:-1])
135
+
136
+ x1, y1, z1 = xyz1
137
+ xyzfil1 = (x1[:-1], y1[:-1], z1[:-1])
138
+ dlxyzfil1 = (x1[1:] - x1[:-1], y1[1:] - y1[:-1], z1[1:] - z1[:-1])
139
+
140
+ inductance = inductance_piecewise_linear_filaments(
141
+ xyzfil0, dlxyzfil0, xyzfil1, dlxyzfil1, False
142
+ ) # [H]
143
+
144
+ return inductance # [H]
145
+
146
+
147
+ def flux_density_ideal_solenoid(
148
+ current: float, num_turns: float, length: float
149
+ ) -> float:
150
+ """
151
+ Axial B-field on centerline of an ideal (infinitely long) solenoid.
152
+
153
+ This calc converges reasonably well for coil L/D > 20.
154
+
155
+ Args:
156
+ current: [A] solenoid current
157
+ num_turns: [#] number of conductor turns
158
+ length: [m] length of winding pack
159
+
160
+ Returns:
161
+ [T] B-field on axis (in the direction aligned with the axis)
162
+ """
163
+ b_on_axis = MU_0 * num_turns * current / length
164
+ return b_on_axis # [T]
165
+
166
+
167
+ def self_inductance_lyle6(r: float, dr: float, dz: float, n: float) -> float:
168
+ """
169
+ Self-inductance of a cylindrically-symmetric coil of rectangular
170
+ cross-section, estimated to 6th order.
171
+
172
+ References:
173
+ [1] T. R. Lyle,
174
+ “IX. On the self-inductance of circular coils of rectangular section,”
175
+ Philosophical Transactions of the Royal Society of London.
176
+ Series A, Containing Papers of a Mathematical or Physical Character,
177
+ vol. 213, no. 497-508, pp. 421-435, Jan. 1914, doi: [10.1098/rsta.1914.0009](https://doi.org/10.1098/rsta.1914.0009)
178
+
179
+ Args:
180
+ r: [m] radius, coil center
181
+ dr: [m] radial width of coil
182
+ dz: [m] cylindrical height of coil
183
+ n: number of turns
184
+
185
+ Returns:
186
+ [H] self-inductance
187
+ """
188
+
189
+ # Guarantee 64-bit floats needed for 6th-order shape term
190
+ a = np.float64(r)
191
+ b = np.float64(dz)
192
+ c = np.float64(dr)
193
+
194
+ # Build up reusable terms for calculation of shape parameter
195
+ d = np.sqrt(b**2 + c**2) # [m] diagonal length
196
+ u = ((b / c) ** 2) * 2 * np.log(d / b)
197
+ v = ((c / b) ** 2) * 2 * np.log(d / c)
198
+ w = (b / c) * np.arctan(c / b)
199
+ ww = (c / b) * np.arctan(b / c)
200
+
201
+ bd2 = (b / d) ** 2
202
+ cd2 = (c / d) ** 2
203
+ da2 = (d / a) ** 2
204
+ ml = np.log(8 * a / d)
205
+
206
+ f = (
207
+ ml
208
+ + (1 + u + v - 8 * (w + ww)) / 12.0 # 0th order in d/a
209
+ + (
210
+ da2
211
+ * (
212
+ cd2 * (221 + 60 * ml - 6 * v)
213
+ + 3 * bd2 * (69 + 60 * ml + 10 * u - 64 * w)
214
+ )
215
+ )
216
+ / 5760.0 # 2nd order
217
+ + (
218
+ da2**2
219
+ * (
220
+ 2 * cd2**2 * (5721 + 3080 * ml - 345 * v)
221
+ + 5 * bd2 * cd2 * (407 + 5880 * ml + 6720 * u - 14336 * w)
222
+ - 10 * bd2**2 * (3659 + 2520 * ml + 805 * u - 6144 * w)
223
+ )
224
+ )
225
+ / 2.58048e7 # 4th order
226
+ + (
227
+ da2**3
228
+ * (
229
+ 3 * cd2**3 * (4308631 + 86520 * ml - 10052 * v)
230
+ - 14 * bd2**2 * cd2 * (617423 + 289800 * ml + 579600 * u - 1474560 * w)
231
+ + 21 * bd2**3 * (308779 + 63000 * ml + 43596 * u - 409600 * w)
232
+ + 42 * bd2 * cd2**2 * (-8329 + 46200 * ml + 134400 * u - 172032 * w)
233
+ )
234
+ )
235
+ / 1.73408256e10 # 6th order
236
+ ) # [nondim] shape parameter
237
+
238
+ self_inductance = MU_0 * (n**2) * a * f
239
+
240
+ return self_inductance # [H]
241
+
242
+
243
+ def mutual_inductance_of_circular_filaments(rzn1: NDArray, rzn2: NDArray) -> float:
244
+ """
245
+ Analytic mutual inductance between a pair of ideal cylindrically-symmetric coaxial filaments.
246
+
247
+ This is equivalent to taking the flux produced by each circular filament from
248
+ either collection of filaments to the other. Mutual inductance is reflexive,
249
+ so the order of the inputs is not important.
250
+
251
+ Args:
252
+ rzn1 (array): 3x1 array (r [m], z [m], n []) coordinates and number of turns
253
+ rzn2 (array): 3x1 array (r [m], z [m], n []) coordinates and number of turns
254
+
255
+ Returns:
256
+ float: [H] mutual inductance
257
+ """
258
+
259
+ m = mutual_inductance_of_cylindrical_coils(
260
+ rzn1.reshape((3, 1)), rzn2.reshape((3, 1))
261
+ )
262
+
263
+ return m # [H]
264
+
265
+
266
+ def mutual_inductance_of_cylindrical_coils(f1: NDArray, f2: NDArray) -> float:
267
+ """
268
+ Analytical mutual inductance between two coaxial collections of ideal filaments.
269
+
270
+ Each collection typically represents a discretized "real" cylindrically-symmetric
271
+ coil of rectangular cross-section, but could have any cross-section as long as it
272
+ maintains cylindrical symmetry.
273
+
274
+ Args:
275
+ f1: 3 x N array of filament definitions like (r [m], z [m], n [])
276
+ f2: 3 x N array of filament definitions like (r [m], z [m], n [])
277
+
278
+ Returns:
279
+ [H] mutual inductance of the two discretized coils
280
+ """
281
+ r1, z1, n1 = f1
282
+ r2, z2, n2 = f2
283
+
284
+ # Using n2 as the current per filament is equivalent to examining a 1A reference current,
285
+ # which gives us the flux per amp (inductance)
286
+ m = np.sum(
287
+ n1 * flux_circular_filament(n2, r2, z2, r1, z1)
288
+ ) # [H] total mutual inductance
289
+
290
+ return m # [H]
291
+
292
+
293
+ def filament_coil(
294
+ r: float, z: float, w: float, h: float, nt: float, nr: int, nz: int
295
+ ) -> NDArray:
296
+ """
297
+ Create an array of filaments from coil cross-section, evenly spaced
298
+ _inside_ the winding pack. No filaments are coincident with the coil surface.
299
+
300
+ Args:
301
+ r: [m] radius, coil center
302
+ z: [m] axial position, coil center
303
+ w: [m] width of coil pack
304
+ h: [m] height of coil pack
305
+ nt: turns
306
+ nr: radial discretizations
307
+ nz: axial discretizations
308
+
309
+ Returns:
310
+ (nr*nz) x 3, (r,z,n) of each filament
311
+ """
312
+
313
+ # Build a 2D mesh of points evenly spaced in the interior of the bounding rectangle
314
+ rs = np.linspace(r - w * (nr - 1) / nr / 2, r + w * (nr - 1) / nr / 2, nr) # [m]
315
+ zs = np.linspace(z - h * (nz - 1) / nz / 2, z + h * (nz - 1) / nz / 2, nz) # [m]
316
+ rmesh, zmesh = np.meshgrid(rs, zs, indexing="ij") # [m]
317
+
318
+ # Number of turns attributed to each point is not necessarily an integer
319
+ n = np.full_like(rmesh.flatten(), float(nt) / (nr * nz))
320
+
321
+ # Pack filament locations and number of turns into an array together
322
+ filaments = np.dstack([rmesh.flatten(), zmesh.flatten(), n]).reshape(nr * nz, 3)
323
+
324
+ return filaments # [m], [m], [dimensionless]
325
+
326
+
327
+ def self_inductance_circular_ring_wien(
328
+ major_radius: NDArray, minor_radius: NDArray
329
+ ) -> NDArray:
330
+ """
331
+ Wien's formula for the self-inductance of a circular ring
332
+ with thin circular cross section.
333
+
334
+ Uses equation 7 from reference [1].
335
+
336
+ References:
337
+ [1] E Rosa and L Cohen, "On the Self-Inductance of Circles,"
338
+ Bulletin of the Bureau of Standards, 1908.
339
+ [Online]. Available:
340
+ <https://nvlpubs.nist.gov/nistpubs/bulletin/04/nbsbulletinv4n1p149_A2b.pdf>
341
+
342
+ Args:
343
+ major_radius: [m] Major radius of the ring.
344
+ minor_radius: [m] Radius of the ring's cross section.
345
+
346
+ Returns:
347
+ [H] self-inductance
348
+ """
349
+ ar = major_radius / minor_radius # [], a / rho, dimensionless
350
+ ra2 = (minor_radius / major_radius) ** 2 # [], (rho / a)^2, dimensionless
351
+ # Equation 7 in Rosa & Cohen lacks the factor of 1e-7
352
+ # because it is not in SI base units.
353
+ self_inductance = (
354
+ MU_0 * major_radius * ((1 + 0.125 * ra2) * np.log(8 * ar) - 0.0083 * ra2 - 1.75)
355
+ ) # [H]
356
+ return self_inductance # [H]
357
+
358
+
359
+ def self_inductance_distributed_axisymmetric_conductor(
360
+ current: float,
361
+ grid: Tuple[NDArray, NDArray],
362
+ mesh: Tuple[NDArray, NDArray],
363
+ b_part: Tuple[NDArray, NDArray],
364
+ psi_part: NDArray,
365
+ mask: NDArray,
366
+ edge_path: Tuple[NDArray, NDArray],
367
+ ) -> Tuple[float, float, float]:
368
+ """
369
+ Calculation of a distributed conductor's self-inductance from two components:
370
+
371
+ * External inductance: the portion related to the poloidal flux exactly at the conductor edge
372
+ * Internal inductance: the portion related to the poloidal magnetic field inside the conductor,
373
+ where the filamentized method used for coils does not apply due to the parallel arrangement
374
+ and variable current density.
375
+
376
+ Note: the B-field and flux inputs are _not_ the total from all sources - they are only the
377
+ contribution from the distributed conductor under examination.
378
+
379
+ This calculation was developed for use with tokamak plasmas, but applies similarly to
380
+ other kinds of distributed axisymmetric conductor.
381
+
382
+ Assumptions:
383
+
384
+ * Cylindrically-symmetric, distributed, single-winding, contiguous conductor
385
+ * No high-magnetic-permeability materials in the vicinity
386
+ * Isopotential on the edge contour
387
+ * This is slightly less restrictive than isopotential on the section,
388
+ but notably does _not_ allow the calc to be used with, for example,
389
+ large, shell conductors where different regions are meaningfully
390
+ independent of each other.
391
+ * Conductor interior does not touch the edge of the computational domain
392
+ * At least one grid cell of padding is needed to support finite differences
393
+
394
+ References:
395
+ [1] S. Ejima, R. W. Callis, J. L. Luxon, R. D. Stambaugh, T. S. Taylor, and J. C. Wesley,
396
+ “Volt-second analysis and consumption in Doublet III plasmas,”
397
+ Nucl. Fusion, vol. 22, no. 10, pp. 1313-1319, Oct. 1982,
398
+ doi: [10.1088/0029-5515/22/10/006](https://doi.org/10.1088/0029-5515/22/10/006)
399
+
400
+ [2] J. A. Romero and J.-E. Contributors, “Plasma internal inductance dynamics in a tokamak,”
401
+ arXiv.org. Accessed: Dec. 21, 2023. [Online]. Available: https://arxiv.org/abs/1009.1984v1
402
+ doi: [10.1088/0029-5515/50/11/115002](https://doi.org/10.1088/0029-5515/50/11/115002)
403
+
404
+ [3] J. T. Wai and E. Kolemen, “GSPD: An algorithm for time-dependent tokamak equilibria design.”
405
+ arXiv, Jun. 22, 2023. Accessed: Sep. 15, 2023. [Online]. Available: https://arxiv.org/abs/2306.13163
406
+ doi: [10.48550/arXiv.2306.13163](https://doi.org/10.48550/arXiv.2306.13163)
407
+
408
+ Args:
409
+ current: [A] total toroidal current in this conductor
410
+ grid: [m] (1 X nr) grids of (R coords, Z coords)
411
+ mesh: [m] (nr X nz) meshgrids of (R coords, Z coords)
412
+ b_part: [T] (nr X nz) Flux density (R-component, Z-component) due to this conductor
413
+ psi_part: [V-s] or [T-m^2] (nr X nz) this conductor's poloidal flux field
414
+ mask: (nr X nz) positive mask of the conductor's interior region
415
+ edge_path: [m] (2 x N) closed (r, z) path along conductor edge
416
+
417
+ Returns:
418
+ (Lt, Li, Le) Total, internal, and external self-inductance components
419
+ """
420
+ # Unpack
421
+ rgrid, zgrid = grid # [m]
422
+ rmesh, zmesh = mesh # [m]
423
+ br, bz = b_part # [T]
424
+
425
+ # Set up
426
+ nr = rgrid.size
427
+ nz = zgrid.size
428
+ psi_interpolator = MulticubicRectilinear.new([rgrid, zgrid], psi_part.flatten())
429
+
430
+ # Same-length diffs assuming last grid cell is the same size
431
+ # as the previous one. In general the conductor can't touch the
432
+ # edge of the computational domain without breaking the solver,
433
+ # so it's ok to allow some potential error at the last index.
434
+ drmesh = np.zeros_like(rmesh) # [m]
435
+ drmesh[: nr - 1, :] = np.diff(rmesh, axis=0)
436
+ drmesh[-1, :] = drmesh[-2, :]
437
+
438
+ dzmesh = np.zeros_like(zmesh) # [m]
439
+ dzmesh[:, : nz - 1] = np.diff(zmesh, axis=1)
440
+ dzmesh[:, -1] = dzmesh[:, -2]
441
+
442
+ # Toroidal volume of each cell in the mesh.
443
+ #
444
+ # Ideally we'd use a two-sided difference for
445
+ # calculating dr and dz in order to properly handle cell volume
446
+ # for nonuniform grids, but (1) that's a lot of extra array handling
447
+ # for minimal real benefit and (2) the grid is almost always uniform
448
+ volmesh = 2.0 * np.pi * rmesh * drmesh * dzmesh # [m^3]
449
+
450
+ # Stored poloidal magnetic energy inside the conductor volume.
451
+ #
452
+ # Because E ~ B^2 and B = sqrt(Br^2 + Bz^2 + Btor^2) -> B^2 = Br^2 + Bz^2 + Btor^2,
453
+ # the components of magnetic energy (and induction) due to a current on different axes
454
+ # are separable, and we can ignore the toroidal field entirely when treating the
455
+ # poloidal inductance.
456
+ #
457
+ # That doesn't mean there isn't store energy related to the conductor's toroidal field,
458
+ # only that it can be separated from the poloidal inductance.
459
+ wmag_pol = (1.0 / (2.0 * MU_0)) * np.sum((br**2 + bz**2) * volmesh * mask) # [J]
460
+
461
+ # Internal inductance.
462
+ #
463
+ # Because E = (1/2) * L * I^2, we can superpose multiple inductance terms
464
+ # for a given current-carrying element, and say E = (1/2) * (Li + Le) * I^2 .
465
+ #
466
+ # Now that we have the internal stored magnetic energy, we can calculate
467
+ # the part of the self-inductance related to that energy directly.
468
+ internal_inductance = 2.0 * wmag_pol / current**2 # [H]
469
+
470
+ # External inductance.
471
+ #
472
+ # This is a hack based on Poynting's Theorem relating surface conditions to
473
+ # magnetic energy.
474
+ #
475
+ # Do weighted average of flux along the edge contour
476
+ # to adjust for the length of individual segments
477
+ edge_dr = np.diff(edge_path[0]) # [m]
478
+ edge_dz = np.diff(edge_path[1]) # [m]
479
+ edge_dl = (edge_dr**2 + edge_dz**2) ** 0.5 # [m] length of each segment
480
+ edge_length = np.sum(edge_dl) # [m] total length of conductor limit contour
481
+ edge_psi = psi_interpolator.eval([x[:-1] for x in edge_path]) # [V-s]
482
+ edge_psi_mean = np.sum(edge_psi * edge_dl) / edge_length # [V-s]
483
+ # Take the inductance
484
+ external_inductance = edge_psi_mean / current # [H]
485
+
486
+ # Total inductance.
487
+ total_inductance = internal_inductance + external_inductance # [H]
488
+
489
+ return (
490
+ float(total_inductance),
491
+ float(internal_inductance),
492
+ float(external_inductance),
493
+ )
494
+
495
+
496
+ def self_inductance_annular_ring(r: float, a: float, b: float) -> float:
497
+ """
498
+ Low-frequency self-inductance of a thick-walled tube bent in a circle.
499
+
500
+ Uses Wien's method, per the 1912 NIST handbook [1], Eqn. 64 on pg. 112,
501
+ with a correction to a misprint in term 5 and a unit conversion factor
502
+ in the final expression.
503
+
504
+ This is an approximation that drops terms of order higher than `(a/r)^2`
505
+ and `(b/r)^2`.
506
+
507
+ References:
508
+ [1] E. B. Rosa and F. W. Grover, “Formulas and tables for the calculation of mutual and self-inductance (Revised),”
509
+ BULL. NATL. BUR. STAND., vol. 8, no. 1, p. 1, Jan. 1912,
510
+ doi: [10.6028/bulletin.185](https://doi.org/10.6028/bulletin.185)
511
+
512
+ Args:
513
+ r: [m] major radius of loop
514
+ a: [m] inner minor radius (tube inside radius)
515
+ b: [m] outer minor radius (tube outside radius)
516
+
517
+ Returns:
518
+ [H] self-inductance [H]
519
+ """
520
+
521
+ if not ((r > 0.0) and (a > 0.0) and (b > 0.0)):
522
+ raise ValueError("All radii must be positive and nonzero.")
523
+ if not (a < b):
524
+ raise ValueError("Inner minor radius must be less than outer minor radius.")
525
+ if not (b < r):
526
+ raise ValueError("Outer minor radius must be less than major radius.")
527
+
528
+ term1 = (1.0 + (a**2 + b**2) / (8.0 * r**2)) * np.log(8.0 * r / b)
529
+ term2 = -1.75 + (2.0 * b**2 + a**2) / (32.0 * r**2)
530
+ term3 = -0.5 * a**2 / (b**2 - a**2)
531
+ term4 = (a**4 / ((b**2 - a**2) ** 2)) * (1.0 + a**2 / (8.0 * r**2)) * np.log(b / a)
532
+ term5 = -(a**4 + a**2 * b**2 + b**4) / (48.0 * r**2 * (b**2 - a**2))
533
+
534
+ self_inductance = MU_0 * r * (term1 + term2 + term3 + term4 + term5) # [H]
535
+
536
+ return self_inductance # [H]
Binary file
cfsem/bindings.py ADDED
@@ -0,0 +1,362 @@
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_biot_savart as em_flux_density_biot_savart
15
+ from ._cfsem import flux_density_circular_filament as em_flux_density_circular_filament
16
+ from ._cfsem import gs_operator_order2 as em_gs_operator_order2
17
+ from ._cfsem import gs_operator_order4 as em_gs_operator_order4
18
+ from ._cfsem import (
19
+ inductance_piecewise_linear_filaments as em_inductance_piecewise_linear_filaments,
20
+ )
21
+ from ._cfsem import filament_helix_path as em_filament_helix_path
22
+ from ._cfsem import rotate_filaments_about_path as em_rotate_filaments_about_path
23
+
24
+
25
+ def flux_circular_filament(
26
+ ifil: NDArray[float64],
27
+ rfil: NDArray[float64],
28
+ zfil: NDArray[float64],
29
+ rprime: NDArray[float64],
30
+ zprime: NDArray[float64],
31
+ ) -> NDArray[float64]:
32
+ """
33
+ Flux contributions from some circular filaments to some observation points,
34
+ which happens to be the Green's function for the Grad-Shafranov solve.
35
+
36
+ This represents the integral of $\\vec{B} \\cdot \\hat{n} \\, dA$ from the z-axis to each
37
+ (`rprime`, `zprime`) observation location with $\\hat{n}$ oriented parallel to the z-axis.
38
+
39
+ A convenient interpretation of the flux is as the mutual inductance
40
+ per secondary coil current between a filament at (`rfil`, `zfil`) and a secondary
41
+ filament at (`rprime`, `zprime`); this can be used to get the mutual inductance
42
+ between two filamentized coils as the sum of I1 * I2 * flux_from_coil_1_to_coil_2.
43
+ Because mutual inductance is reflexive, the order of the coils can be reversed and
44
+ the same result is obtained.
45
+
46
+ This function is fairly well-optimized and runs about >10x faster than an
47
+ equivalent implementation in numpy+scipy, with lower RAM usage
48
+ (O(n) in number of observation locations) than what would be required
49
+ for a tiled implementation. Numba does not have an implementation of
50
+ elliptic integrals and will not accept scipy's.
51
+
52
+ Args:
53
+ ifil: [A] filament current
54
+ rfil: [m] filament R-coord
55
+ zfil: [m] filament Z-coord
56
+ rprime: [m] Observation point R-coord
57
+ zprime: [m] Observation point Z-coord
58
+
59
+ Returns:
60
+ [T-m^2] or [V-s] psi, poloidal flux at each observation point
61
+ """
62
+ ifil = ascontiguousarray(ifil)
63
+ rfil = ascontiguousarray(rfil)
64
+ zfil = ascontiguousarray(zfil)
65
+ rprime = ascontiguousarray(rprime)
66
+ zprime = ascontiguousarray(zprime)
67
+ psi = em_flux_circular_filament(ifil, rfil, zfil, rprime, zprime)
68
+ return psi # [T-m^2] or [V-s]
69
+
70
+
71
+ def flux_density_circular_filament(
72
+ ifil: NDArray[float64],
73
+ rfil: NDArray[float64],
74
+ zfil: NDArray[float64],
75
+ rprime: NDArray[float64],
76
+ zprime: NDArray[float64],
77
+ ) -> tuple[NDArray[float64], NDArray[float64]]:
78
+ """
79
+ Off-axis Br,Bz components for a circular current filament in vacuum.
80
+
81
+ Near-exact formula (except numerically-evaluated elliptic integrals)
82
+ See eqns. 12, 13 pg. 34 in [1], eqn 9.8.7 in [2], and all of [3].
83
+
84
+ Note the formula for Br as given by [1] is incorrect and does not satisfy the
85
+ constraints of the calculation without correcting by a factor of ($z / r$).
86
+
87
+ References:
88
+ [1] D. B. Montgomery and J. Terrell,
89
+ “Some Useful Information For The Design Of Aircore Solenoids,
90
+ Part I. Relationships Between Magnetic Field, Power, Ampere-Turns
91
+ And Current Density. Part II. Homogeneous Magnetic Fields,”
92
+ Massachusetts Inst. Of Tech. Francis Bitter National Magnet Lab, Cambridge, MA,
93
+ Nov. 1961. Accessed: May 18, 2021. [Online].
94
+ Available: <https://apps.dtic.mil/sti/citations/tr/AD0269073>
95
+
96
+ [2] 8.02 Course Notes. Available:
97
+ <https://web.mit.edu/8.02t/www/802TEAL3D/visualizations/coursenotes/modules/guide09.pdf>
98
+
99
+ [3] Eric Dennyson, "Magnet Formulas". Available:
100
+ <https://tiggerntatie.github.io/emagnet-py/offaxis/off_axis_loop.html>
101
+
102
+ Args:
103
+ ifil: [A] filament current
104
+ rfil: [m] filament R-coord
105
+ zfil: [m] filament Z-coord
106
+ rprime: [m] Observation point R-coord
107
+ zprime: [m] Observation point Z-coord
108
+
109
+ Returns:
110
+ [T] (Br, Bz) flux density components
111
+ """
112
+ ifil = ascontiguousarray(ifil)
113
+ rfil = ascontiguousarray(rfil)
114
+ zfil = ascontiguousarray(zfil)
115
+ rprime = ascontiguousarray(rprime)
116
+ zprime = ascontiguousarray(zprime)
117
+ Br, Bz = em_flux_density_circular_filament(ifil, rfil, zfil, rprime, zprime)
118
+ return Br, Bz # [T]
119
+
120
+
121
+ def flux_density_biot_savart(
122
+ xyzp: Array3xN,
123
+ xyzfil: Array3xN,
124
+ dlxyzfil: Array3xN,
125
+ ifil: NDArray[float64],
126
+ par: bool = True,
127
+ ) -> Array3xN:
128
+ """
129
+ Biot-Savart law calculation for B-field contributions from many filament segments
130
+ to many observation points.
131
+
132
+ This calc is fairly well-optimized under the constraint to keep peak RAM usage to
133
+ O(n) in n observation points, which precludes tiling. It comes out about 20% faster
134
+ than an equivalent implementation in numba, which is about 4x faster than an
135
+ equivalent implementation in numpy.
136
+
137
+ Args:
138
+ xyzp: [m] x,y,z coords of observation points
139
+ xyzfil: [m] x,y,z coords of current filament origins (start of segment)
140
+ dlxyzfil: [m] x,y,z length delta of current filaments
141
+ ifil: [A] current in each filament segment
142
+ par: Whether to use CPU parallelism
143
+
144
+ Returns:
145
+ [T] (Bx, By, Bz) magnetic flux density at observation points
146
+ """
147
+ xyzp = (
148
+ ascontiguousarray(xyzp[0]),
149
+ ascontiguousarray(xyzp[1]),
150
+ ascontiguousarray(xyzp[2]),
151
+ )
152
+ xyzfil = (
153
+ ascontiguousarray(xyzfil[0]),
154
+ ascontiguousarray(xyzfil[1]),
155
+ ascontiguousarray(xyzfil[2]),
156
+ )
157
+ dlxyzfil = (
158
+ ascontiguousarray(dlxyzfil[0]),
159
+ ascontiguousarray(dlxyzfil[1]),
160
+ ascontiguousarray(dlxyzfil[2]),
161
+ )
162
+ ifil = ascontiguousarray(ifil)
163
+ return em_flux_density_biot_savart(xyzp, xyzfil, dlxyzfil, ifil, par)
164
+
165
+
166
+ def inductance_piecewise_linear_filaments(
167
+ xyzfil0: Array3xN,
168
+ dlxyzfil0: Array3xN,
169
+ xyzfil1: Array3xN,
170
+ dlxyzfil1: Array3xN,
171
+ self_inductance: bool = False,
172
+ ) -> float:
173
+ """
174
+ Estimate the mutual inductance between two piecewise-linear current filaments,
175
+ or estimate self-inductance by passing the same filaments twice and setting
176
+ `self_inductance = True`.
177
+
178
+ It may be easier to use wrappers of this function that are specialized for self- and mutual-inductance
179
+ calculations:
180
+ [`self_inductance_piecewise_linear_filaments`][cfsem.self_inductance_piecewise_linear_filaments]
181
+ and [`mutual_inductance_piecewise_linear_filaments`][cfsem.mutual_inductance_piecewise_linear_filaments].
182
+
183
+ Uses Neumann's Formula for the mutual inductance of arbitrary loops, which is
184
+ originally from [2] and can be found in a more friendly format on wikipedia.
185
+
186
+ When self_inductance flag is set, zeroes-out the contributions from self-pairings
187
+ to resolve the thin-filament self-inductance singularity and replaces the
188
+ segment self-inductance term with an analytic value from [3].
189
+
190
+ Assumes:
191
+
192
+ * Thin, well-behaved filaments
193
+ * Uniform current distribution within segments
194
+ * Low frequency operation; no skin effect
195
+ (which would reduce the segment self-field term)
196
+ * Vacuum permeability everywhere
197
+ * Each filament has a constant current in all segments
198
+ (otherwise we need an inductance matrix)
199
+
200
+ References:
201
+ [1] “Inductance,” Wikipedia. Dec. 12, 2022. Accessed: Jan. 23, 2023. [Online].
202
+ Available: <https://en.wikipedia.org/w/index.php?title=Inductance>
203
+
204
+ [2] F. E. Neumann, “Allgemeine Gesetze der inducirten elektrischen Ströme,”
205
+ Jan. 1846, doi: [10.1002/andp.18461430103](https://doi.org/10.1002/andp.18461430103)
206
+
207
+ [3] R. Dengler, “Self inductance of a wire loop as a curve integral,”
208
+ AEM, vol. 5, no. 1, p. 1, Jan. 2016, doi: [10.7716/aem.v5i1.331](https://doi.org/10.7716/aem.v5i1.331)
209
+
210
+ Args:
211
+ xyzfil0: [m] Nx3 point series describing the filament origins
212
+ dlxyzfil0: [m] Nx3 length vector of each filament
213
+ xyzfil1: [m] Nx3 point series describing the filament origins
214
+ dlxyzfil1: [m] Nx3 length vector of each filament
215
+ self_inductance: Whether this is being used as a self-inductance calc
216
+
217
+ Returns:
218
+ [H] Scalar inductance
219
+ """
220
+ xyzfil0 = (
221
+ ascontiguousarray(xyzfil0[0]),
222
+ ascontiguousarray(xyzfil0[1]),
223
+ ascontiguousarray(xyzfil0[2]),
224
+ )
225
+ dlxyzfil0 = (
226
+ ascontiguousarray(dlxyzfil0[0]),
227
+ ascontiguousarray(dlxyzfil0[1]),
228
+ ascontiguousarray(dlxyzfil0[2]),
229
+ )
230
+ xyzfil1 = (
231
+ ascontiguousarray(xyzfil1[0]),
232
+ ascontiguousarray(xyzfil1[1]),
233
+ ascontiguousarray(xyzfil1[2]),
234
+ )
235
+ dlxyzfil1 = (
236
+ ascontiguousarray(dlxyzfil1[0]),
237
+ ascontiguousarray(dlxyzfil1[1]),
238
+ ascontiguousarray(dlxyzfil1[2]),
239
+ )
240
+ return em_inductance_piecewise_linear_filaments(
241
+ xyzfil0, dlxyzfil0, xyzfil1, dlxyzfil1, self_inductance
242
+ )
243
+
244
+
245
+ def gs_operator_order2(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
246
+ """Build second-order Grad-Shafranov operator in triplet format.
247
+ Assumes regular grid spacing.
248
+
249
+ Args:
250
+ rs: [m] r-coordinates of finite difference grid
251
+ zs: [m] z-coordinates of finite difference grid
252
+
253
+ Returns:
254
+ Differential operator as triplet format sparse matrix
255
+ """
256
+ rs = ascontiguousarray(rs)
257
+ zs = ascontiguousarray(zs)
258
+ return em_gs_operator_order2(rs, zs)
259
+
260
+
261
+ def gs_operator_order4(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
262
+ """
263
+ Build fourth-order Grad-Shafranov operator in triplet format.
264
+ Assumes regular grid spacing.
265
+
266
+ Args:
267
+ rs: [m] r-coordinates of finite difference grid
268
+ zs: [m] z-coordinates of finite difference grid
269
+
270
+ Returns:
271
+ Differential operator as triplet format sparse matrix
272
+ """
273
+ rs = ascontiguousarray(rs)
274
+ zs = ascontiguousarray(zs)
275
+ return em_gs_operator_order4(rs, zs)
276
+
277
+
278
+ def filament_helix_path(
279
+ path: Array3xN,
280
+ helix_start_offset: tuple[float, float, float],
281
+ twist_pitch: float,
282
+ angle_offset: float,
283
+ ) -> NDArray[float64]:
284
+ """
285
+ Filamentize a helix about an arbitrary piecewise-linear path.
286
+
287
+ Assumes angle between sequential path segments is small and will fail
288
+ if that angle approaches or exceeds 90 degrees.
289
+
290
+ The helix initial position vector, helix_start_offset, must be in a plane normal to
291
+ the first path segment in order to produce good results. If it is not in-plane,
292
+ it will be projected on to that plane and then scaled to the magnitude of its
293
+ original length s.t. the distance from the helix to the path center is preserved
294
+ but its orientation is not.
295
+
296
+ Description of the method:
297
+
298
+ 1. Translate [filament segment n-1] to the base of [path segment n]
299
+ and call it [filament segment n]
300
+ 2. Take cross product of [path segment n] with [path segment n-1]
301
+ 3. Rotate [filament segment n] segment about the axis of that cross product
302
+ to bring it into the plane defined by [path segment n] as a normal vector
303
+ 4. Rotate [filament seg. n] about [path seg. n] to continue the helix orbit
304
+
305
+ Args:
306
+ path: [m] 3xN Centerline points
307
+ helix_start_offset: [m] (3x1) Initial position of helix rel. to centerline path
308
+ twist_pitch: [m] (scalar) Centerline length per helix orbit
309
+ angle_offset: [rad] (scalar) Initial rotation offset about centerline
310
+
311
+ Returns:
312
+ [m] 3xN array of points on the helix that twists around the path
313
+ """
314
+
315
+ # Make sure input is contiguous, reallocating only if necessary
316
+ path = ascontiguousarray(path)
317
+
318
+ # Allocate output
319
+ helix = zeros_like(path) # [m]
320
+
321
+ # Calculate, mutating output
322
+ em_filament_helix_path(
323
+ (*path,),
324
+ helix_start_offset,
325
+ twist_pitch,
326
+ angle_offset,
327
+ (*helix,),
328
+ )
329
+
330
+ return helix # [m]
331
+
332
+
333
+ def rotate_filaments_about_path(
334
+ path: Array3xN, angle_offset: float, fils: Array3xN
335
+ ) -> NDArray[float64]:
336
+ """
337
+ Rotate a path of point about another path.
338
+
339
+ Intended for rotating a helix generated by [`filament_helix_path`][cfsem.filament_helix_path]
340
+ about the centerline that was used to generate it.
341
+
342
+ Args:
343
+ path: [m] x,y,z Centerline points
344
+ angle_offset: [rad] (scalar) Initial rotation offset about centerline
345
+ fils: [m] x,y,z Filaments to rotate around centerline
346
+
347
+ Returns:
348
+ [m] 3xN array of points on the helix that twists around the path
349
+ """
350
+
351
+ # Make sure input is contiguous, reallocating only if necessary
352
+ path = ascontiguousarray(path)
353
+
354
+ new_fils = ascontiguousarray(fils).copy()
355
+
356
+ em_rotate_filaments_about_path(
357
+ (*path,),
358
+ angle_offset,
359
+ (*new_fils,),
360
+ )
361
+
362
+ return new_fils # [m]
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.3
2
+ Name: cfsem
3
+ Version: 2.0.4
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.2
9
+ Requires-Dist: pytest >=7.1.3 ; extra == 'dev'
10
+ Requires-Dist: coverage >=6.5.0 ; extra == 'dev'
11
+ Requires-Dist: ruff >=0.4.2 ; extra == 'dev'
12
+ Requires-Dist: pyright >=1.1.331 ; 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: jlogan@cfs.energy
22
+ License: MIT
23
+ Requires-Python: >=3.9, <3.13
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.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 compiled during installation with no intervention from the user. 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.0.4.dist-info/METADATA,sha256=HBjZT0DVx0JtWcRp5McLcmP0RdKdLnX2_KCi3QnLe8M,3649
2
+ cfsem-2.0.4.dist-info/WHEEL,sha256=iLyJQhiiBYwo5FdZQS-97uaKimQdR9d5M5S1bVJlIxM,94
3
+ cfsem-2.0.4.dist-info/license_files/LICENSE,sha256=Z0UP8fLqfO3bA_i2g9fzRBRjwB7lHUsnAB3jmtIW2PU,1105
4
+ cfsem/bindings.py,sha256=1moqLIQUbikQOW4Av4S4ePK13prKsHIYIoEE9Tb4B3A,13698
5
+ cfsem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ cfsem/types.py,sha256=rr8VSLL4VkEZVmV4lSh9e6SO_MLBPTUcKME5nrIiS1Q,201
7
+ cfsem/__init__.py,sha256=EK8b36_CPGgrTkYRRyQG56Zx3lHCHUnND654xv-4lKY,20536
8
+ cfsem/_cfsem.cp39-win_amd64.pyd,sha256=Yu99cdcGpFylbEUrGkudxORt54yx6BdxnLxJfKQoG-A,486400
9
+ cfsem-2.0.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.7.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-none-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.