cavsim2d 0.1.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 (118) hide show
  1. cavsim2d-0.1.0/LICENSE +21 -0
  2. cavsim2d-0.1.0/MANIFEST.in +6 -0
  3. cavsim2d-0.1.0/PKG-INFO +666 -0
  4. cavsim2d-0.1.0/README.md +622 -0
  5. cavsim2d-0.1.0/cavsim2d/__init__.py +83 -0
  6. cavsim2d-0.1.0/cavsim2d/analysis/__init__.py +0 -0
  7. cavsim2d-0.1.0/cavsim2d/analysis/impedance.py +366 -0
  8. cavsim2d-0.1.0/cavsim2d/analysis/lorentz/__init__.py +7 -0
  9. cavsim2d-0.1.0/cavsim2d/analysis/lorentz/pressure.py +226 -0
  10. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/__init__.py +21 -0
  11. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/driver.py +487 -0
  12. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/fields.py +134 -0
  13. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/integrators.py +825 -0
  14. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/metrics.py +82 -0
  15. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/particles.py +283 -0
  16. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/plots.py +831 -0
  17. cavsim2d-0.1.0/cavsim2d/analysis/multipacting/sey.py +47 -0
  18. cavsim2d-0.1.0/cavsim2d/analysis/optimisation.py +1423 -0
  19. cavsim2d-0.1.0/cavsim2d/analysis/tune/__init__.py +0 -0
  20. cavsim2d-0.1.0/cavsim2d/analysis/tune/pyTuner.py +935 -0
  21. cavsim2d-0.1.0/cavsim2d/analysis/tune/tuner.py +207 -0
  22. cavsim2d-0.1.0/cavsim2d/analysis/uq/__init__.py +7 -0
  23. cavsim2d-0.1.0/cavsim2d/analysis/uq/models.py +140 -0
  24. cavsim2d-0.1.0/cavsim2d/analysis/uq/plots.py +229 -0
  25. cavsim2d-0.1.0/cavsim2d/analysis/uq/sobol_sa.py +204 -0
  26. cavsim2d-0.1.0/cavsim2d/analysis/wakefield/__init__.py +0 -0
  27. cavsim2d-0.1.0/cavsim2d/config_files/optimisation_config.py +56 -0
  28. cavsim2d-0.1.0/cavsim2d/config_files/uq_config.py +19 -0
  29. cavsim2d-0.1.0/cavsim2d/constants.py +119 -0
  30. cavsim2d-0.1.0/cavsim2d/data_module/__init__.py +0 -0
  31. cavsim2d-0.1.0/cavsim2d/data_module/abci_data.py +759 -0
  32. cavsim2d-0.1.0/cavsim2d/data_module/operating_points.py +311 -0
  33. cavsim2d-0.1.0/cavsim2d/data_module/sey/sey +23 -0
  34. cavsim2d-0.1.0/cavsim2d/geometry/__init__.py +35 -0
  35. cavsim2d-0.1.0/cavsim2d/geometry/beampipes.py +187 -0
  36. cavsim2d-0.1.0/cavsim2d/geometry/contours.py +209 -0
  37. cavsim2d-0.1.0/cavsim2d/geometry/plotting.py +653 -0
  38. cavsim2d-0.1.0/cavsim2d/geometry/primitives.py +152 -0
  39. cavsim2d-0.1.0/cavsim2d/geometry/profile.py +1241 -0
  40. cavsim2d-0.1.0/cavsim2d/geometry/tangency.py +390 -0
  41. cavsim2d-0.1.0/cavsim2d/geometry/writers/__init__.py +11 -0
  42. cavsim2d-0.1.0/cavsim2d/geometry/writers/cst.py +93 -0
  43. cavsim2d-0.1.0/cavsim2d/geometry/writers/gmsh.py +1719 -0
  44. cavsim2d-0.1.0/cavsim2d/geometry/writers/multipac.py +783 -0
  45. cavsim2d-0.1.0/cavsim2d/models/__init__.py +39 -0
  46. cavsim2d-0.1.0/cavsim2d/models/assembly.py +726 -0
  47. cavsim2d-0.1.0/cavsim2d/models/base.py +3510 -0
  48. cavsim2d-0.1.0/cavsim2d/models/beampipe.py +200 -0
  49. cavsim2d-0.1.0/cavsim2d/models/bellows.py +283 -0
  50. cavsim2d-0.1.0/cavsim2d/models/bla.py +203 -0
  51. cavsim2d-0.1.0/cavsim2d/models/elliptical.py +1524 -0
  52. cavsim2d-0.1.0/cavsim2d/models/elliptical_flattop.py +772 -0
  53. cavsim2d-0.1.0/cavsim2d/models/pillbox.py +326 -0
  54. cavsim2d-0.1.0/cavsim2d/models/rfgun.py +585 -0
  55. cavsim2d-0.1.0/cavsim2d/models/spline.py +567 -0
  56. cavsim2d-0.1.0/cavsim2d/models/taper.py +206 -0
  57. cavsim2d-0.1.0/cavsim2d/processes/__init__.py +5 -0
  58. cavsim2d-0.1.0/cavsim2d/processes/eigenmode.py +168 -0
  59. cavsim2d-0.1.0/cavsim2d/processes/tune.py +1259 -0
  60. cavsim2d-0.1.0/cavsim2d/processes/uq.py +728 -0
  61. cavsim2d-0.1.0/cavsim2d/processes/wakefield.py +423 -0
  62. cavsim2d-0.1.0/cavsim2d/solvers/ABCI/abci.py +148 -0
  63. cavsim2d-0.1.0/cavsim2d/solvers/NGSolve/__init__.py +0 -0
  64. cavsim2d-0.1.0/cavsim2d/solvers/NGSolve/eigen_ngsolve.py +3310 -0
  65. cavsim2d-0.1.0/cavsim2d/solvers/NGSolve/eigen_ports.py +615 -0
  66. cavsim2d-0.1.0/cavsim2d/solvers/NGSolve/frequency_domain_ngsolve.py +217 -0
  67. cavsim2d-0.1.0/cavsim2d/solvers/NGSolve/ports.py +266 -0
  68. cavsim2d-0.1.0/cavsim2d/solvers/__init__.py +16 -0
  69. cavsim2d-0.1.0/cavsim2d/solvers/eigenmode_result.py +97 -0
  70. cavsim2d-0.1.0/cavsim2d/solvers/objectives.py +186 -0
  71. cavsim2d-0.1.0/cavsim2d/solvers/solver_objects.py +4496 -0
  72. cavsim2d-0.1.0/cavsim2d/solvers/wakefield/__init__.py +33 -0
  73. cavsim2d-0.1.0/cavsim2d/solvers/wakefield/abci.py +139 -0
  74. cavsim2d-0.1.0/cavsim2d/solvers/wakefield/base.py +118 -0
  75. cavsim2d-0.1.0/cavsim2d/study.py +4274 -0
  76. cavsim2d-0.1.0/cavsim2d/utils/__init__.py +13 -0
  77. cavsim2d-0.1.0/cavsim2d/utils/config_validation.py +229 -0
  78. cavsim2d-0.1.0/cavsim2d/utils/cubature.py +709 -0
  79. cavsim2d-0.1.0/cavsim2d/utils/data_utils.py +337 -0
  80. cavsim2d-0.1.0/cavsim2d/utils/printing.py +106 -0
  81. cavsim2d-0.1.0/cavsim2d/utils/quadrature.py +852 -0
  82. cavsim2d-0.1.0/cavsim2d/utils/run_log.py +76 -0
  83. cavsim2d-0.1.0/cavsim2d/utils/sampling.py +402 -0
  84. cavsim2d-0.1.0/cavsim2d/utils/sensitivity.py +808 -0
  85. cavsim2d-0.1.0/cavsim2d/utils/shapes.py +449 -0
  86. cavsim2d-0.1.0/cavsim2d/utils/shared_classes.py +5 -0
  87. cavsim2d-0.1.0/cavsim2d/utils/shared_functions.py +30 -0
  88. cavsim2d-0.1.0/cavsim2d/utils/slope_triangles.py +165 -0
  89. cavsim2d-0.1.0/cavsim2d/utils/style.py +157 -0
  90. cavsim2d-0.1.0/cavsim2d/utils/surrogate.py +385 -0
  91. cavsim2d-0.1.0/cavsim2d/utils/welcome.py +40 -0
  92. cavsim2d-0.1.0/cavsim2d.egg-info/PKG-INFO +666 -0
  93. cavsim2d-0.1.0/cavsim2d.egg-info/SOURCES.txt +116 -0
  94. cavsim2d-0.1.0/cavsim2d.egg-info/dependency_links.txt +1 -0
  95. cavsim2d-0.1.0/cavsim2d.egg-info/requires.txt +28 -0
  96. cavsim2d-0.1.0/cavsim2d.egg-info/top_level.txt +1 -0
  97. cavsim2d-0.1.0/pyproject.toml +66 -0
  98. cavsim2d-0.1.0/setup.cfg +4 -0
  99. cavsim2d-0.1.0/tests/test_api_guards.py +132 -0
  100. cavsim2d-0.1.0/tests/test_assembly.py +672 -0
  101. cavsim2d-0.1.0/tests/test_cavity_types.py +121 -0
  102. cavsim2d-0.1.0/tests/test_config_validation.py +120 -0
  103. cavsim2d-0.1.0/tests/test_dielectric.py +790 -0
  104. cavsim2d-0.1.0/tests/test_eigenmode.py +1204 -0
  105. cavsim2d-0.1.0/tests/test_geometry.py +881 -0
  106. cavsim2d-0.1.0/tests/test_impedance.py +423 -0
  107. cavsim2d-0.1.0/tests/test_multipacting.py +649 -0
  108. cavsim2d-0.1.0/tests/test_null.py +2 -0
  109. cavsim2d-0.1.0/tests/test_objectives.py +158 -0
  110. cavsim2d-0.1.0/tests/test_optimisation.py +203 -0
  111. cavsim2d-0.1.0/tests/test_plot_compare.py +95 -0
  112. cavsim2d-0.1.0/tests/test_plotting_style.py +124 -0
  113. cavsim2d-0.1.0/tests/test_slope_triangles.py +43 -0
  114. cavsim2d-0.1.0/tests/test_standalone.py +49 -0
  115. cavsim2d-0.1.0/tests/test_sweep.py +124 -0
  116. cavsim2d-0.1.0/tests/test_tune.py +528 -0
  117. cavsim2d-0.1.0/tests/test_uq.py +445 -0
  118. cavsim2d-0.1.0/tests/test_wakefield.py +482 -0
