pyffag 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.
- pyffag-0.1.0/LICENSE +21 -0
- pyffag-0.1.0/PKG-INFO +101 -0
- pyffag-0.1.0/README.md +81 -0
- pyffag-0.1.0/pyffag/__init__.py +51 -0
- pyffag-0.1.0/pyffag/constants.py +73 -0
- pyffag-0.1.0/pyffag/elements.py +116 -0
- pyffag-0.1.0/pyffag/optics.py +118 -0
- pyffag-0.1.0/pyffag/ring.py +173 -0
- pyffag-0.1.0/pyffag/sector.py +99 -0
- pyffag-0.1.0/pyffag.egg-info/PKG-INFO +101 -0
- pyffag-0.1.0/pyffag.egg-info/SOURCES.txt +17 -0
- pyffag-0.1.0/pyffag.egg-info/dependency_links.txt +1 -0
- pyffag-0.1.0/pyffag.egg-info/requires.txt +2 -0
- pyffag-0.1.0/pyffag.egg-info/top_level.txt +1 -0
- pyffag-0.1.0/pyproject.toml +27 -0
- pyffag-0.1.0/setup.cfg +4 -0
- pyffag-0.1.0/tests/test_adts.py +147 -0
- pyffag-0.1.0/tests/test_basic.py +231 -0
- pyffag-0.1.0/tests/test_crossval.py +161 -0
pyffag-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eremey Valetov
|
|
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.
|
pyffag-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyffag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DA-based FFAG accelerator tracking using differential algebra
|
|
5
|
+
Author-email: Eremey Valetov <evv@msu.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/evvaletov/pyffag
|
|
8
|
+
Keywords: FFAG,accelerator,beam physics,differential algebra,transfer map
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: daceypy>=1.3.0
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# pyffag
|
|
22
|
+
|
|
23
|
+
DA-based FFAG accelerator tracking using differential algebra.
|
|
24
|
+
|
|
25
|
+
Built on [daceypy](https://pypi.org/project/daceypy/) for arbitrary-order
|
|
26
|
+
transfer map computation through FFAG sector magnets via integration of the
|
|
27
|
+
exact midplane Hamiltonian.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pyffag
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import numpy as np
|
|
39
|
+
from daceypy import DA
|
|
40
|
+
from pyffag import sector_map, compose_sequence, compose_n, tune, twiss
|
|
41
|
+
from pyffag.constants import kinetic_to_brho, M_PROTON
|
|
42
|
+
|
|
43
|
+
# 150 MeV proton FFAG ring: 12 FDF-triplet cells
|
|
44
|
+
DA.init(7, 2) # DA order 7, 2 variables (x, px)
|
|
45
|
+
Brho = kinetic_to_brho(150.0, M_PROTON) # magnetic rigidity [T·m]
|
|
46
|
+
|
|
47
|
+
# F magnet: B(x) = 1.2 + 3.0*x + 4.0*x² [T], 12° sector
|
|
48
|
+
F = sector_map([1.2, 3.0, 4.0], Brho, angle=np.radians(12.0))
|
|
49
|
+
|
|
50
|
+
# D magnet: B(x) = 1.2 − 5.0*x − 5.0*x² [T], 6° sector
|
|
51
|
+
D = sector_map([1.2, -5.0, -5.0], Brho, angle=np.radians(6.0))
|
|
52
|
+
|
|
53
|
+
# One cell = F + D + F, full ring = 12 cells
|
|
54
|
+
cell = compose_sequence([F, D, F])
|
|
55
|
+
ring = compose_n(cell, 12)
|
|
56
|
+
|
|
57
|
+
print(f"Cell tune: {twiss(cell)['tune']:.4f}")
|
|
58
|
+
print(f"Ring tune: {tune(ring):.4f}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **Sector magnet tracking**: Exact midplane Hamiltonian integration
|
|
64
|
+
(no paraxial approximation) through sector magnets with polynomial
|
|
65
|
+
field profiles B(x) = B₀ + B₁x + B₂x² + ...
|
|
66
|
+
- **Element maps**: Drift (exact), thin quadrupole, sextupole, octupole,
|
|
67
|
+
edge kicks for rectangular magnets
|
|
68
|
+
- **Ring operations**: Map composition, N-fold composition, closed orbit
|
|
69
|
+
finding via Newton's method with DA Jacobian
|
|
70
|
+
- **Optics**: Tune, Twiss parameters, stability check, symplecticity error
|
|
71
|
+
|
|
72
|
+
## Physics
|
|
73
|
+
|
|
74
|
+
The core `sector_map()` integrates the equations of motion in Frenet-Serret
|
|
75
|
+
(curvilinear) coordinates with arc length as the independent variable:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
dx/ds = (1 + hx) · px / √(1 − px²)
|
|
79
|
+
dpx/ds = h · √(1 − px²) − (1 + hx) · By(x) / (Bρ)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
where h = 1/ρ is the reference curvature. Sector magnets have radial edge
|
|
83
|
+
faces (no edge focusing). The exact sqrt formulation captures kinematic
|
|
84
|
+
nonlinearities that the paraxial approximation misses.
|
|
85
|
+
|
|
86
|
+
## Integration with danf
|
|
87
|
+
|
|
88
|
+
Use with [danf](https://pypi.org/project/danf/) for nonlinear normal form
|
|
89
|
+
analysis (amplitude-dependent tune shifts, resonance driving terms):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from danf import NormalForm
|
|
93
|
+
|
|
94
|
+
nf = NormalForm(ring)
|
|
95
|
+
nf.compute()
|
|
96
|
+
print(f"ADTS: dν/dε = {nf.detuning['dnux_dJx'] / 2:.4f}")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
pyffag-0.1.0/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# pyffag
|
|
2
|
+
|
|
3
|
+
DA-based FFAG accelerator tracking using differential algebra.
|
|
4
|
+
|
|
5
|
+
Built on [daceypy](https://pypi.org/project/daceypy/) for arbitrary-order
|
|
6
|
+
transfer map computation through FFAG sector magnets via integration of the
|
|
7
|
+
exact midplane Hamiltonian.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyffag
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import numpy as np
|
|
19
|
+
from daceypy import DA
|
|
20
|
+
from pyffag import sector_map, compose_sequence, compose_n, tune, twiss
|
|
21
|
+
from pyffag.constants import kinetic_to_brho, M_PROTON
|
|
22
|
+
|
|
23
|
+
# 150 MeV proton FFAG ring: 12 FDF-triplet cells
|
|
24
|
+
DA.init(7, 2) # DA order 7, 2 variables (x, px)
|
|
25
|
+
Brho = kinetic_to_brho(150.0, M_PROTON) # magnetic rigidity [T·m]
|
|
26
|
+
|
|
27
|
+
# F magnet: B(x) = 1.2 + 3.0*x + 4.0*x² [T], 12° sector
|
|
28
|
+
F = sector_map([1.2, 3.0, 4.0], Brho, angle=np.radians(12.0))
|
|
29
|
+
|
|
30
|
+
# D magnet: B(x) = 1.2 − 5.0*x − 5.0*x² [T], 6° sector
|
|
31
|
+
D = sector_map([1.2, -5.0, -5.0], Brho, angle=np.radians(6.0))
|
|
32
|
+
|
|
33
|
+
# One cell = F + D + F, full ring = 12 cells
|
|
34
|
+
cell = compose_sequence([F, D, F])
|
|
35
|
+
ring = compose_n(cell, 12)
|
|
36
|
+
|
|
37
|
+
print(f"Cell tune: {twiss(cell)['tune']:.4f}")
|
|
38
|
+
print(f"Ring tune: {tune(ring):.4f}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Sector magnet tracking**: Exact midplane Hamiltonian integration
|
|
44
|
+
(no paraxial approximation) through sector magnets with polynomial
|
|
45
|
+
field profiles B(x) = B₀ + B₁x + B₂x² + ...
|
|
46
|
+
- **Element maps**: Drift (exact), thin quadrupole, sextupole, octupole,
|
|
47
|
+
edge kicks for rectangular magnets
|
|
48
|
+
- **Ring operations**: Map composition, N-fold composition, closed orbit
|
|
49
|
+
finding via Newton's method with DA Jacobian
|
|
50
|
+
- **Optics**: Tune, Twiss parameters, stability check, symplecticity error
|
|
51
|
+
|
|
52
|
+
## Physics
|
|
53
|
+
|
|
54
|
+
The core `sector_map()` integrates the equations of motion in Frenet-Serret
|
|
55
|
+
(curvilinear) coordinates with arc length as the independent variable:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
dx/ds = (1 + hx) · px / √(1 − px²)
|
|
59
|
+
dpx/ds = h · √(1 − px²) − (1 + hx) · By(x) / (Bρ)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
where h = 1/ρ is the reference curvature. Sector magnets have radial edge
|
|
63
|
+
faces (no edge focusing). The exact sqrt formulation captures kinematic
|
|
64
|
+
nonlinearities that the paraxial approximation misses.
|
|
65
|
+
|
|
66
|
+
## Integration with danf
|
|
67
|
+
|
|
68
|
+
Use with [danf](https://pypi.org/project/danf/) for nonlinear normal form
|
|
69
|
+
analysis (amplitude-dependent tune shifts, resonance driving terms):
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from danf import NormalForm
|
|
73
|
+
|
|
74
|
+
nf = NormalForm(ring)
|
|
75
|
+
nf.compute()
|
|
76
|
+
print(f"ADTS: dν/dε = {nf.detuning['dnux_dJx'] / 2:.4f}")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""pyffag — DA-based FFAG accelerator tracking.
|
|
2
|
+
|
|
3
|
+
Built on daceypy for arbitrary-order differential algebra arithmetic,
|
|
4
|
+
pyffag provides transfer map computation through FFAG sector magnets
|
|
5
|
+
via integration of the exact midplane Hamiltonian.
|
|
6
|
+
|
|
7
|
+
Example
|
|
8
|
+
-------
|
|
9
|
+
>>> from daceypy import DA
|
|
10
|
+
>>> from pyffag import sector_map, compose_n, tune
|
|
11
|
+
>>> from pyffag.constants import kinetic_to_brho, M_PROTON
|
|
12
|
+
>>>
|
|
13
|
+
>>> DA.init(5, 2)
|
|
14
|
+
>>> Brho = kinetic_to_brho(150.0, M_PROTON)
|
|
15
|
+
>>> cell = sector_map([1.0], Brho, angle=0.5236) # 30-degree uniform dipole
|
|
16
|
+
>>> ring = compose_n(cell, 12)
|
|
17
|
+
>>> print(f"Tune: {tune(ring):.4f}")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pyffag.sector import sector_map
|
|
21
|
+
from pyffag.elements import (
|
|
22
|
+
drift_map,
|
|
23
|
+
drift_map_paraxial,
|
|
24
|
+
edge_kick,
|
|
25
|
+
thin_quad,
|
|
26
|
+
thin_sext,
|
|
27
|
+
thin_oct,
|
|
28
|
+
)
|
|
29
|
+
from pyffag.ring import compose, compose_sequence, compose_n, find_closed_orbit
|
|
30
|
+
from pyffag.optics import transfer_matrix, tune, is_stable, twiss, symplecticity_error
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"sector_map",
|
|
36
|
+
"drift_map",
|
|
37
|
+
"drift_map_paraxial",
|
|
38
|
+
"edge_kick",
|
|
39
|
+
"thin_quad",
|
|
40
|
+
"thin_sext",
|
|
41
|
+
"thin_oct",
|
|
42
|
+
"compose",
|
|
43
|
+
"compose_sequence",
|
|
44
|
+
"compose_n",
|
|
45
|
+
"find_closed_orbit",
|
|
46
|
+
"transfer_matrix",
|
|
47
|
+
"tune",
|
|
48
|
+
"is_stable",
|
|
49
|
+
"twiss",
|
|
50
|
+
"symplecticity_error",
|
|
51
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Physical constants for accelerator physics (SI + natural units)."""
|
|
2
|
+
|
|
3
|
+
# Speed of light [m/s]
|
|
4
|
+
C_LIGHT = 299792458.0
|
|
5
|
+
|
|
6
|
+
# Proton mass [MeV/c^2]
|
|
7
|
+
M_PROTON = 938.27208816
|
|
8
|
+
|
|
9
|
+
# Electron mass [MeV/c^2]
|
|
10
|
+
M_ELECTRON = 0.51099895069
|
|
11
|
+
|
|
12
|
+
# Muon mass [MeV/c^2]
|
|
13
|
+
M_MUON = 105.6583755
|
|
14
|
+
|
|
15
|
+
# Elementary charge [C]
|
|
16
|
+
E_CHARGE = 1.602176634e-19
|
|
17
|
+
|
|
18
|
+
# Conversion: momentum [MeV/c] to magnetic rigidity [T·m]
|
|
19
|
+
# Brho = p / (q * c) = p [eV/c] / (e * c) = p [MeV/c] * 1e6 / (c)
|
|
20
|
+
# In convenient units: Brho [T·m] = p [MeV/c] / 299.792458
|
|
21
|
+
BRHO_FACTOR = 1e6 / C_LIGHT # multiply by p [MeV/c] to get Brho [T·m]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def kinetic_to_momentum(T, mass):
|
|
25
|
+
"""Convert kinetic energy to momentum.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
T : float
|
|
30
|
+
Kinetic energy [MeV].
|
|
31
|
+
mass : float
|
|
32
|
+
Rest mass [MeV/c^2].
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
float
|
|
37
|
+
Momentum [MeV/c].
|
|
38
|
+
"""
|
|
39
|
+
return (T * (T + 2 * mass)) ** 0.5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def momentum_to_brho(p):
|
|
43
|
+
"""Convert momentum to magnetic rigidity.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
p : float
|
|
48
|
+
Momentum [MeV/c].
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
float
|
|
53
|
+
Magnetic rigidity Bρ [T·m].
|
|
54
|
+
"""
|
|
55
|
+
return p * BRHO_FACTOR
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def kinetic_to_brho(T, mass):
|
|
59
|
+
"""Convert kinetic energy to magnetic rigidity.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
T : float
|
|
64
|
+
Kinetic energy [MeV].
|
|
65
|
+
mass : float
|
|
66
|
+
Rest mass [MeV/c^2].
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
float
|
|
71
|
+
Magnetic rigidity Bρ [T·m].
|
|
72
|
+
"""
|
|
73
|
+
return momentum_to_brho(kinetic_to_momentum(T, mass))
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Standard beam-line elements as DA maps (midplane, 1 DOF)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from daceypy import DA
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def drift_map(length):
|
|
8
|
+
"""Exact drift-space map (no paraxial approximation).
|
|
9
|
+
|
|
10
|
+
Uses the exact expression x' = x + L * px / sqrt(1 - px^2).
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
length : float
|
|
15
|
+
Drift length [m].
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
list of DA
|
|
20
|
+
[x_out, px_out].
|
|
21
|
+
"""
|
|
22
|
+
x, px = DA(1), DA(2)
|
|
23
|
+
ps = DA.sqrt(1.0 - px * px)
|
|
24
|
+
return [x + length * px / ps, px]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def drift_map_paraxial(length):
|
|
28
|
+
"""Paraxial drift-space map: x' = x + L*px.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
length : float
|
|
33
|
+
Drift length [m].
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
list of DA
|
|
38
|
+
[x_out, px_out].
|
|
39
|
+
"""
|
|
40
|
+
x, px = DA(1), DA(2)
|
|
41
|
+
return [x + length * px, px]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def edge_kick(h, e1):
|
|
45
|
+
"""Thin edge-focusing kick for a rectangular (non-sector) magnet.
|
|
46
|
+
|
|
47
|
+
At an edge tilted by angle e1 from the radial direction, the
|
|
48
|
+
horizontal kick is dpx = +h * tan(e1) * x.
|
|
49
|
+
|
|
50
|
+
For a sector magnet (radial edges), e1 = 0 and there is no kick.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
h : float
|
|
55
|
+
Reference curvature 1/rho [1/m].
|
|
56
|
+
e1 : float
|
|
57
|
+
Edge angle [rad]. Positive = edge rotated toward the magnet body.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
list of DA
|
|
62
|
+
[x_out, px_out].
|
|
63
|
+
"""
|
|
64
|
+
x, px = DA(1), DA(2)
|
|
65
|
+
return [x, px + h * np.tan(e1) * x]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def thin_quad(k1L):
|
|
69
|
+
"""Thin quadrupole kick: dpx = -k1L * x.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
k1L : float
|
|
74
|
+
Integrated quadrupole strength [1/m].
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
list of DA
|
|
79
|
+
[x_out, px_out].
|
|
80
|
+
"""
|
|
81
|
+
x, px = DA(1), DA(2)
|
|
82
|
+
return [x, px - k1L * x]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def thin_sext(k2L):
|
|
86
|
+
"""Thin sextupole kick: dpx = -(k2L/2) * x^2.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
k2L : float
|
|
91
|
+
Integrated sextupole strength [1/m^2].
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
list of DA
|
|
96
|
+
[x_out, px_out].
|
|
97
|
+
"""
|
|
98
|
+
x, px = DA(1), DA(2)
|
|
99
|
+
return [x, px - (k2L / 2) * x ** 2]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def thin_oct(k3L):
|
|
103
|
+
"""Thin octupole kick: dpx = -(k3L/6) * x^3.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
k3L : float
|
|
108
|
+
Integrated octupole strength [1/m^3].
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
list of DA
|
|
113
|
+
[x_out, px_out].
|
|
114
|
+
"""
|
|
115
|
+
x, px = DA(1), DA(2)
|
|
116
|
+
return [x, px - (k3L / 6) * x ** 3]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Linear optics extraction from DA transfer maps."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def transfer_matrix(da_map):
|
|
7
|
+
"""Extract the 2x2 transfer matrix from a DA map.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
da_map : list of DA
|
|
12
|
+
[x', px'] map components.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
ndarray, shape (2, 2)
|
|
17
|
+
Transfer matrix M.
|
|
18
|
+
"""
|
|
19
|
+
M = np.zeros((2, 2))
|
|
20
|
+
M[0, 0] = da_map[0].getCoefficient([1, 0])
|
|
21
|
+
M[0, 1] = da_map[0].getCoefficient([0, 1])
|
|
22
|
+
M[1, 0] = da_map[1].getCoefficient([1, 0])
|
|
23
|
+
M[1, 1] = da_map[1].getCoefficient([0, 1])
|
|
24
|
+
return M
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def tune(da_map):
|
|
28
|
+
"""Extract the fractional tune from a DA map.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
da_map : list of DA
|
|
33
|
+
One-turn (or one-cell) [x', px'] map.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
float
|
|
38
|
+
Fractional tune nu in [0, 0.5].
|
|
39
|
+
"""
|
|
40
|
+
M = transfer_matrix(da_map)
|
|
41
|
+
cos_mu = (M[0, 0] + M[1, 1]) / 2.0
|
|
42
|
+
cos_mu = np.clip(cos_mu, -1.0, 1.0)
|
|
43
|
+
return np.arccos(cos_mu) / (2.0 * np.pi)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_stable(da_map):
|
|
47
|
+
"""Check if the linear map is stable (|Tr M| < 2).
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
da_map : list of DA
|
|
52
|
+
[x', px'] map.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
bool
|
|
57
|
+
"""
|
|
58
|
+
M = transfer_matrix(da_map)
|
|
59
|
+
return abs(M[0, 0] + M[1, 1]) < 2.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def twiss(da_map):
|
|
63
|
+
"""Extract Courant-Snyder (Twiss) parameters from a periodic map.
|
|
64
|
+
|
|
65
|
+
Assumes the map is one full period (cell or ring) and is stable.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
da_map : list of DA
|
|
70
|
+
One-period [x', px'] map.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
dict
|
|
75
|
+
{'beta': float, 'alpha': float, 'gamma': float, 'tune': float}
|
|
76
|
+
beta in [m], alpha dimensionless, gamma in [1/m].
|
|
77
|
+
"""
|
|
78
|
+
M = transfer_matrix(da_map)
|
|
79
|
+
cos_mu = (M[0, 0] + M[1, 1]) / 2.0
|
|
80
|
+
|
|
81
|
+
if abs(cos_mu) >= 1.0:
|
|
82
|
+
raise ValueError(f"Unstable map: Tr/2 = {cos_mu:.6f}")
|
|
83
|
+
|
|
84
|
+
mu = np.arccos(np.clip(cos_mu, -1.0, 1.0))
|
|
85
|
+
sin_mu = np.sin(mu)
|
|
86
|
+
|
|
87
|
+
# Sign of sin(mu): from M12
|
|
88
|
+
if M[0, 1] < 0:
|
|
89
|
+
sin_mu = -sin_mu
|
|
90
|
+
mu = 2 * np.pi - mu
|
|
91
|
+
|
|
92
|
+
beta = M[0, 1] / sin_mu
|
|
93
|
+
alpha = (M[0, 0] - M[1, 1]) / (2.0 * sin_mu)
|
|
94
|
+
gamma = -M[1, 0] / sin_mu
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
'beta': beta,
|
|
98
|
+
'alpha': alpha,
|
|
99
|
+
'gamma': gamma,
|
|
100
|
+
'tune': mu / (2.0 * np.pi),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def symplecticity_error(da_map):
|
|
105
|
+
"""Check how far the linear map is from symplectic (det M - 1).
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
da_map : list of DA
|
|
110
|
+
[x', px'] map.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
float
|
|
115
|
+
|det(M) - 1|.
|
|
116
|
+
"""
|
|
117
|
+
M = transfer_matrix(da_map)
|
|
118
|
+
return abs(np.linalg.det(M) - 1.0)
|