cavsim2d 0.1.0__py3-none-any.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.
- cavsim2d/__init__.py +83 -0
- cavsim2d/analysis/__init__.py +0 -0
- cavsim2d/analysis/impedance.py +366 -0
- cavsim2d/analysis/lorentz/__init__.py +7 -0
- cavsim2d/analysis/lorentz/pressure.py +226 -0
- cavsim2d/analysis/multipacting/__init__.py +21 -0
- cavsim2d/analysis/multipacting/driver.py +487 -0
- cavsim2d/analysis/multipacting/fields.py +134 -0
- cavsim2d/analysis/multipacting/integrators.py +825 -0
- cavsim2d/analysis/multipacting/metrics.py +82 -0
- cavsim2d/analysis/multipacting/particles.py +283 -0
- cavsim2d/analysis/multipacting/plots.py +831 -0
- cavsim2d/analysis/multipacting/sey.py +47 -0
- cavsim2d/analysis/optimisation.py +1423 -0
- cavsim2d/analysis/tune/__init__.py +0 -0
- cavsim2d/analysis/tune/pyTuner.py +935 -0
- cavsim2d/analysis/tune/tuner.py +207 -0
- cavsim2d/analysis/uq/__init__.py +7 -0
- cavsim2d/analysis/uq/models.py +140 -0
- cavsim2d/analysis/uq/plots.py +229 -0
- cavsim2d/analysis/uq/sobol_sa.py +204 -0
- cavsim2d/analysis/wakefield/__init__.py +0 -0
- cavsim2d/config_files/optimisation_config.py +56 -0
- cavsim2d/config_files/uq_config.py +19 -0
- cavsim2d/constants.py +119 -0
- cavsim2d/data_module/__init__.py +0 -0
- cavsim2d/data_module/abci_data.py +759 -0
- cavsim2d/data_module/operating_points.py +311 -0
- cavsim2d/data_module/sey/sey +23 -0
- cavsim2d/geometry/__init__.py +35 -0
- cavsim2d/geometry/beampipes.py +187 -0
- cavsim2d/geometry/contours.py +209 -0
- cavsim2d/geometry/plotting.py +653 -0
- cavsim2d/geometry/primitives.py +152 -0
- cavsim2d/geometry/profile.py +1241 -0
- cavsim2d/geometry/tangency.py +390 -0
- cavsim2d/geometry/writers/__init__.py +11 -0
- cavsim2d/geometry/writers/cst.py +93 -0
- cavsim2d/geometry/writers/gmsh.py +1719 -0
- cavsim2d/geometry/writers/multipac.py +783 -0
- cavsim2d/models/__init__.py +39 -0
- cavsim2d/models/assembly.py +726 -0
- cavsim2d/models/base.py +3510 -0
- cavsim2d/models/beampipe.py +200 -0
- cavsim2d/models/bellows.py +283 -0
- cavsim2d/models/bla.py +203 -0
- cavsim2d/models/elliptical.py +1524 -0
- cavsim2d/models/elliptical_flattop.py +772 -0
- cavsim2d/models/pillbox.py +326 -0
- cavsim2d/models/rfgun.py +585 -0
- cavsim2d/models/spline.py +567 -0
- cavsim2d/models/taper.py +206 -0
- cavsim2d/processes/__init__.py +5 -0
- cavsim2d/processes/eigenmode.py +168 -0
- cavsim2d/processes/tune.py +1259 -0
- cavsim2d/processes/uq.py +728 -0
- cavsim2d/processes/wakefield.py +423 -0
- cavsim2d/solvers/ABCI/abci.py +148 -0
- cavsim2d/solvers/NGSolve/__init__.py +0 -0
- cavsim2d/solvers/NGSolve/eigen_ngsolve.py +3310 -0
- cavsim2d/solvers/NGSolve/eigen_ports.py +615 -0
- cavsim2d/solvers/NGSolve/frequency_domain_ngsolve.py +217 -0
- cavsim2d/solvers/NGSolve/ports.py +266 -0
- cavsim2d/solvers/__init__.py +16 -0
- cavsim2d/solvers/eigenmode_result.py +97 -0
- cavsim2d/solvers/objectives.py +186 -0
- cavsim2d/solvers/solver_objects.py +4496 -0
- cavsim2d/solvers/wakefield/__init__.py +33 -0
- cavsim2d/solvers/wakefield/abci.py +139 -0
- cavsim2d/solvers/wakefield/base.py +118 -0
- cavsim2d/study.py +4274 -0
- cavsim2d/utils/__init__.py +13 -0
- cavsim2d/utils/config_validation.py +229 -0
- cavsim2d/utils/cubature.py +709 -0
- cavsim2d/utils/data_utils.py +337 -0
- cavsim2d/utils/printing.py +106 -0
- cavsim2d/utils/quadrature.py +852 -0
- cavsim2d/utils/run_log.py +76 -0
- cavsim2d/utils/sampling.py +402 -0
- cavsim2d/utils/sensitivity.py +808 -0
- cavsim2d/utils/shapes.py +449 -0
- cavsim2d/utils/shared_classes.py +5 -0
- cavsim2d/utils/shared_functions.py +30 -0
- cavsim2d/utils/slope_triangles.py +165 -0
- cavsim2d/utils/style.py +157 -0
- cavsim2d/utils/surrogate.py +385 -0
- cavsim2d/utils/welcome.py +40 -0
- cavsim2d-0.1.0.dist-info/METADATA +666 -0
- cavsim2d-0.1.0.dist-info/RECORD +92 -0
- cavsim2d-0.1.0.dist-info/WHEEL +5 -0
- cavsim2d-0.1.0.dist-info/licenses/LICENSE +21 -0
- cavsim2d-0.1.0.dist-info/top_level.txt +1 -0
cavsim2d/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cavsim2d - 2D axisymmetric RF cavity simulation toolkit.
|
|
3
|
+
|
|
4
|
+
Main entry points:
|
|
5
|
+
Study - Container for multiple Cavity objects
|
|
6
|
+
EllipticalCavity - Standard elliptical cavity
|
|
7
|
+
EllipticalCavityFlatTop - Flat-top elliptical cavity
|
|
8
|
+
SplineCavity - Spline-based cavity
|
|
9
|
+
RFGun - RF gun cavity
|
|
10
|
+
Pillbox - Pillbox cavity
|
|
11
|
+
Beampipe - Straight circular beam pipe (drift element)
|
|
12
|
+
Bellows - Corrugated bellows section
|
|
13
|
+
BLA - Beam line absorber (pipe + lossy ring)
|
|
14
|
+
Taper - Conical transition between two bore radii
|
|
15
|
+
Assembly - Several devices concatenated into one beam line
|
|
16
|
+
|
|
17
|
+
Solver objects (attached as lazy properties on Cavity/Study):
|
|
18
|
+
cav.tune - TuneSolver
|
|
19
|
+
cav.eigenmode - EigenmodeSolver
|
|
20
|
+
cav.wakefield - WakefieldSolver
|
|
21
|
+
cavs.optimisation - OptimisationSolver
|
|
22
|
+
"""
|
|
23
|
+
__version__ = '0.1.0'
|
|
24
|
+
|
|
25
|
+
from cavsim2d.models import (
|
|
26
|
+
Cavity,
|
|
27
|
+
EllipticalCavity,
|
|
28
|
+
EllipticalCavityFlatTop,
|
|
29
|
+
SplineCavity,
|
|
30
|
+
RFGun,
|
|
31
|
+
Pillbox,
|
|
32
|
+
Beampipe,
|
|
33
|
+
Bellows,
|
|
34
|
+
BLA,
|
|
35
|
+
Taper,
|
|
36
|
+
Assembly,
|
|
37
|
+
CircularWaveguide,
|
|
38
|
+
)
|
|
39
|
+
from cavsim2d.study import Study
|
|
40
|
+
from cavsim2d.data_module.operating_points import OperationPoints
|
|
41
|
+
from cavsim2d.utils.welcome import show_welcome
|
|
42
|
+
|
|
43
|
+
from cavsim2d.solvers import (
|
|
44
|
+
TuneSolver,
|
|
45
|
+
EigenmodeSolver,
|
|
46
|
+
WakefieldSolver,
|
|
47
|
+
OptimisationSolver,
|
|
48
|
+
EigenmodeResult,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from cavsim2d.utils.style import apply_style, house_style, WARM
|
|
52
|
+
from cavsim2d.analysis.uq import (plot_uq_comparison, uq_comparison_table,
|
|
53
|
+
perturbation_slots, perturbation_nodes)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
'Study',
|
|
57
|
+
'Cavity',
|
|
58
|
+
'EllipticalCavity',
|
|
59
|
+
'EllipticalCavityFlatTop',
|
|
60
|
+
'SplineCavity',
|
|
61
|
+
'RFGun',
|
|
62
|
+
'Pillbox',
|
|
63
|
+
'Beampipe',
|
|
64
|
+
'Bellows',
|
|
65
|
+
'BLA',
|
|
66
|
+
'Taper',
|
|
67
|
+
'Assembly',
|
|
68
|
+
'OperationPoints',
|
|
69
|
+
'show_welcome',
|
|
70
|
+
'CircularWaveguide',
|
|
71
|
+
'TuneSolver',
|
|
72
|
+
'EigenmodeSolver',
|
|
73
|
+
'WakefieldSolver',
|
|
74
|
+
'OptimisationSolver',
|
|
75
|
+
'EigenmodeResult',
|
|
76
|
+
'apply_style',
|
|
77
|
+
'house_style',
|
|
78
|
+
'WARM',
|
|
79
|
+
'plot_uq_comparison',
|
|
80
|
+
'uq_comparison_table',
|
|
81
|
+
'perturbation_slots',
|
|
82
|
+
'perturbation_nodes',
|
|
83
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Impedance reconstructed from eigenmode results (equivalent-circuit model).
|
|
2
|
+
|
|
3
|
+
Each resonant mode is treated as a parallel RLC resonator, so a handful of
|
|
4
|
+
eigenmode quantities — the resonant frequency, the R/Q and the Q — reproduce the
|
|
5
|
+
impedance spectrum the beam sees around those resonances::
|
|
6
|
+
|
|
7
|
+
Z(f) = sum_i R_i / (1 + i Q_i (f/f_i - f_i/f)) [Ohm]
|
|
8
|
+
|
|
9
|
+
with the shunt impedance ``R = 1/2 Q (R/Q)``.
|
|
10
|
+
|
|
11
|
+
This is a *reconstruction*, not a wakefield simulation: it contains exactly the
|
|
12
|
+
modes that were solved for and nothing else — no broadband/resistive-wall
|
|
13
|
+
contribution, and nothing above the highest computed mode. Within that range it
|
|
14
|
+
is cheap, has no wake-length truncation, and lets an eigenmode result be compared
|
|
15
|
+
directly against a beam spectrum or against a wakefield solve (the frames use the
|
|
16
|
+
same columns as ``cav.wakefield.wake_z`` / ``wake_t``).
|
|
17
|
+
|
|
18
|
+
The cavity is axisymmetric, so the two transverse planes are degenerate — a
|
|
19
|
+
single transverse R/Q describes both, and the x/y average collapses to it.
|
|
20
|
+
"""
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from cavsim2d.constants import c0
|
|
27
|
+
|
|
28
|
+
C0 = c0 # speed of light [m/s]
|
|
29
|
+
|
|
30
|
+
# Impedance is quoted in kOhm as often as Ohm, so the unit is an argument rather
|
|
31
|
+
# than a convention to memorise: '' = Ohm, 'k' = kOhm, 'M' = MOhm, 'G' = GOhm
|
|
32
|
+
# (per metre for the transverse impedance).
|
|
33
|
+
SI_PREFIXES = {'': 1.0, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
|
34
|
+
|
|
35
|
+
# The unit the wakefield backends store their impedance in (ABCI's native kOhm).
|
|
36
|
+
# The eigenmode reconstruction is computed in SI Ohm and converted on the way out,
|
|
37
|
+
# so both end up in the same default unit.
|
|
38
|
+
NATIVE_Z_UNIT = 'k'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prefix_factor(unit):
|
|
42
|
+
"""``(divisor, prefix)`` for an SI prefix. ``'k'`` -> ``(1e3, 'k')``."""
|
|
43
|
+
u = '' if unit in (None, 'Ohm', 'ohm') else str(unit)
|
|
44
|
+
if u not in SI_PREFIXES:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"unknown unit prefix {unit!r}; use one of {sorted(SI_PREFIXES)} "
|
|
47
|
+
f"— '' for Ohm, 'k' for kOhm, 'M' for MOhm.")
|
|
48
|
+
return SI_PREFIXES[u], u
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def impedance_unit(unit, transverse=False):
|
|
52
|
+
"""Column/axis unit string, e.g. ``'kOhm'`` or ``'kOhm/m'``."""
|
|
53
|
+
_, u = prefix_factor(unit)
|
|
54
|
+
return f'{u}Ohm/m' if transverse else f'{u}Ohm'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def frame_unit(df, default=None):
|
|
58
|
+
"""The SI prefix a frame *declares* in its ``|Z|`` column.
|
|
59
|
+
|
|
60
|
+
``'|Z| [kOhm]'`` -> ``'k'``, ``'|Z| [Ohm/m]'`` -> ``''``. Read the unit from
|
|
61
|
+
the data rather than assuming a backend's convention: a backend that reports
|
|
62
|
+
in Ohm and one that reports in kOhm then both convert correctly.
|
|
63
|
+
"""
|
|
64
|
+
if df is None or df.empty:
|
|
65
|
+
return NATIVE_Z_UNIT if default is None else default
|
|
66
|
+
col = next((c for c in df.columns if c.startswith('|Z|')), None)
|
|
67
|
+
if col is None:
|
|
68
|
+
return NATIVE_Z_UNIT if default is None else default
|
|
69
|
+
match = re.search(r'\[([kMG]?)Ohm', col)
|
|
70
|
+
return match.group(1) if match else (NATIVE_Z_UNIT if default is None else default)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def convert_impedance_frame(df, from_unit, to_unit, transverse=False):
|
|
74
|
+
"""Rescale and relabel the ``|Z|``/``Re(Z)``/``Im(Z)`` columns of a frame.
|
|
75
|
+
|
|
76
|
+
Columns are matched on their stem, so this works whatever prefix the source
|
|
77
|
+
frame declares. Other columns (``f [MHz]``, ``s [m]``, ``W``) pass through.
|
|
78
|
+
"""
|
|
79
|
+
if df is None or df.empty:
|
|
80
|
+
return df
|
|
81
|
+
f_from, _ = prefix_factor(from_unit)
|
|
82
|
+
f_to, _ = prefix_factor(to_unit)
|
|
83
|
+
if f_from == f_to:
|
|
84
|
+
return df
|
|
85
|
+
ratio = f_from / f_to
|
|
86
|
+
label = impedance_unit(to_unit, transverse)
|
|
87
|
+
|
|
88
|
+
out = df.copy()
|
|
89
|
+
renames = {}
|
|
90
|
+
for stem in ('|Z|', 'Re(Z)', 'Im(Z)'):
|
|
91
|
+
col = next((c for c in out.columns if c.startswith(stem)), None)
|
|
92
|
+
if col is None:
|
|
93
|
+
continue
|
|
94
|
+
out[col] = out[col] * ratio
|
|
95
|
+
renames[col] = f'{stem} [{label}]'
|
|
96
|
+
return out.rename(columns=renames)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def impedance_frame(f_mhz, z, unit='k', transverse=False):
|
|
100
|
+
"""Standard impedance frame from a complex spectrum *z* in **Ohm** (SI)."""
|
|
101
|
+
factor, _ = prefix_factor(unit)
|
|
102
|
+
label = impedance_unit(unit, transverse)
|
|
103
|
+
return pd.DataFrame({
|
|
104
|
+
'f [MHz]': f_mhz,
|
|
105
|
+
f'|Z| [{label}]': np.abs(z) / factor,
|
|
106
|
+
f'Re(Z) [{label}]': z.real / factor,
|
|
107
|
+
f'Im(Z) [{label}]': z.imag / factor,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def reconstruct_impedance(freqs, r_over_q, q_factors, f_span, transverse=False):
|
|
112
|
+
"""Sum-of-resonators impedance spectrum. All frequencies in **Hz**.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
freqs : array-like
|
|
117
|
+
Resonant frequency of each mode [Hz].
|
|
118
|
+
r_over_q : array-like
|
|
119
|
+
R/Q of each mode [Ohm]. Longitudinal for ``transverse=False``; for
|
|
120
|
+
``transverse=True`` this is the transverse (Panofsky-Wenzel) R/Q, also
|
|
121
|
+
in Ohm — the ``omega/c`` factor below turns it into Ohm/m.
|
|
122
|
+
q_factors : array-like
|
|
123
|
+
Quality factor of each mode. Pass the *loaded* Q if that is what the
|
|
124
|
+
beam sees; the eigenmode solver reports the unloaded Q0.
|
|
125
|
+
f_span : array-like
|
|
126
|
+
Frequencies to evaluate the spectrum at [Hz]. ``f = 0`` is allowed and
|
|
127
|
+
is evaluated by its limit.
|
|
128
|
+
transverse : bool
|
|
129
|
+
Transverse (dipole) impedance [Ohm/m] instead of longitudinal [Ohm].
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
ndarray of complex
|
|
134
|
+
The impedance at each frequency in *f_span*: Ohm (longitudinal) or
|
|
135
|
+
Ohm/m (transverse).
|
|
136
|
+
"""
|
|
137
|
+
f0 = np.asarray(freqs, dtype=float)
|
|
138
|
+
roq = np.asarray(r_over_q, dtype=float)
|
|
139
|
+
q = np.asarray(q_factors, dtype=float)
|
|
140
|
+
f = np.asarray(f_span, dtype=float)
|
|
141
|
+
|
|
142
|
+
if not (f0.shape == roq.shape == q.shape):
|
|
143
|
+
raise ValueError(f"freqs, r_over_q and q_factors must have the same length; "
|
|
144
|
+
f"got {f0.shape}, {roq.shape}, {q.shape}.")
|
|
145
|
+
if np.any(f0 <= 0):
|
|
146
|
+
raise ValueError("mode frequencies must be positive.")
|
|
147
|
+
|
|
148
|
+
# Shunt impedance of each resonator. The transverse case picks up omega_0/c,
|
|
149
|
+
# which is what converts a transverse R/Q [Ohm] into a shunt impedance [Ohm/m].
|
|
150
|
+
r_shunt = 0.5 * q * roq
|
|
151
|
+
if transverse:
|
|
152
|
+
r_shunt = r_shunt * (2 * np.pi * f0 / C0)
|
|
153
|
+
|
|
154
|
+
z = np.zeros(f.shape, dtype=complex)
|
|
155
|
+
live = f > 0 # f = 0 is singular in the resonator term
|
|
156
|
+
f_live = f[live]
|
|
157
|
+
|
|
158
|
+
for fi, ri, qi in zip(f0, r_shunt, q):
|
|
159
|
+
# Breit-Wigner resonator: 1 + i Q (f/f0 - f0/f)
|
|
160
|
+
denom = 1 + 1j * qi * (f_live / fi - fi / f_live)
|
|
161
|
+
term = ri / denom
|
|
162
|
+
if transverse:
|
|
163
|
+
term = term * (fi / f_live)
|
|
164
|
+
z[live] += term
|
|
165
|
+
|
|
166
|
+
if not live.all():
|
|
167
|
+
# DC limit. denom -> -i Q f0/f as f -> 0, so the longitudinal term tends
|
|
168
|
+
# to 0 while the transverse term tends to R/(-iQ) = i R/Q (finite).
|
|
169
|
+
z[~live] = np.sum(1j * r_shunt / q) if transverse else 0.0
|
|
170
|
+
|
|
171
|
+
return z
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def reconstruct_impedance_qnm(freqs, r_over_q_complex, q_factors, f_span,
|
|
175
|
+
transverse=False):
|
|
176
|
+
"""Pole-expansion impedance from **complex** mode residues. Frequencies in Hz.
|
|
177
|
+
|
|
178
|
+
:func:`reconstruct_impedance` gives every mode a positive-real shunt impedance,
|
|
179
|
+
which is exact for an isolated resonance and wrong for overlapping ones: it makes
|
|
180
|
+
neighbouring poles add in phase. Above a beam-pipe cutoff, where radiating modes
|
|
181
|
+
have Q of order 10 and linewidths wider than the mode spacing, that coherent
|
|
182
|
+
addition overestimates ``|Z|`` by roughly the number of modes overlapping at each
|
|
183
|
+
frequency.
|
|
184
|
+
|
|
185
|
+
.. warning::
|
|
186
|
+
**Experimental, and currently not validated on radiating modes.** The residue
|
|
187
|
+
*magnitudes* are exact — ``|(R/Q)~|`` reproduces ``R/Q`` to machine precision —
|
|
188
|
+
but their *phases* do not yet come out physical: on a PML solve of a TESLA
|
|
189
|
+
3-cell the resulting spectrum violates passivity (``Re Z < 0``) over most of
|
|
190
|
+
the band above cutoff, and is further from a wakefield reference than the plain
|
|
191
|
+
RLC sum. Correct QNM normalisation of a PML eigenvector needs more than the
|
|
192
|
+
unconjugated volume integral used here. Treat the output as a diagnostic, not
|
|
193
|
+
as an impedance. Above the cutoff the reliable route today is to filter the
|
|
194
|
+
mode set with :func:`pml_stable_modes` and sum it with
|
|
195
|
+
:func:`reconstruct_impedance`.
|
|
196
|
+
|
|
197
|
+
This builds the same spectrum from the quasi-normal-mode residues instead. With
|
|
198
|
+
``w~ = w(1 - i/2Q)`` the decaying complex eigenfrequency and ``a~ = w~ (R/Q)~ / 2``
|
|
199
|
+
the wake amplitude, the wake ``W(t) = Re[a~ exp(-i w~ t)]`` transforms to::
|
|
200
|
+
|
|
201
|
+
Z(w) = (i/2) sum_n [ a~_n / (w - w~_n) + conj(a~_n) / (w + conj(w~_n)) ]
|
|
202
|
+
|
|
203
|
+
The conjugate-pole partner is what enforces ``Z(-w) = conj(Z(w))``, i.e. a real
|
|
204
|
+
wake. For a real ``(R/Q)~`` and large Q this reduces term by term to the RLC form,
|
|
205
|
+
peaking at ``1/2 Q (R/Q)``.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
freqs : array-like
|
|
210
|
+
Real part of each mode frequency [Hz].
|
|
211
|
+
r_over_q_complex : array-like of complex
|
|
212
|
+
``(R/Q)~ = V~^2 / (w~ U~)`` per mode [Ohm] — the solver's
|
|
213
|
+
``'Re(R/Q) [Ohm]'`` + 1j*``'Im(R/Q) [Ohm]'``.
|
|
214
|
+
q_factors : array-like
|
|
215
|
+
Quality factor of each mode.
|
|
216
|
+
f_span : array-like
|
|
217
|
+
Frequencies to evaluate at [Hz]. ``f = 0`` is evaluated by its limit.
|
|
218
|
+
transverse : bool
|
|
219
|
+
Transverse impedance [Ohm/m], using the same ``w_0/c`` and ``w_0/w``
|
|
220
|
+
convention as :func:`reconstruct_impedance`.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
ndarray of complex
|
|
225
|
+
"""
|
|
226
|
+
f0 = np.asarray(freqs, dtype=float)
|
|
227
|
+
roq = np.asarray(r_over_q_complex, dtype=complex)
|
|
228
|
+
q = np.asarray(q_factors, dtype=float)
|
|
229
|
+
f = np.asarray(f_span, dtype=float)
|
|
230
|
+
|
|
231
|
+
if not (f0.shape == roq.shape == q.shape):
|
|
232
|
+
raise ValueError(f"freqs, r_over_q_complex and q_factors must have the same "
|
|
233
|
+
f"length; got {f0.shape}, {roq.shape}, {q.shape}.")
|
|
234
|
+
if np.any(f0 <= 0):
|
|
235
|
+
raise ValueError("mode frequencies must be positive.")
|
|
236
|
+
|
|
237
|
+
w0 = 2 * np.pi * f0
|
|
238
|
+
w = 2 * np.pi * f
|
|
239
|
+
z = np.zeros(f.shape, dtype=complex)
|
|
240
|
+
|
|
241
|
+
for w0i, roqi, qi in zip(w0, roq, q):
|
|
242
|
+
w_c = w0i * (1 - 0.5j / max(abs(qi), 1e-30)) # decaying pole
|
|
243
|
+
a_c = w_c * roqi / 2 # wake amplitude
|
|
244
|
+
term = 0.5j * (a_c / (w - w_c) + np.conj(a_c) / (w + np.conj(w_c)))
|
|
245
|
+
if transverse:
|
|
246
|
+
# Same convention as the RLC path: R/Q_t [Ohm] -> Ohm/m via w_0/c, and
|
|
247
|
+
# the extra w_0/w that makes the transverse resonance peak at w_0.
|
|
248
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
249
|
+
term = term * (w0i / C0) * np.where(w > 0, w0i / np.where(w > 0, w, 1), 0.0)
|
|
250
|
+
z += term
|
|
251
|
+
|
|
252
|
+
return z
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def classify_modes(modes, other, rtol_f=2e-4, linewidth_frac=0.02, rtol_q=0.25,
|
|
256
|
+
pair_frac=0.1, runner_up=3.0):
|
|
257
|
+
"""Cavity mode or artefact, for each mode of an open solve, from a second solve.
|
|
258
|
+
|
|
259
|
+
An open (PML) solve returns genuine resonances of the cavity mixed with modes of
|
|
260
|
+
the beam pipe and of the absorbing block, and they look alike in the results.
|
|
261
|
+
Re-solving with a different ``beampipe_length`` (or ``pml_length``) separates
|
|
262
|
+
them: a cavity mode does not care how long the pipe is, a pipe mode moves. This
|
|
263
|
+
is the best discriminant found so far, and it is not perfect. On pillbox and
|
|
264
|
+
TESLA test cases it miscounts a few per cent of the modes, in both directions.
|
|
265
|
+
|
|
266
|
+
Each mode of *modes* is judged against *other*, per azimuthal order ``m``:
|
|
267
|
+
|
|
268
|
+
1. Frequency, the primary test. Its partner must be its mutual nearest neighbour
|
|
269
|
+
(each is the other's closest mode), within ``max(rtol_f, linewidth_frac / Q)``
|
|
270
|
+
relative. The tolerance scales with the linewidth ``f/Q``: a broad radiating
|
|
271
|
+
mode moves by a larger fraction than a narrow one for the same physical
|
|
272
|
+
change. A partner is ambiguous, and rejected, when a runner-up sits within
|
|
273
|
+
the tolerance and less than ``runner_up`` times further away.
|
|
274
|
+
2. Q, a tie-breaker only. A mode whose Q moved by more than ``rtol_q`` is still
|
|
275
|
+
kept, since the Q of a mode near the cutoff converges slowly with pipe length
|
|
276
|
+
(the layer keeps reaching its evanescent tail). It is dropped only when it
|
|
277
|
+
also has a near-degenerate partner of the same ``m``, closer than
|
|
278
|
+
``pair_frac`` of the typical spacing. At fixed ``m`` an axisymmetric cavity
|
|
279
|
+
has no degeneracy, so such a pair is the left and right pipe, not one mode.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
modes, other : pandas.DataFrame
|
|
284
|
+
Two ``cav.eigenmode.qois_df`` frames of the same cavity, solved with a
|
|
285
|
+
different pipe (or PML) length. Needs ``'freq [MHz]'`` and ``'Q []'``;
|
|
286
|
+
``'m'`` is used when present.
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
pandas.DataFrame
|
|
291
|
+
Indexed like *modes*: ``'cavity mode'`` (the verdict), and the evidence behind
|
|
292
|
+
it, ``'shift [ppm]'``, ``'tolerance [ppm]'``, ``'mutual'``, ``'f ok'``,
|
|
293
|
+
``'Q ok'``, ``'paired'`` and ``'reached'``.
|
|
294
|
+
|
|
295
|
+
Notes
|
|
296
|
+
-----
|
|
297
|
+
Compare only where both runs reach. Each solve returns its lowest ``n_modes``,
|
|
298
|
+
and a mode above *other*'s highest has nothing to be matched against: it comes
|
|
299
|
+
back ``'reached' == False`` and is not called a cavity mode (unverified, not
|
|
300
|
+
disproved). Raise ``n_modes`` until both runs cover the band of interest.
|
|
301
|
+
"""
|
|
302
|
+
out = pd.DataFrame(index=modes.index)
|
|
303
|
+
for col, default in (('shift [ppm]', np.nan), ('tolerance [ppm]', np.nan),
|
|
304
|
+
('mutual', False), ('f ok', False), ('Q ok', False),
|
|
305
|
+
('paired', False), ('reached', False)):
|
|
306
|
+
out[col] = default
|
|
307
|
+
|
|
308
|
+
def groups(df):
|
|
309
|
+
if 'm' in df.columns:
|
|
310
|
+
return {m: g for m, g in df.groupby('m')}
|
|
311
|
+
return {0: df}
|
|
312
|
+
|
|
313
|
+
other_groups = groups(other)
|
|
314
|
+
for m, a in groups(modes).items():
|
|
315
|
+
b = other_groups.get(m)
|
|
316
|
+
f_a = a['freq [MHz]'].to_numpy(dtype=float)
|
|
317
|
+
q_a = a['Q []'].to_numpy(dtype=float)
|
|
318
|
+
if b is None or len(b) == 0:
|
|
319
|
+
continue
|
|
320
|
+
f_b = b['freq [MHz]'].to_numpy(dtype=float)
|
|
321
|
+
q_b = b['Q []'].to_numpy(dtype=float)
|
|
322
|
+
|
|
323
|
+
rel = np.abs(f_a[:, None] - f_b[None, :]) / f_a[:, None]
|
|
324
|
+
j = rel.argmin(axis=1)
|
|
325
|
+
d1 = rel[np.arange(len(f_a)), j]
|
|
326
|
+
d2 = (np.partition(rel, 1, axis=1)[:, 1] if rel.shape[1] > 1
|
|
327
|
+
else np.full(len(f_a), np.inf))
|
|
328
|
+
back = (np.abs(f_b[:, None] - f_a[None, :]) / f_b[:, None]).argmin(axis=1)
|
|
329
|
+
mutual = back[j] == np.arange(len(f_a))
|
|
330
|
+
tol = np.maximum(rtol_f, linewidth_frac / np.maximum(np.abs(q_a), 1e-30))
|
|
331
|
+
ambiguous = (d2 <= tol) & (d2 < runner_up * d1)
|
|
332
|
+
f_ok = mutual & (d1 <= tol) & ~ambiguous
|
|
333
|
+
q_ok = np.abs(q_b[j] - q_a) <= rtol_q * np.maximum(np.abs(q_a), 1e-30)
|
|
334
|
+
|
|
335
|
+
# near-degenerate partner of the same m, relative to the typical spacing
|
|
336
|
+
if len(f_a) > 1:
|
|
337
|
+
gaps = np.abs(f_a[:, None] - f_a[None, :]) / f_a[:, None]
|
|
338
|
+
np.fill_diagonal(gaps, np.inf)
|
|
339
|
+
nearest = gaps.min(axis=1)
|
|
340
|
+
paired = nearest < pair_frac * np.median(nearest)
|
|
341
|
+
else:
|
|
342
|
+
paired = np.zeros(len(f_a), dtype=bool)
|
|
343
|
+
|
|
344
|
+
idx = a.index
|
|
345
|
+
out.loc[idx, 'shift [ppm]'] = d1 * 1e6
|
|
346
|
+
out.loc[idx, 'tolerance [ppm]'] = tol * 1e6
|
|
347
|
+
out.loc[idx, 'mutual'] = mutual
|
|
348
|
+
out.loc[idx, 'f ok'] = f_ok
|
|
349
|
+
out.loc[idx, 'Q ok'] = q_ok
|
|
350
|
+
out.loc[idx, 'paired'] = paired
|
|
351
|
+
out.loc[idx, 'reached'] = f_a <= f_b.max() * (1 + rtol_f)
|
|
352
|
+
|
|
353
|
+
out['cavity mode'] = (out['f ok'] & ~(~out['Q ok'] & out['paired'])
|
|
354
|
+
& out['reached']).astype(bool)
|
|
355
|
+
return out
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def pml_stable_modes(modes, other, **kwargs):
|
|
359
|
+
"""Boolean mask over *modes*: the cavity modes, judged against a second solve
|
|
360
|
+
with a different pipe or PML length. See :func:`classify_modes`, which returns
|
|
361
|
+
the same verdict with the evidence behind it.
|
|
362
|
+
|
|
363
|
+
>>> keep = pml_stable_modes(cav_a.eigenmode.qois_df, cav_b.eigenmode.qois_df)
|
|
364
|
+
>>> trusted = cav_a.eigenmode.qois_df[keep]
|
|
365
|
+
"""
|
|
366
|
+
return classify_modes(modes, other, **kwargs)['cavity mode'].to_numpy(dtype=bool)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Lorentz radiation pressure, wall deflection and Lorentz detuning."""
|
|
2
|
+
from cavsim2d.analysis.lorentz.pressure import (pressure_cf, wall_pressure,
|
|
3
|
+
stored_energy, slater_shift,
|
|
4
|
+
detuning_coefficient)
|
|
5
|
+
|
|
6
|
+
__all__ = ['pressure_cf', 'wall_pressure', 'stored_energy', 'slater_shift',
|
|
7
|
+
'detuning_coefficient']
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
r"""Lorentz radiation pressure on a cavity wall, and the detuning it causes.
|
|
2
|
+
|
|
3
|
+
Theory
|
|
4
|
+
------
|
|
5
|
+
The RF field exerts a pressure on the conducting wall. With fields written as peak
|
|
6
|
+
amplitudes the time-averaged energy densities are :math:`u_H=\tfrac14\mu_0|H|^2` and
|
|
7
|
+
:math:`u_E=\tfrac14\varepsilon_0|E|^2`, and the outward pressure is their difference:
|
|
8
|
+
|
|
9
|
+
.. math:: P = \tfrac14\left(\mu_0|H|^2 - \varepsilon_0|E|^2\right)
|
|
10
|
+
|
|
11
|
+
so the magnetic field near the equator pushes the wall **out** and the electric field near
|
|
12
|
+
the iris pulls it **in**. On a perfect conductor the tangential electric and normal
|
|
13
|
+
magnetic components vanish, so :math:`|E|` is purely normal there and :math:`|H|` purely
|
|
14
|
+
tangential.
|
|
15
|
+
|
|
16
|
+
Given a wall displacement :math:`\mathbf{u}`, Slater's theorem gives the frequency shift
|
|
17
|
+
as the work the pressure does against that displacement, normalised by the stored energy:
|
|
18
|
+
|
|
19
|
+
.. math:: \frac{\Delta\omega}{\omega}
|
|
20
|
+
= -\frac{1}{U}\oint_S P\,(\mathbf{u}\cdot\mathbf{n})\,\mathrm{d}S
|
|
21
|
+
|
|
22
|
+
Pushing the wall **in** where the magnetic field is strong raises the frequency; pushing
|
|
23
|
+
it in where the electric field is strong lowers it. The static deflection is itself
|
|
24
|
+
proportional to the pressure, and the pressure to :math:`E_\mathrm{acc}^2`, so the shift
|
|
25
|
+
is quoted as the Lorentz detuning coefficient
|
|
26
|
+
|
|
27
|
+
.. math:: K_L = \frac{\Delta f}{E_\mathrm{acc}^2}\quad[\mathrm{Hz/(MV/m)^2}]
|
|
28
|
+
|
|
29
|
+
which is negative for an unstiffened cavity: the inward pull at the iris and the outward
|
|
30
|
+
push at the equator both lower the frequency of the accelerating mode.
|
|
31
|
+
|
|
32
|
+
Scope
|
|
33
|
+
-----
|
|
34
|
+
The pressure follows from the RF solution with no further modelling. The **displacement
|
|
35
|
+
does not** - it depends on the wall thickness, the elastic constants, and above all on how
|
|
36
|
+
the cavity is held, none of which the RF model knows. :func:`slater_shift` therefore takes
|
|
37
|
+
the displacement as an argument rather than computing it: supply one from a mechanical
|
|
38
|
+
solve and this module turns it into a frequency shift.
|
|
39
|
+
|
|
40
|
+
Validation
|
|
41
|
+
----------
|
|
42
|
+
A TM010 pillbox has :math:`f = c\,x_{01}/(2\pi R)`, which depends on the radius and not on
|
|
43
|
+
the length. Both identities are reproduced: a uniform outward displacement of the barrel
|
|
44
|
+
gives :math:`\Delta f/f = -\alpha/R` to 0.001%, and the same displacement of the end
|
|
45
|
+
plates gives zero.
|
|
46
|
+
"""
|
|
47
|
+
import numpy as np
|
|
48
|
+
from ngsolve import (Conj, GridFunction, H1, Integrate, InnerProduct, Norm,
|
|
49
|
+
VectorH1, specialcf, y)
|
|
50
|
+
|
|
51
|
+
from cavsim2d.constants import eps0, mu0
|
|
52
|
+
from cavsim2d.solvers.NGSolve.eigen_ngsolve import get_boundary_nodes
|
|
53
|
+
|
|
54
|
+
__all__ = ['MU0', 'EPS0', 'pressure_cf', 'wall_pressure', 'stored_energy',
|
|
55
|
+
'slater_shift', 'wall_force', 'detuning_coefficient']
|
|
56
|
+
|
|
57
|
+
MU0 = mu0
|
|
58
|
+
EPS0 = eps0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _projected_fields(mesh, gfu_E, gfu_H, mode, order):
|
|
62
|
+
"""E and H projected into continuous H1, so their traces integrate on a boundary.
|
|
63
|
+
|
|
64
|
+
Two separate reasons, both of which silently corrupt a wall integral:
|
|
65
|
+
|
|
66
|
+
* **H** is reconstructed as ``curl(E)/(mu0 w)``, and a GridFunction curl cannot
|
|
67
|
+
be SIMD-evaluated on a boundary - the integral fails outright with "SIMD:
|
|
68
|
+
don't know how I shall evaluate".
|
|
69
|
+
* **E** lives in HCurl, whose boundary trace keeps only the TANGENTIAL part. On
|
|
70
|
+
a perfect conductor that part is zero by definition, so an unprojected
|
|
71
|
+
``|E|^2`` evaluates to nothing on the wall and the electric half of the
|
|
72
|
+
pressure disappears without any error being raised.
|
|
73
|
+
|
|
74
|
+
The eigenmode solver projects H the same way for its wall-loss integral. On a
|
|
75
|
+
PML mesh the projection is restricted to the physical region: H1 is continuous,
|
|
76
|
+
so projecting across the interface would average in the complex-stretched
|
|
77
|
+
layer's own field.
|
|
78
|
+
"""
|
|
79
|
+
u_gf, uphi_gf = gfu_E[mode].components
|
|
80
|
+
H_inplane, H_phi = gfu_H[mode]
|
|
81
|
+
kw = ({'definedon': mesh.Materials('phys')}
|
|
82
|
+
if 'pml' in mesh.GetMaterials() else {})
|
|
83
|
+
ein = GridFunction(VectorH1(mesh, order=order, complex=True))
|
|
84
|
+
ein.Set(u_gf, **kw)
|
|
85
|
+
hphi = GridFunction(H1(mesh, order=order, complex=True))
|
|
86
|
+
hphi.Set(H_phi, **kw)
|
|
87
|
+
hin = GridFunction(VectorH1(mesh, order=order, complex=True))
|
|
88
|
+
hin.Set(H_inplane, **kw)
|
|
89
|
+
return ein, uphi_gf, hin, hphi
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _field_norms(gfu_E, gfu_H, mode=0, m=0, mesh=None, project=False):
|
|
93
|
+
"""``(|E|^2, |H|^2)`` as coefficient functions.
|
|
94
|
+
|
|
95
|
+
A monopole keeps all of E in the in-plane block and all of H in the azimuthal
|
|
96
|
+
one, so the other terms drop out. The general form is kept because an m>=1 mode
|
|
97
|
+
populates both.
|
|
98
|
+
|
|
99
|
+
Set *project* for a boundary integral; see :func:`_projected_H`.
|
|
100
|
+
"""
|
|
101
|
+
u_gf, uphi_gf = gfu_E[mode].components
|
|
102
|
+
if project:
|
|
103
|
+
u_gf, uphi_gf, H_inplane, H_phi = _projected_fields(
|
|
104
|
+
mesh, gfu_E, gfu_H, mode, u_gf.space.globalorder)
|
|
105
|
+
e2 = InnerProduct(u_gf, u_gf)
|
|
106
|
+
h2 = InnerProduct(H_inplane, H_inplane) + Norm(H_phi) ** 2
|
|
107
|
+
if m != 0:
|
|
108
|
+
e2 = e2 + Norm(uphi_gf) ** 2 / (y * y + 1e-30)
|
|
109
|
+
return e2, h2
|
|
110
|
+
H_inplane, H_phi = gfu_H[mode]
|
|
111
|
+
e2 = InnerProduct(u_gf, u_gf)
|
|
112
|
+
h2 = InnerProduct(H_inplane, H_inplane) + Norm(H_phi) ** 2
|
|
113
|
+
if m != 0:
|
|
114
|
+
# E_phi = u_phi / r. The wall reaches the axis only at a beam-pipe end cap,
|
|
115
|
+
# where u_phi vanishes anyway, so the guard only avoids a 0/0.
|
|
116
|
+
e2 = e2 + Norm(uphi_gf) ** 2 / (y * y + 1e-30)
|
|
117
|
+
return e2, h2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _azimuthal_factor(m):
|
|
121
|
+
"""Integral over phi: 2*pi for a monopole, pi for m>=1 (cos^2 averages to 1/2)."""
|
|
122
|
+
return 2 * np.pi if m == 0 else np.pi
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def pressure_cf(gfu_E, gfu_H, mode=0, m=0, scale=1.0, mesh=None, project=False):
|
|
126
|
+
"""Lorentz radiation pressure as a coefficient function, in pascals.
|
|
127
|
+
|
|
128
|
+
Positive pushes the wall outward (magnetic), negative inward (electric). *scale*
|
|
129
|
+
multiplies the field amplitude, so the pressure scales with its square - pass the
|
|
130
|
+
factor that takes the stored solution to the gradient you want.
|
|
131
|
+
"""
|
|
132
|
+
e2, h2 = _field_norms(gfu_E, gfu_H, mode, m, mesh, project)
|
|
133
|
+
return 0.25 * (MU0 * h2 - EPS0 * e2) * (scale ** 2)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def stored_energy(mesh, gfu_E, gfu_H, mode=0, m=0, eps_cf=None):
|
|
137
|
+
r"""Total stored energy of the mode, in joules.
|
|
138
|
+
|
|
139
|
+
The time-averaged electric and magnetic energies are equal at resonance, so the
|
|
140
|
+
total is twice either one: :math:`U=\tfrac12\varepsilon_0\int|E|^2\,\mathrm{d}V`.
|
|
141
|
+
"""
|
|
142
|
+
e2, _ = _field_norms(gfu_E, gfu_H, mode, m)
|
|
143
|
+
eps = EPS0 if eps_cf is None else EPS0 * eps_cf
|
|
144
|
+
return float(Integrate(0.5 * eps * e2 * y, mesh).real) * _azimuthal_factor(m)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def wall_pressure(mesh, gfu_E, gfu_H, mode=0, m=0, scale=1.0, wall='PEC',
|
|
148
|
+
points=None):
|
|
149
|
+
"""Sample the pressure on the wall.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
points : array_like, optional
|
|
154
|
+
``(n, 2)`` array of ``(z, r)`` in metres to evaluate at. Defaults to the
|
|
155
|
+
mesh's boundary nodes, which are ordered by coordinate rather than along the
|
|
156
|
+
contour - pass ``profile.contour_points(ds)`` when the samples must follow
|
|
157
|
+
the wall, as a line plot needs.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
(points, p) : ndarray, ndarray
|
|
162
|
+
Sample coordinates in metres, and the pressure at each in pascals.
|
|
163
|
+
"""
|
|
164
|
+
cf = pressure_cf(gfu_E, gfu_H, mode, m, scale)
|
|
165
|
+
if points is None:
|
|
166
|
+
points = sorted(get_boundary_nodes(mesh, wall))
|
|
167
|
+
pts = np.asarray(points, dtype=float).reshape(-1, 2)
|
|
168
|
+
vals = np.asarray(cf(mesh(pts[:, 0], pts[:, 1]))).ravel().real
|
|
169
|
+
return pts, vals
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def slater_shift(mesh, gfu_E, gfu_H, normal_displacement, freq_hz,
|
|
173
|
+
mode=0, m=0, wall='PEC', scale=1.0, U=None):
|
|
174
|
+
r"""Frequency shift from a wall displacement, by Slater's theorem, in hertz.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
normal_displacement : CoefficientFunction
|
|
179
|
+
The OUTWARD normal component of the wall displacement, in metres. A
|
|
180
|
+
GridFunction from a mechanical solve is the intended input; an analytic
|
|
181
|
+
expression built from ``ngsolve.x`` / ``ngsolve.y`` also works.
|
|
182
|
+
freq_hz : float
|
|
183
|
+
Unperturbed frequency.
|
|
184
|
+
U : float, optional
|
|
185
|
+
Stored energy. Computed from the fields when omitted; pass it to avoid
|
|
186
|
+
recomputing it across a sweep.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
float
|
|
191
|
+
:math:`\Delta f`, negative when the wall moves so as to lower the frequency,
|
|
192
|
+
which is the usual sense for an unstiffened cavity.
|
|
193
|
+
|
|
194
|
+
Notes
|
|
195
|
+
-----
|
|
196
|
+
The mesh is a meridian, so the surface element of the body of revolution carries
|
|
197
|
+
the factor ``r``, and the azimuthal integral contributes ``2*pi`` (``pi`` for
|
|
198
|
+
m>=1).
|
|
199
|
+
"""
|
|
200
|
+
p_cf = pressure_cf(gfu_E, gfu_H, mode, m, scale, mesh, project=True)
|
|
201
|
+
if U is None:
|
|
202
|
+
U = stored_energy(mesh, gfu_E, gfu_H, mode, m)
|
|
203
|
+
work = float(Integrate(p_cf * normal_displacement * y, mesh,
|
|
204
|
+
definedon=mesh.Boundaries(wall)).real)
|
|
205
|
+
return -freq_hz * work * _azimuthal_factor(m) / U
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def wall_force(mesh, gfu_E, gfu_H, mode=0, m=0, wall='PEC', scale=1.0):
|
|
209
|
+
"""Net force the field exerts on *wall*, as ``(F_z, F_r)`` in newtons.
|
|
210
|
+
|
|
211
|
+
A cheap sanity check on the pressure: for a mode symmetric about the mid-plane
|
|
212
|
+
the axial components cancel, so ``F_z`` should come out near zero.
|
|
213
|
+
"""
|
|
214
|
+
p_cf = pressure_cf(gfu_E, gfu_H, mode, m, scale, mesh, project=True)
|
|
215
|
+
n = specialcf.normal(2)
|
|
216
|
+
az = _azimuthal_factor(m)
|
|
217
|
+
fz = float(Integrate(p_cf * n[0] * y, mesh,
|
|
218
|
+
definedon=mesh.Boundaries(wall)).real) * az
|
|
219
|
+
fr = float(Integrate(p_cf * n[1] * y, mesh,
|
|
220
|
+
definedon=mesh.Boundaries(wall)).real) * az
|
|
221
|
+
return fz, fr
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def detuning_coefficient(delta_f_hz, eacc_mv_per_m):
|
|
225
|
+
r"""Lorentz detuning coefficient :math:`K_L`, in Hz/(MV/m)^2."""
|
|
226
|
+
return delta_f_hz / (float(eacc_mv_per_m) ** 2)
|