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.
- {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/test_python.yml +4 -1
- {cfsem-2.4.3 → cfsem-2.5.0}/CHANGELOG_PYTHON.md +11 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/CHANGELOG_RUST.md +1 -1
- {cfsem-2.4.3 → cfsem-2.5.0}/Cargo.lock +1 -1
- {cfsem-2.4.3 → cfsem-2.5.0}/Cargo.toml +1 -1
- cfsem-2.5.0/PKG-INFO +27 -0
- cfsem-2.5.0/cfsem/solenoid_stress/solenoid_1d.py +317 -0
- cfsem-2.5.0/cfsem/solenoid_stress/solenoid_handcalc.py +91 -0
- cfsem-2.5.0/cfsem/solenoid_stress/thick_wall_cylinder_handcalc.py +69 -0
- cfsem-2.5.0/docs/python/solenoid_stress.md +5 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/examples/helmholtz.py +3 -1
- {cfsem-2.4.3 → cfsem-2.5.0}/examples/inductance.py +1 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/mkdocs.yml +2 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/pyproject.toml +4 -1
- cfsem-2.5.0/test/__init__.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_electromagnetics.py +2 -2
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_funcs.py +2 -1
- cfsem-2.5.0/test/test_solenoid_stress.py +290 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/uv.lock +222 -1
- cfsem-2.4.3/PKG-INFO +0 -124
- {cfsem-2.4.3 → cfsem-2.5.0}/.cargo/config.toml +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/release_python.yml +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/release_rust.yml +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/.github/workflows/test_rust.yml +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/.gitignore +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/.readthedocs.yml +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/CONTRIBUTING.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/LICENSE +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/README.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/benches/circular_filament.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/benches/linear_filament.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/build_docs.sh +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/__init__.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/bindings.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/py.typed +0 -0
- {cfsem-2.4.3/test → cfsem-2.5.0/cfsem/solenoid_stress}/__init__.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/cfsem/types.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/assets/logo.svg +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/index.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/javascripts/mathjax.js +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/example_outputs/.placeholder +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/examples.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/filament.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/flux_density.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/force.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/grad_shafranov.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/inductance.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/math.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/types.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/python/vector_potential.md +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/requirements.txt +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/docs/stylesheets/extra.css +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/katex-header.html +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/lib.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/math.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/mesh.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/biotsavart.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/circular_filament.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/gradshafranov.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/linear_filament.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/mesh_filament.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/mod.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/physics/point_source.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/python.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/src/testing.rs +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_examples.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_filaments.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_gradshafranov.py +0 -0
- {cfsem-2.4.3 → cfsem-2.5.0}/test/test_math.py +0 -0
|
@@ -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
|
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
|
|
@@ -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
|
-
|
|
7
|
+
|
|
8
|
+
from cfsem import MU_0, flux_density_circular_filament
|
|
7
9
|
|
|
8
10
|
coil_radius = 0.2 # [m]
|
|
9
11
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cfsem"
|
|
7
|
-
version = "2.
|
|
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
|
|
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
|
|
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
|
-
|
|
10
|
+
|
|
10
11
|
import cfsem
|
|
11
12
|
|
|
12
13
|
|