cavsim2d-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2022] [Sosoho-Abasi Udongwo]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # ABCI is free to use but not open source, and KEK distributes it only from
2
+ # https://abci.kek.jp/abci.htm. A local copy kept in a checkout (gitignored) must
3
+ # never reach the sdist or the wheel, even when an old egg-info manifest lists it.
4
+ exclude cavsim2d/solvers/ABCI/*.exe
5
+ prune cavsim2d/solvers/ABCI/TopDrawer*
6
+ global-exclude *.py[cod]
@@ -0,0 +1,666 @@
1
+ Metadata-Version: 2.4
2
+ Name: cavsim2d
3
+ Version: 0.1.0
4
+ Summary: Quick 2D axisymmetric RF cavity analysis: eigenmode, tuning, wakefield, UQ and optimisation.
5
+ Author-email: Sosoho-Abasi Udongwo <numurho@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Dark-Elektron/cavsim2d
8
+ Project-URL: Repository, https://github.com/Dark-Elektron/cavsim2d
9
+ Keywords: rf-cavity,accelerator,eigenmode,wakefield,finite-element,ngsolve
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=1.26.4
19
+ Requires-Dist: scipy>=1.13.1
20
+ Requires-Dist: pandas>=2.2.2
21
+ Requires-Dist: matplotlib>=3.8.4
22
+ Requires-Dist: ngsolve>=6.2.2402
23
+ Requires-Dist: gmsh>=4.13
24
+ Requires-Dist: paretoset>=1.2.4
25
+ Requires-Dist: termcolor>=2.1.0
26
+ Requires-Dist: tqdm>=4.66.4
27
+ Requires-Dist: openpyxl
28
+ Requires-Dist: jinja2
29
+ Requires-Dist: joblib
30
+ Requires-Dist: scikit-learn>=1.6
31
+ Requires-Dist: SALib
32
+ Provides-Extra: jupyter
33
+ Requires-Dist: ipython>=8.26.0; extra == "jupyter"
34
+ Requires-Dist: ipywidgets>=8.1.3; extra == "jupyter"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: flake8; extra == "dev"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx-immaterial>=0.11; extra == "docs"
40
+ Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
41
+ Requires-Dist: numpydoc; extra == "docs"
42
+ Requires-Dist: myst-nb; extra == "docs"
43
+ Dynamic: license-file
44
+
45
+ ![License](https://img.shields.io/badge/license-MIT-blue)
46
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
47
+ ![Status](https://img.shields.io/badge/status-beta-orange)
48
+
49
+ # cavsim2d
50
+
51
+ `cavsim2d` is a Python toolkit for designing and analysing 2D axisymmetric RF
52
+ structures. It provides, through one small object-oriented API:
53
+
54
+ - **Eigenmode analysis** — fundamental (monopole) *and* higher-order-mode
55
+ (dipole, quadrupole, sextupole, …) passbands, with the usual figures of merit
56
+ (frequency, R/Q, G, Q, Epk/Eacc, Bpk/Eacc, field flatness, cell-to-cell
57
+ coupling).
58
+ - **Frequency tuning** to a target frequency.
59
+ - **Wakefield / impedance analysis** via the ABCI solver.
60
+ - **Uncertainty quantification (UQ)** on any of the above.
61
+ - **Multi-objective shape optimisation.**
62
+ - **Plotting and comparison** helpers for all of the above.
63
+
64
+ The eigenmode solver is built on [NGSolve](https://ngsolve.org); wakefield
65
+ analysis uses the free [ABCI](https://abci.kek.jp/abci.htm) code, which you
66
+ download from KEK yourself (see [Installation](#installation)).
67
+
68
+ ## Cavity types and what is supported
69
+
70
+ Every cavity type supports every analysis; the notes below the table cover the
71
+ caveats:
72
+
73
+ | Cavity type | Eigenmode | Tuning | Wakefield | UQ / Optimisation |
74
+ |---|---|---|---|---|
75
+ | `EllipticalCavity` | ✅ | ✅ | ✅ | ✅ |
76
+ | `EllipticalCavityFlatTop` | ✅ | ✅ | ✅ | ✅ |
77
+ | `Pillbox` | ✅ | ✅ | ✅ | ✅ |
78
+ | `SplineCavity` | ✅ | ✅² | ✅ | ✅² |
79
+ | `RFGun` | ✅¹ | ✅ | ✅ | ✅ |
80
+
81
+ ✅ supported and exercised · ¹ ² see notes.
82
+
83
+ Every type builds its mesh from the same `Profile` blueprint, implements a single
84
+ `rebuild()` hook, and declares its own tunable parameters via `tune_variables()`.
85
+ Adding a geometry therefore needs no edit to any central list:
86
+
87
+ ```python
88
+ tesla.tune_variables() # {'A', 'B', 'a', 'b', 'Ri', 'L', 'Req'}
89
+ gun.tune_variables() # {'y1', 'R2', 'T2', 'L3', ..., 'R6', ..., 'x'}
90
+ spline.tune_variables() # {'p0_z', 'p0_r', ..., 'p5_z', 'p5_r'}
91
+ ```
92
+
93
+ Any of those names can be a tune variable (`tune_config['cell_type']`) or a UQ /
94
+ optimisation variable. A geometry imported from a mesh or CAD file has no
95
+ parameters, and says so rather than reporting an unknown variable name. Unless
96
+ noted, examples below use `EllipticalCavity`.
97
+
98
+ ¹ RF-gun eigenmode gives correct frequencies and fields; the length-normalised
99
+ QOIs (`Eacc`, `Epk/Eacc`) default to the on-axis field extent as the active
100
+ length (override with `eigenmode_config['normalization_length']`, mm). Its
101
+ `Epk/Eacc` does not converge under mesh refinement — the cathode corner is a
102
+ field singularity, so treat that number as mesh-dependent.
103
+
104
+ ² A `SplineCavity` is parameterised by control points rather than by scalars, so
105
+ its handles are the point *coordinates*: `'p3_r'` moves the radial coordinate of
106
+ `p3` and leaves `p3_z` alone.
107
+
108
+ ---
109
+
110
+ ## Installation
111
+
112
+ Install the released package from PyPI:
113
+
114
+ ```bash
115
+ pip install cavsim2d
116
+ ```
117
+
118
+ or clone the repository and install it in editable mode, to follow development or
119
+ change the code:
120
+
121
+ ```bash
122
+ git clone https://github.com/Dark-Elektron/cavsim2d.git
123
+ cd cavsim2d
124
+ pip install -e .
125
+ ```
126
+
127
+ Either way this installs the core dependencies (NGSolve, gmsh, numpy, scipy,
128
+ pandas, matplotlib, …). For the example notebooks and the Jupyter banner, also
129
+ install the optional extra:
130
+
131
+ ```bash
132
+ pip install "cavsim2d[jupyter]" # from PyPI
133
+ pip install -e ".[jupyter]" # from a clone
134
+ ```
135
+
136
+ Requirements: **Python ≥ 3.10**. NGSolve (the finite-element backend) and gmsh
137
+ are installed from PyPI as dependencies.
138
+
139
+ ### Third-party code — ABCI (wakefield only)
140
+
141
+ Wakefield analysis uses ABCI, the electromagnetic time-domain solver written by
142
+ Yong Ho Chin at KEK. ABCI is free to use but not open source, and KEK asks that
143
+ it be obtained from its own page, so **cavsim2d does not include it**. To run
144
+ wakefield analyses:
145
+
146
+ 1. Download ABCI from the [ABCI page at KEK](https://abci.kek.jp/abci.htm)
147
+ (for 64-bit Windows, `ABCI_MP64_12_5.zip` at the time of writing) and unzip it
148
+ anywhere. No installation is needed.
149
+ 2. Point cavsim2d at the executable with the `CAVSIM2D_ABCI` environment
150
+ variable. It may also name the folder that holds `ABCI.exe`.
151
+
152
+ ```shell
153
+ # Windows, persistent for new terminals and Jupyter sessions
154
+ setx CAVSIM2D_ABCI "C:\ABCI\ABCI_MP64_12_5.exe"
155
+ # Linux / macOS
156
+ export CAVSIM2D_ABCI="$HOME/abci/ABCI_MP64_12_5.exe"
157
+ ```
158
+
159
+ or from Python, before the first wakefield run:
160
+
161
+ ```python
162
+ import os
163
+ os.environ['CAVSIM2D_ABCI'] = r'C:\ABCI\ABCI_MP64_12_5.exe'
164
+ ```
165
+
166
+ If you know someone else who wants ABCI, point them to the KEK page rather than
167
+ passing on a copy. In a source checkout, a copy at
168
+ `cavsim2d/solvers/ABCI/ABCI.exe` is used when `CAVSIM2D_ABCI` is unset; that path
169
+ is gitignored and is never packaged.
170
+
171
+ ABCI is a Windows binary. On Windows it runs directly; on Linux/macOS it is
172
+ launched through [`wine`](https://www.winehq.org) if that is installed. If ABCI
173
+ or `wine` is missing, wakefield runs fail early with a message saying where to
174
+ get it. Eigenmode, tuning, UQ and optimisation do not need ABCI.
175
+
176
+ > [!TIP]
177
+ > `pip install pprintpp` gives nicer nested-dict printing for the quantities of
178
+ > interest, but is not required.
179
+
180
+ ---
181
+
182
+ ## Quickstart
183
+
184
+ Everything is driven by two classes: **`Study`**, a container that owns a
185
+ project folder, and **`Cavity`** (here `EllipticalCavity`), a single cavity.
186
+ Results are written under the project folder and cached on the objects.
187
+
188
+ ```python
189
+ import pprint
190
+ pp = pprint.PrettyPrinter(indent=4)
191
+ from cavsim2d import Study, EllipticalCavity
192
+
193
+ # A project folder — all results and geometry are written here.
194
+ cavs = Study(r'/path/to/my_project')
195
+
196
+ # A 1-cell TESLA mid-cell: A, B, a, b, Ri, L, Req [mm]
197
+ midcell = [42, 42, 12, 19, 35, 57.7, 103.353]
198
+ tesla = EllipticalCavity(1, midcell, midcell, midcell, beampipe='none')
199
+ cavs.add_cavity([tesla], names=['TESLA'])
200
+
201
+ # Fundamental-mode eigenmode analysis.
202
+ cavs.run_eigenmode({'processes': 1, 'rerun': True, 'boundary_conditions': 'mm'})
203
+ pp.pprint(cavs.eigenmode_qois)
204
+ ```
205
+
206
+ For the 1-cell TESLA cavity this reports `freq ≈ 1300 MHz`, `R/Q ≈ 113 Ω`,
207
+ `G ≈ 271 Ω`, `Epk/Eacc ≈ 2.04`, `Bpk/Eacc ≈ 4.16 mT/(MV/m)`.
208
+
209
+ > [!TIP]
210
+ > Prefer runnable scripts to prose? The [`examples/`](https://github.com/Dark-Elektron/cavsim2d/tree/master/examples) directory has
211
+ > one self-contained script per feature (`01_eigenmode.py` … `06_optimisation.py`
212
+ > and `run_all.py`) that writes results and plots to disk. They are the quickest
213
+ > way to see each workflow end to end.
214
+
215
+ ### Project folder layout
216
+
217
+ ```
218
+ my_project/
219
+ └── cavities/
220
+ └── TESLA/
221
+ ├── geometry/ geodata.geo, snapshot
222
+ ├── eigenmode/
223
+ │ ├── monopole/ qois.json, qois_all_modes.json, fields, mesh
224
+ │ ├── dipole/ (if requested)
225
+ │ └── quadrupole/ (if requested)
226
+ ├── tuned/ tuned cavity + tune_info/
227
+ ├── wakefield/
228
+ │ ├── longitudinal/ ABCI output
229
+ │ └── transversal/
230
+ └── uq/ UQ nodes and statistics
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Eigenmode analysis
236
+
237
+ `run_eigenmode` accepts a configuration dictionary; every key is optional:
238
+
239
+ | key | meaning | default |
240
+ |-----|---------|---------|
241
+ | `processes` | parallel worker processes | 1 |
242
+ | `rerun` | recompute even if results exist | True |
243
+ | `boundary_conditions` | end-wall BCs, e.g. `'mm'` (magnetic-magnetic) | `'mm'` |
244
+ | `polarisation` | azimuthal order(s) to solve — see below | `'monopole'` |
245
+ | `n_modes` | physical modes per m-pole solve | `n_cells + 2` |
246
+ | `mesh_config` | `{'h': ..., 'p': ..., 'adaptive': ...}` mesh size / order / adaptive refinement | h=20, p=3 |
247
+ | `conductivity` | wall conductivity [S/m] for Q/Ploss (normal conductor) | copper, 5.96e7 |
248
+ | `surface_resistance` | fixed surface resistance [Ω] (e.g. SRF) — overrides `conductivity` | — |
249
+ | `uq_config` | enable uncertainty quantification (see below) | — |
250
+
251
+ > [!TIP]
252
+ > Q, Rsh and wall power depend on the surface resistance. By default the walls
253
+ > are copper; pass `conductivity` for another normal conductor, or
254
+ > `surface_resistance` (a fixed Rs, e.g. `1e-8` Ω) to model an SRF cavity. The
255
+ > geometric factor `G` is material-independent.
256
+
257
+ > [!TIP]
258
+ > **Adaptive mesh refinement.** Set `mesh_config['adaptive']` to `True` (e.g.
259
+ > `{'h': 20, 'p': 3, 'adaptive': True}`) and the solver refines the mesh where
260
+ > the modes need it, stopping once every mode's recovery error is below the
261
+ > tolerance, then reports the QOIs on the finest mesh. Use a dict to tune it:
262
+ > `{'adaptive': {'tol': 1e-12, 'max_refinements': 6, 'max_ndof': 80000,
263
+ > 'theta': 0.25}}`. The per-level history is written to
264
+ > `eigenmode/monopole/adaptive_history.json`. `cav.study_mesh_convergence()`
265
+ > uses the same machinery to sweep error-vs-DOF at each polynomial order.
266
+
267
+ Access results either on the container or per cavity:
268
+
269
+ ```python
270
+ cavs.eigenmode_qois # {name: {qoi: value}} for the fundamental mode
271
+ tesla.get_eigenmode_qois() # populate tesla.eigenmode_qois, tesla.freq, ...
272
+ tesla.eigenmode.qois # fundamental-mode QOIs (solver-object API)
273
+ tesla.eigenmode.modes # list of all computed modes
274
+ ```
275
+
276
+ Compare several cavities:
277
+
278
+ ```python
279
+ cavs.add_cavity([EllipticalCavity(1,
280
+ [53.58, 36.58, 8.08, 9.84, 35, 57.7, 98.27], # a re-entrant mid-cell
281
+ [53.58, 36.58, 8.08, 9.84, 35, 57.7, 98.27],
282
+ [53.58, 36.58, 8.08, 9.84, 35, 57.7, 98.27], beampipe='none')], ['reentrant'])
283
+ cavs.run_eigenmode({'processes': 1, 'rerun': True, 'boundary_conditions': 'mm'})
284
+ cavs.eigenmode.plot_fm_bar()
285
+ ```
286
+
287
+ ### Visualising the mesh and fields
288
+
289
+ Meshes and fields belong to a **`Cavity`** (not the container), so index or name
290
+ the cavity:
291
+
292
+ ```python
293
+ cavs['TESLA'].plot_mesh()
294
+ cavs['TESLA'].plot_fields(mode=1, which='E') # |E| of the fundamental
295
+ cavs['TESLA'].plot_fields(mode=1, which='H') # |H|
296
+ cavs['TESLA'].plot_axis_field() # on-axis Ez and field flatness
297
+ ```
298
+
299
+ All `plot*` methods return a `matplotlib` axes/figure so you can restyle or save
300
+ them, and share one warm-cast, publication-oriented palette and font (STIX;
301
+ `cavsim2d.utils.style`, `apply_style()` to make it your session default). For
302
+ multi-cell cavities, `plot_dispersion()` draws the Brillouin diagram: it overlays
303
+ every computed polarisation, colour-coded, and shows all modes (grouped into
304
+ passbands of `n_cells`). Options:
305
+
306
+ - `pol=` — a name ('dipole'), a list, or `None` for all computed polarisations;
307
+ - `bands=` — 1-based passband selection; a flat `[1, 2]` for all polarisations or
308
+ a nested `[[1, 2], [1]]` for one selection per polarisation;
309
+ - `break_axis=` (default `True`) — splits the y-axis where passbands are far
310
+ apart so the empty space between them is not wasted; the split points are found
311
+ automatically from the frequency grouping. Pass `breaks=[(1350, 1600), (1900,
312
+ 2350)]` to place them by hand (each is an approximate `(low, high)` gap).
313
+ - `light_line=` (default `True`) — overlays the speed-of-light line `f = c·μ/(2π·d)`.
314
+ Because the diagram is a reduced (folded) zone, it appears as a triangle wave
315
+ crossing every band; the accelerating π-mode sits on it. Skipped for geometries
316
+ with no defined cell period.
317
+
318
+ ---
319
+
320
+ ## Higher-order-mode (m-pole) eigenmodes
321
+
322
+ Dipole, quadrupole, and higher azimuthal modes ($m \ge 1$) are solved on the same
323
+ 2D meridian mesh via an $H(\mathrm{curl}) \times H^1$ product-space formulation.
324
+ Select them with the `polarisation` key (names or azimuthal mode numbers, a
325
+ single value or a list):
326
+
327
+ ```python
328
+ cavs.run_eigenmode({
329
+ 'processes': 1,
330
+ 'rerun': True,
331
+ 'boundary_conditions': 'mm',
332
+ 'polarisation': ['monopole', 'dipole', 'quadrupole'], # or [0, 1, 2]
333
+ 'n_modes': 4,
334
+ })
335
+
336
+ # Per-polarisation results:
337
+ tesla.eigenmode.mpole_qois('dipole') # all dipole modes, by index
338
+ tesla.eigenmode.mpole_modes('quadrupole') # list of EigenmodeResult
339
+ tesla.plot_fields(mode=0, which='E', pol='dipole') # in-plane |E| envelope
340
+ tesla.plot_fields(mode=0, which='Ephi', pol='dipole') # azimuthal envelope
341
+ ```
342
+
343
+ Each polarisation is written to its own folder (`eigenmode/monopole/`,
344
+ `eigenmode/dipole/`, …). Reruns are per polarisation: rerunning the dipole never
345
+ overwrites monopole results, and `rerun=False` only solves the polarisations
346
+ whose results are missing.
347
+
348
+ For $m \ge 1$ the accelerating quantities are the **transverse** analogues: since
349
+ $E_z \sim r^m$ vanishes on axis, the longitudinal voltage is evaluated off-axis
350
+ and converted to a transverse kick voltage via Panofsky–Wenzel. The QOI keys
351
+ `Vacc`/`Eacc`/`R/Q` hold these transverse values, and `R/Q_t [Ohm/m^(2(m-1))]`
352
+ is normalised so it is independent of the evaluation radius (equal to `R/Q` for
353
+ the dipole).
354
+
355
+ **Conventions to note.** The headline `qois.json` for an m-pole solve is the
356
+ **lowest** mode (index 0), whereas the monopole headline is the accelerating
357
+ π-mode (index `n_cells`). Use `mpole_qois(pol)` for the full set. The `kcc [%]`
358
+ cell-to-cell coupling for $m \ge 1$ reuses the monopole passband formula
359
+ (relative to mode index 1) and is of limited physical meaning for a deflecting
360
+ passband. Plot the passband with `cav.plot_dispersion(pol='dipole')`.
361
+
362
+ M-pole QOIs are not yet available as UQ or optimisation objectives (those still
363
+ operate on the monopole figures of merit).
364
+
365
+ ---
366
+
367
+ ## Cavity tuning
368
+
369
+ Tuning drives a chosen geometry parameter until the fundamental hits a target
370
+ frequency. Give a target `freqs` and a `cell_type` mapping (cell → parameter to
371
+ vary); a nested `eigenmode_config` controls the solves.
372
+
373
+ ```python
374
+ cavs = Study(r'/path/to/my_project')
375
+ midcell = [42, 42, 12, 19, 35, 57.7, 100] # deliberately wrong Req
376
+ tesla = EllipticalCavity(1, midcell, midcell, midcell, beampipe='none')
377
+ cavs.add_cavity([tesla], ['TESLA'])
378
+
379
+ cavs.run_tune({
380
+ 'freqs': 1300, # MHz
381
+ 'cell_type': {'mid-cell': 'Req'}, # vary Req of the mid-cell
382
+ 'processes': 1,
383
+ 'rerun': True,
384
+ 'eigenmode_config': {'processes': 1, 'rerun': True, 'boundary_conditions': 'mm'},
385
+ })
386
+
387
+ pp.pprint(tesla.tune.qois) # tuned parameters and achieved frequency
388
+ tuned = tesla.tuned # the tuned Cavity (its own folder + results)
389
+ ```
390
+
391
+ `Req` converges to ≈ 103.35 mm and the frequency to 1300 MHz. The tuned cavity is
392
+ a full `Cavity` living under `<cavity>/tuned/`, so you can run eigenmode/wakefield
393
+ on it directly (`tesla.tuned.run_eigenmode(...)`).
394
+
395
+ ---
396
+
397
+ ## Wakefield analysis
398
+
399
+ ```python
400
+ cavs = Study(r'/path/to/my_project')
401
+ midcell = [42, 42, 12, 19, 35, 57.7, 103.353]
402
+ endcell_l = [40.34, 40.34, 10, 13.5, 39, 55.716, 103.353]
403
+ endcell_r = [42, 42, 9, 12.8, 39, 56.815, 103.353]
404
+ tesla = EllipticalCavity(9, midcell, endcell_l, endcell_r, beampipe='both')
405
+ cavs.add_cavity([tesla], ['TESLA'])
406
+
407
+ cavs.run_wakefield({
408
+ 'processes': 1,
409
+ 'rerun': True,
410
+ 'MROT': 'both', # 'monopole'/'longitudinal' (0), 'dipole'/'transverse' (1), 'both' (2)
411
+ 'wakelength': 50, # metres
412
+ 'bunch_length': 25, # mm
413
+ })
414
+ ```
415
+
416
+ > [!NOTE]
417
+ > In **wakefield** configs the beam mode is `MROT` (`'polarisation'` is accepted
418
+ > as a deprecated alias). This is *not* the same as `polarisation` in an
419
+ > **eigenmode** config, which selects the azimuthal mode order.
420
+
421
+ ABCI requires a beam pipe at **each** end of the structure, of at least five mesh
422
+ lengths. Any end that does not already have one is given a pipe of **three times
423
+ the device's axial length**; ends that already have one are left alone. Override
424
+ with `wakefield_config['beampipe_length']` (metres). This means geometries built
425
+ with `beampipe='none'` still run — the pipes exist only in the ABCI deck, not in
426
+ the cavity you defined.
427
+
428
+ Plot the impedance spectra and wake potentials:
429
+
430
+ ```python
431
+ ax = tesla.plot('zl') # longitudinal impedance |Z_L|
432
+ ax = tesla.plot('zt', ax) # transverse impedance |Z_T| on the same axes
433
+ tesla.plot('wpl'); tesla.plot('wpt') # wake potentials
434
+
435
+ # Or via the solver-object API, which returns DataFrames:
436
+ tesla.wakefield.wake_z # f [MHz], |Z|, Re(Z), Im(Z), s [m], W
437
+ tesla.wakefield.plot_z(quantity='impedance')
438
+ tesla.wakefield.plot_t(quantity='wake')
439
+
440
+ # The main run always reports the loss/kick factors:
441
+ tesla.wakefield.qois # {'|k_loss| [V/pC]', 'k_FM [V/pC]',
442
+ # 'k_loss_HOM [V/pC]', '|k_kick| [V/pC/m]'}
443
+ ```
444
+
445
+ Any wakefield solver plugs in behind `cav.wakefield` — ABCI is the default
446
+ backend, selected by `wakefield_config['solver']` (`'abci'`). The results above
447
+ are read through a normalised schema, so nothing above changes when the solver
448
+ does.
449
+
450
+ ### Loss/kick factors and HOM power at operating points
451
+
452
+ The main run's loss/kick factors are always in `cav.wakefield.qois`. To also get
453
+ higher-order-mode *power* at machine operating points, pass an `operating_points`
454
+ dictionary (this re-runs per bunch length and needs R/Q from a prior eigenmode
455
+ run); those results are reported separately in `cav.wakefield.qois_op`:
456
+
457
+ ```python
458
+ op_points = {
459
+ "Z": {
460
+ "freq [MHz]": 400.79, "E [GeV]": 45.6, "I0 [mA]": 1280, "V [GV]": 0.12,
461
+ "Eacc [MV/m]": 5.72, "nu_s []": 0.0370, "alpha_p [1e-5]": 2.85,
462
+ "tau_z [ms]": 354.91, "tau_xy [ms]": 709.82, "f_rev [kHz]": 3.07,
463
+ "beta_xy [m]": 56, "N_c []": 56, "T [K]": 4.5,
464
+ "sigma_SR [mm]": 4.32, "sigma_BS [mm]": 15.2, "Nb [1e11]": 2.76,
465
+ }
466
+ }
467
+ cavs.run_wakefield({
468
+ 'processes': 1, 'rerun': True,
469
+ 'bunch_length': 25, 'wakelength': 50,
470
+ 'operating_points': op_points,
471
+ })
472
+ pp.pprint(cavs.wakefield_qois)
473
+ cavs.wakefield.plot_hom_bar('Z_SR_4.32mm')
474
+ ```
475
+
476
+ ---
477
+
478
+ ## Objective names
479
+
480
+ Monopole and m-pole results share QOI key names — `freq [MHz]`, `R/Q [Ohm]`,
481
+ `G [Ohm]` and 15 others appear in every polarisation's `qois.json`. So every UQ
482
+ and optimisation objective must say which polarisation it means:
483
+
484
+ ```text
485
+ monopole:R/Q [Ohm] # the polarisation's primary mode of interest
486
+ dipole:R/Q_t [Ohm/m^(2(m-1))] # a dipole-only QOI
487
+ dipole:2:freq [MHz] # dipole, mode 2 (1-based)
488
+ 1:freq [MHz] # azimuthal number instead of a name
489
+ ```
490
+
491
+ The separator is `:`; no QOI name contains one, and only the first two fields
492
+ are split, so the QOI itself may contain anything. A bare `'freq [MHz]'` is
493
+ **rejected** — it used to silently mean *the monopole's*, which becomes a wrong
494
+ answer the moment a second polarisation is solved. An objective naming a QOI or
495
+ mode that does not exist raises a `ValueError` listing what is available;
496
+ previously it was dropped without a word and the statistics were quietly
497
+ computed over whichever objectives happened to match.
498
+
499
+ Naming a polarisation in an objective is enough — the UQ run adds it to
500
+ `eigenmode_config['polarisation']` automatically. Wakefield objectives (`ZL`,
501
+ `ZT`) carry no polarisation and are written as before.
502
+
503
+ ### Which mode is reported: `mode_of_interest`
504
+
505
+ `eigenmode_config['mode_of_interest']` selects the modes whose QOIs are reported.
506
+ It is **1-based** (mode 1 is the lowest of the passband) and may be an int, a list
507
+ of **any length**, or a dict keyed by polarisation — each polarisation may carry a
508
+ different number of modes. Order is preserved and duplicates collapse. The
509
+ **first** mode listed is the primary one and lands in `qois.json`; all of them are
510
+ written to `qois_moi.json` keyed by 1-based mode, and each is addressable as an
511
+ objective (`monopole:4:freq [MHz]`).
512
+
513
+ ```python
514
+ 'mode_of_interest': 9 # every polarisation
515
+ 'mode_of_interest': [1, 2, 3] # three, for every one
516
+ 'mode_of_interest': {'monopole': [1, 2, 3, 4], 'dipole': 1} # different counts
517
+ ```
518
+
519
+ Defaults: **monopole → `n_cells`**, the π-mode — the operating mode of an
520
+ accelerating structure. A 1-cell cavity, gun or pillbox therefore gets mode 1,
521
+ so the convention degrades gracefully to non-accelerator geometries. **m-pole →
522
+ mode 1**, the lowest of the deflecting passband (an m-pole spectrum has no
523
+ spurious DC mode to skip).
524
+
525
+ ## Uncertainty quantification
526
+
527
+ Any analysis can carry uncertainty quantification by adding a `uq_config`. It
528
+ propagates geometry uncertainties through the solve and reports the mean and
529
+ standard deviation of each objective. Result keys are the canonical objective
530
+ names, e.g. `uq.json['dipole:freq [MHz]']['expe'][0]`.
531
+
532
+ ```python
533
+ cavs.run_eigenmode({
534
+ 'processes': 1,
535
+ 'rerun': True,
536
+ 'boundary_conditions': 'mm',
537
+ 'uq_config': {
538
+ 'variables': ['A', 'B', 'a', 'b'], # varied parameters
539
+ # Objectives must name a polarisation (see "Objective names" below)
540
+ 'objectives': ["monopole:freq [MHz]", "monopole:R/Q [Ohm]", "monopole:Epk/Eacc []",
541
+ "monopole:Bpk/Eacc [mT/MV/m]", "monopole:G [Ohm]"],
542
+ 'delta': [0.05, 0.05, 0.05, 0.05], # 5% std on each
543
+ 'processes': 1,
544
+ 'distribution': 'gaussian',
545
+ 'method': ['Quadrature', 'Stroud3'],
546
+ 'cell_type': 'mid-cell',
547
+ 'cell_complexity': 'simplecell',
548
+ },
549
+ })
550
+ pp.pprint(cavs.uq_fm_results)
551
+ cavs.eigenmode.plot_fm_bar(uq=True) # bar chart with error bars
552
+ ```
553
+
554
+ > [!IMPORTANT]
555
+ > The key is `cell_complexity` (underscore). If a config contains an unrecognised
556
+ > key, `cavsim2d` emits a `UserWarning` with a "did you mean …?" suggestion —
557
+ > useful for catching typos that would otherwise be silently ignored.
558
+
559
+ ---
560
+
561
+ ## Optimisation
562
+
563
+ Multi-objective shape optimisation uses a genetic algorithm. Each candidate is
564
+ tuned to the target frequency (via a nested `tune_config`) before its objectives
565
+ are evaluated.
566
+
567
+ ```python
568
+ cavs = Study(r'/path/to/my_project')
569
+
570
+ optimisation_config = {
571
+ 'initial_points': 6,
572
+ 'no_of_generation': 2,
573
+ 'method': {'LHS': {'seed': 5}},
574
+ 'bounds': { # search space; fix a variable with equal bounds
575
+ 'A': [38, 46], 'B': [38, 46], 'a': [10, 15], 'b': [15, 22],
576
+ 'Ri': [33, 37], 'L': [57.7, 57.7], 'Req': [100, 106],
577
+ },
578
+ 'objectives': [
579
+ ['min', 'monopole:Epk/Eacc []'],
580
+ ['min', 'monopole:Bpk/Eacc [mT/MV/m]'],
581
+ # ['max', 'monopole:R/Q [Ohm]'],
582
+ # ['min', 'dipole:R/Q_t [Ohm/m^(2(m-1))]'], # deflecting-mode impedance
583
+ # ['min', 'ZL', [1, 2, 5]], # impedance peak in a frequency window (needs wakefield)
584
+ ],
585
+ 'tune_config': {
586
+ 'freqs': 1300,
587
+ 'cell_type': {'mid-cell': 'Req'},
588
+ 'processes': 1,
589
+ 'eigenmode_config': {'n_cells': 1, 'processes': 1, 'boundary_conditions': 'mm'},
590
+ },
591
+ # GA controls — these are integer offspring counts, not fractions:
592
+ 'mutation_factor': 4,
593
+ 'crossover_factor': 4,
594
+ 'elites_for_crossover': 2,
595
+ 'chaos_factor': 2,
596
+ 'weights': [1, 1],
597
+ }
598
+ cavs.run_optimisation(optimisation_config)
599
+
600
+ opt = cavs.optimisation
601
+ opt.history # every evaluated candidate (DataFrame)
602
+ opt.pareto # Pareto-optimal candidates
603
+ opt.plot_pareto(kind='scatter')
604
+ opt.plot_convergence()
605
+ ```
606
+
607
+ Supported objective quantities: `freq [MHz]`, `Epk/Eacc []`,
608
+ `Bpk/Eacc [mT/MV/m]`, `R/Q [Ohm]`, `G [Ohm]`, `Q []`, and the wakefield impedance
609
+ peaks `ZL`, `ZT` (with a `[low, ..., high]` frequency window as the third
610
+ element). Each objective is `['min'|'max'|'equal', name]` (plus a target value
611
+ for `'equal'`, or the window for `ZL`/`ZT`).
612
+
613
+ > [!NOTE]
614
+ > `mutation_factor`, `crossover_factor` and `chaos_factor` are **counts** of
615
+ > offspring / random individuals per generation (integers), not probabilities.
616
+
617
+ ---
618
+
619
+ ## Configuration dictionaries
620
+
621
+ Every analysis is driven by a config dict, and they nest consistently. UQ slots
622
+ into any solver config via `uq_config`; optimisation wraps a `tune_config` (which
623
+ itself wraps an `eigenmode_config`), and optionally a `wakefield_config`:
624
+
625
+ ```
626
+ optimisation_config
627
+ ├── tune_config
628
+ │ └── eigenmode_config
629
+ │ └── uq_config
630
+ ├── wakefield_config
631
+ │ └── uq_config
632
+ ├── bounds, objectives, initial_points, no_of_generation, ...
633
+ └── mutation_factor, crossover_factor, elites_for_crossover, chaos_factor
634
+ ```
635
+
636
+ Unrecognised keys are reported as warnings (with suggestions), so a mistyped key
637
+ never silently changes behaviour. For the full accepted-key reference, see
638
+ `cavsim2d/utils/config_validation.py`, or `help(cavs.run_eigenmode)` etc.
639
+
640
+ ---
641
+
642
+ ## Parallelisation
643
+
644
+ Set `processes` in any config to run cavities in parallel. When UQ is enabled, a
645
+ second `processes` inside `uq_config` parallelises the quadrature/sample points.
646
+ The default is a single process (which also gives the cleanest tracebacks and
647
+ Jupyter output).
648
+
649
+ ---
650
+
651
+ ## Running the tests
652
+
653
+ ```bash
654
+ pip install -e ".[dev]"
655
+ pytest tests/ -q
656
+ ```
657
+
658
+ The solver tests self-skip where NGSolve/gmsh are unavailable, and the wakefield
659
+ tests skip unless ABCI is available on Windows (see above), so the suite stays
660
+ green in minimal environments.
661
+
662
+ ---
663
+
664
+ ## License
665
+
666
+ MIT — see [LICENSE](https://github.com/Dark-Elektron/cavsim2d/blob/master/LICENSE).