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 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)