cfsem 2.4.3__tar.gz → 2.5.0__tar.gz

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.
Files changed (69) hide show
  1. {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/test_python.yml +4 -1
  2. {cfsem-2.4.3 → cfsem-2.5.0}/CHANGELOG_PYTHON.md +11 -0
  3. {cfsem-2.4.3 → cfsem-2.5.0}/CHANGELOG_RUST.md +1 -1
  4. {cfsem-2.4.3 → cfsem-2.5.0}/Cargo.lock +1 -1
  5. {cfsem-2.4.3 → cfsem-2.5.0}/Cargo.toml +1 -1
  6. cfsem-2.5.0/PKG-INFO +27 -0
  7. cfsem-2.5.0/cfsem/solenoid_stress/solenoid_1d.py +317 -0
  8. cfsem-2.5.0/cfsem/solenoid_stress/solenoid_handcalc.py +91 -0
  9. cfsem-2.5.0/cfsem/solenoid_stress/thick_wall_cylinder_handcalc.py +69 -0
  10. cfsem-2.5.0/docs/python/solenoid_stress.md +5 -0
  11. {cfsem-2.4.3 → cfsem-2.5.0}/examples/helmholtz.py +3 -1
  12. {cfsem-2.4.3 → cfsem-2.5.0}/examples/inductance.py +1 -0
  13. {cfsem-2.4.3 → cfsem-2.5.0}/mkdocs.yml +2 -0
  14. {cfsem-2.4.3 → cfsem-2.5.0}/pyproject.toml +4 -1
  15. cfsem-2.5.0/test/__init__.py +0 -0
  16. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_electromagnetics.py +2 -2
  17. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_funcs.py +2 -1
  18. cfsem-2.5.0/test/test_solenoid_stress.py +290 -0
  19. {cfsem-2.4.3 → cfsem-2.5.0}/uv.lock +222 -1
  20. cfsem-2.4.3/PKG-INFO +0 -124
  21. {cfsem-2.4.3 → cfsem-2.5.0}/.cargo/config.toml +0 -0
  22. {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/release_python.yml +0 -0
  23. {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/release_rust.yml +0 -0
  24. {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/test_rust.yml +0 -0
  25. {cfsem-2.4.3 → cfsem-2.5.0}/.gitignore +0 -0
  26. {cfsem-2.4.3 → cfsem-2.5.0}/.readthedocs.yml +0 -0
  27. {cfsem-2.4.3 → cfsem-2.5.0}/CONTRIBUTING.md +0 -0
  28. {cfsem-2.4.3 → cfsem-2.5.0}/LICENSE +0 -0
  29. {cfsem-2.4.3 → cfsem-2.5.0}/README.md +0 -0
  30. {cfsem-2.4.3 → cfsem-2.5.0}/benches/circular_filament.rs +0 -0
  31. {cfsem-2.4.3 → cfsem-2.5.0}/benches/linear_filament.rs +0 -0
  32. {cfsem-2.4.3 → cfsem-2.5.0}/build_docs.sh +0 -0
  33. {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/__init__.py +0 -0
  34. {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/bindings.py +0 -0
  35. {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/py.typed +0 -0
  36. {cfsem-2.4.3/test → cfsem-2.5.0/cfsem/solenoid_stress}/__init__.py +0 -0
  37. {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/types.py +0 -0
  38. {cfsem-2.4.3 → cfsem-2.5.0}/docs/assets/logo.svg +0 -0
  39. {cfsem-2.4.3 → cfsem-2.5.0}/docs/index.md +0 -0
  40. {cfsem-2.4.3 → cfsem-2.5.0}/docs/javascripts/mathjax.js +0 -0
  41. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/example_outputs/.placeholder +0 -0
  42. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/examples.md +0 -0
  43. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/filament.md +0 -0
  44. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/flux_density.md +0 -0
  45. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/force.md +0 -0
  46. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/grad_shafranov.md +0 -0
  47. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/inductance.md +0 -0
  48. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/math.md +0 -0
  49. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/types.md +0 -0
  50. {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/vector_potential.md +0 -0
  51. {cfsem-2.4.3 → cfsem-2.5.0}/docs/requirements.txt +0 -0
  52. {cfsem-2.4.3 → cfsem-2.5.0}/docs/stylesheets/extra.css +0 -0
  53. {cfsem-2.4.3 → cfsem-2.5.0}/katex-header.html +0 -0
  54. {cfsem-2.4.3 → cfsem-2.5.0}/src/lib.rs +0 -0
  55. {cfsem-2.4.3 → cfsem-2.5.0}/src/math.rs +0 -0
  56. {cfsem-2.4.3 → cfsem-2.5.0}/src/mesh.rs +0 -0
  57. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/biotsavart.rs +0 -0
  58. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/circular_filament.rs +0 -0
  59. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/gradshafranov.rs +0 -0
  60. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/linear_filament.rs +0 -0
  61. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/mesh_filament.rs +0 -0
  62. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/mod.rs +0 -0
  63. {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/point_source.rs +0 -0
  64. {cfsem-2.4.3 → cfsem-2.5.0}/src/python.rs +0 -0
  65. {cfsem-2.4.3 → cfsem-2.5.0}/src/testing.rs +0 -0
  66. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_examples.py +0 -0
  67. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_filaments.py +0 -0
  68. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_gradshafranov.py +0 -0
  69. {cfsem-2.4.3 → cfsem-2.5.0}/test/test_math.py +0 -0
@@ -5,7 +5,10 @@ name: test_python
5
5
 
6
6
  on:
7
7
  # push: [] # Save time on actions
8
- pull_request: []
8
+ pull_request:
9
+ branches: [ "*" ]
10
+ push:
11
+ branches: [ "main", "develop", "release" ]
9
12
  workflow_dispatch:
10
13
  tag: "Manual Run"
11
14
  workflow_call:
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 2025-08-05
4
+
5
+ ### Added
6
+
7
+ * Add `solenoid_stress` module
8
+ * 1D finite-difference solver for solenoid stress given arbitrary B-field, current density, and surface load
9
+ * Generalizes over both long-solenoid stress and ideal thick-walled pressure vessel stress
10
+ * Tested against handcalcs for both ideal solenoid & thick-walled pressure vessel
11
+ * 1D handcalc for solenoid stress under uniform current density and linear B-field profile
12
+ * 1D handcalc for thick-walled pressure vessel
13
+
3
14
  ## 2.4.3 2025-07-07
4
15
 
5
16
  ### Changed
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 2.4.3 2025-07-07
3
+ ## 2.4.3 2025-07-07, 2.5.0 2025-08-05
4
4
 
5
5
  ### Changed
6
6
 
@@ -64,7 +64,7 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
64
64
 
65
65
  [[package]]
66
66
  name = "cfsem"
67
- version = "2.4.3"
67
+ version = "2.5.0"
68
68
  dependencies = [
69
69
  "criterion",
70
70
  "itertools 0.14.0",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cfsem"
3
- version = "2.4.3"
3
+ version = "2.5.0"
4
4
  edition = "2024"
5
5
  authors = ["Commonwealth Fusion Systems <jlogan@cfs.energy>"]
6
6
  license = "MIT"
cfsem-2.5.0/PKG-INFO ADDED
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: cfsem
3
+ Version: 2.5.0
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: findiff>=0.12.1
10
+ Requires-Dist: pydantic>=2
11
+ Requires-Dist: pydantic-numpy>=6
12
+ Requires-Dist: pytest>=7.1.3 ; extra == 'dev'
13
+ Requires-Dist: coverage>=6.5.0 ; extra == 'dev'
14
+ Requires-Dist: ruff>=0.6.2 ; extra == 'dev'
15
+ Requires-Dist: pyright==1.1.402 ; extra == 'dev'
16
+ Requires-Dist: scipy>=1.8 ; extra == 'dev'
17
+ Requires-Dist: mkdocs>=1.6.0 ; extra == 'dev'
18
+ Requires-Dist: mkdocs-material>=9.5.25 ; extra == 'dev'
19
+ Requires-Dist: mkdocstrings[python]>=0.25.1 ; extra == 'dev'
20
+ Requires-Dist: matplotlib>=3.9 ; extra == 'dev'
21
+ Provides-Extra: dev
22
+ License-File: LICENSE
23
+ Summary: Quasi-steady electromagnetics including filamentized approximations, Biot-Savart, and Grad-Shafranov.
24
+ Home-Page: https://github.com/cfs-energy/cfsem-py
25
+ Author-email: Commonwealth Fusion Systems <jlogan@cfs.energy>
26
+ License-Expression: MIT
27
+ Requires-Python: >=3.9, <3.14
@@ -0,0 +1,317 @@
1
+ """
2
+ Finite-difference solution to structural PDE for pancake or
3
+ infinite-length solenoid coil, following Iwasa 2e section 3.6.
4
+
5
+ Assumes
6
+ * Uniform current density within each r-section
7
+ * Zero R-Z shear (deck-of-cards structure)
8
+ * Zero Z-force,stress,strain (no axial compression accounted here; can be evaluated separately;
9
+ usually relatively small)
10
+ * Isotropic material
11
+ * This method could be extended to handle orthotropic material, with some effort
12
+ * Single material region
13
+ * This method could be extended to handle multiple regions of isotropic materials
14
+
15
+ Supports
16
+ * Non-uniform r-grid
17
+ * Arbitrary current density and B-field defined on r-grid
18
+ * Nonzero pressure on inner/outer wall (fluid pressure, bucking load, etc)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from functools import cached_property
25
+ from logging import getLogger
26
+ from pathlib import Path
27
+ from typing import Callable, Literal
28
+
29
+ import findiff
30
+ import numpy as np
31
+ from numpy.typing import NDArray
32
+ from pydantic import ConfigDict
33
+ from pydantic_numpy.model import NumpyModel
34
+ from pydantic_numpy.typing import NpNDArray # Array of any type or dimensionality
35
+ from scipy import io, sparse
36
+ from scipy.sparse import csc_matrix as CSC
37
+ from scipy.sparse import csr_matrix as CSR
38
+ from scipy.sparse.linalg import factorized
39
+
40
+
41
+ def solenoid_1d_structural_factor(elasticity_modulus: float, poisson_ratio: float) -> float:
42
+ """Structural factor applied to RHS of solenoid stress solve"""
43
+ c = (1.0 - poisson_ratio**2) / elasticity_modulus # [m/N]
44
+ return c
45
+
46
+
47
+ def solenoid_1d_structural_rhs(
48
+ c: float,
49
+ j: NDArray | list[float],
50
+ bz: NDArray | list[float],
51
+ pi: float = 0.0,
52
+ po: float = 0.0,
53
+ ) -> NDArray:
54
+ """
55
+ Right-hand-side for solenoid stress solve,
56
+ including zero values at the BCs.
57
+
58
+ From Iwasa 2e eqn. 3.64a
59
+
60
+ Recommend padding the grid with a dummy value at either end
61
+ to make room for the BCs without losing accounting of nonzero
62
+ current density at the inner/outer radius.
63
+
64
+ Padding for BCs can be done like:
65
+ `rgrid = np.array([r0 - 1e-6] + rgrid.tolist() + [r1 + 1e-6])`
66
+
67
+ Padding region is ultimately treated as structural material,
68
+ so the padded region should be small to avoid introducing error,
69
+ but not so small that it causes numerical error in the finite difference scheme.
70
+
71
+ Args:
72
+ c: [m/N] scalar structural factor; see `solenoid_1d_structural_factor()`
73
+ j: [A/m^2] with shape (n x 1), current density at each point in the r-grid
74
+ bz: [T] with shape (n x 1), Z-axis B-field at each point in the r-grid
75
+ pi: [Pa] scalar pressure on inner wall, defined in +r direction
76
+ po: [Pa] scalar pressure on outer wall, defined in -r direction
77
+
78
+ Returns:
79
+ -c * j * bz, [1/m^2] with shape (n x 1), the right-hand side of the solenoid stress PDE
80
+ """
81
+ # Guarantee arrays
82
+ j = np.array(j)
83
+ bz = np.array(bz)
84
+
85
+ # RHS without BCs
86
+ rhs = -c * j * bz
87
+
88
+ # BC for r-stress at inner and outer radius
89
+ # is the fluid or mechanical pressure, which is usually going to be set to zero
90
+ # to represent an unsupported system, but could be set to a nonzero value
91
+ # to represent a surface load.
92
+ rhs[0] = -pi # Sign convention: compression is negative stress
93
+ rhs[-1] = -po
94
+
95
+ return rhs
96
+
97
+
98
+ class SolenoidStress1D(NumpyModel):
99
+ model_config = ConfigDict(validate_assignment=True, frozen=True, extra="forbid")
100
+
101
+ rgrid: NpNDArray
102
+ """[m] 1D grid of r-coordinates"""
103
+ elasticity_modulus: float
104
+ """[Pa] diagonal terms in material property matrix"""
105
+ poisson_ratio: float
106
+ """[dimensionless] factor determining off-diagonal terms in material property matrix"""
107
+ order: Literal[2, 4] = 4
108
+ """Finite-difference stencil polynomial order.
109
+ Higher order operators produce excessive numerical error under typical use."""
110
+ direct_inverse: bool = False
111
+ """Whether to generate fully-dense direct inverse of the system, which
112
+ can be useful as a linear operator. Alternatively, the system can be solved
113
+ using an LU solver with reduced memory usage and better numerical conditioning."""
114
+
115
+ @cached_property
116
+ def operators(self) -> SolenoidStress1DOperators:
117
+ """
118
+ Linear operators for solving stress and strain in a pancake coil
119
+ following Iwasa 2e section 3.6.
120
+ """
121
+ return solenoid_1d_structural_operators(
122
+ np.array(self.rgrid), self.elasticity_modulus, self.poisson_ratio, self.order, self.direct_inverse
123
+ )
124
+
125
+ @cached_property
126
+ def displacement_solver(self) -> Callable[[NDArray], NDArray]:
127
+ """LU solver for load-displacement relation (A_ub)
128
+ as an alternative to taking a direct inverse of A_bu"""
129
+ return factorized(self.operators.a_bu)
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class SolenoidStress1DOperators:
134
+ """
135
+ Linear operators for solving stress and strain in a pancake coil
136
+ following Iwasa 2e section 3.6.
137
+
138
+ A_bu, (n x n) sparse operator mapping displacement to the RHS like A @ u_r = -c * j * bz
139
+ A_ub, (n x n) fully-dense direct inverse of A_bu mapping RHS to displacement
140
+ A_eu (2n x n), A_eu_radial (n x n), A_eu_hoop (n x n), sparse operators mapping displacement to strain
141
+ * First entry is combined operator producing both strain components
142
+ * Second and third entries are split operators, which are equivalent because they are fully decoupled
143
+ A_se (2n x 2n), sparse operator mapping strain to stress
144
+ """
145
+
146
+ a_bu: CSC
147
+ """(n x n) sparse operator mapping displacement to the RHS like A @ u_r = -c * j * bz"""
148
+ a_ub: NDArray | None
149
+ """(n x n) fully-dense direct inverse of A_bu mapping RHS to displacement.
150
+ Only generated if `direct_inverse` flag is set."""
151
+ a_eu: CSR
152
+ """(2n x n), sparse operator mapping displacement to strain; contains both radial and hoop components"""
153
+ a_eu_radial: CSR
154
+ """(n x n), sparse operators mapping displacement to strain; radial component only"""
155
+ a_eu_hoop: CSR
156
+ """(n x n), sparse operators mapping displacement to strain; hoop component only"""
157
+ a_se: CSR
158
+ """(2n x 2n), sparse operator mapping strain to stress"""
159
+
160
+ def write_mat(self, dst: str | Path) -> str:
161
+ """Write the collection of operators in .mat format.
162
+
163
+ Args:
164
+ dst: Target directory to place the file named "stress_operators.mat"
165
+
166
+ Raises:
167
+ IOError: If the directory does not exist
168
+ """
169
+ # Check directory
170
+ dst = Path(dst).absolute()
171
+ fpath = dst / "stress_operators.mat"
172
+ getLogger("cfsem").info(f"Saving stress operator data to {fpath}")
173
+
174
+ to_save = {
175
+ "A_bu": self.a_bu,
176
+ "A_ub": self.a_ub,
177
+ "A_eu": self.a_eu,
178
+ "A_eu_radial": self.a_eu_radial,
179
+ "A_eu_hoop": self.a_eu_hoop,
180
+ "A_se": self.a_se,
181
+ }
182
+
183
+ if self.a_ub is None: # savemat fails on None value
184
+ to_save.pop("A_ub")
185
+
186
+ # Note this will implicitly convert all CSR matrices to CSC, which is .mat's preferred I/O
187
+ io.savemat(fpath, to_save)
188
+
189
+ return f"{fpath}"
190
+
191
+
192
+ def solenoid_1d_structural_operators(
193
+ rgrid: NDArray | list,
194
+ elasticity_modulus: float,
195
+ poisson_ratio: float,
196
+ order: Literal[2, 4],
197
+ direct_inverse: bool = False,
198
+ ) -> SolenoidStress1DOperators:
199
+ """
200
+ Linear operators for solving stress and strain in a pancake coil
201
+ following Iwasa 2e section 3.6.
202
+
203
+ Assumes
204
+ * Uniform current density within each r-section
205
+ * Zero R-Z shear (deck-of-cards structure)
206
+ * Zero Z-force,stress,strain (no axial compression accounted here; can be evaluated separately;
207
+ usually relatively small)
208
+ * Isotropic material
209
+ * This method could be extended to handle orthotropic material, with some effort
210
+ * Single material region
211
+ * This method could be extended to handle multiple regions of isotropic materials
212
+
213
+ Supports
214
+ * Non-uniform r-grid
215
+ * Arbitrary current density and B-field defined on r-grid
216
+
217
+ Args:
218
+ rgrid: [m] with shape (n x 1), Grid of r-coordinates. Must be sorted ascending.
219
+ elasticity_modulus: [N/m] Material property; Young's modulus
220
+ poisson_ratio: [dimensionless] Material property; off-axis stress coupling term
221
+ order: Finite-difference stencil polynomial order
222
+ direct_inverse: Whether to generate fully-dense direct inverse of the system, which
223
+ can be useful as a linear operator.
224
+ Alternatively, the system can be solved using an LU solver with
225
+ reduced memory usage and better numerical conditioning.
226
+
227
+ Returns:
228
+ object containing linear operators
229
+ """
230
+ # Guarantee arrays
231
+ rgrid = np.array(rgrid)
232
+ nr = len(rgrid)
233
+
234
+ #
235
+ # Stencils / differential operators
236
+ #
237
+
238
+ # Note these will include the 1/dr and 1/dr^2 scalings,
239
+ # not just the normalized stencil
240
+ ddr = findiff.Diff(0, rgrid, acc=order)
241
+ d2dr2 = ddr**2
242
+
243
+ ddr = ddr.matrix((nr,))
244
+ d2dr2 = d2dr2.matrix((nr,))
245
+
246
+ rinv = CSR(np.diag(1.0 / rgrid))
247
+ rinv2 = CSR(np.diag(1.0 / rgrid**2))
248
+
249
+ #
250
+ # Displacement-load relation
251
+ #
252
+
253
+ # The first and last row of the u-b relation will be modified later
254
+ # to include the boundary conditions
255
+ #
256
+ # Iwasa 2e eqn. 3.64a left hand side
257
+ #
258
+ # This one is converted to CSC because, while it's easiest to build it as CSR,
259
+ # it is more useful for solving a system as CSC
260
+ a_bu = CSC(d2dr2 + (rinv @ ddr) - rinv2) # Operator maps displacement TO rhs (-c*j*B)
261
+
262
+ # Stress-strain relation components
263
+ # that are needed for BCs
264
+ # Iwasa 2e eqns. 3.63g
265
+ a_stress_strain = np.array(
266
+ [
267
+ [1.0 / elasticity_modulus, -poisson_ratio / elasticity_modulus],
268
+ [-poisson_ratio / elasticity_modulus, 1.0 / elasticity_modulus],
269
+ ]
270
+ )
271
+ a_strain_stress = np.linalg.inv(a_stress_strain)
272
+ diag_term = a_strain_stress[0, 0]
273
+ off_diag_term = a_strain_stress[1, 0]
274
+
275
+ # We only need the first and last row of this matrix.
276
+ # Actualizing the whole thing is easy but unnecessary
277
+ # This is extracted by hand-calc starting from
278
+ # Iwasa 2e eqns. 3.63a-d
279
+ bcmat = diag_term * ddr + off_diag_term * rinv
280
+
281
+ # Apply BC to displacement operator
282
+ a_bu[0, :] = bcmat[0, :]
283
+ a_bu[-1, :] = bcmat[-1, :]
284
+
285
+ # Invert A_bu directly to get A_ub
286
+ # This is fully dense, which may be prohibitive in some situations
287
+ a_ub = (
288
+ None if not direct_inverse else np.linalg.inv(a_bu.todense())
289
+ ) # Operator maps RHS=-c*j*bz to displacement
290
+
291
+ #
292
+ # Strain-displacement operator(s)
293
+ #
294
+ # Iwasa 2e eqns. 3.63a-d
295
+ a_eu_hoop = rinv # Just the hoop part
296
+ a_eu_radial = ddr # Just the radial part
297
+ # fmt: off
298
+ a_eu = CSR(sparse.block_array(
299
+ [[a_eu_radial],
300
+ [a_eu_hoop]]
301
+ )) # Operator for getting strain from displacement
302
+ # fmt: on
303
+
304
+ #
305
+ # Stress-strain operators
306
+ #
307
+ # In this case, we're manually assembling the inverse of the A_es matrix
308
+ # because we know all the components from handcalc arithmetic
309
+ eye = sparse.eye(nr, nr)
310
+ # fmt: off
311
+ a_se = CSR(sparse.block_array(
312
+ [[ diag_term * eye, off_diag_term * eye],
313
+ [off_diag_term * eye, diag_term * eye]]
314
+ )) # Operator maps strain to stress
315
+ # fmt: on
316
+
317
+ return SolenoidStress1DOperators(a_bu, a_ub, a_eu, a_eu_radial, a_eu_hoop, a_se)
@@ -0,0 +1,91 @@
1
+ """
2
+ Radial and hoop stress in an infinitely long solenoid under linearly-varying self field.
3
+ The "infinite length" assumption is equivalent to assuming zero R-Z shear ("deck of cards")
4
+ and assuming no B-field in the R-direction (no Z-load or stress).
5
+
6
+ The linearly varying B-field from inside to outside allows slightly extending this
7
+ to partially account for the finite length of a real solenoid, which produces
8
+ a region of negative Bz near the outer radius (as opposed to the true infinite solenoid,
9
+ for which Bz trends to exactly zero at the outer radius).
10
+
11
+ Iwasa 2e pg 101 eqns 3.77a,b .
12
+ """
13
+
14
+ from numpy.typing import NDArray
15
+
16
+
17
+ def s_long_solenoid(
18
+ r: NDArray,
19
+ ri: float,
20
+ ro: float,
21
+ j: float,
22
+ bzi: float,
23
+ bzo: float,
24
+ poisson_ratio: float,
25
+ ) -> tuple[NDArray, NDArray]:
26
+ """
27
+ Radial and hoop stress in an infinitely long solenoid under linearly-varying self field.
28
+ The "infinite length" assumption is equivalent to assuming zero R-Z shear ("deck of cards")
29
+ and assuming no B-field in the R-direction (no Z-load or stress).
30
+
31
+ The linearly varying B-field from inside to outside allows slightly extending this
32
+ to partially account for the finite length of a real solenoid, which produces
33
+ a region of negative Bz near the outer radius (as opposed to the true infinite solenoid,
34
+ for which Bz trends to exactly zero at the outer radius).
35
+
36
+ Iwasa 2e pg 101 eqns 3.77a,b .
37
+
38
+ Assumes
39
+ * Infinitely long solenoid (no R-field or Z-stress).
40
+ * Linear B-field fall-off between inner and outer radius
41
+ * "Very long" solenoid - allows some negative field at the OD, but always linearly varying
42
+ * Uniform current density; no bulk regions of non-conducting structure
43
+ * Radial stress at inner and outer radius is zero (BC due to no support)
44
+ * Isotropic material
45
+ * No thermal stress
46
+
47
+ Can acommodate a uniform or linearly-varying background field, but not general fields.
48
+
49
+ Args:
50
+ r: [m] (n x 1) array of radius points at which to evaluate the stress
51
+ ri: [m] inner radius
52
+ ro: [m] outer radius
53
+ j: [A/m^2] current density
54
+ bzi: [T] axial B-field at inner radius
55
+ bzo: [T] axial B-field at outer radius
56
+ poisson_ratio: [dimensionless] Material property; off-axis stress coupling term
57
+
58
+ Returns:
59
+ s_radial, s_hoop - each (n x 1) with units of [Pa]
60
+ """
61
+
62
+ # Terms shared between s_radial and s_hoop
63
+ nu = poisson_ratio
64
+ rho = r / ri
65
+ alpha = ro / ri
66
+ kappa = bzo / bzi
67
+
68
+ jbr = j * bzi * ri # [Pa]
69
+ term1 = jbr / (alpha - 1.0) # [Pa]
70
+ term2 = (2.0 + nu) / 3.0
71
+ term3 = (3.0 + nu) / 8.0
72
+ term4 = alpha - kappa
73
+ term5 = 1.0 - kappa
74
+
75
+ # s_radial
76
+ term6 = ((alpha**2 + alpha + 1.0 - alpha**2 / rho**2) / (alpha + 1.0)) - rho
77
+ term7 = term3 * term5 * (alpha**2 + 1.0 - alpha**2 / rho**2 - rho**2)
78
+ s_radial = term1 * (term2 * term4 * term6 - term7) # [Pa]
79
+
80
+ # s_hoop
81
+ term8 = term2 * (alpha**2 + alpha + 1.0 + alpha**2 / rho**2) / (alpha + 1.0)
82
+ term9 = rho * (1.0 + 2.0 * nu) / 3.0
83
+ term10 = term4 * (term8 - term9)
84
+
85
+ term11 = term3 * (alpha**2 + 1.0 + alpha**2 / rho**2)
86
+ term12 = rho**2 * (1.0 + 3.0 * nu) / 8.0
87
+ term13 = term5 * (term11 - term12)
88
+
89
+ s_hoop = term1 * (term10 - term13) # [Pa]
90
+
91
+ return s_radial, s_hoop # [Pa]
@@ -0,0 +1,69 @@
1
+ """
2
+ Lamé's Equations - solutions to thick walled cylinder stress distribution for a configuration
3
+ of infinite length, with a fixed pressure boundary condition on the inner and outer radii,
4
+ with constant and isotropic material properties and small displacements.
5
+ """
6
+
7
+ from numpy.typing import NDArray
8
+
9
+
10
+ def s_hoop_thick_wall_cylinder(
11
+ r: NDArray, ri: float, ro: float, pin: float, pout: float
12
+ ) -> NDArray:
13
+ """
14
+ Hoop stress at a location in a thick walled cylinder under pressure load
15
+ with ends "capped", although the capped constraint does not affect the hoop or radial stress
16
+ compared to an infinite-length constraint.
17
+
18
+ https://www.engineeringtoolbox.com/stress-thick-walled-tube-d_949.html
19
+ https://www.suncam.com/miva/downloads/docs/303.pdf
20
+
21
+ Args:
22
+ r: [m] radius at which to evaluate
23
+ ri: [m] inner radius
24
+ ro: [m] outer radius
25
+ pin: [Pa] inside pressure
26
+ pout: [Pa] outside pressure
27
+
28
+ Returns:
29
+ [Pa] hoop stress
30
+ """
31
+ # Factors of pi cancel out
32
+ # fmt: off
33
+ s_hoop = (
34
+ (pin * ri ** 2 - pout * ro ** 2) / (ro ** 2 - ri ** 2) -
35
+ (ri ** 2 * ro ** 2 * (pout - pin) / (r ** 2 * (ro ** 2 - ri ** 2)))
36
+ )
37
+ # fmt: on
38
+
39
+ return s_hoop # [Pa] hoop stress
40
+
41
+
42
+ def s_radial_thick_wall_cylinder(r: NDArray, ri: float, ro: float, pin: float, pout: float) -> NDArray:
43
+ """
44
+ Radial stress at a location in a thick walled cylinder under pressure load
45
+ with ends "capped", although the capped constraint does not affect the hoop or radial stress
46
+ compared to an infinite-length constraint.
47
+
48
+ https://www.engineeringtoolbox.com/stress-thick-walled-tube-d_949.html
49
+ https://www.suncam.com/miva/downloads/docs/303.pdf
50
+
51
+ Args:
52
+ r: [m] radius at which to evaluate
53
+ ri: [m] inner radius
54
+ ro: [m] outer radius
55
+ pin: [Pa] inside pressure
56
+ pout: [Pa] outside pressure
57
+
58
+ Returns:
59
+ [Pa] radial stress
60
+ """
61
+ # Factors of pi cancel out
62
+ # fmt: off
63
+ s_radial = (
64
+ ((pin * ri**2 - pout * ro**2) / (ro**2 - ri**2)) + \
65
+ (ri**2 * ro**2 * (pout - pin) / (r**2 * (ro**2 - ri**2)))
66
+ )
67
+ # fmt: on
68
+
69
+ return s_radial # [Pa] radial stress
@@ -0,0 +1,5 @@
1
+ # Solenoid Stress & Strain
2
+
3
+ ::: cfsem.solenoid_stress
4
+ options:
5
+ show_submodules: true
@@ -1,9 +1,11 @@
1
1
  """Calculate the B-field from a Helmholtz coil pair."""
2
2
 
3
3
  import os
4
+
4
5
  import numpy as np
5
6
  from matplotlib import pyplot as plt
6
- from cfsem import flux_density_circular_filament, MU_0
7
+
8
+ from cfsem import MU_0, flux_density_circular_filament
7
9
 
8
10
  coil_radius = 0.2 # [m]
9
11
 
@@ -4,6 +4,7 @@ or as a piecewise-linear helix.
4
4
  """
5
5
 
6
6
  import numpy as np
7
+
7
8
  import cfsem
8
9
 
9
10
  # Center radius and height, winding pack width and height,
@@ -27,6 +27,8 @@ nav:
27
27
  - "python/vector_potential.md"
28
28
  - "python/inductance.md"
29
29
  - "python/grad_shafranov.md"
30
+ - "python/force.md"
31
+ - "python/solenoid_stress.md"
30
32
  - "python/math.md"
31
33
  - "python/types.md"
32
34
  - "Rust API": "https://docs.rs/cfsem"
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cfsem"
7
- version = "2.4.3"
7
+ version = "2.5.0"
8
8
  description = "Quasi-steady electromagnetics including filamentized approximations, Biot-Savart, and Grad-Shafranov."
9
9
  authors = [{name = "Commonwealth Fusion Systems", email = "jlogan@cfs.energy"}]
10
10
  requires-python = ">=3.9, <3.14"
@@ -16,6 +16,9 @@ classifiers = [
16
16
  dependencies = [
17
17
  "numpy >= 2",
18
18
  "interpn >= 0.2.5",
19
+ "findiff >= 0.12.1",
20
+ "pydantic >= 2",
21
+ "pydantic-numpy >= 6"
19
22
  ]
20
23
  license = "MIT"
21
24
 
File without changes
@@ -516,13 +516,13 @@ def test_wien_against_paper_examples():
516
516
  minor_radius_1 = 0.05e-2
517
517
  L_ref_1 = 654.40537 * np.pi * 1e-7 * 1e-2 # units: henry
518
518
  L_1 = cfsem.self_inductance_circular_ring_wien(major_radius_1, minor_radius_1)
519
- assert L_1 == approx(L_ref_1)
519
+ assert approx(L_ref_1) == L_1
520
520
 
521
521
  major_radius_2 = 25e-2
522
522
  minor_radius_2 = 0.5e-2
523
523
  L_ref_2 = 424.1761 * np.pi * 1e-7 * 1e-2 # units: henry
524
524
  L_2 = cfsem.self_inductance_circular_ring_wien(major_radius_2, minor_radius_2)
525
- assert L_2 == approx(L_ref_2)
525
+ assert approx(L_ref_2) == L_2
526
526
 
527
527
 
528
528
  @mark.parametrize("r", [0.775, 1.5])
@@ -3,10 +3,11 @@
3
3
  from typing import Optional
4
4
 
5
5
  import numpy as np
6
+ from interpn import MultilinearRectilinear
6
7
  from numpy.typing import NDArray
7
8
  from scipy.constants import mu_0
8
9
  from scipy.special import ellipe, ellipk
9
- from interpn import MultilinearRectilinear
10
+
10
11
  import cfsem
11
12
 
12
13