From 0df034b8244d8fd1b29ca30cf143c58190368956 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 5 Jan 2026 11:57:43 -0500 Subject: [PATCH 001/183] Start envelope 2D module --- examples/Envelope/test_env_2d.py | 1 + py/orbit/envelope/__init__.py | 1 + py/orbit/envelope/envelope_2d.py | 13 +++++++++++++ py/orbit/envelope/meson.build | 11 +++++++++++ py/orbit/meson.build | 1 + 5 files changed, 27 insertions(+) create mode 100644 examples/Envelope/test_env_2d.py create mode 100644 py/orbit/envelope/__init__.py create mode 100644 py/orbit/envelope/envelope_2d.py create mode 100644 py/orbit/envelope/meson.build diff --git a/examples/Envelope/test_env_2d.py b/examples/Envelope/test_env_2d.py new file mode 100644 index 00000000..edbde9b7 --- /dev/null +++ b/examples/Envelope/test_env_2d.py @@ -0,0 +1 @@ +from orbit.envelope import Envelope2D diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py new file mode 100644 index 00000000..62641531 --- /dev/null +++ b/py/orbit/envelope/__init__.py @@ -0,0 +1 @@ +from .envelope_2d import Envelope2D \ No newline at end of file diff --git a/py/orbit/envelope/envelope_2d.py b/py/orbit/envelope/envelope_2d.py new file mode 100644 index 00000000..9b621721 --- /dev/null +++ b/py/orbit/envelope/envelope_2d.py @@ -0,0 +1,13 @@ +import math +import numpy as np + +from ..core.bunch import SyncParticle + + +class Envelope2D: + def __init__(self, sync_part: SyncParticle, cov_matrix: np.ndarray = None) -> None: + self.sync_part = sync_part + + self.cov_matrix = cov_matrix + if self.cov_matrix is None: + self.cov_matrix = np.eye(4) diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build new file mode 100644 index 00000000..0b6d4b4d --- /dev/null +++ b/py/orbit/envelope/meson.build @@ -0,0 +1,11 @@ +py_sources = files([ + '__init__.py', + 'envelope_2d.py', +]) + +python.install_sources( + py_sources, + subdir: 'orbit/envelope', + # pure: true, +) + diff --git a/py/orbit/meson.build b/py/orbit/meson.build index 2ba16e73..651b14d5 100644 --- a/py/orbit/meson.build +++ b/py/orbit/meson.build @@ -24,6 +24,7 @@ subdir('space_charge') subdir('errors') subdir('matrix_lattice') subdir('teapot') +subdir('envelope') py_sources = files([ From 1ef0323419bffa5f47a7413f376744e387e330b4 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 5 Jan 2026 12:04:09 -0500 Subject: [PATCH 002/183] Envelope stores empty Bunch object This will be used to set the correct particle mass and energy. --- py/orbit/envelope/envelope_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope_2d.py b/py/orbit/envelope/envelope_2d.py index 9b621721..ddd8abd0 100644 --- a/py/orbit/envelope/envelope_2d.py +++ b/py/orbit/envelope/envelope_2d.py @@ -1,12 +1,12 @@ import math import numpy as np -from ..core.bunch import SyncParticle +from ..core.bunch import Bunch class Envelope2D: - def __init__(self, sync_part: SyncParticle, cov_matrix: np.ndarray = None) -> None: - self.sync_part = sync_part + def __init__(self, bunch: Bunch, cov_matrix: np.ndarray = None) -> None: + self.bunch = bunch self.cov_matrix = cov_matrix if self.cov_matrix is None: From 1b037fb6832f9550f4020ef2a00ea55167422a46 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 5 Jan 2026 12:09:06 -0500 Subject: [PATCH 003/183] Add track method to Envelope2D This method applies the linear transformation S -> M S M^T. --- py/orbit/envelope/envelope_2d.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/py/orbit/envelope/envelope_2d.py b/py/orbit/envelope/envelope_2d.py index ddd8abd0..b602805e 100644 --- a/py/orbit/envelope/envelope_2d.py +++ b/py/orbit/envelope/envelope_2d.py @@ -11,3 +11,8 @@ def __init__(self, bunch: Bunch, cov_matrix: np.ndarray = None) -> None: self.cov_matrix = cov_matrix if self.cov_matrix is None: self.cov_matrix = np.eye(4) + + def track(self, matrix: np.ndarray) -> None: + S = self.cov_matrix + M = matrix[:4, :4] + self.cov_matrix = np.linalg.multi_dot([M, S, M.T]) From 6b8471179e3c17e38b85bfc3be34fbc5c3eb00ae Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 5 Jan 2026 12:09:38 -0500 Subject: [PATCH 004/183] Start test script for envelope2D tracker --- examples/Envelope/test_env_2d.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/examples/Envelope/test_env_2d.py b/examples/Envelope/test_env_2d.py index edbde9b7..8bfef03a 100644 --- a/examples/Envelope/test_env_2d.py +++ b/examples/Envelope/test_env_2d.py @@ -1 +1,23 @@ +import math +import numpy as np + +from orbit.core.bunch import Bunch +from orbit.core.bunch import SyncParticle from orbit.envelope import Envelope2D +from orbit.utils.consts import mass_proton + + +mass = mass_proton # [GeV] +kin_energy = 1.000 # [GeV] + +bunch = Bunch() +bunch.mass(mass) +bunch.getSyncParticle().kinEnergy(kin_energy) + +cov_matrix = np.identity(4) +cov_matrix[0, 0] = 10.0e-3 ** 2 +cov_matrix[2, 2] = 10.0e-3 ** 2 + +envelope = Envelope2D(bunch=bunch, cov_matrix=cov_matrix) + + From 13d36e9dc0b5b18a9143a9f6f1b71bc215afb876 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 5 Jan 2026 12:43:12 -0500 Subject: [PATCH 005/183] Add empty spaceChargeMatrix method --- py/orbit/envelope/envelope_2d.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/py/orbit/envelope/envelope_2d.py b/py/orbit/envelope/envelope_2d.py index b602805e..fd8f5717 100644 --- a/py/orbit/envelope/envelope_2d.py +++ b/py/orbit/envelope/envelope_2d.py @@ -13,6 +13,11 @@ def __init__(self, bunch: Bunch, cov_matrix: np.ndarray = None) -> None: self.cov_matrix = np.eye(4) def track(self, matrix: np.ndarray) -> None: + """Evolve covariance matrix: S -> M S M^T.""" S = self.cov_matrix M = matrix[:4, :4] self.cov_matrix = np.linalg.multi_dot([M, S, M.T]) + + def spaceChargeMatrix(self) -> np.ndarray: + """Return transfer matrix for linear space charge kick.""" + raise NotImplementedError() \ No newline at end of file From 75c133cfa8e0ea5f4cbbd03866fde65dc2d3bc7c Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 11:31:43 -0400 Subject: [PATCH 006/183] Add .idea to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 47650c0e..7133da9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea build PyORBIT.egg-info .vscode From 7aa7a4762aae54b1370b42f13f640ab6850e6b4d Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 11:32:19 -0400 Subject: [PATCH 007/183] Change Envelope2D to Envelope --- py/orbit/envelope/__init__.py | 2 +- py/orbit/envelope/envelope.py | 44 ++++++++++++++++++++++++++++++++ py/orbit/envelope/envelope_2d.py | 23 ----------------- py/orbit/envelope/meson.build | 2 +- 4 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 py/orbit/envelope/envelope.py delete mode 100644 py/orbit/envelope/envelope_2d.py diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index 62641531..48eac66f 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1 +1 @@ -from .envelope_2d import Envelope2D \ No newline at end of file +from .envelope import Envelope \ No newline at end of file diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py new file mode 100644 index 00000000..899dc66d --- /dev/null +++ b/py/orbit/envelope/envelope.py @@ -0,0 +1,44 @@ +import math + +import numpy as np + + +class Envelope: + def __init__(self, cov_matrix: np.ndarray = None, mean: np.ndarray = None) -> None: + if mean is None: + mean = np.zeros(6) + + if cov_matrix is None: + cov_matrix = np.eye(6) + + self.matrix = np.zeros((7, 7)) + self.matrix[0:6, 0:6] = cov_matrix + self.matrix[0:6, 6] = mean + self.matrix[6, 0:6] = mean + self.matrix[6, 6] = 1.0 + + def mean(self) -> np.ndarray: + return self.matrix[0:6, 6] + + def cov(self) -> np.ndarray: + return self.matrix[0:6, 0:6] + + def rms(self) -> np.ndarray: + return np.sqrt(np.diag(self.cov())) + + def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: + """Linear propagation of beam matrix. + + Args: + transfer_matrix: 7x7 transfer matrix. + """ + self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) + + def space_charge_matrix(self, length: float) -> np.ndarray: + """Return transfer matrix from linear space charge kick.""" + raise NotImplementedError() + + + + + \ No newline at end of file diff --git a/py/orbit/envelope/envelope_2d.py b/py/orbit/envelope/envelope_2d.py deleted file mode 100644 index fd8f5717..00000000 --- a/py/orbit/envelope/envelope_2d.py +++ /dev/null @@ -1,23 +0,0 @@ -import math -import numpy as np - -from ..core.bunch import Bunch - - -class Envelope2D: - def __init__(self, bunch: Bunch, cov_matrix: np.ndarray = None) -> None: - self.bunch = bunch - - self.cov_matrix = cov_matrix - if self.cov_matrix is None: - self.cov_matrix = np.eye(4) - - def track(self, matrix: np.ndarray) -> None: - """Evolve covariance matrix: S -> M S M^T.""" - S = self.cov_matrix - M = matrix[:4, :4] - self.cov_matrix = np.linalg.multi_dot([M, S, M.T]) - - def spaceChargeMatrix(self) -> np.ndarray: - """Return transfer matrix for linear space charge kick.""" - raise NotImplementedError() \ No newline at end of file diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 0b6d4b4d..1d87060f 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -1,6 +1,6 @@ py_sources = files([ '__init__.py', - 'envelope_2d.py', + 'envelope.py', ]) python.install_sources( From 3d0d65cb7e58328126b330c12830e3678dbc9d46 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 11:32:35 -0400 Subject: [PATCH 008/183] Update example --- examples/Envelope/test_env_2d.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/Envelope/test_env_2d.py b/examples/Envelope/test_env_2d.py index 8bfef03a..df56fb48 100644 --- a/examples/Envelope/test_env_2d.py +++ b/examples/Envelope/test_env_2d.py @@ -3,7 +3,7 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle -from orbit.envelope import Envelope2D +from orbit.envelope import Envelope from orbit.utils.consts import mass_proton @@ -14,10 +14,11 @@ bunch.mass(mass) bunch.getSyncParticle().kinEnergy(kin_energy) -cov_matrix = np.identity(4) +cov_matrix = np.identity(6) cov_matrix[0, 0] = 10.0e-3 ** 2 cov_matrix[2, 2] = 10.0e-3 ** 2 +cov_matrix[4, 4] = 50.0 -envelope = Envelope2D(bunch=bunch, cov_matrix=cov_matrix) +envelope = Envelope(cov_matrix=cov_matrix) From 63eb825331b10f3927c8831866be12efefd265b7 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 11:35:19 -0400 Subject: [PATCH 009/183] Docstrings --- py/orbit/envelope/envelope.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 899dc66d..6dfa066d 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,29 +1,41 @@ -import math - import numpy as np class Envelope: - def __init__(self, cov_matrix: np.ndarray = None, mean: np.ndarray = None) -> None: - if mean is None: - mean = np.zeros(6) + """Represents beam envelope and centroid in 6D phase space. + + Attributes: + matrix: 7x7 matrix containing 6x6 covariance matrix and 6x1 centroid vector. + """ + def __init__(self, cov_matrix: np.ndarray = None, centroid: np.ndarray = None) -> None: + """Constructor. + + Args: + cov_matrix: 6x6 covariance matrix. + centroid: 6x1 centroid vector. + """ + if centroid is None: + centroid = np.zeros(6) if cov_matrix is None: cov_matrix = np.eye(6) self.matrix = np.zeros((7, 7)) self.matrix[0:6, 0:6] = cov_matrix - self.matrix[0:6, 6] = mean - self.matrix[6, 0:6] = mean + self.matrix[0:6, 6] = centroid + self.matrix[6, 0:6] = centroid self.matrix[6, 6] = 1.0 - def mean(self) -> np.ndarray: + def centroid(self) -> np.ndarray: + """Return centroid vector.""" return self.matrix[0:6, 6] def cov(self) -> np.ndarray: + """Return covariance matrix.""" return self.matrix[0:6, 0:6] def rms(self) -> np.ndarray: + """Return rms beam sizes.""" return np.sqrt(np.diag(self.cov())) def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: @@ -39,6 +51,3 @@ def space_charge_matrix(self, length: float) -> np.ndarray: raise NotImplementedError() - - - \ No newline at end of file From dfeadbd5190a92b0d7e2149a9d37bd650ee34056 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 11:50:58 -0400 Subject: [PATCH 010/183] Add envelope/matrix.py --- py/orbit/envelope/envelope.py | 7 ++- py/orbit/envelope/matrix.py | 90 +++++++++++++++++++++++++++++++++++ py/orbit/envelope/meson.build | 1 + 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 py/orbit/envelope/matrix.py diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 6dfa066d..7ec8b3a8 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,5 +1,9 @@ +import math + import numpy as np +from .matrix import MatrixFactory + class Envelope: """Represents beam envelope and centroid in 6D phase space. @@ -49,5 +53,4 @@ def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: def space_charge_matrix(self, length: float) -> np.ndarray: """Return transfer matrix from linear space charge kick.""" raise NotImplementedError() - - + \ No newline at end of file diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py new file mode 100644 index 00000000..4e831ba5 --- /dev/null +++ b/py/orbit/envelope/matrix.py @@ -0,0 +1,90 @@ +import math + +import numpy as np + + +class MatrixFactory: + """Factory for 7x7 transfer matrices.""" + + @staticmethod + def drift(length: float, gamma: float) -> np.ndarray: + matrix = np.identity(7) + matrix[0, 1] = length + matrix[2, 3] = length + matrix[4, 5] = length / gamma**2 + return matrix + + @staticmethod + def quad(length: float, kq: float) -> np.ndarray: + sqrt_abs_kq = math.sqrt(abs(kq)) + + matrix = np.identity(7) + if (kq > 0): + cx = np.cos(sqrt_abs_kq * length) + sx = np.sin(sqrt_abs_kq * length) + cy = np.cosh(sqrt_abs_kq * length) + sy = np.sinh(sqrt_abs_kq * length) + matrix[0, 0] = cx + matrix[0, 1] = +sx / sqrt_abs_kq + matrix[1, 0] = -sx * sqrt_abs_kq + matrix[1, 1] = cx + matrix[2, 2] = cy + matrix[2, 3] = sy / sqrt_abs_kq + matrix[3, 2] = sy * sqrt_abs_kq + matrix[3, 3] = cy + elif (kq < 0): + cx = np.cosh(sqrt_abs_kq * length) + sx = np.sinh(sqrt_abs_kq * length) + cy = np.cos(sqrt_abs_kq * length) + sy = np.sin(sqrt_abs_kq * length) + matrix[0, 0] = cx + matrix[0, 1] = sx / sqrt_abs_kq + matrix[1, 0] = sx * sqrt_abs_kq + matrix[1, 1] = cx + matrix[2, 2] = cy + matrix[2, 3] = +sy / sqrt_abs_kq + matrix[3, 2] = -sy * sqrt_abs_kq + matrix[3, 3] = cy + return matrix + + @staticmethod + def bend(length: float, theta: float, gamma: float) -> np.ndarray: + rho = length / theta + + cx = math.cos(theta) + sx = math.sin(theta) + + matrix = np.identity(7) + matrix[0, 0] = cx + matrix[0, 1] = sx * rho + matrix[0, 5] = (1.0 - cx) * rho + matrix[1, 0] = -sx / rho + matrix[1, 1] = cx + matrix[1, 5] = sx + matrix[2, 3] = length + matrix[4, 0] = -sx + matrix[4, 1] = -(1.0 - cx) * rho + matrix[4, 5] = (length / gamma**2) - rho * (theta - sx) + return matrix + + @staticmethod + def tilt(angle: float) -> np.ndarray: + cs = math.cos(angle) + sn = math.sin(angle) + + matrix = np.identity(7) + matrix[0, 0] = matrix[1, 1] = +cs + matrix[0, 2] = matrix[1, 3] = +sn + matrix[2, 0] = matrix[3, 1] = -sn + matrix[2, 2] = matrix[3, 3] = +cs + return matrix + + @staticmethod + def kick(kx: float, ky: float, dE: float) -> np.ndarray: + matrix = np.identity(7) + matrix[1, 6] = kx + matrix[3, 6] = ky + matrix[5, 6] = dE + return matrix + + \ No newline at end of file diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 1d87060f..75fdcdb9 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -1,6 +1,7 @@ py_sources = files([ '__init__.py', 'envelope.py', + 'matrix.py' ]) python.install_sources( From a1b32fa35520ea37d3ad0e4b5dbc0ecd7dd7837a Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:04:53 -0400 Subject: [PATCH 011/183] Add MatrixFactory class --- py/orbit/envelope/envelope.py | 17 ++++++++++- py/orbit/envelope/matrix.py | 53 ++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 7ec8b3a8..c77fee74 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -2,9 +2,20 @@ import numpy as np +from ..lattice import AccNode +from ..lattice import AccLattice + from .matrix import MatrixFactory +ENTRANCE = AccNode.ENTRANCE +BODY = AccNode.BODY +EXIT = AccNode.EXIT + +BEFORE = AccNode.BEFORE +AFTER = AccNode.AFTER + + class Envelope: """Represents beam envelope and centroid in 6D phase space. @@ -53,4 +64,8 @@ def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: def space_charge_matrix(self, length: float) -> np.ndarray: """Return transfer matrix from linear space charge kick.""" raise NotImplementedError() - \ No newline at end of file + + +def get_matrix(node: AccNode) -> np.ndarray: + """Return transfer matrix for given node.""" + diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 4e831ba5..3bc667ef 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -2,6 +2,15 @@ import numpy as np +from ..core.bunch import Bunch +from ..core.bunch import SyncParticle +from ..lattice import AccNode +from ..teapot import DriftTEAPOT +from ..teapot import QuadTEAPOT +from ..teapot import BendTEAPOT +from ..teapot import TiltTEAPOT +from ..teapot import KickTEAPOT + class MatrixFactory: """Factory for 7x7 transfer matrices.""" @@ -87,4 +96,46 @@ def kick(kx: float, ky: float, dE: float) -> np.ndarray: matrix[5, 6] = dE return matrix - \ No newline at end of file + def __call__(self, node: AccNode, bunch: Bunch, part_index: int) -> np.ndarray: + if type(node) is DriftTEAPOT: + length = node.getLength(part_index) + gamma = bunch.getSyncParticle().gamma() + return self.drift(length=length, gamma=gamma) + + elif type(node) is QuadTEAPOT: + nparts = node.getnParts() + length = node.getLength(part_index) + + scale = 1.0 + if node.waveform: + scale = node.waveform.getStrength() + + kq = scale * node.getParam("kq") + return self.quad(length=length, kq=kq) + + elif type(node) is BendTEAPOT: + nparts = node.getnParts() + length = node.getLength(part_index) + theta = node.getParam("theta") / (nparts - 1) + gamma = bunch.getSyncParticle().gamma() + return self.bend(length=length, theta=theta, gamma=gamma) + + elif type(node) is KickTEAPOT: + nparts = node.getnParts() + + scale = 1.0 + if node.waveform: + scale = node.waveform.getStrength() + + kx = scale * node.getParam("kx") / (nparts - 1) + ky = scale * node.getParam("ky") / (nparts - 1) + dE = node.getParam("dE") / (nparts - 1) + return self.kick(kx, ky, dE) + + elif type(node) is TiltTEAPOT: + angle = node.getTiltAngle() + return self.tilt(angle) + + else: + raise NotImplementedError("Unsupported node type: {}".format(type(node))) + From 3003c0bca6f9f4f74c70ea00accc9b023697063e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:15:37 -0400 Subject: [PATCH 012/183] Add several classes to teapot __init__ --- py/orbit/teapot/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/py/orbit/teapot/__init__.py b/py/orbit/teapot/__init__.py index 945450d4..9886b7fe 100644 --- a/py/orbit/teapot/__init__.py +++ b/py/orbit/teapot/__init__.py @@ -16,6 +16,10 @@ from .teapot import SolenoidTEAPOT from .teapot import TiltTEAPOT from .teapot import NodeTEAPOT +from .teapot import TurnCounterTEAPOT +from .teapot import ApertureTEAPOT +from .teapot import MonitorTEAPOT +from .teapot import BunchWrapTEAPOT from .teapot import TPB @@ -38,3 +42,6 @@ __all__.append("NodeTEAPOT") __all__.append("TPB") __all__.append("TEAPOT_MATRIX_Lattice") +__all__.append("TurnCounterTEAPOT") +__all__.append("ApertureTEAPOT") +__all__.append("MonitorTEAPOT") From e27c57a84b8343c91294df1ff48c4f2d57e4be1f Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:17:20 -0400 Subject: [PATCH 013/183] Add node types to ignore in matrix factory Replace these with identity matrix --- py/orbit/envelope/matrix.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 3bc667ef..fb565c51 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -10,11 +10,25 @@ from ..teapot import BendTEAPOT from ..teapot import TiltTEAPOT from ..teapot import KickTEAPOT +from ..teapot import ApertureTEAPOT +from ..teapot import BunchWrapTEAPOT +from ..teapot import FringeFieldTEAPOT +from ..teapot import MonitorTEAPOT +from ..teapot import TurnCounterTEAPOT class MatrixFactory: """Factory for 7x7 transfer matrices.""" + def __init__(self) -> None: + self.ignore_node_types = [ + ApertureTEAPOT, + BunchWrapTEAPOT, + FringeFieldTEAPOT, + MonitorTEAPOT, + TurnCounterTEAPOT, + ] + @staticmethod def drift(length: float, gamma: float) -> np.ndarray: matrix = np.identity(7) @@ -136,6 +150,9 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int) -> np.ndarray: angle = node.getTiltAngle() return self.tilt(angle) + elif type(node) in self.ignore_node_types: + return np.identity(7) + else: raise NotImplementedError("Unsupported node type: {}".format(type(node))) From fe8190257aa16c869b9c11e0bf09122ed7ce509e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:50:09 -0400 Subject: [PATCH 014/183] Add envelope tracker --- examples/Envelope/style.mplstyle | 7 ++ examples/Envelope/test_env_2d.py | 6 +- examples/Envelope/test_env_fodo.py | 172 +++++++++++++++++++++++++++++ py/orbit/envelope/__init__.py | 3 +- py/orbit/envelope/envelope.py | 54 +++++---- py/orbit/envelope/matrix.py | 6 +- 6 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 examples/Envelope/style.mplstyle create mode 100644 examples/Envelope/test_env_fodo.py diff --git a/examples/Envelope/style.mplstyle b/examples/Envelope/style.mplstyle new file mode 100644 index 00000000..be0d2046 --- /dev/null +++ b/examples/Envelope/style.mplstyle @@ -0,0 +1,7 @@ +axes.linewidth: 1.25 +axes.titlesize: "medium" +figure.constrained_layout.use: True +savefig.dpi: 300 +savefig.format: "png" +xtick.minor.visible: True +ytick.minor.visible: True diff --git a/examples/Envelope/test_env_2d.py b/examples/Envelope/test_env_2d.py index df56fb48..b9911a88 100644 --- a/examples/Envelope/test_env_2d.py +++ b/examples/Envelope/test_env_2d.py @@ -12,13 +12,15 @@ bunch = Bunch() bunch.mass(mass) -bunch.getSyncParticle().kinEnergy(kin_energy) + +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(kin_energy) cov_matrix = np.identity(6) cov_matrix[0, 0] = 10.0e-3 ** 2 cov_matrix[2, 2] = 10.0e-3 ** 2 cov_matrix[4, 4] = 50.0 -envelope = Envelope(cov_matrix=cov_matrix) +envelope = Envelope(sync_part=sync_part, cov_matrix=cov_matrix) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py new file mode 100644 index 00000000..d8539d64 --- /dev/null +++ b/examples/Envelope/test_env_fodo.py @@ -0,0 +1,172 @@ +"""Test envelope tracker in FODO lattice.""" +import argparse +import copy +import math + +import numpy as np +import matplotlib.pyplot as plt + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.lattice import AccLattice +from orbit.lattice import AccNode +from orbit.teapot import DriftTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import TEAPOT_Lattice +from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.utils.consts import mass_proton + +plt.style.use("style.mplstyle") + + +# Parse arguments +# ------------------------------------------------------------------------------ + +parser = argparse.ArgumentParser() +parser.add_argument("--nslice", type=int, default=10) +parser.add_argument("--mismatch", type=float, default=0.0) +parser.add_argument("--turns", type=int, default=25) +parser.add_argument("--nparts", type=int, default=10_000) +args = parser.parse_args() + + +# Create lattice +# ------------------------------------------------------------------------------ + +nodes = [ + QuadTEAPOT(length=0.5, kq=+0.25), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=1.0, kq=-0.25), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=0.5, kq=+0.25), +] + +lattice = TEAPOT_Lattice() +for node in nodes: + node.setnParts(args.nslice) + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + lattice.addNode(node) + +lattice.initialize() + + +# Create envelope +# ------------------------------------------------------------------------------ + +# Create bunch +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(1.0) + +# Find periodic lattice parameters +matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) +matrix_lattice_params = matrix_lattice.getRingParametersDict() +alpha_x = matrix_lattice_params["alpha x"] +alpha_y = matrix_lattice_params["alpha y"] +beta_x = matrix_lattice_params["beta x [m]"] +beta_y = matrix_lattice_params["beta y [m]"] +eps_x = 10.0e-06 +eps_y = 10.0e-06 + +# Generate covariance matrix +cov_matrix = np.zeros((6, 6)) +cov_matrix[0, 0] = eps_x * beta_x +cov_matrix[2, 2] = eps_y * beta_y +cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x +cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y +cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x +cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y +cov_matrix[4, 4] = 10.0 ** 2 +cov_matrix[5, 5] = 0.0 + +# Mismatch x +cov_matrix[0, 0] *= (1.0 + args.mismatch) ** 2 + +# Create envelope +envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix, + centroid=np.zeros(6), +) + + +# Track envelope +# ------------------------------------------------------------------------------ + +print("TRACK ENVELOPE") + +tracker = EnvelopeTracker(lattice) + +history = {"xrms": [], "yrms": []} +for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) + + cov_matrix = envelope.cov() + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + print(f"turn={turn + 1} xrms={xrms:0.5f} yrms={yrms:0.5f}") + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + +histories = {} +histories["envelope"] = copy.deepcopy(history) + + +# Track bunch +# ------------------------------------------------------------------------------ + +print("TRACK BUNCH") + +rng = np.random.default_rng() +bunch_coords = rng.multivariate_normal( + mean=np.zeros(6), + cov=cov_matrix, + size=args.nparts, +) +for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + +history = {"xrms": [], "yrms": []} +for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + print(f"turn={turn + 1} xrms={xrms:0.5f} yrms={yrms:0.5f}") + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + +histories["bunch"] = copy.deepcopy(history) + +# Analysis +# ------------------------------------------------------------------------------ + +for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + +for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + + +# fig, ax = plt.subplots() \ No newline at end of file diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index 48eac66f..d945ed8f 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1 +1,2 @@ -from .envelope import Envelope \ No newline at end of file +from .envelope import Envelope +from .envelope import EnvelopeTracker \ No newline at end of file diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index c77fee74..767ccce3 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -2,6 +2,7 @@ import numpy as np +from ..core.bunch import SyncParticle from ..lattice import AccNode from ..lattice import AccLattice @@ -17,18 +18,9 @@ class Envelope: - """Represents beam envelope and centroid in 6D phase space. - - Attributes: - matrix: 7x7 matrix containing 6x6 covariance matrix and 6x1 centroid vector. - """ - def __init__(self, cov_matrix: np.ndarray = None, centroid: np.ndarray = None) -> None: - """Constructor. + def __init__(self, sync_part: SyncParticle, cov_matrix: np.ndarray = None, centroid: np.ndarray = None) -> None: + self.sync_part = sync_part - Args: - cov_matrix: 6x6 covariance matrix. - centroid: 6x1 centroid vector. - """ if centroid is None: centroid = np.zeros(6) @@ -42,23 +34,15 @@ def __init__(self, cov_matrix: np.ndarray = None, centroid: np.ndarray = None) - self.matrix[6, 6] = 1.0 def centroid(self) -> np.ndarray: - """Return centroid vector.""" return self.matrix[0:6, 6] def cov(self) -> np.ndarray: - """Return covariance matrix.""" return self.matrix[0:6, 0:6] def rms(self) -> np.ndarray: - """Return rms beam sizes.""" return np.sqrt(np.diag(self.cov())) def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: - """Linear propagation of beam matrix. - - Args: - transfer_matrix: 7x7 transfer matrix. - """ self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) def space_charge_matrix(self, length: float) -> np.ndarray: @@ -66,6 +50,34 @@ def space_charge_matrix(self, length: float) -> np.ndarray: raise NotImplementedError() -def get_matrix(node: AccNode) -> np.ndarray: - """Return transfer matrix for given node.""" +class EnvelopeTracker: + def __init__(self, lattice: AccLattice) -> None: + self.lattice = lattice + self.matrix_factory = MatrixFactory() + + def track(self, envelope: Envelope) -> None: + for node in self.lattice.getNodes(): + for child_node in node.getChildNodes(ENTRANCE): + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) + + envelope.apply_transfer_matrix( + self.matrix_factory(node, envelope.sync_part, part_index) + ) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) + for child_node in node.getChildNodes(EXIT): + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) \ No newline at end of file diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index fb565c51..129f9edc 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -110,10 +110,10 @@ def kick(kx: float, ky: float, dE: float) -> np.ndarray: matrix[5, 6] = dE return matrix - def __call__(self, node: AccNode, bunch: Bunch, part_index: int) -> np.ndarray: + def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) - gamma = bunch.getSyncParticle().gamma() + gamma = sync_part.gamma() return self.drift(length=length, gamma=gamma) elif type(node) is QuadTEAPOT: @@ -131,7 +131,7 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int) -> np.ndarray: nparts = node.getnParts() length = node.getLength(part_index) theta = node.getParam("theta") / (nparts - 1) - gamma = bunch.getSyncParticle().gamma() + gamma = sync_part.gamma() return self.bend(length=length, theta=theta, gamma=gamma) elif type(node) is KickTEAPOT: From 6fad0f711dbdf1ad336ebf68d38e5a227fdcb844 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:50:27 -0400 Subject: [PATCH 015/183] Delete test script --- examples/Envelope/test_env_2d.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 examples/Envelope/test_env_2d.py diff --git a/examples/Envelope/test_env_2d.py b/examples/Envelope/test_env_2d.py deleted file mode 100644 index b9911a88..00000000 --- a/examples/Envelope/test_env_2d.py +++ /dev/null @@ -1,26 +0,0 @@ -import math -import numpy as np - -from orbit.core.bunch import Bunch -from orbit.core.bunch import SyncParticle -from orbit.envelope import Envelope -from orbit.utils.consts import mass_proton - - -mass = mass_proton # [GeV] -kin_energy = 1.000 # [GeV] - -bunch = Bunch() -bunch.mass(mass) - -sync_part = bunch.getSyncParticle() -sync_part.kinEnergy(kin_energy) - -cov_matrix = np.identity(6) -cov_matrix[0, 0] = 10.0e-3 ** 2 -cov_matrix[2, 2] = 10.0e-3 ** 2 -cov_matrix[4, 4] = 50.0 - -envelope = Envelope(sync_part=sync_part, cov_matrix=cov_matrix) - - From d7586c7823021a75322276fb2eb9d7fd8fa0eaa6 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:51:01 -0400 Subject: [PATCH 016/183] Format --- examples/Envelope/test_env_fodo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index d8539d64..ab0fc543 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -1,4 +1,5 @@ """Test envelope tracker in FODO lattice.""" + import argparse import copy import math @@ -80,7 +81,7 @@ cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y -cov_matrix[4, 4] = 10.0 ** 2 +cov_matrix[4, 4] = 10.0**2 cov_matrix[5, 5] = 0.0 # Mismatch x @@ -155,6 +156,7 @@ histories["bunch"] = copy.deepcopy(history) + # Analysis # ------------------------------------------------------------------------------ @@ -169,4 +171,4 @@ print("avg_abs_delta:", np.mean(np.abs(deltas))) -# fig, ax = plt.subplots() \ No newline at end of file +# fig, ax = plt.subplots() From 6982c2fd52444baa3db2823ac7043900bb99294e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 12:51:16 -0400 Subject: [PATCH 017/183] Format --- py/orbit/envelope/__init__.py | 2 +- py/orbit/envelope/envelope.py | 29 ++++++++++++++------- py/orbit/envelope/matrix.py | 47 ++++++++++++++++++----------------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index d945ed8f..70a5fcbc 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1,2 +1,2 @@ from .envelope import Envelope -from .envelope import EnvelopeTracker \ No newline at end of file +from .envelope import EnvelopeTracker diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 767ccce3..b8e04a77 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -18,9 +18,14 @@ class Envelope: - def __init__(self, sync_part: SyncParticle, cov_matrix: np.ndarray = None, centroid: np.ndarray = None) -> None: + def __init__( + self, + sync_part: SyncParticle, + cov_matrix: np.ndarray = None, + centroid: np.ndarray = None, + ) -> None: self.sync_part = sync_part - + if centroid is None: centroid = np.zeros(6) @@ -35,20 +40,22 @@ def __init__(self, sync_part: SyncParticle, cov_matrix: np.ndarray = None, centr def centroid(self) -> np.ndarray: return self.matrix[0:6, 6] - + def cov(self) -> np.ndarray: return self.matrix[0:6, 0:6] - + def rms(self) -> np.ndarray: return np.sqrt(np.diag(self.cov())) def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: - self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) + self.matrix = np.linalg.multi_dot( + [transfer_matrix, self.matrix, transfer_matrix.T] + ) def space_charge_matrix(self, length: float) -> np.ndarray: """Return transfer matrix from linear space charge kick.""" raise NotImplementedError() - + class EnvelopeTracker: def __init__(self, lattice: AccLattice) -> None: @@ -63,7 +70,9 @@ def track(self, envelope: Envelope) -> None: ) for part_index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + for child_node in node.getChildNodes( + BODY, part_index, place_in_part=BEFORE + ): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) @@ -72,7 +81,9 @@ def track(self, envelope: Envelope) -> None: self.matrix_factory(node, envelope.sync_part, part_index) ) - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + for child_node in node.getChildNodes( + BODY, part_index, place_in_part=AFTER + ): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) @@ -80,4 +91,4 @@ def track(self, envelope: Envelope) -> None: for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) - ) \ No newline at end of file + ) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 129f9edc..e434bf3a 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -22,11 +22,11 @@ class MatrixFactory: def __init__(self) -> None: self.ignore_node_types = [ - ApertureTEAPOT, - BunchWrapTEAPOT, + ApertureTEAPOT, + BunchWrapTEAPOT, FringeFieldTEAPOT, - MonitorTEAPOT, - TurnCounterTEAPOT, + MonitorTEAPOT, + TurnCounterTEAPOT, ] @staticmethod @@ -36,13 +36,13 @@ def drift(length: float, gamma: float) -> np.ndarray: matrix[2, 3] = length matrix[4, 5] = length / gamma**2 return matrix - + @staticmethod def quad(length: float, kq: float) -> np.ndarray: sqrt_abs_kq = math.sqrt(abs(kq)) matrix = np.identity(7) - if (kq > 0): + if kq > 0: cx = np.cos(sqrt_abs_kq * length) sx = np.sin(sqrt_abs_kq * length) cy = np.cosh(sqrt_abs_kq * length) @@ -55,7 +55,7 @@ def quad(length: float, kq: float) -> np.ndarray: matrix[2, 3] = sy / sqrt_abs_kq matrix[3, 2] = sy * sqrt_abs_kq matrix[3, 3] = cy - elif (kq < 0): + elif kq < 0: cx = np.cosh(sqrt_abs_kq * length) sx = np.sinh(sqrt_abs_kq * length) cy = np.cos(sqrt_abs_kq * length) @@ -69,14 +69,14 @@ def quad(length: float, kq: float) -> np.ndarray: matrix[3, 2] = -sy * sqrt_abs_kq matrix[3, 3] = cy return matrix - + @staticmethod def bend(length: float, theta: float, gamma: float) -> np.ndarray: rho = length / theta - cx = math.cos(theta) - sx = math.sin(theta) - + cx = math.cos(theta) + sx = math.sin(theta) + matrix = np.identity(7) matrix[0, 0] = cx matrix[0, 1] = sx * rho @@ -87,9 +87,9 @@ def bend(length: float, theta: float, gamma: float) -> np.ndarray: matrix[2, 3] = length matrix[4, 0] = -sx matrix[4, 1] = -(1.0 - cx) * rho - matrix[4, 5] = (length / gamma**2) - rho * (theta - sx) + matrix[4, 5] = (length / gamma**2) - rho * (theta - sx) return matrix - + @staticmethod def tilt(angle: float) -> np.ndarray: cs = math.cos(angle) @@ -101,7 +101,7 @@ def tilt(angle: float) -> np.ndarray: matrix[2, 0] = matrix[3, 1] = -sn matrix[2, 2] = matrix[3, 3] = +cs return matrix - + @staticmethod def kick(kx: float, ky: float, dE: float) -> np.ndarray: matrix = np.identity(7) @@ -109,13 +109,15 @@ def kick(kx: float, ky: float, dE: float) -> np.ndarray: matrix[3, 6] = ky matrix[5, 6] = dE return matrix - - def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: + + def __call__( + self, node: AccNode, sync_part: SyncParticle, part_index: int = 0 + ) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) gamma = sync_part.gamma() return self.drift(length=length, gamma=gamma) - + elif type(node) is QuadTEAPOT: nparts = node.getnParts() length = node.getLength(part_index) @@ -126,14 +128,14 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) kq = scale * node.getParam("kq") return self.quad(length=length, kq=kq) - + elif type(node) is BendTEAPOT: nparts = node.getnParts() length = node.getLength(part_index) theta = node.getParam("theta") / (nparts - 1) gamma = sync_part.gamma() return self.bend(length=length, theta=theta, gamma=gamma) - + elif type(node) is KickTEAPOT: nparts = node.getnParts() @@ -145,14 +147,13 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) ky = scale * node.getParam("ky") / (nparts - 1) dE = node.getParam("dE") / (nparts - 1) return self.kick(kx, ky, dE) - + elif type(node) is TiltTEAPOT: angle = node.getTiltAngle() return self.tilt(angle) - + elif type(node) in self.ignore_node_types: return np.identity(7) - + else: raise NotImplementedError("Unsupported node type: {}".format(type(node))) - From bc002e18c67a7bae2d7308cecc8a822e6a69ba19 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 13:15:17 -0400 Subject: [PATCH 018/183] Fix error in centroid coordinates in example --- examples/Envelope/test_env_fodo.py | 79 +++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index ab0fc543..d8009b47 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -3,6 +3,8 @@ import argparse import copy import math +import os +import pathlib import numpy as np import matplotlib.pyplot as plt @@ -27,12 +29,23 @@ parser = argparse.ArgumentParser() parser.add_argument("--nslice", type=int, default=10) -parser.add_argument("--mismatch", type=float, default=0.0) +parser.add_argument("--mismatch-x", type=float, default=0.0) +parser.add_argument("--mismatch-y", type=float, default=0.0) +parser.add_argument("--offset-x", type=float, default=0.0) +parser.add_argument("--offset-y", type=float, default=0.0) parser.add_argument("--turns", type=int, default=25) parser.add_argument("--nparts", type=int, default=10_000) args = parser.parse_args() +# Setup +# ------------------------------------------------------------------------------ + +path = pathlib.Path(__file__) +output_dir = os.path.join("outputs", path.stem) +os.makedirs(output_dir, exist_ok=True) + + # Create lattice # ------------------------------------------------------------------------------ @@ -85,13 +98,20 @@ cov_matrix[5, 5] = 0.0 # Mismatch x -cov_matrix[0, 0] *= (1.0 + args.mismatch) ** 2 +cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 +cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 +cov_matrix_init = np.copy(cov_matrix) + +# Offset x +centroid_init = np.zeros(6) +centroid_init[0] += args.offset_x +centroid_init[2] += args.offset_y # Create envelope envelope = Envelope( sync_part=sync_part, - cov_matrix=cov_matrix, - centroid=np.zeros(6), + cov_matrix=cov_matrix_init, + centroid=centroid_init, ) @@ -102,18 +122,25 @@ tracker = EnvelopeTracker(lattice) -history = {"xrms": [], "yrms": []} +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): if turn > 0: tracker.track(envelope) cov_matrix = envelope.cov() + centroid = envelope.centroid() + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - print(f"turn={turn + 1} xrms={xrms:0.5f} yrms={yrms:0.5f}") + xavg = 1000.0 * centroid[0] + yavg = 1000.0 * centroid[2] + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") history["xrms"].append(xrms) history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) histories = {} histories["envelope"] = copy.deepcopy(history) @@ -126,14 +153,14 @@ rng = np.random.default_rng() bunch_coords = rng.multivariate_normal( - mean=np.zeros(6), - cov=cov_matrix, + mean=centroid_init, + cov=cov_matrix_init, size=args.nparts, ) for i in range(bunch_coords.shape[0]): bunch.addParticle(*bunch_coords[i]) -history = {"xrms": [], "yrms": []} +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): if turn > 0: lattice.trackBunch(bunch) @@ -149,10 +176,15 @@ xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - print(f"turn={turn + 1} xrms={xrms:0.5f} yrms={yrms:0.5f}") + xavg = 1000.0 * twiss_calc.getAverage(0) + yavg = 1000.0 * twiss_calc.getAverage(2) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") history["xrms"].append(xrms) history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) histories["bunch"] = copy.deepcopy(history) @@ -160,15 +192,38 @@ # Analysis # ------------------------------------------------------------------------------ +# Process history arrays. for history in histories.values(): for key in history: history[key] = np.array(history[key]) +# Print errors for key in histories["envelope"]: deltas = histories["bunch"][key] - histories["envelope"][key] print("key:", key) print("max_abs_delta:", np.max(np.abs(deltas))) print("avg_abs_delta:", np.mean(np.abs(deltas))) - -# fig, ax = plt.subplots() +# Plot rms bunch sizes +for key in ["xrms", "yrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for model in ["envelope", "bunch"]: + ax.plot(histories[model][key], label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}.png"), dpi=300) + plt.close("all") + +# Plot centroids +for key in ["xavg", "yavg"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for model in ["envelope", "bunch"]: + ax.plot(histories[model][key], label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel("AVG [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}.png"), dpi=300) + plt.close("all") \ No newline at end of file From 7c9abab01f5a07f261d4afae9d3a0afdf6f19930 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 14:39:00 -0400 Subject: [PATCH 019/183] Add space charge calculation --- py/orbit/envelope/envelope.py | 74 ++++++++++++++++++++++++++--------- py/orbit/envelope/matrix.py | 18 +++++++++ 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b8e04a77..9f9d544b 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -17,15 +17,23 @@ AFTER = AccNode.AFTER +def get_perveance(mass: float, kin_energy: float, line_density: float) -> float: + classical_proton_radius = 1.53469e-18 # [m] + gamma = 1.0 + (kin_energy / mass) # Lorentz factor + beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) # velocity/speed_of_light + return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) + + class Envelope: def __init__( self, sync_part: SyncParticle, cov_matrix: np.ndarray = None, centroid: np.ndarray = None, + intensity: float = 0.0, ) -> None: self.sync_part = sync_part - + if centroid is None: centroid = np.zeros(6) @@ -38,6 +46,20 @@ def __init__( self.matrix[6, 0:6] = centroid self.matrix[6, 6] = 1.0 + self.intensity = 0.0 + self.perveance = 0.0 + self.set_intensity(intensity) + + def set_intensity(self, intensity: float) -> None: + self.intensity = intensity + cov_matrix = self.cov() + length = 2.0 * math.sqrt(cov_matrix[4, 4]) # assume uniform density + self.perveance = get_perveance( + mass=self.sync_part.mass(), + kin_energy=self.sync_part.kinEnergy(), + line_density=(self.intensity / length) + ) + def centroid(self) -> np.ndarray: return self.matrix[0:6, 6] @@ -48,46 +70,60 @@ def rms(self) -> np.ndarray: return np.sqrt(np.diag(self.cov())) def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: - self.matrix = np.linalg.multi_dot( - [transfer_matrix, self.matrix, transfer_matrix.T] - ) - - def space_charge_matrix(self, length: float) -> np.ndarray: - """Return transfer matrix from linear space charge kick.""" - raise NotImplementedError() - + self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) + class EnvelopeTracker: - def __init__(self, lattice: AccLattice) -> None: + def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: self.lattice = lattice self.matrix_factory = MatrixFactory() + self.space_charge = space_charge def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): + # Child nodes before node for child_node in node.getChildNodes(ENTRANCE): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) for part_index in range(node.getnParts()): - for child_node in node.getChildNodes( - BODY, part_index, place_in_part=BEFORE - ): + # Child nodes before part + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) - envelope.apply_transfer_matrix( - self.matrix_factory(node, envelope.sync_part, part_index) - ) + # Main node + matrix = self.matrix_factory(node, envelope.sync_part, part_index) + + if self.space_charge: + length = node.getLength(part_index) + cov_matrix = envelope.cov() + + if self.space_charge == "2d": + matrix_sc = self.matrix_factory.space_charge_2d( + length=length, cov_matrix=cov_matrix, perveance=envelope.perveance + ) + elif self.space_charge == "3d": + matrix_sc = self.matrix_factory.space_charge_3d( + length=length, cov_matrix=cov_matrix, intensity=envelope.intensity + ) + else: + raise ValueError(f"Invalid space charge model: {self.space_charge}") + + # matrix = np.matmul(matrix, matrix_sc) + envelope.apply_transfer_matrix(matrix_sc) + + envelope.apply_transfer_matrix(matrix) - for child_node in node.getChildNodes( - BODY, part_index, place_in_part=AFTER - ): + # Child nodes after part + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) + # Child nodes after node for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index e434bf3a..0edc2e33 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -109,6 +109,24 @@ def kick(kx: float, ky: float, dE: float) -> np.ndarray: matrix[3, 6] = ky matrix[5, 6] = dE return matrix + + @staticmethod + def space_charge_2d(length: float, cov_matrix: np.ndarray, perveance: float) -> np.ndarray: + # Start by assuming upright beam + cx = 2.0 * math.sqrt(cov_matrix[0, 0]) + cy = 2.0 * math.sqrt(cov_matrix[2, 2]) + + kappa_x = 2.0 * perveance / (cx * (cx + cy)) + kappa_y = 2.0 * perveance / (cy * (cx + cy)) + + matrix = np.identity(7) + matrix[1, 0] = kappa_x * length + matrix[3, 2] = kappa_y * length + return matrix + + @staticmethod + def space_charge_3d(length: float, cov_matrix: np.ndarray, intensity: float) -> np.ndarray: + raise NotImplementedError() def __call__( self, node: AccNode, sync_part: SyncParticle, part_index: int = 0 From 2f67e2ce37137db8c2d4524dfeec5708cdfeaf6f Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 14:39:14 -0400 Subject: [PATCH 020/183] Add space charge in PIC test + visual comparison of distribution --- examples/Envelope/plot.py | 140 +++++++++++++++++++++++++++++ examples/Envelope/test_env_fodo.py | 93 +++++++++++++++---- examples/Envelope/utils.py | 9 ++ 3 files changed, 225 insertions(+), 17 deletions(-) create mode 100644 examples/Envelope/plot.py create mode 100644 examples/Envelope/utils.py diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py new file mode 100644 index 00000000..425cf204 --- /dev/null +++ b/examples/Envelope/plot.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + + +def calc_rms_ellipse_params(cov_matrix: np.ndarray) -> tuple[float, float, float]: + """Return rms ellipse dimensions and orientation.""" + (i, j) = (0, 1) + + sii = cov_matrix[i, i] + sjj = cov_matrix[j, j] + sij = cov_matrix[i, j] + + angle = -0.5 * np.arctan2(2.0 * sij, sii - sjj) + + _sin = np.sin(angle) + _cos = np.cos(angle) + _sin2 = _sin**2 + _cos2 = _cos**2 + + c1 = np.sqrt(abs(sii * _cos2 + sjj * _sin2 - 2 * sij * _sin * _cos)) + c2 = np.sqrt(abs(sii * _sin2 + sjj * _cos2 + 2 * sij * _sin * _cos)) + + return (c1, c2, angle) + + +def plot_ellipse( + r1: float = 1.0, + r2: float = 1.0, + angle: float = 0.0, + center: tuple[float, float] = None, + ax=None, + **kws, +): + kws.setdefault("fill", False) + kws.setdefault("color", "black") + kws.setdefault("lw", 1.25) + + center = (0.0, 0.0) + + d1 = r1 * 2.0 + d2 = r2 * 2.0 + angle = -np.degrees(angle) + + ax.add_patch(patches.Ellipse(center, d1, d2, angle=angle, **kws)) + return ax + + +def plot_rms_ellipse( + cov_matrix: np.ndarray, + level: float = 1.0, + ax=None, + **ellipse_kws, +): + """Plot rms ellipse from 2 x 2 covariance matrix.""" + r1, r2, angle = calc_rms_ellipse_params(cov_matrix) + plot_ellipse(r1 * level, r2 * level, angle=angle, ax=ax, **ellipse_kws) + return ax + + +def plot_corner( + particles: np.ndarray, + limits: list[tuple[float, float]] = None, + bins: int = 64, + labels: list[str] = None, + blur: float = None, +) -> tuple: + """Generate corner plot.""" + ndim = particles.shape[1] + + if limits is None: + xmax = np.max(particles, axis=0) + xmin = np.min(particles, axis=0) + limits = list(zip(xmin, xmax)) + + if labels is None: + labels = ndim * [""] + + fig, axs = plt.subplots( + ncols=ndim, nrows=ndim, sharex=None, sharey=None, figsize=(8, 8) + ) + for i in range(ndim): + for j in range(ndim): + axis = (j, i) + ax = axs[i, j] + if i > j: + values, edges = np.histogramdd( + particles[:, axis], bins=bins, range=[limits[k] for k in axis] + ) + if blur: + values = scipy.ndimage.gaussian_filter(values, sigma=blur) + ax.pcolormesh( + edges[0], + edges[1], + values.T, + linewidth=0.0, + rasterized=True, + shading="auto", + ) + elif i == j: + values, edges = np.histogram( + particles[:, i], bins=bins, range=limits[i] + ) + if blur: + values = scipy.ndimage.gaussian_filter(values, sigma=blur) + ax.stairs(values, edges, lw=1.5, color="black") + else: + ax.axis("off") + + for i in range(0, ndim - 1): + for j in range(0, ndim): + axs[i, j].set_xticklabels([]) + for i in range(0, ndim): + for j in range(1, ndim): + axs[i, j].set_yticklabels([]) + + ymax = 0.0 + ymin = np.inf + for i in range(ndim): + ymax = max(ymax, axs[i, i].get_ylim()[1]) + ymin = min(ymin, axs[i, i].get_ylim()[0]) + for i in range(ndim): + axs[i, i].set_ylim(ymin, ymax) + + for ax in axs.flat: + for loc in ["top", "right"]: + ax.spines[loc].set_visible(False) + + for i, label in enumerate(labels): + axs[-1, i].set_xlabel(label) + for i, label in enumerate(labels[1:], start=1): + axs[i, 0].set_ylabel(label) + + axs[0, 0].set_yticklabels([]) + axs[0, 0].set_ylabel(None) + + fig.align_ylabels() + fig.align_xlabels() + + return fig, axs \ No newline at end of file diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index d8009b47..dfbfde73 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -11,16 +11,23 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.bunch_utils import collect_bunch from orbit.envelope import Envelope from orbit.envelope import EnvelopeTracker from orbit.lattice import AccLattice from orbit.lattice import AccNode +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.space_charge.sc2p5d import setSC2p5DAccNodes from orbit.teapot import DriftTEAPOT from orbit.teapot import QuadTEAPOT from orbit.teapot import TEAPOT_Lattice from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.utils.consts import mass_proton +from plot import plot_rms_ellipse +from utils import gen_kv + plt.style.use("style.mplstyle") @@ -29,12 +36,17 @@ parser = argparse.ArgumentParser() parser.add_argument("--nslice", type=int, default=10) +parser.add_argument("--kq", type=float, default=0.1) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) parser.add_argument("--offset-y", type=float, default=0.0) parser.add_argument("--turns", type=int, default=25) -parser.add_argument("--nparts", type=int, default=10_000) +parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--intensity", type=float, default=1e10) +parser.add_argument("--zrms", type=float, default=5.0) +parser.add_argument("--kin-energy", type=float, default=0.025) args = parser.parse_args() @@ -50,11 +62,11 @@ # ------------------------------------------------------------------------------ nodes = [ - QuadTEAPOT(length=0.5, kq=+0.25), + QuadTEAPOT(length=0.5, kq=+args.kq), DriftTEAPOT(length=1.0), - QuadTEAPOT(length=1.0, kq=-0.25), + QuadTEAPOT(length=1.0, kq=-args.kq), DriftTEAPOT(length=1.0), - QuadTEAPOT(length=0.5, kq=+0.25), + QuadTEAPOT(length=0.5, kq=+args.kq), ] lattice = TEAPOT_Lattice() @@ -74,7 +86,7 @@ bunch = Bunch() bunch.mass(mass_proton) sync_part = bunch.getSyncParticle() -sync_part.kinEnergy(1.0) +sync_part.kinEnergy(args.kin_energy) # Find periodic lattice parameters matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) @@ -83,8 +95,8 @@ alpha_y = matrix_lattice_params["alpha y"] beta_x = matrix_lattice_params["beta x [m]"] beta_y = matrix_lattice_params["beta y [m]"] -eps_x = 10.0e-06 -eps_y = 10.0e-06 +eps_x = 0.25e-06 +eps_y = eps_x # Generate covariance matrix cov_matrix = np.zeros((6, 6)) @@ -94,7 +106,7 @@ cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y -cov_matrix[4, 4] = 10.0**2 +cov_matrix[4, 4] = args.zrms**2 cov_matrix[5, 5] = 0.0 # Mismatch x @@ -112,6 +124,7 @@ sync_part=sync_part, cov_matrix=cov_matrix_init, centroid=centroid_init, + intensity=args.intensity, ) @@ -120,7 +133,7 @@ print("TRACK ENVELOPE") -tracker = EnvelopeTracker(lattice) +tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): @@ -151,15 +164,29 @@ print("TRACK BUNCH") +# Generate particles from KV distribution + uniform longitudinal distribution. rng = np.random.default_rng() -bunch_coords = rng.multivariate_normal( - mean=centroid_init, - cov=cov_matrix_init, - size=args.nparts, -) + +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, :4] = gen_kv(args.nparts, cov_matrix_init[0:4, 0:4]) +bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) +bunch_coords += centroid_init[None, :6] + for i in range(bunch_coords.shape[0]): bunch.addParticle(*bunch_coords[i]) + +# Add space charge nodes to lattice. +if args.sc: + sc_calc = SpaceChargeCalc2p5D(128, 128, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + + +# Track bunch through lattice. history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): if turn > 0: @@ -213,7 +240,7 @@ ax.set_xlabel("Turn") ax.set_ylabel("RMS [mm]") ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}.png"), dpi=300) + plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close("all") # Plot centroids @@ -225,5 +252,37 @@ ax.set_xlabel("Turn") ax.set_ylabel("AVG [mm]") ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}.png"), dpi=300) - plt.close("all") \ No newline at end of file + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close("all") + + +# Plot bunch +fig, ax = plt.subplots(figsize=(4, 4)) + +X = collect_bunch(bunch)["coords"] +X[:, :4] *= 1000.0 + +env_cov_matrix = envelope.cov() +env_cov_matrix[:4, :4] *= 1000.0**2 + +env_centroid = envelope.centroid() +env_centroid[:4] *= 1000.0 + +xmax = 4.0 * np.std(X, axis=0) +limits = list(zip(-xmax, xmax)) +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + +ax.hist2d(X[:, 0], X[:, 1], bins=100, range=[limits[0], limits[1]]) + +plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, +) + +ax.set_xlabel(labels[0]) +ax.set_ylabel(labels[1]) +plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) +plt.close("all") \ No newline at end of file diff --git a/examples/Envelope/utils.py b/examples/Envelope/utils.py new file mode 100644 index 00000000..80a8e513 --- /dev/null +++ b/examples/Envelope/utils.py @@ -0,0 +1,9 @@ +import numpy as np + +def gen_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: + rng = np.random.default_rng() + X = rng.normal(size=(n, cov_matrix.shape[0])) + X /= np.linalg.norm(X, axis=1)[:, None] + X /= np.std(X, axis=0) + X = np.matmul(X, np.linalg.cholesky(cov_matrix).T) + return X \ No newline at end of file From 7cbe59afa084644db46748b7ecc5bf09d2d355ae Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 14:41:14 -0400 Subject: [PATCH 021/183] Plot dots --- examples/Envelope/test_env_fodo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index dfbfde73..a9fd0540 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -235,7 +235,7 @@ for key in ["xrms", "yrms"]: fig, ax = plt.subplots(figsize=(5, 3)) for model in ["envelope", "bunch"]: - ax.plot(histories[model][key], label=model) + ax.plot(histories[model][key], marker=".", label=model) ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) ax.set_xlabel("Turn") ax.set_ylabel("RMS [mm]") @@ -247,7 +247,7 @@ for key in ["xavg", "yavg"]: fig, ax = plt.subplots(figsize=(5, 3)) for model in ["envelope", "bunch"]: - ax.plot(histories[model][key], label=model) + ax.plot(histories[model][key], marker=".", label=model) ax.set_ylim(-5.0, 5.0) ax.set_xlabel("Turn") ax.set_ylabel("AVG [mm]") From 33b3f16301856d2191e6fecf29600b02d28c4bcd Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 14:44:04 -0400 Subject: [PATCH 022/183] Fix factor of 2 in perveance calculation --- .../Envelope/outputs/test_env_fodo/.DS_Store | Bin 0 -> 6148 bytes .../outputs/test_env_fodo/fig_dist_x_xp.png | Bin 0 -> 71449 bytes .../Envelope/outputs/test_env_fodo/fig_xavg.png | Bin 0 -> 36812 bytes .../Envelope/outputs/test_env_fodo/fig_xrms.png | Bin 0 -> 49751 bytes .../Envelope/outputs/test_env_fodo/fig_yavg.png | Bin 0 -> 35942 bytes .../Envelope/outputs/test_env_fodo/fig_yrms.png | Bin 0 -> 49118 bytes py/orbit/envelope/envelope.py | 2 +- py/orbit/envelope/matrix.py | 1 + 8 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 examples/Envelope/outputs/test_env_fodo/.DS_Store create mode 100644 examples/Envelope/outputs/test_env_fodo/fig_dist_x_xp.png create mode 100644 examples/Envelope/outputs/test_env_fodo/fig_xavg.png create mode 100644 examples/Envelope/outputs/test_env_fodo/fig_xrms.png create mode 100644 examples/Envelope/outputs/test_env_fodo/fig_yavg.png create mode 100644 examples/Envelope/outputs/test_env_fodo/fig_yrms.png diff --git a/examples/Envelope/outputs/test_env_fodo/.DS_Store b/examples/Envelope/outputs/test_env_fodo/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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
}7&# 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 HSzY}f-LkLm literal 0 HcmV?d00001 diff --git a/examples/Envelope/outputs/test_env_fodo/fig_xavg.png b/examples/Envelope/outputs/test_env_fodo/fig_xavg.png new file mode 100644 index 0000000000000000000000000000000000000000..3904dc509c04cc44e539b5b823b2c08107a7cd09 GIT binary patch literal 36812 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 z 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 z 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 KlQ--+{=WbwNHTi> literal 0 HcmV?d00001 diff --git a/examples/Envelope/outputs/test_env_fodo/fig_yrms.png b/examples/Envelope/outputs/test_env_fodo/fig_yrms.png new file mode 100644 index 0000000000000000000000000000000000000000..feceeca91c2de5a61f6f52bd99da0b2ee2a1d81a GIT binary patch literal 49118 zcmcG$2Ut^C+b*0@6v0tc1V^fl3Iftgq$7$bQk2l7D!qo@yMm|)sPqm30Ya1BK|n-$ z34{_t5vhToQUVF(Um1xr-}&G7obS8Nd$_K-CMJ8Yz4uzrddhv@&wg=VRq+HZ6D@J;RYwU6L`;w}n0E--s@7x%|bW)PLfE)KT#F1A+27u?L8 zoUQEP!hGUJigzpmhzu!EvWZfW; z#(Q^f-O%tzTEcCn zJzQO@PA%%5b*iQUH}cnC&E*`M+QXE8J^jE+`AB!cf%@R{hr`Fle>?d64RXkxa)?yG z|LpJ!BlOfadLQWN<(1nEv$^`KtG``#t5y3%xtWx{zP_|CU%q7E*(KfbteP#mc_$JQ-%IoI7GT;a)6>wQARQJXbZ`#D0}?1n`OGFa zb@JeI0QIRV${AC?QQ;|vc*woyJb7qe#2v41W7RY5uw^TC@YAW!$Houq`1v(3UelTS z`#Fo`rr16R2@6L@MmlQ*d&A*{y}za^uw%}tTEBsfsz&YcxxX&|@9tK>iuz?jM|DFIdbG7`{DBkk0^I8{!u*= zsWdw~i_FQn@9SG%6hZ~@Ycnt9x_tRoad9!)7*5S}U>X5@R4c8mt=v33u$woZ7KI#! z`0WdqR#a$RxpD=4hJhjL>sQm@;9#!%eJwxDEiJOCf1=*o_1n~qW;e(}P)CQ-%0#`M ztE(&RS;_<^|Bg{PejXw>KQugS)0-mwoAdNr%383Rj(sj;s0axO$x};}(Mk-Onwkno zWjO^IpLLNaP;y%$7nfsD>-KPWoS9YJ=_Asq60@?@Zprm?ZTo)H)YMcgm$_g|4I#w4Lr(l5 zFqPx}TYL&$nw2tV)MfB1CS#vZRUDRUi?b$t{`{GdpsAselYQx6Yb^wz179zWo`ft^ zq0jNq9BeYLhx?mEB9W00@AnZB5N=~I4IgujPc40VeaGVgHRM~v;jMvj?M;eWd( zD{FL|@!GcyfrD?;v@bZ&J&KQy?`UrqM>b1Cp~Ds?hkpJTAg2Iqw6J=P$;PVi{rmfi zi;FVMzwD2jL*>Z9ApdY7>qxOKvBYlOF1rtrl)HvN{%`!sKOXJBc!4LejS-I`!~Zup2Nt6t&YupW8;SYkSe-n0@^>#JPrzKR~mx>xSMzlSI?Qc`+p-WB&a ztUFSlFIFpGCw(YSTQ5RZNJt&*%>+@Kn75IU(oW$>B)9MP)^ps~u4yyGbi#S#^JP|Or3GIbSqymNi51Upt$sjpdn;^ptgvQQ1#e{2sINBe`{8E3>pORgqQ5i6&m_oUgFXodP z8r(f~12?QUB~-Rrb`L7?m*ZDKGqRwA)%??)Q}y#>o!o6^ z@hxNeE+2`O=7)~&KY`42cL{+3u*=1!crHiiCPI>nG@Xq z$|+mS)`BZ)^y_+$z3#L1KE-Q^>jbNW1~8+@%D9eIE*L`R9CJ8B1@zeu1k--qS`TW} zBDLU>Vz|qa&m3+odDTZzc4wLn^E-ywh-c3DV-OAf`p#Of`@+_ot7H3>Se?}zfwt>f zBk;JFgvp&bD3gp&m1{cH3dMZ{%toq^T)!JN4kEEDA&VS7P)z)aqYt8E=gD!t_fIv8k%MwO0$?K`wV^24~OQMPw_5fc$fICMjTaQwzKtJD>M>4w9GLgv%j-e z5HNx2I`VcAm2ETI5zUK-8WlA?rPez~*X2B(KvSY(wx518M8c-)*KheHnw2Y0O zKBL&A&3b*I|7CbMytsg-v#4>;tA6K4C!7^icT(kEdPPd!`nrosf=IhpiV7)x?&Zsu zA{IR?yoPmEEfLw!iw7<=K(p9yrjutj$Nsz%Tj*$yty-2hNoM;iJ&%;=(C0Aaywk8& z_*g@n^*S&*B}|1~DbZP$xRAj+<^~o-(rdZ6ZCoZb_k&oI<*i#m_y!U@PY6Tx6){;Q z9xL9)@4wy2C|#{xpqpip;yD-BX3O?EacZaaoY8WhbDxhly?P>}K$~$&9-ZvYdn;^E zC&HksCt0%Gep1FJACX$Um<;zyNIZTftjpP!ZGUqGGs`t6|8X|f2)6Q=T>1BL=iY~v-L-#q*`$?~4K8gr!m$##P1*fz zwY&)LOU{nX8ZDIs{EJU3e6`C$Hu_=8z1wZ<{^MSL-6pyXKGhpi?AIMW&RsB>B@RiA_!G;p1 z?F{z%GI{3B7YO$j&S66 zJ%4tV&bL?Y<-fSaob34NJbGlzSl*QWC{*ZiDa~Ye+h*MRtqKg>Mi;uGuYAY%8lug2 zMiQcI=H<;mLdX(%E5`@Q;0=s(Bjj}wBnTRr(EC9NXg&Lft4Nl@?fYQ&JseuG z6D*=boiWFu3sea4Vqz=hXu)$XU9C@UqYx^n#{Jz@H_~Kc5vkX2Qy6DJ9p*ar=Lk~Gv_BL^Mdo2s|hn);##Jasbx*oQ^A+Y}WQpCb= z`PD&b| z+vK>KD(mkT+AgM&7(KJyAK{m|PuSlxLJ@h2>Wnq-RGR;(gu?g&FQc;JhAuSl9)Dl| zW>Ils&loah29O&v4}-_l!y4?rXN^5+fwpU_*Q0j>@gDR5J+Hh}ytf z&z(duyAnQ^vbOa|sPuSZCDmAo*}cRFtVTB79|CgJbZlj45@FST#h$N^CBl=h%S2#z zk$KYdQOY@CD?9B$>PJ&}(mmgwAyzIx@VD7WHJQ;e_!F4>ld-wz4vsr^>z|nItIAUs zsr~ETV+8^ci`WVMHL*)6aZ*F;j8IKnm~z%;@h5ZcQRWu677dMB<_hNSybTo(dp-%5 z=<~3(_t>B{bA7zP>IH#x&r7I;R;mR&@q2{7&o~%P8e3Gegk&U$R%BRp`*bsbA(W?Z zs=!}P?056C#lNsktlVpYg5-K+yxO^-l3mm&fl&T4bqR2f^TT)(@OHY;0(E(Lc?p+S zC^gyo#R{4X6}6R_>h*zpSeXW^ODO#{hwAd<{B`dYZ0PAT6Xoiq5t=Wo@=$fO*!37$ zEd_t)Nzw#i@U*#n{~WJ-xTea)X>yz>hCi`$=P6QojU|3*ZVo<=wNqsV&|31mvy3Xc!}>H=?f-)gmsygfFzr@LQr!bapL|Zl+&C}{d7dD zk3rXn)HVzCyB=su;H2wBIRy4ellv3z6xxB89G|NIg#fQ^c-*D{g1SnwSKG|YEL6$bURt{iH8NlC$>6K_ ziWyqiO2lJpv13J-9@jpe5PuULo#l!{i_ZGbVHprdR5Nx-gh~(9u8(zy;rgP~8M#{^ z>FmlpZ|-O@@YDDp9;#MWxor`nhDW?IzJ2?q%08)aqq2=^j^p}du}MhzUK2IDPLr43 z#r;nH#&P36dpC76;{tX)R;=p2>$CQP@RR^gJPyHcO4<(=88w=)6-jsJeZ~%Xv9FyB znhUHgGK{D{O-bvai|ovW3YE#1{Y$eJo)`K*_RJ>ilQbivqX!iC)}=_gYZJ36UtA<+ zZ8)wsg1DKF;kfTV8?C**mb$-_vyz~OwC2vm93QU-mtOr6DB1NfEi6GTU~JU=6bmRc z3U;L&3+GJez7A)|{z)kmIGQ#uoLIno7wE=x#c`N^v>L*Phd9v-e{os*Y+CdxkJE-V zLFUKjO9>)Y>apSC)`M5K6KHx?>j@JGISdu9;IKPt?Bn`rdl8WJOW5b2xI`lW2cberQy%4G#H3Z#j0F2>`>~HJgfWS`4kGmLp_RTQw<9mnzj>_%)TgI6 zElp0;l@KK-w^eJ~gn2Cz2+YvH+W2+Ua6&}8PN}&{$fkY0FK&i4v^t>wG}r}X--q(j z(TP)s!Srk+-@YAq_a|2Qn9b(X`O{V2ah{I*ll!}=e!#kMfz)h$t9}H@df+8=Yg}^M zIQ@U#+P6&D?aR0g_GHWQ7giD5bs!&!s7Q8E?=%Ew5_1m))AS`)UKRYUmS!w$du={1 zCnsmo8Khlr^a?a~zHgI@iW(6F8dE7VQOqLc@&0ByDb~m@cWi77vGIMTjpy>^%vLdT z-%qd3^Nyi?Hiop}sfY2G90(BM>86`>(wQLb;*pfpi#7Dhg*R-QgXG?zW;RA|y?)hK z4TLwCvhpkQ?u4P)y*oeecfyz(Jz3}YY=YFT?7JEim>?oxILhUlA7PFix0g{f^Clrw zlgHi#Kc_K*f*e`y(V=yFe-cIoN7`ILDYE+x=DG=m^+>ICG2+yuH==xaIXSaiYq*eh zw%*QCh}|wd`}(0TGN_kr2Hl*=(kbX(Pn;t=PTcQDJ116#dR%d8LIK!l%Ci&92uXa^ zO;1nH6}U~I2uSjDKt|hDmvfiq=QN+%^4ly; z`IQnVV}>)9d1U zjMjg!d)hv@vbZZa@3x>+`wgtfm)uO{b`#IKQRW7yT| z`4kigXIL=zCo(Q6>-bLxpVLhi4bRHV{AfLjs#}`SD7G*&E3fvowaxbf6CzfS_~H?N z(?^t6_o?Gj4~K%J zNnw$B36H#+gJ`@!5v&)qgb01+X`qsHy}#x|-)H5QUUjrP7DoACdbpYmL@ks(+bOm(jJ%En+q(YdD4A`g~c|w-f?1ri@zv<2NJKjjEl@LSwG2gtcijCxV0~e&s6a?WZ8DecktG<#4`FaY}m?ad0L? z$u2Ul&IOg2c4?OOFrm zTQ7F{3-;qx{YW?H#d4Z{j0tyGcTh&-?uvbZn-Bsr$X?Enp9mSzH(j7b5kG1;CA$A6 zl3KN{qpq%=-*d*kVH>fD$70^OH*m($Jc{MjGvCOC@&<;tY9u5s`9)sb0}f?TV`O5B zC80tPxX;P(?X7N`g;&BoeMrr`^{4DRX*b)T4C{!3{uo}p{NWhS*w*iHwBDsFJKJlR z{JFMa+rqgM!4CTKNzUOL-2;{&ny&aoZZofmF|QqdC%T4yT~;QN`sy!l6OhuZ@dS>k zC}{OXIE@Br%PGdaG=rUoH%}j?KB;4;qs7134Dgm*r@T%H* zz9J_Rx;w7&9&X5k@S6PUQEJg!!6)MJaxbC&vKRxMJEVJ{i<*(pI$a#N2KI}D7#Umun4j%WwX77{)~bn1~>1;pWK>yN~f!Q>Ji7ipRN`H`EbEu6@tpntpg(Hf_bl@XJ5$()4{OK9u^L)h9I>54UmBu$v~}6(&zxBY z1+Nu=IeBIL7qeoQfG-^ckugo%&@*ds4?5`s0(>6LlHQo45P>`?aD;;Mj>Tdm(II3po(>gTworPgm7)NlDdplgQYO zN}^Z+)>oy7(6}?pk8ZoIS7F-^(myE~)q~@u6@ll`jKfrQB})zu-;_Vsr~XL=pzs!3vF5GLxsw}T)lPsv#HB=LpeK>avKE+o>~@YzXDO0hi$Ar*3+DX=#2 z&p-dn-xLIRo(f@O5v54aq3l5VLtI>(_Yh&WGuGWx|L)zp=r4s;RaJbc6j<6pM7P65 z4)@4=p)Z|x>?4aBnD)?(FJFu!!cH9A>XaT8o>jp3<8wxwpr9b@SmVZK$Vt{8pQ|V{ z)c`V=-vE$jTa0(OE>Y&MqM{PI@%i(eh*!rcLwJ*iaQyNygX6-=b2P8%nKNgkg!m4A z4Ol&0<^Dl@J_+^C!rRa=BR z1^_`Tb;qFq3X2G*&{tEE;9@QuuHRhAWZfi{%eR5J#DjIi*zw|Pzkgz8U}nY*6(7L-kQ%x`Z|Zn9 zyg$fxOiM=-@FV~>Ki15v#P=I&X(0eyOdzkF=7R_QF8kl6TU<#imF~DI3TZgOusl0M zoe5JFS0HZ-zl}U2Q7qwm8@tVqukA{A?>_MIssU_Kxk=l}AvC&T1bXq}jeQW92D8=J z*bR}aiMCob*}z=g><&viWjxusV^O^pQa!;+6) zQXzEga4#vaIbTLZ5I&169u$vC4l>P zj~r(dDX7&iG=YP;CM-3O@HV>p%cZ^fyR0We*u0kocLDrV#>Y}-UWwTtL--Lg#GIoi zS#rXW1qDN7;$*iG;B{qiY>i2Hf6{yqy=tH6uzm)p7i(X>dbQl=Y3RG5iTiFhT9Vb> ztClF%O-$%Y5-)9u_^qD$0AheNJg8Fl+84`xG}@oaqr=VXucegAoxG6~CDwoL(sIA@ zECFQrc_2Od5{HR{kO1_GBEyTgnTHn1SNt_~bS$M)0P)1`ocQe7(Or;b4RWUXyaN@d z4M>3D>|_*V5TMY7Ag-~x>p8Qn)g@fHetm>?0()Y;sD4wI48*U%Ex2*rM~>3()q2~m z%d8(j5g&#td@e})YhfOHvAD9UBz;Y)&7L?Nhjp&EcY$ zC7W@~1h^|Gsfm{y>}2+~OmG!6&ML3rx*hrY)#}Vpco{sgf~))HL_z{np7cwa%H8g6 z=wyC(B2;haD-`PzSz$j}_~gkUo>yW8MoRbY6);}Ac=4hNDXv!*WE*YL72?lmnpMjE zFQMr7ppDyMKYsiub6+*YdFmA#2Qz$5)Dp9A+^yKq4}SKnX7>+6pYIAhBT!HzE9bqN zc!gPagtm#DqnYr+@n#_q700mMGe^ec58Oroe&9ZT^tlfHdXg}qW%ITZUySe*oHv7&i;?nc>ZR@MUcy>B5}nw5+6aE=ubK#{n#vAQn+sJV2f9q2B;_B=^-vUKx zZ1j{hiGcRA=S8IYC@2JDBATjjg|#k#^s5{_w?`(IdhEHE@{q`UQY@c=b?}dwPhrkD zsRiksRnDI=NiI$3F^S%U;r?d9_r|vM(sFg5HqQCwlL&^Fxj8agBeuUYi*+HNK^ca_ zQvCNkrPupzz6=YCmi7GlMse(SOk(YqmptpBmerN+JK=n>Nh2bWMiuiT!hfeF;r9wp z4C`7CWNo~9#)H1Q!d#wM(Y!hLjE=o9b>}N5`eRYpg#W(pA$OKwiatONS`8}np=`C( z8MynW^A-zWWVmioVIexNsDazx_{j>npoH6bgQTfK2fFeod=^~FB-9mFjE9d*oS&0_ zmd$?Hi=QHWNn?&fZg%q3cCDG!{4Kp(C(^RBEO zPa~}Ef!EOP#23Zt9Z|0KdYKXIrW7e1$R19vqH2Wd-o29tLJ?XG#c&p@Vm4ORmC|BJ z@(lpx{#aWpf7Eoyi0)@_6)B;<_xK=amtd2hRo&bypOu|WvQ_@;Q9r0KyVh$dE1<}d;(lb0eOi}6?->&n^*I$-dVVYM$84vs z7-O1noCw3uz-iPe0VD2_j4CNWAzfvGUL`)~?hOp8M)TWXuA;wzu6Et(WMkt^1_I(r zv247k4MwP%#=FFbOm@E42z~w&aU55|h{TMCP(oraY;@u=05-i= z%7@1%_JD~ihHjt>o`yYtehjD>#0rWGYPD3eQ3W&mAVNen-UWh)I6;#H^u2rcHhy&S zlU$VL6n|aDGIu7+*^kozYE{k;K)|-uH3FFk!~G)Na%8KUsHmvdS~pY_NX7D=$cTt&0L9A> zsJT7@tD>W$gYpH@We^mn8|11qvc1A>>)Ydc;ChI7_r^Q`&f-AeU7mYz*y#A)s^UQc zWoU{-J3BiU#PEaszb{GLIqLQ6M@PPM{NCHy2?NU)>Z`o4GjNZeOGaidu1&P9YJZR9 z+R0lfg0(ngfwJb{IkP2r39s=uDW83!&JOcLM8=|vp>MqL(l zkayPH1*xFu`6XZ^`F^`Z*3zt(G>IzA4D>yYHrp3_KKp4 z@=}p`Pg1TOree{(k(69gQZj}`OWDtT`3Rs!k1iVX-|S1PJ&F{Hj0<b*j7XawfZU(Wqk|1B|p0 zdyV=e%X>8S?|+aH4x7HT8%yoV`1sTzumf4G6 z!tF#(9w1)RlnTCcTF@?rd@_-hs*{}4g_$MjCeHb35&O8uAR z1aM-Vy}iSr2=|euWc>>aWT_!2!~vrhj&$hl?%o)UJ21ox_fJ<*zXzbhC*1@dAWQ;9 z**^q_FYm%A<=p>KtK|^cZ7#Va6Wci(Sqza=o41~sx6qTd@IFJ?htMNOSXr|Mhlfd~ z*C2f;Q4x`<4d)8Z#nFtP)zHFi#1EnC*Q;860Z1;xOb1yv1r!x8dc{qWqG+qX3#hBm z4N!z{*@7`5fv~QOv{ugfi!uu!2SFUzu!_OAApx79uw>JmRy~ku_I2L6eOuR#E&&K2 zF1S!bpz+oGo6DolgPFMx9z0lD)D>g>8I0s=mHnqjMs!#K8?z`w&lLads+5$zFLAwC z{t|r$VZD7H@&N`C;H9M{4MRgXuX(-rT50JpCp8rnX=JJ-B+Ud+(jt#O1O(*3p*a*1P=0|8NJdIZ zgur}ldEJ5ZW!zFw;1U-frNtgTLSZM*$-C1t#xn|o`O*qKFYJl5f*yukCt4tAhP$Qy zFsI2(`^97iP|IJ2_FlUkV@X#-%VMx3j*L~vY!;tZ||Av&b zuSGhb8=yP73C`D9$jkQgk4oqe0TPp%S_EpVEw4WgzU|{V1=}H-H|j$W}}* zJC1k1JY3r2)A+yHb|B!|iX@Zo>& zxPXZM|Dj0vAEIjV?)H)lX-Q9(bVrtzX@ELJ>CT<|*4D^BFJ1DL7CD$oIUt8xeMin= zwMGnKFw~d5si{XmI(63~*);}U_x&~q_e9%Q2S-Czfx`J+7&ElO(;o) zIjAh8c;nw-;e>^45Dhie)xT_R0$vOJlQK6(;M}7_LqlXbDnlWZaS`i2sR@L$ZQ($^ zr-lRN^4iGHv=Ce^xg|3<`BCYph%eDa8q`iecNL*=|9%=!KjsxE1K~0QauP%jS~@!W z;z@vU6oQ;u8K8MG%oGspgCt;}7Qo2EfO6XaK;XhA#(p?AI|a;!h{CN~R{@8TaBTw! zrG@}6rwxLI3K=ge-mmjq%SXCU=I2rIPjfo`S$T*AkllPhb(JhOWM*ClNDr5g&;Y0O zxSIHER0bZZNt|n@JnXs#kSQyvsSzIT-KBHq7q&A4TEHV=e+V`w+$k5l3}iYDd{t9V z_}C|d2cex>$RX@~uA=9($Qg`+6A|B>yi*t7~Ws8cPcb_FI@x?%0DZ&>Q-&&cm^m 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 literal 0 HcmV?d00001 diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 9f9d544b..8dd79089 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -21,7 +21,7 @@ def get_perveance(mass: float, kin_energy: float, line_density: float) -> float: classical_proton_radius = 1.53469e-18 # [m] gamma = 1.0 + (kin_energy / mass) # Lorentz factor beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) # velocity/speed_of_light - return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) + return (classical_proton_radius * line_density) / (beta**2 * gamma**3) class Envelope: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 0edc2e33..52ffbe49 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -175,3 +175,4 @@ def __call__( else: raise NotImplementedError("Unsupported node type: {}".format(type(node))) + \ No newline at end of file From 38f884e798ea3312fe42c399812f9e6c8367e215 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:00:56 -0400 Subject: [PATCH 023/183] Add kv/waterbag/gauss dist --- .../Envelope/outputs/test_env_fodo/.DS_Store | Bin 6148 -> 0 bytes .../outputs/test_env_fodo/fig_dist_x_xp.png | Bin 71449 -> 0 bytes .../outputs/test_env_fodo/fig_xavg.png | Bin 36812 -> 0 bytes .../outputs/test_env_fodo/fig_xrms.png | Bin 49751 -> 0 bytes .../outputs/test_env_fodo/fig_yavg.png | Bin 35942 -> 0 bytes .../outputs/test_env_fodo/fig_yrms.png | Bin 49118 -> 0 bytes examples/Envelope/test_env_fodo.py | 34 ++++++++------- examples/Envelope/utils.py | 39 +++++++++++++++--- 8 files changed, 53 insertions(+), 20 deletions(-) delete mode 100644 examples/Envelope/outputs/test_env_fodo/.DS_Store delete mode 100644 examples/Envelope/outputs/test_env_fodo/fig_dist_x_xp.png delete mode 100644 examples/Envelope/outputs/test_env_fodo/fig_xavg.png delete mode 100644 examples/Envelope/outputs/test_env_fodo/fig_xrms.png delete mode 100644 examples/Envelope/outputs/test_env_fodo/fig_yavg.png delete mode 100644 examples/Envelope/outputs/test_env_fodo/fig_yrms.png diff --git a/examples/Envelope/outputs/test_env_fodo/.DS_Store b/examples/Envelope/outputs/test_env_fodo/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 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
}7&# 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 HSzY}f-LkLm diff --git a/examples/Envelope/outputs/test_env_fodo/fig_xavg.png b/examples/Envelope/outputs/test_env_fodo/fig_xavg.png deleted file mode 100644 index 3904dc509c04cc44e539b5b823b2c08107a7cd09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36812 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 z 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 z z{5I`X#AxIfpRU0ic1XX-U3n!?k`=JP={Y{iR%mKNaaUaleWkLf(E0%Y1PaASjW=kV zL9Wy+aFK;y&-=x`xD{~3TQS}*d#1Y$1DRTu%7DYmi3g1t2lsEd@F!@lMw_7x`W zc~%7-aE7;n$lgM9*xZQK43-xdLLCHuI0lt?^yCTb00C7&*;WI0@GdpQKKs59nJZOAAAEX5y1}C3h4k|Yaq7dW0nP#%do0i|B z5oji=<|c&hD1&oJ+do*oF*DgLI}U7z^<2;SkM9i)5{d95n?l$tM-QHiExxiWGv_Lg z&W`I7C?s;%@eWEq4p(qA4hMuOsCdAXW-x{jMLnS+E6a0%`p&ZVMZy$l{7+em>{n02(<9pxre$cb=U=XZRa~3G-kYDfChZA> zeL7s#)6HX`W`702T``t>gSCTfncBYT&mClDW{{Y~=$8KvETV5}KIcG?bqgz5v*0bT z$eJ~?o5eBmeuv-Y{MN*J4NOC=x$uB&!8tfX>eom}Wu0 zwE>z{Cd%Pd?CUe<-Vhc%qfoQ76L|KWv4*F7Y7rfGir0v$?wanI{7iL2lDv$R38lnY zNtFL|G`Tx1hV>GBR#b?E`LDd;b^T`IaxJj_2ZtX&IQ4lAR1p4cTAOqRP^ceFxzh#F zfI-RaZtcaXZ^;|P!qcNTG(53??y(1%OBmsyxP>xB)-@zne9^IC&Y=>Gpkw|Xmrlk= zd+N-!7cf1v-UBFl1#H1ljs}r(aVk)KtH*bKby0#aB+Yr7dQm^r zL~*Z*pNWYJgnc@>cdzH>z3ozxODlk3vM>U$^2pY@49CPI!@zfa?E1#0twy$|(YuNNCpAlO@D1JLXPi5}1+?p^! zO3Rk{S-eZoyUeZ^v-T?OX9ng-b=0Oyv@O3LHA(&sJ6p*qexlOZe7=)=YhGSTt(#$C zV)OC&MNQXchF_dOSjT*;-&Sm#cD43*&*SCg^Fa0*CV!sF(DrW|iQf1kBkw5KuL`6g zS=<+%(1+bC#UN_eNUh;@gOM>l`G)?6`X4ei&gTb;TCmLO9;<9be(=wF@6k(W!)pGr ze~jVRyHUv>F+VxZDsolq^2JW@Dc|maS|lM75Y^6_-8)2C<+6$%>XorQw`3QEcm44v zNuCqaVc2Ud5PL_)Ta6KIyO2F0`Y4JHfwLJmqBL~UV*=indfe#>r|nCEXMWeh7pqRj zMhk7}i0}0il!EzQ2lI(+EUJ^u*nJ|gcLJ9lRR7YCMHvUx!v6k`28O?#bjVjVNTksY zI^K5bJj;cz774Rqg%8J+x~oY`gi5`<@o*YF{3JfSps2nYWN#(7$+&e=hPO3&DmPFR zaiwhi7iIYB$jfJY@@sfHq+KcBPTCMie1PpPdN&Ue{w|+ zg@6sH_Fxt7tE2Nk!FIO?A48rGxz(H6-ahmk>3+(WI3``VxdTB9tye71Z^mitemO!^ zl6i_($RIX;i>W`a(y7q%uLCYFouPf>xBkqW#2seQ51&BdUJm3VHHuIr=*& zu@xH6Q1(LOX-%rSk-#uw;ckz^iIH$?`()#E!-a0hA&W(oli*AdA>|;;D&JWiX&o}d z2kGDas^2Wj1K#>=FY-UOq^|_);BZ`W#g2_qgNW6zPORR-Gd^|EA$1RQ=FY<=lj@iJ z5*wD>Z~Lhf2>*)5%F)BWzpTWvkTpY&t}T^g0gpiUrLonr64XXoZqzthA?lM{vAWpb04_r6CLM@?G~s#B?w0Keb4= z9T^~-i7zk9LDk4K=zmir{8YQ3YtPKnpPamRPx?#|8%bK_QupdFQN_ceaSx+6Zd&%4 z;`JS=B{T`E9OXeoh}pom4NY^d1UfyqYwF@XEmLFL79Iz%{AQ7jH(Zp$r`VNxwSz@N zuq@=FLDERNlC)QCTeB%nyWUG{RR|sm#RkwNG#C}kx7!NkucXeK;vH?Z>a7M_VEw?A zUjsdoDa)=F>}%?#TRnG4_8o^ylVdl_`HT%Y)XU6#lIQ*e6`7u6jZ*a5i_IQTe+YD_ zp!TgG5xuzO5!YtRbMH&nCU?!Y?8i2+d)`Ki2xw`fC3s9m!bplrmBEToKb4RBz^F^t zCYR{AwYDE=^_-~li(H*z{h_DEGQW939CF)5B^ROPsppPFH_LIau+};!zc7O;C{4b> zIZ3%#=|bHze`}pvT?4j%7fZ(AI_2n<<#)HG)?kOhRg5U34@sNSa*{BTozhs(n_Ja| z)~9DlYYYQjv-jE_-Tb>lC>EMx(W%6hSk?02^_Qv2M6ZptJly)`;m=M77r&du)CNgZ zPAFi#Xw|WAT{v~LA#-yn`{HWqPKtGd-jtO5?(*sa?;MnSBGv9nuRz+a(AnvWWY^TU zJ8(=SeK8!en!Q>?EYm>XBJYj_)OPnHMN5Y$^qU2tnLx+fL#IQSE)LQTZ99dqjTn{j zdhN^0SDjRv8oVw^t6?zn?XljqNa$<5pfPK?Al>znKJcykmM#%IjCE&jfqIPXvbOXP z-k!$K#2k5xH5P_xte3f$@S|%)wkz9!brAHbt4CmYwbtRO(N=Y+V9#cdU}Tw!K(v>T zBTFyYavTwMVk)zYk0pFDU+~g@>qDF8Qz{!t>h>}CNx6*PCns>2{dUIJ1{ZW56%H@y zH#_gvQES!TIHNdxbz;)syhEoK1<%UFNs?62gGR$Gpld2?ZLIL?BH_$qaaKuF?b_b! zKfl7)FOawqHC>0$8ECJ$A9C(Te25M_IGS~o zw#5{R52s<1$&Vi^PxK6L@e&4XDO%^+Q z5&C}Va4-K;&m)yxSFlwl*Dul1J9ztUPYrTAE3hlsyS4)sFk*Q_VExg@N4?1tErPwI zEDvsT@dfRI?r=d3Z#;d$b#(Bwq3uNBA)jNDv*6g@HspIHTs4&C9}k>r;@n4xt_>+KceFXC!{|G5TjD+V5VmE|D%I!%0sh)vj6gbG=&ADmw1s2cv1A!ULmqk^W@uw?Rk zsT)ZtFoojVXiv_D%7nE*$_4w@9?PHe7*%Dq^x$b%Lu0Su=1^Pg+S^Fx*PKI=7Kv=Z z^vEwPY*zwH?_rs_w_=jWwr_Ae@vHpi13f`3tTA}_Q3LR2PQ_YMh3eb|udBW#qw(`g zB(?lPJ)X10=U0&xP+*x33`<%Js4z5t()B@2Wvw<}8iHMUgo;%nNVw2yKUEhVkT9pd z^B7CsF!mLhV+SrBo_bqgp_HsxF2Tp*B1Og)2jg+b*O3)sZI*h*4tj* z7j1i|3$^IJxxdj+pmK=)E_rui57-eq$9_^Z0S_+dwQr&bR`W03^?PtUS;}gf_dSvG zx2r3glaQXdrzxM3>?CS)p?i)6oAFEbyQaqvR(?tU@M*1eGk}ixP3tV9mpL^FC+_;` z3jab`J^p3k(weJ+Hy}}6lXfdI^$ak~#RcW5q+-HV5#Tj&x6cTmNMfaaa^iD0A;fkD z+&OcupO6EicR9NII-d2uS@79X|KUpeWzSTf00;07VevF?Psl4weIhcgXR%r6v~)uQ zDjTdUUH3f@Mqko}HmiP;EjUaj8oQAs5KrqOSu zUQRJVFX^a`9kUO*-vY3qs#11Yu{H`93aE`Y^YE&+AXA$opNH?@0nvf7L0y(|aBX2kcCot2jWjJto&(Ur5si2j=2D zxlA+M9VSXGtALs-GHz>_zPyI{66FYrXFA?dTDVc69%*pbtjabz7EYgIBMP7(z2Mo? zarC%L-^}WF!Q)zGw}CK_B7^wX`!`7C_x%u8uwAoz-SN({{mVtl#rzKd$apOxB(V!` zY}IFZW{*eZUYUqzlq5T_Bl1e6|1-hpW*kwQ26kLFJSifvTfn_hSJW<3gU1$Kn+HnN z)Wsv50GjGNNfTIg%HeaeUp<#kp6_BYzmA1Dx<`jIrd#872{?2C{0(u9cM@c6!vWx_ za4HJa<7#QO4xCU`8;23$uBGtXRMw(74+3k`C>va^E^$T%_P2FK2?Rl0{kZ5@mI`4i z_R7ni+OL>1cW!&iMEDDmWq+GhJalag(!=5pcCzcuuWqanS2~P?Ee=(sTJ;`JUHvJ7 zn@u0-ZTrc(Dn?dYQZaDKoQq{5kB>$VpVi;4m)!%o)32C_u`RfFc5IB3v%FS7+*zj+ zvm}Lo(vUWIypbpGxpUyA7b)k`ftcH#;8n0`t2kkBeTnuaO>56Si!)2vluTAu7@T+J z$HRK76a;^fxphFk)JyZ$831jv_U8pv7k*f%x~1sZ7mstz>P4igl~j`caEMG_okce$ z?fdybzF|YUeKpzM_Fr!1)dV=(wm4iuQ^7tc5Y@)^)Y#^WH6oX7WRGa+yrGC#iB&ud zmg$@~f$vNzJp+;kuI%(*YoRUZN15)? zfogRp>mlv0BZdryp`iGmbmsRP#rX-#4iSCZ@H}r5^&N8x$I3EBb5sFUcccQ<=E4}N z+!zmZI2Ie}Dl$W*tC11X3lkR8?4IWh_&}FJjI6^7qu`XS8hJy5FoZp6 zcIMCsXy<#q={=3}&5?FQ#cw=DUBpw55{Z=SR?OJjLgX?w*Ec_w^g|$~L(O89p&L#C z9ZE~%0A)a?%hyj84x()CN-RBjXaPLN)h05c`!BwSgn*^YGj-Nh^zS~#2p)KNJ_=g{ z|4mUfpRv6bX%P9#AL)MA2fjKUL`*!K%|TB2_@ycIn5ic^!~7ddl(zFIjd2JWev6qi zNl9DBDt|7_hc{8;A^tOX6llODt3qF3_S9J!4S1eJb+fkUrY{zBsxxdek`IH3Krs%0 zgXAVj%4>YJ{3tj4<-`wURq#L?RE}>ahz0~mM(3;{?4I9&VxZFGtsb)rDljqb z;_JWB)>1KDB1!k?*0HZpDmnz^&SCg{(msWSP-;iDM@{Zkht$nfUZsw(V`;Y&Y^31o zTYyw-VQRhw7GsxBBS2nBanUYsG}ya3MR7c3;V?ftu?H7ywvSdGbIYU$G+<+wki#7& z%m+J#9As#djzf!XaDb*`Ldt31|8*-q+d6t;qjxD0z>Sm1ysPPk`6!SPV|K}kKD1fY zqlYIjY=&te`x!I?=8$0zt@9qK%#wVG3pB%^+mXK1bwUik5FJ{`HoG!#q@|v3LmjOw+5@Z%gCXco?{*=( zT)D;7cA2t$Rnn9-Vz|O1oIi3xQDz*qm7O=dvX(;c&fzA!=e+fO69_`iW?q{-O>1yLHVe4#4{&tu*^E&PQs7j8nBpBXP75~+8HN?`cwZVnfDZlO z$5oRVeqOXEFnIY7$-H#7zDgq0(mMFkOV;R$j2mZ$wa= zj>lV!HnJ$H9{m*zt)L`g3ygFI(96Uk$TQm6x6(NZtZ#6&JG)AX7qeM#EF@nnna5!C zE}?zEW+lbnk!QRO*=BZNmDBGS!TeGIm+3Xkt+`;8!^2S5PJbC=SPgwl>7yLjs`^lM z#zHu>k~SEcFkuUu=r*{w(~{iqxJ~|ZgS^Dm-v7Iw2seAj)ocehZhR=lUPhGXX3cFI KlQ--+{=WbwNHTi> diff --git a/examples/Envelope/outputs/test_env_fodo/fig_yrms.png b/examples/Envelope/outputs/test_env_fodo/fig_yrms.png deleted file mode 100644 index feceeca91c2de5a61f6f52bd99da0b2ee2a1d81a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49118 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%0DZ&>Q-&&cm^m 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 diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index a9fd0540..9a9e56c3 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -26,7 +26,7 @@ from orbit.utils.consts import mass_proton from plot import plot_rms_ellipse -from utils import gen_kv +from utils import gen_dist plt.style.use("style.mplstyle") @@ -35,18 +35,22 @@ # ------------------------------------------------------------------------------ parser = argparse.ArgumentParser() +parser.add_argument("--zrms", type=float, default=5.0) +parser.add_argument("--kin-energy", type=float, default=0.025) +parser.add_argument("--intensity", type=float, default=5e10) + +parser.add_argument("--dist-name", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) +parser.add_argument("--dist-mismatch-x", type=float, default=0.0) +parser.add_argument("--dist-mismatch-y", type=float, default=0.0) +parser.add_argument("--dist-offset-x", type=float, default=0.0) +parser.add_argument("--dist-offset-y", type=float, default=0.0) + parser.add_argument("--nslice", type=int, default=10) -parser.add_argument("--kq", type=float, default=0.1) -parser.add_argument("--mismatch-x", type=float, default=0.0) -parser.add_argument("--mismatch-y", type=float, default=0.0) -parser.add_argument("--offset-x", type=float, default=0.0) -parser.add_argument("--offset-y", type=float, default=0.0) -parser.add_argument("--turns", type=int, default=25) +parser.add_argument("--kq", type=float, default=0.25) + parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--turns", type=int, default=25) parser.add_argument("--sc", type=int, default=0) -parser.add_argument("--intensity", type=float, default=1e10) -parser.add_argument("--zrms", type=float, default=5.0) -parser.add_argument("--kin-energy", type=float, default=0.025) args = parser.parse_args() @@ -110,14 +114,14 @@ cov_matrix[5, 5] = 0.0 # Mismatch x -cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 -cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 +cov_matrix[0, 0] *= (1.0 + args.dist_mismatch_x) ** 2 +cov_matrix[2, 2] *= (1.0 + args.dist_mismatch_y) ** 2 cov_matrix_init = np.copy(cov_matrix) # Offset x centroid_init = np.zeros(6) -centroid_init[0] += args.offset_x -centroid_init[2] += args.offset_y +centroid_init[0] += args.dist_offset_x +centroid_init[2] += args.dist_offset_y # Create envelope envelope = Envelope( @@ -168,7 +172,7 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_kv(args.nparts, cov_matrix_init[0:4, 0:4]) +bunch_coords[:, :4] = gen_dist(args.nparts, cov_matrix_init[0:4, 0:4], args.dist_name) bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) bunch_coords += centroid_init[None, :6] diff --git a/examples/Envelope/utils.py b/examples/Envelope/utils.py index 80a8e513..88183f56 100644 --- a/examples/Envelope/utils.py +++ b/examples/Envelope/utils.py @@ -1,9 +1,38 @@ import numpy as np -def gen_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: - rng = np.random.default_rng() - X = rng.normal(size=(n, cov_matrix.shape[0])) + +def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: + return np.random.multivariate_normal( + mean=np.zeros(cov_matrix.shape[0]), + cov=cov_matrix, + size=n, + ) + +def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: + X = np.random.normal(size=(n, cov_matrix.shape[0])) X /= np.linalg.norm(X, axis=1)[:, None] X /= np.std(X, axis=0) - X = np.matmul(X, np.linalg.cholesky(cov_matrix).T) - return X \ No newline at end of file + return X + + +def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: + X = gen_dist_kv(n, cov_matrix) + dim = X.shape[1] + r = np.random.uniform(size=n) ** (1.0 / dim) + X *= r[:, None] + X /= np.std(X, axis=0) + return X + + +def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: + if name == "kv": + X = gen_dist_kv(n, cov_matrix) + elif name == "waterbag": + X = gen_dist_waterbag(n, cov_matrix) + elif name == "gauss": + X = gen_dist_gauss(n, cov_matrix) + else: + raise ValueError(f"Invalid distribution name: {name}") + + L = np.linalg.cholesky(cov_matrix) + return np.matmul(X, L.T) \ No newline at end of file From c37fc3ae8ae8346aeba86c0307580a078dc0e018 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:38:08 -0400 Subject: [PATCH 024/183] Add tilt to 2D envelope space charge calculation --- py/orbit/envelope/__init__.py | 2 +- py/orbit/envelope/envelope.py | 6 +++++ py/orbit/envelope/matrix.py | 47 +++++++++++++++++++---------------- py/orbit/envelope/meson.build | 3 ++- py/orbit/envelope/utils.py | 38 ++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 py/orbit/envelope/utils.py diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index 70a5fcbc..d945ed8f 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1,2 +1,2 @@ from .envelope import Envelope -from .envelope import EnvelopeTracker +from .envelope import EnvelopeTracker \ No newline at end of file diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 8dd79089..15b58064 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -7,6 +7,7 @@ from ..lattice import AccLattice from .matrix import MatrixFactory +from .utils import gen_dist ENTRANCE = AccNode.ENTRANCE @@ -71,6 +72,11 @@ def rms(self) -> np.ndarray: def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) + + def sample(self, n: int, dist: str = "kv") -> np.ndarray: + particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) + particles = particles + self.centroid() + return particles class EnvelopeTracker: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 52ffbe49..cb55cc6b 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -29,16 +29,14 @@ def __init__(self) -> None: TurnCounterTEAPOT, ] - @staticmethod - def drift(length: float, gamma: float) -> np.ndarray: + def drift(self, length: float, gamma: float) -> np.ndarray: matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length matrix[4, 5] = length / gamma**2 return matrix - @staticmethod - def quad(length: float, kq: float) -> np.ndarray: + def quad(self, length: float, kq: float) -> np.ndarray: sqrt_abs_kq = math.sqrt(abs(kq)) matrix = np.identity(7) @@ -70,8 +68,7 @@ def quad(length: float, kq: float) -> np.ndarray: matrix[3, 3] = cy return matrix - @staticmethod - def bend(length: float, theta: float, gamma: float) -> np.ndarray: + def bend(self, length: float, theta: float, gamma: float) -> np.ndarray: rho = length / theta cx = math.cos(theta) @@ -90,8 +87,7 @@ def bend(length: float, theta: float, gamma: float) -> np.ndarray: matrix[4, 5] = (length / gamma**2) - rho * (theta - sx) return matrix - @staticmethod - def tilt(angle: float) -> np.ndarray: + def tilt(self, angle: float) -> np.ndarray: cs = math.cos(angle) sn = math.sin(angle) @@ -102,30 +98,37 @@ def tilt(angle: float) -> np.ndarray: matrix[2, 2] = matrix[3, 3] = +cs return matrix - @staticmethod - def kick(kx: float, ky: float, dE: float) -> np.ndarray: + def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix = np.identity(7) matrix[1, 6] = kx matrix[3, 6] = ky matrix[5, 6] = dE return matrix - @staticmethod - def space_charge_2d(length: float, cov_matrix: np.ndarray, perveance: float) -> np.ndarray: - # Start by assuming upright beam - cx = 2.0 * math.sqrt(cov_matrix[0, 0]) - cy = 2.0 * math.sqrt(cov_matrix[2, 2]) - - kappa_x = 2.0 * perveance / (cx * (cx + cy)) - kappa_y = 2.0 * perveance / (cy * (cx + cy)) - + def space_charge_2d(self, length: float, cov_matrix: np.ndarray, perveance: float) -> np.ndarray: + cov_xx = cov_matrix[0, 0] + cov_yy = cov_matrix[2, 2] + cov_xy = cov_matrix[0, 2] + + angle = -0.5 * math.atan2(2.0 * cov_xy, cov_xx - cov_yy) + _sin = math.sin(angle) + _cos = math.cos(angle) + + rx = 2.0 * np.sqrt(abs(cov_xx * _cos**2 + cov_yy * _sin**2 - 2.0 * cov_xy * _sin * _cos)) + ry = 2.0 * np.sqrt(abs(cov_xx * _sin**2 + cov_yy * _cos**2 + 2.0 * cov_xy * _sin * _cos)) + + kappa_x = 2.0 * perveance / (rx * (rx + ry)) + kappa_y = 2.0 * perveance / (ry * (rx + ry)) + matrix = np.identity(7) matrix[1, 0] = kappa_x * length matrix[3, 2] = kappa_y * length - return matrix + + R = self.tilt(angle) + M = np.linalg.multi_dot([R, matrix, np.linalg.inv(R)]) + return M - @staticmethod - def space_charge_3d(length: float, cov_matrix: np.ndarray, intensity: float) -> np.ndarray: + def space_charge_3d(self, length: float, cov_matrix: np.ndarray, intensity: float) -> np.ndarray: raise NotImplementedError() def __call__( diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 75fdcdb9..31c9033b 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -1,7 +1,8 @@ py_sources = files([ '__init__.py', 'envelope.py', - 'matrix.py' + 'matrix.py', + 'utils.py' ]) python.install_sources( diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py new file mode 100644 index 00000000..88183f56 --- /dev/null +++ b/py/orbit/envelope/utils.py @@ -0,0 +1,38 @@ +import numpy as np + + +def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: + return np.random.multivariate_normal( + mean=np.zeros(cov_matrix.shape[0]), + cov=cov_matrix, + size=n, + ) + +def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: + X = np.random.normal(size=(n, cov_matrix.shape[0])) + X /= np.linalg.norm(X, axis=1)[:, None] + X /= np.std(X, axis=0) + return X + + +def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: + X = gen_dist_kv(n, cov_matrix) + dim = X.shape[1] + r = np.random.uniform(size=n) ** (1.0 / dim) + X *= r[:, None] + X /= np.std(X, axis=0) + return X + + +def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: + if name == "kv": + X = gen_dist_kv(n, cov_matrix) + elif name == "waterbag": + X = gen_dist_waterbag(n, cov_matrix) + elif name == "gauss": + X = gen_dist_gauss(n, cov_matrix) + else: + raise ValueError(f"Invalid distribution name: {name}") + + L = np.linalg.cholesky(cov_matrix) + return np.matmul(X, L.T) \ No newline at end of file From 752bae352767f669705481aceb13b7cee6e04b62 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:38:44 -0400 Subject: [PATCH 025/183] Add corner plot and tilt test --- examples/Envelope/plot.py | 8 ---- examples/Envelope/style.mplstyle | 1 + examples/Envelope/test_env_fodo.py | 69 ++++++++++++++++++++++-------- examples/Envelope/utils.py | 22 +++++++++- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py index 425cf204..053cb8cd 100644 --- a/examples/Envelope/plot.py +++ b/examples/Envelope/plot.py @@ -114,14 +114,6 @@ def plot_corner( for j in range(1, ndim): axs[i, j].set_yticklabels([]) - ymax = 0.0 - ymin = np.inf - for i in range(ndim): - ymax = max(ymax, axs[i, i].get_ylim()[1]) - ymin = min(ymin, axs[i, i].get_ylim()[0]) - for i in range(ndim): - axs[i, i].set_ylim(ymin, ymax) - for ax in axs.flat: for loc in ["top", "right"]: ax.spines[loc].set_visible(False) diff --git a/examples/Envelope/style.mplstyle b/examples/Envelope/style.mplstyle index be0d2046..71f36605 100644 --- a/examples/Envelope/style.mplstyle +++ b/examples/Envelope/style.mplstyle @@ -1,5 +1,6 @@ axes.linewidth: 1.25 axes.titlesize: "medium" +image.cmap: "Greys" figure.constrained_layout.use: True savefig.dpi: 300 savefig.format: "png" diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index 9a9e56c3..36fd73e5 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -26,7 +26,10 @@ from orbit.utils.consts import mass_proton from plot import plot_rms_ellipse +from plot import plot_corner from utils import gen_dist +from utils import build_rotation_matrix_xy +from utils import project_cov_matrix plt.style.use("style.mplstyle") @@ -44,6 +47,7 @@ parser.add_argument("--dist-mismatch-y", type=float, default=0.0) parser.add_argument("--dist-offset-x", type=float, default=0.0) parser.add_argument("--dist-offset-y", type=float, default=0.0) +parser.add_argument("--dist-tilt", action="store_true") parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--kq", type=float, default=0.25) @@ -113,12 +117,18 @@ cov_matrix[4, 4] = args.zrms**2 cov_matrix[5, 5] = 0.0 -# Mismatch x +# Tilt +if args.dist_tilt: + rot_matrix = np.identity(6) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(0.25 * math.pi)) + cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) + +# Mismatch cov_matrix[0, 0] *= (1.0 + args.dist_mismatch_x) ** 2 cov_matrix[2, 2] *= (1.0 + args.dist_mismatch_y) ** 2 cov_matrix_init = np.copy(cov_matrix) -# Offset x +# Offset centroid_init = np.zeros(6) centroid_init[0] += args.dist_offset_x centroid_init[2] += args.dist_offset_y @@ -223,7 +233,6 @@ # Analysis # ------------------------------------------------------------------------------ -# Process history arrays. for history in histories.values(): for key in history: history[key] = np.array(history[key]) @@ -238,46 +247,52 @@ # Plot rms bunch sizes for key in ["xrms", "yrms"]: fig, ax = plt.subplots(figsize=(5, 3)) - for model in ["envelope", "bunch"]: - ax.plot(histories[model][key], marker=".", label=model) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) ax.set_xlabel("Turn") ax.set_ylabel("RMS [mm]") ax.legend(loc="upper right") plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close("all") + plt.close() # Plot centroids for key in ["xavg", "yavg"]: fig, ax = plt.subplots(figsize=(5, 3)) - for model in ["envelope", "bunch"]: - ax.plot(histories[model][key], marker=".", label=model) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) ax.set_ylim(-5.0, 5.0) ax.set_xlabel("Turn") ax.set_ylabel("AVG [mm]") ax.legend(loc="upper right") plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close("all") - + plt.close() -# Plot bunch -fig, ax = plt.subplots(figsize=(4, 4)) -X = collect_bunch(bunch)["coords"] -X[:, :4] *= 1000.0 +# Collect bunch/envelope data on final turn. +particles = collect_bunch(bunch)["coords"] +particles[:, :4] *= 1000.0 env_cov_matrix = envelope.cov() env_cov_matrix[:4, :4] *= 1000.0**2 +print(env_cov_matrix) + env_centroid = envelope.centroid() env_centroid[:4] *= 1000.0 -xmax = 4.0 * np.std(X, axis=0) +xmax = 4.0 * np.std(particles, axis=0) limits = list(zip(-xmax, xmax)) labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] -ax.hist2d(X[:, 0], X[:, 1], bins=100, range=[limits[0], limits[1]]) +# Plot x-x' +fig, ax = plt.subplots(figsize=(4, 4)) +ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) plot_rms_ellipse( env_cov_matrix[0:2, 0:2], center=(env_centroid[0], env_centroid[1]), @@ -285,8 +300,26 @@ color="red", ax=ax, ) - ax.set_xlabel(labels[0]) ax.set_ylabel(labels[1]) plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) -plt.close("all") \ No newline at end of file +plt.close() + +# Plot corner +fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, +) +for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + level=2.0, + color="red", + ax=axs[i, j], + ) +plt.savefig(os.path.join(output_dir, "fig_dist_corner")) +plt.close() diff --git a/examples/Envelope/utils.py b/examples/Envelope/utils.py index 88183f56..23c7c668 100644 --- a/examples/Envelope/utils.py +++ b/examples/Envelope/utils.py @@ -35,4 +35,24 @@ def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: raise ValueError(f"Invalid distribution name: {name}") L = np.linalg.cholesky(cov_matrix) - return np.matmul(X, L.T) \ No newline at end of file + return np.matmul(X, L.T) + + +def build_rotation_matrix_xy(angle: float) -> np.ndarray: + cs = np.cos(angle) + sn = np.sin(angle) + + matrix = np.identity(4) + matrix[0, 0] = matrix[1, 1] = +cs + matrix[0, 2] = matrix[1, 3] = +sn + matrix[2, 0] = matrix[3, 1] = -sn + matrix[2, 2] = matrix[3, 3] = +cs + return matrix + + +def project_cov_matrix(cov_matrix: np.ndarray, axis: tuple[int, ...]) -> np.ndarray: + cov_matrix_proj = np.zeros((len(axis), len(axis))) + for i in range(len(axis)): + for j in range(len(axis)): + cov_matrix_proj[i, j] = cov_matrix[axis[i], axis[j]] + return cov_matrix_proj \ No newline at end of file From e704cdc26e776aeb1dc7d8d1d780e64f3f43e3bb Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:39:03 -0400 Subject: [PATCH 026/183] Format --- examples/Envelope/plot.py | 2 +- examples/Envelope/test_env_fodo.py | 14 ++++++++++---- examples/Envelope/utils.py | 11 ++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py index 053cb8cd..e4a536e3 100644 --- a/examples/Envelope/plot.py +++ b/examples/Envelope/plot.py @@ -129,4 +129,4 @@ def plot_corner( fig.align_ylabels() fig.align_xlabels() - return fig, axs \ No newline at end of file + return fig, axs diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index 36fd73e5..4de31431 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -42,7 +42,9 @@ parser.add_argument("--kin-energy", type=float, default=0.025) parser.add_argument("--intensity", type=float, default=5e10) -parser.add_argument("--dist-name", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) +parser.add_argument( + "--dist-name", type=str, default="kv", choices=["kv", "waterbag", "gauss"] +) parser.add_argument("--dist-mismatch-x", type=float, default=0.0) parser.add_argument("--dist-mismatch-y", type=float, default=0.0) parser.add_argument("--dist-offset-x", type=float, default=0.0) @@ -162,7 +164,9 @@ xavg = 1000.0 * centroid[0] yavg = 1000.0 * centroid[2] - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) history["xrms"].append(xrms) history["yrms"].append(yrms) @@ -219,8 +223,10 @@ yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) xavg = 1000.0 * twiss_calc.getAverage(0) yavg = 1000.0 * twiss_calc.getAverage(2) - - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) history["xrms"].append(xrms) history["yrms"].append(yrms) diff --git a/examples/Envelope/utils.py b/examples/Envelope/utils.py index 23c7c668..432e4945 100644 --- a/examples/Envelope/utils.py +++ b/examples/Envelope/utils.py @@ -1,14 +1,15 @@ import numpy as np -def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: +def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: return np.random.multivariate_normal( mean=np.zeros(cov_matrix.shape[0]), cov=cov_matrix, size=n, ) -def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: + +def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: X = np.random.normal(size=(n, cov_matrix.shape[0])) X /= np.linalg.norm(X, axis=1)[:, None] X /= np.std(X, axis=0) @@ -22,7 +23,7 @@ def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: X *= r[:, None] X /= np.std(X, axis=0) return X - + def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: if name == "kv": @@ -33,7 +34,7 @@ def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: X = gen_dist_gauss(n, cov_matrix) else: raise ValueError(f"Invalid distribution name: {name}") - + L = np.linalg.cholesky(cov_matrix) return np.matmul(X, L.T) @@ -55,4 +56,4 @@ def project_cov_matrix(cov_matrix: np.ndarray, axis: tuple[int, ...]) -> np.ndar for i in range(len(axis)): for j in range(len(axis)): cov_matrix_proj[i, j] = cov_matrix[axis[i], axis[j]] - return cov_matrix_proj \ No newline at end of file + return cov_matrix_proj From f8987bbc502b386177676cfe3feb81d480f67748 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:39:21 -0400 Subject: [PATCH 027/183] Format --- py/orbit/envelope/__init__.py | 2 +- py/orbit/envelope/envelope.py | 14 +++++--------- py/orbit/envelope/matrix.py | 23 ++++++++++++----------- py/orbit/envelope/utils.py | 11 ++++++----- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index d945ed8f..70a5fcbc 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1,2 +1,2 @@ from .envelope import Envelope -from .envelope import EnvelopeTracker \ No newline at end of file +from .envelope import EnvelopeTracker diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 15b58064..b364ae87 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -34,7 +34,7 @@ def __init__( intensity: float = 0.0, ) -> None: self.sync_part = sync_part - + if centroid is None: centroid = np.zeros(6) @@ -58,7 +58,7 @@ def set_intensity(self, intensity: float) -> None: self.perveance = get_perveance( mass=self.sync_part.mass(), kin_energy=self.sync_part.kinEnergy(), - line_density=(self.intensity / length) + line_density=(self.intensity / length), ) def centroid(self) -> np.ndarray: @@ -77,7 +77,7 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles - + class EnvelopeTracker: def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: @@ -89,9 +89,7 @@ def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): # Child nodes before node for child_node in node.getChildNodes(ENTRANCE): - envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) - ) + envelope.apply_transfer_matrix(self.matrix_factory(child_node, envelope.sync_part)) for part_index in range(node.getnParts()): # Child nodes before part @@ -131,6 +129,4 @@ def track(self, envelope: Envelope) -> None: # Child nodes after node for child_node in node.getChildNodes(EXIT): - envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) - ) + envelope.apply_transfer_matrix(self.matrix_factory(child_node, envelope.sync_part)) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index cb55cc6b..f93e4fda 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -104,8 +104,10 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix[3, 6] = ky matrix[5, 6] = dE return matrix - - def space_charge_2d(self, length: float, cov_matrix: np.ndarray, perveance: float) -> np.ndarray: + + def space_charge_2d( + self, length: float, cov_matrix: np.ndarray, perveance: float + ) -> np.ndarray: cov_xx = cov_matrix[0, 0] cov_yy = cov_matrix[2, 2] cov_xy = cov_matrix[0, 2] @@ -113,13 +115,13 @@ def space_charge_2d(self, length: float, cov_matrix: np.ndarray, perveance: floa angle = -0.5 * math.atan2(2.0 * cov_xy, cov_xx - cov_yy) _sin = math.sin(angle) _cos = math.cos(angle) - + rx = 2.0 * np.sqrt(abs(cov_xx * _cos**2 + cov_yy * _sin**2 - 2.0 * cov_xy * _sin * _cos)) ry = 2.0 * np.sqrt(abs(cov_xx * _sin**2 + cov_yy * _cos**2 + 2.0 * cov_xy * _sin * _cos)) - + kappa_x = 2.0 * perveance / (rx * (rx + ry)) kappa_y = 2.0 * perveance / (ry * (rx + ry)) - + matrix = np.identity(7) matrix[1, 0] = kappa_x * length matrix[3, 2] = kappa_y * length @@ -127,13 +129,13 @@ def space_charge_2d(self, length: float, cov_matrix: np.ndarray, perveance: floa R = self.tilt(angle) M = np.linalg.multi_dot([R, matrix, np.linalg.inv(R)]) return M - - def space_charge_3d(self, length: float, cov_matrix: np.ndarray, intensity: float) -> np.ndarray: - raise NotImplementedError() - def __call__( - self, node: AccNode, sync_part: SyncParticle, part_index: int = 0 + def space_charge_3d( + self, length: float, cov_matrix: np.ndarray, intensity: float ) -> np.ndarray: + raise NotImplementedError() + + def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) gamma = sync_part.gamma() @@ -178,4 +180,3 @@ def __call__( else: raise NotImplementedError("Unsupported node type: {}".format(type(node))) - \ No newline at end of file diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index 88183f56..e846d83c 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -1,14 +1,15 @@ import numpy as np -def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: +def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: return np.random.multivariate_normal( mean=np.zeros(cov_matrix.shape[0]), cov=cov_matrix, size=n, ) -def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: + +def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: X = np.random.normal(size=(n, cov_matrix.shape[0])) X /= np.linalg.norm(X, axis=1)[:, None] X /= np.std(X, axis=0) @@ -22,7 +23,7 @@ def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: X *= r[:, None] X /= np.std(X, axis=0) return X - + def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: if name == "kv": @@ -33,6 +34,6 @@ def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: X = gen_dist_gauss(n, cov_matrix) else: raise ValueError(f"Invalid distribution name: {name}") - + L = np.linalg.cholesky(cov_matrix) - return np.matmul(X, L.T) \ No newline at end of file + return np.matmul(X, L.T) From b9fec4143f510e4a58bd3ac28658ba1f1d47ec5f Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:41:10 -0400 Subject: [PATCH 028/183] Add comment on cholesky issue --- py/orbit/envelope/envelope.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b364ae87..b2aca635 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -74,6 +74,8 @@ def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) def sample(self, n: int, dist: str = "kv") -> np.ndarray: + # Issue: covariance matrix is becoming non semi-positive definite, + # giving error in cholesky decomposition. particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles From 09838f69fe4d787d17a3d12cefd7830c02acb7a7 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:46:14 -0400 Subject: [PATCH 029/183] Delete commented line --- py/orbit/envelope/envelope.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b2aca635..05c14d9e 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -100,28 +100,28 @@ def track(self, envelope: Envelope) -> None: self.matrix_factory(child_node, envelope.sync_part) ) - # Main node - matrix = self.matrix_factory(node, envelope.sync_part, part_index) - + # Space charge if self.space_charge: length = node.getLength(part_index) cov_matrix = envelope.cov() if self.space_charge == "2d": - matrix_sc = self.matrix_factory.space_charge_2d( + matrix = self.matrix_factory.space_charge_2d( length=length, cov_matrix=cov_matrix, perveance=envelope.perveance ) elif self.space_charge == "3d": - matrix_sc = self.matrix_factory.space_charge_3d( + matrix = self.matrix_factory.space_charge_3d( length=length, cov_matrix=cov_matrix, intensity=envelope.intensity ) else: raise ValueError(f"Invalid space charge model: {self.space_charge}") - # matrix = np.matmul(matrix, matrix_sc) - envelope.apply_transfer_matrix(matrix_sc) + envelope.apply_transfer_matrix(matrix) - envelope.apply_transfer_matrix(matrix) + # Main node part + envelope.apply_transfer_matrix( + self.matrix_factory(node, envelope.sync_part, part_index) + ) # Child nodes after part for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): From 271717acb9ad2370a8cb0c6292506efdd7eb1246 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 15:46:31 -0400 Subject: [PATCH 030/183] Edit .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7133da9d..1583fd4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store .idea build PyORBIT.egg-info From 5fa7c2bb17378a084953257af53a47a8347ca889 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Fri, 24 Apr 2026 16:03:19 -0400 Subject: [PATCH 031/183] Docstrings --- py/orbit/envelope/envelope.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 05c14d9e..596be246 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -26,6 +26,25 @@ def get_perveance(mass: float, kin_energy: float, line_density: float) -> float: class Envelope: + """Represents beam envelope and centroid. + + Attributes: + matrix: 7 x 7 covariance matrix for augmented phase space vector. + + Define the phase space vector X = [x, x', y, y', z, dE]^T and + augmented vector Y = [x, x', y, y', z, dE, 1]. + + Let X evolve according to X -> MX + U, where M is a 6 x 6 transfer matrix + and U is 6 x 1 "driving" vector. The augmented vector Y evolves according + to Y -> NY, where N = [[M, U], [0, 1]] is a 7 x 7 matrix. + + We track the 7 x 7 covariance matrix of Y: + + R = = [[, ], [, 1]], + + which contains both the phase space covariance matrix and centroid vector. + R evolves according to R -> N R N^T. + """ def __init__( self, sync_part: SyncParticle, @@ -82,6 +101,7 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: class EnvelopeTracker: + """Tracks envelope through linear lattice with optional linear space charge kicks.""" def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: self.lattice = lattice self.matrix_factory = MatrixFactory() From 0b28b9bc28f53d9451a9948d12dd5800e795153e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 15:02:55 -0400 Subject: [PATCH 032/183] Plot centroid offset for envelope comparison --- examples/Envelope/plot.py | 3 ++- examples/Envelope/test_env_fodo.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py index e4a536e3..0a088a66 100644 --- a/examples/Envelope/plot.py +++ b/examples/Envelope/plot.py @@ -36,7 +36,8 @@ def plot_ellipse( kws.setdefault("color", "black") kws.setdefault("lw", 1.25) - center = (0.0, 0.0) + if center is None: + center = (0.0, 0.0) d1 = r1 * 2.0 d2 = r2 * 2.0 diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index 4de31431..671b00f9 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -323,6 +323,7 @@ env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) plot_rms_ellipse( env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), level=2.0, color="red", ax=axs[i, j], From 18be37585b585f18bea46f941c0f69975750b651 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 15:47:08 -0400 Subject: [PATCH 033/183] Remove print --- examples/Envelope/test_env_fodo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index 671b00f9..ef4223d3 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -286,8 +286,6 @@ env_cov_matrix = envelope.cov() env_cov_matrix[:4, :4] *= 1000.0**2 -print(env_cov_matrix) - env_centroid = envelope.centroid() env_centroid[:4] *= 1000.0 From 82334dd3bfa2bb5f6dfc0317f4c3328ca8c34619 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 15:47:18 -0400 Subject: [PATCH 034/183] Start fixing centroid issue --- py/orbit/envelope/envelope.py | 24 ++++++++++++++++-------- py/orbit/envelope/matrix.py | 30 +++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 596be246..437073fe 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -18,11 +18,11 @@ AFTER = AccNode.AFTER -def get_perveance(mass: float, kin_energy: float, line_density: float) -> float: - classical_proton_radius = 1.53469e-18 # [m] +def get_perveance_2d(mass: float, kin_energy: float, line_density: float) -> float: + classical_proton_radius = 1.534697049469832e-18 # [m] gamma = 1.0 + (kin_energy / mass) # Lorentz factor beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) # velocity/speed_of_light - return (classical_proton_radius * line_density) / (beta**2 * gamma**3) + return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) class Envelope: @@ -67,14 +67,15 @@ def __init__( self.matrix[6, 6] = 1.0 self.intensity = 0.0 - self.perveance = 0.0 + self.perveance_2d = 0.0 + self.perveance_3d = None self.set_intensity(intensity) def set_intensity(self, intensity: float) -> None: self.intensity = intensity cov_matrix = self.cov() - length = 2.0 * math.sqrt(cov_matrix[4, 4]) # assume uniform density - self.perveance = get_perveance( + length = 4.0 * math.sqrt(cov_matrix[4, 4]) # assume uniform density + self.perveance_2d = get_perveance_2d( mass=self.sync_part.mass(), kin_energy=self.sync_part.kinEnergy(), line_density=(self.intensity / length), @@ -124,14 +125,21 @@ def track(self, envelope: Envelope) -> None: if self.space_charge: length = node.getLength(part_index) cov_matrix = envelope.cov() + centroid = envelope.centroid() if self.space_charge == "2d": matrix = self.matrix_factory.space_charge_2d( - length=length, cov_matrix=cov_matrix, perveance=envelope.perveance + length=length, + cov_matrix=cov_matrix, + centroid=centroid, + perveance=envelope.perveance_2d ) elif self.space_charge == "3d": matrix = self.matrix_factory.space_charge_3d( - length=length, cov_matrix=cov_matrix, intensity=envelope.intensity + length=length, + cov_matrix=cov_matrix, + centroid=centroid, + perveance=envelope.perveance_3d, ) else: raise ValueError(f"Invalid space charge model: {self.space_charge}") diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index f93e4fda..993552c0 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -106,8 +106,13 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: return matrix def space_charge_2d( - self, length: float, cov_matrix: np.ndarray, perveance: float + self, + length: float, + cov_matrix: np.ndarray, + centroid: np.ndarray, + perveance: float ) -> np.ndarray: + cov_xx = cov_matrix[0, 0] cov_yy = cov_matrix[2, 2] cov_xy = cov_matrix[0, 2] @@ -122,16 +127,27 @@ def space_charge_2d( kappa_x = 2.0 * perveance / (rx * (rx + ry)) kappa_y = 2.0 * perveance / (ry * (rx + ry)) - matrix = np.identity(7) - matrix[1, 0] = kappa_x * length - matrix[3, 2] = kappa_y * length + M = np.identity(7) + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + + T = np.identity(7) + T[0, -1] = centroid[0] + T[2, -1] = centroid[2] R = self.tilt(angle) - M = np.linalg.multi_dot([R, matrix, np.linalg.inv(R)]) - return M + V = np.matmul(T, R) + V_inv = np.linalg.inv(V) + + return np.linalg.multi_dot([V, M, V_inv]) + def space_charge_3d( - self, length: float, cov_matrix: np.ndarray, intensity: float + self, + length: float, + cov_matrix: np.ndarray, + centroid: np.ndarray, + perveance: float, ) -> np.ndarray: raise NotImplementedError() From ae12bda989897880657180ca3678c23d4f742308 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 16:20:17 -0400 Subject: [PATCH 035/183] Fix centroid issue --- examples/Envelope/test_env_fodo.py | 2 +- py/orbit/envelope/envelope.py | 3 +- py/orbit/envelope/matrix.py | 65 +++++++++++++++++------------- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_fodo.py index ef4223d3..08c7ed7b 100644 --- a/examples/Envelope/test_env_fodo.py +++ b/examples/Envelope/test_env_fodo.py @@ -122,7 +122,7 @@ # Tilt if args.dist_tilt: rot_matrix = np.identity(6) - rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(0.25 * math.pi)) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(0.15 * math.pi)) cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) # Mismatch diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 437073fe..ef548868 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -130,8 +130,7 @@ def track(self, envelope: Envelope) -> None: if self.space_charge == "2d": matrix = self.matrix_factory.space_charge_2d( length=length, - cov_matrix=cov_matrix, - centroid=centroid, + beam_matrix=envelope.matrix, perveance=envelope.perveance_2d ) elif self.space_charge == "3d": diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 993552c0..59eee4aa 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -88,14 +88,18 @@ def bend(self, length: float, theta: float, gamma: float) -> np.ndarray: return matrix def tilt(self, angle: float) -> np.ndarray: - cs = math.cos(angle) - sn = math.sin(angle) - matrix = np.identity(7) - matrix[0, 0] = matrix[1, 1] = +cs - matrix[0, 2] = matrix[1, 3] = +sn - matrix[2, 0] = matrix[3, 1] = -sn - matrix[2, 2] = matrix[3, 3] = +cs + matrix[0, 0] = matrix[1, 1] = +math.cos(angle) + matrix[0, 2] = matrix[1, 3] = +math.sin(angle) + matrix[2, 0] = matrix[3, 1] = -math.sin(angle) + matrix[2, 2] = matrix[3, 3] = +math.cos(angle) + return matrix + + def offset(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: + matrix = np.identity(7) + matrix[0, 6] = x + matrix[2, 6] = y + matrix[4, 6] = z return matrix def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: @@ -108,45 +112,48 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: def space_charge_2d( self, length: float, - cov_matrix: np.ndarray, - centroid: np.ndarray, + beam_matrix: np.ndarray, perveance: float ) -> np.ndarray: - cov_xx = cov_matrix[0, 0] - cov_yy = cov_matrix[2, 2] - cov_xy = cov_matrix[0, 2] + # Extract x-y covariance matrix elements. + cov_xx = beam_matrix[0, 0] + cov_yy = beam_matrix[2, 2] + cov_xy = beam_matrix[0, 2] + + # Extract x-y centroid. + mu_x = beam_matrix[0, -1] + mu_y = beam_matrix[2, -1] + + # Calculate normalization matrix (rotation + translation). + angle = -0.5 * np.atan2(2.0 * cov_xy, cov_xx - cov_yy) + + R = self.tilt(angle) + T = self.offset(mu_x, mu_y) - angle = -0.5 * math.atan2(2.0 * cov_xy, cov_xx - cov_yy) - _sin = math.sin(angle) - _cos = math.cos(angle) + V = np.matmul(T, R) + V_inv = np.linalg.inv(V) - rx = 2.0 * np.sqrt(abs(cov_xx * _cos**2 + cov_yy * _sin**2 - 2.0 * cov_xy * _sin * _cos)) - ry = 2.0 * np.sqrt(abs(cov_xx * _sin**2 + cov_yy * _cos**2 + 2.0 * cov_xy * _sin * _cos)) + # Calculate transfer matrix in normalized frame. + beam_matrix_n = np.linalg.multi_dot([V_inv, beam_matrix, V_inv.T]) + + rx = 2.0 * np.sqrt(beam_matrix_n[0, 0]) + ry = 2.0 * np.sqrt(beam_matrix_n[2, 2]) kappa_x = 2.0 * perveance / (rx * (rx + ry)) kappa_y = 2.0 * perveance / (ry * (rx + ry)) - + M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length - - T = np.identity(7) - T[0, -1] = centroid[0] - T[2, -1] = centroid[2] - - R = self.tilt(angle) - - V = np.matmul(T, R) - V_inv = np.linalg.inv(V) + # Transform matrix. return np.linalg.multi_dot([V, M, V_inv]) def space_charge_3d( self, length: float, - cov_matrix: np.ndarray, - centroid: np.ndarray, + beam_matrix: np.ndarray, perveance: float, ) -> np.ndarray: raise NotImplementedError() From 95a45abacbd7ad18c726861847c1bb455fb5d184 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 17:44:20 -0400 Subject: [PATCH 036/183] Use eigenvector decomposition --- py/orbit/envelope/envelope.py | 5 ++-- py/orbit/envelope/matrix.py | 47 ++++++++++++++++------------------- py/orbit/envelope/utils.py | 9 +++++++ 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index ef548868..fca438f4 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -130,13 +130,14 @@ def track(self, envelope: Envelope) -> None: if self.space_charge == "2d": matrix = self.matrix_factory.space_charge_2d( length=length, - beam_matrix=envelope.matrix, + cov_matrix=cov_matrix, + centroid=centroid, perveance=envelope.perveance_2d ) elif self.space_charge == "3d": matrix = self.matrix_factory.space_charge_3d( length=length, - cov_matrix=cov_matrix, + cov_matrix=cov_matrix, centroid=centroid, perveance=envelope.perveance_3d, ) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 59eee4aa..f683be0a 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -16,6 +16,8 @@ from ..teapot import MonitorTEAPOT from ..teapot import TurnCounterTEAPOT +from .utils import proj_cov_matrix + class MatrixFactory: """Factory for 7x7 transfer matrices.""" @@ -95,7 +97,7 @@ def tilt(self, angle: float) -> np.ndarray: matrix[2, 2] = matrix[3, 3] = +math.cos(angle) return matrix - def offset(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: + def translation(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: matrix = np.identity(7) matrix[0, 6] = x matrix[2, 6] = y @@ -112,34 +114,16 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: def space_charge_2d( self, length: float, - beam_matrix: np.ndarray, + cov_matrix: np.ndarray, + centroid: np.ndarray, perveance: float ) -> np.ndarray: - - # Extract x-y covariance matrix elements. - cov_xx = beam_matrix[0, 0] - cov_yy = beam_matrix[2, 2] - cov_xy = beam_matrix[0, 2] - # Extract x-y centroid. - mu_x = beam_matrix[0, -1] - mu_y = beam_matrix[2, -1] - - # Calculate normalization matrix (rotation + translation). - angle = -0.5 * np.atan2(2.0 * cov_xy, cov_xx - cov_yy) - - R = self.tilt(angle) - T = self.offset(mu_x, mu_y) - - V = np.matmul(T, R) - V_inv = np.linalg.inv(V) - - # Calculate transfer matrix in normalized frame. - beam_matrix_n = np.linalg.multi_dot([V_inv, beam_matrix, V_inv.T]) + cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) - rx = 2.0 * np.sqrt(beam_matrix_n[0, 0]) - ry = 2.0 * np.sqrt(beam_matrix_n[2, 2]) + eig_res = np.linalg.eig(cov_matrix_proj) + rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) kappa_x = 2.0 * perveance / (rx * (rx + ry)) kappa_y = 2.0 * perveance / (ry * (rx + ry)) @@ -147,13 +131,24 @@ def space_charge_2d( M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length - # Transform matrix. + A = np.eye(7) + for i in range(eig_res.eigenvectors.shape[0]): + for j in range(eig_res.eigenvectors.shape[1]): + row = i * 2 + col = j * 2 + A[row, col] = A[row + 1, col + 1] = eig_res.eigenvectors[i, j] + + T = self.translation(centroid[0], centroid[2]) + + V = np.matmul(T, A) + V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) def space_charge_3d( self, length: float, - beam_matrix: np.ndarray, + cov_matrix: np.ndarray, + centroid: np.ndarray, perveance: float, ) -> np.ndarray: raise NotImplementedError() diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index e846d83c..a88fcbae 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -37,3 +37,12 @@ def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: L = np.linalg.cholesky(cov_matrix) return np.matmul(X, L.T) + + + +def proj_cov_matrix(cov_matrix: np.ndarray, axis: tuple[int, ...]) -> np.ndarray: + cov_matrix_proj = np.zeros((len(axis), len(axis))) + for i in range(len(axis)): + for j in range(len(axis)): + cov_matrix_proj[i, j] = cov_matrix[axis[i], axis[j]] + return cov_matrix_proj From ff4e8880a9f947369d028fb3b4af10e456e304b3 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 17:55:28 -0400 Subject: [PATCH 037/183] Start 3D envelope --- py/orbit/envelope/matrix.py | 44 +++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index f683be0a..22ee9b05 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -19,6 +19,16 @@ from .utils import proj_cov_matrix +def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: + A = np.eye(7) + for i in range(eigenvectors.shape[0]): + for j in range(eigenvectors.shape[1]): + row = i * 2 + col = j * 2 + A[row, col] = A[row + 1, col + 1] = eigenvectors[i, j] + return A + + class MatrixFactory: """Factory for 7x7 transfer matrices.""" @@ -123,7 +133,8 @@ def space_charge_2d( eig_res = np.linalg.eig(cov_matrix_proj) - rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) + rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) + kappa_x = 2.0 * perveance / (rx * (rx + ry)) kappa_y = 2.0 * perveance / (ry * (rx + ry)) @@ -131,13 +142,7 @@ def space_charge_2d( M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length - A = np.eye(7) - for i in range(eig_res.eigenvectors.shape[0]): - for j in range(eig_res.eigenvectors.shape[1]): - row = i * 2 - col = j * 2 - A[row, col] = A[row + 1, col + 1] = eig_res.eigenvectors[i, j] - + A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) T = self.translation(centroid[0], centroid[2]) V = np.matmul(T, A) @@ -151,7 +156,28 @@ def space_charge_3d( centroid: np.ndarray, perveance: float, ) -> np.ndarray: - raise NotImplementedError() + + cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) + + eig_res = np.linalg.eig(cov_matrix_proj) + + rx, ry, rz = 2.0 * np.sqrt(eig_res.eigenvalues) + + # kappa_x = ... + # kappa_y = ... + # kappa_z = ... + + # M = np.identity(7) + # M[1, 0] = kappa_x * length + # M[3, 2] = kappa_y * length + # M[5, 4] = kappa_z * length + + A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) + T = self.translation(centroid[0], centroid[2], centroid[4]) + + V = np.matmul(T, A) + V_inv = np.linalg.inv(V) + return np.linalg.multi_dot([V, M, V_inv]) def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: From 478ad15ac2a09d19f014235d89cad35c7c24874e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 18:10:13 -0400 Subject: [PATCH 038/183] Move envelope space charge transfer matrix calculations Move to Envelope class --- py/orbit/envelope/envelope.py | 89 +++++++++++++++++++++++++++++------ py/orbit/envelope/matrix.py | 68 -------------------------- 2 files changed, 74 insertions(+), 83 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index fca438f4..2aa07783 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -8,6 +8,7 @@ from .matrix import MatrixFactory from .utils import gen_dist +from .utils import proj_cov_matrix ENTRANCE = AccNode.ENTRANCE @@ -25,6 +26,20 @@ def get_perveance_2d(mass: float, kin_energy: float, line_density: float) -> flo return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) +def get_perveance_3d(): + raise NotImplementedError() + + +def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: + A = np.eye(7) + for i in range(eigenvectors.shape[0]): + for j in range(eigenvectors.shape[1]): + row = i * 2 + col = j * 2 + A[row, col] = A[row + 1, col + 1] = eigenvectors[i, j] + return A + + class Envelope: """Represents beam envelope and centroid. @@ -99,6 +114,63 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles + + def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: + centroid = self.centroid() + + cov_matrix = self.cov() + cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) + + eig_res = np.linalg.eig(cov_matrix_proj) + + rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) + + kappa_x = 2.0 * self.perveance_2d / (rx * (rx + ry)) + kappa_y = 2.0 * self.perveance_2d / (ry * (rx + ry)) + + M = np.identity(7) + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + + A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) + + T = np.identity(7) + T[0, -1] = centroid[0] + T[2, -1] = centroid[2] + + V = np.matmul(T, A) + V_inv = np.linalg.inv(V) + return np.linalg.multi_dot([V, M, V_inv]) + + def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: + centroid = self.centroid() + + cov_matrix = self.cov() + cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) + + eig_res = np.linalg.eig(cov_matrix_proj) + + cov_xx, cov_yy, cov_zz = np.sqrt(eig_res.eigenvalues) + + kappa_x = ... + kappa_y = ... + kappa_z = ... + + M = np.identity(7) + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + M[5, 4] = kappa_z * length + + A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) + + T = np.identity(7) + T[0, -1] = centroid[0] + T[2, -1] = centroid[2] + T[4, -1] = centroid[4] + + V = np.matmul(T, A) + V_inv = np.linalg.inv(V) + return np.linalg.multi_dot([V, M, V_inv]) class EnvelopeTracker: @@ -124,23 +196,10 @@ def track(self, envelope: Envelope) -> None: # Space charge if self.space_charge: length = node.getLength(part_index) - cov_matrix = envelope.cov() - centroid = envelope.centroid() - if self.space_charge == "2d": - matrix = self.matrix_factory.space_charge_2d( - length=length, - cov_matrix=cov_matrix, - centroid=centroid, - perveance=envelope.perveance_2d - ) + matrix = envelope.sc_transfer_matrix_2d(length) elif self.space_charge == "3d": - matrix = self.matrix_factory.space_charge_3d( - length=length, - cov_matrix=cov_matrix, - centroid=centroid, - perveance=envelope.perveance_3d, - ) + matrix = envelope.sc_transfer_matrix_3d(length) else: raise ValueError(f"Invalid space charge model: {self.space_charge}") diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 22ee9b05..b9086bc4 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -19,16 +19,6 @@ from .utils import proj_cov_matrix -def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: - A = np.eye(7) - for i in range(eigenvectors.shape[0]): - for j in range(eigenvectors.shape[1]): - row = i * 2 - col = j * 2 - A[row, col] = A[row + 1, col + 1] = eigenvectors[i, j] - return A - - class MatrixFactory: """Factory for 7x7 transfer matrices.""" @@ -121,64 +111,6 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix[5, 6] = dE return matrix - def space_charge_2d( - self, - length: float, - cov_matrix: np.ndarray, - centroid: np.ndarray, - perveance: float - ) -> np.ndarray: - - cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) - - eig_res = np.linalg.eig(cov_matrix_proj) - - rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) - - kappa_x = 2.0 * perveance / (rx * (rx + ry)) - kappa_y = 2.0 * perveance / (ry * (rx + ry)) - - M = np.identity(7) - M[1, 0] = kappa_x * length - M[3, 2] = kappa_y * length - - A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) - T = self.translation(centroid[0], centroid[2]) - - V = np.matmul(T, A) - V_inv = np.linalg.inv(V) - return np.linalg.multi_dot([V, M, V_inv]) - - def space_charge_3d( - self, - length: float, - cov_matrix: np.ndarray, - centroid: np.ndarray, - perveance: float, - ) -> np.ndarray: - - cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) - - eig_res = np.linalg.eig(cov_matrix_proj) - - rx, ry, rz = 2.0 * np.sqrt(eig_res.eigenvalues) - - # kappa_x = ... - # kappa_y = ... - # kappa_z = ... - - # M = np.identity(7) - # M[1, 0] = kappa_x * length - # M[3, 2] = kappa_y * length - # M[5, 4] = kappa_z * length - - A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) - T = self.translation(centroid[0], centroid[2], centroid[4]) - - V = np.matmul(T, A) - V_inv = np.linalg.inv(V) - return np.linalg.multi_dot([V, M, V_inv]) - def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) From ae864df0238afaac3757749d8d393197f168af3c Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 18:23:12 -0400 Subject: [PATCH 039/183] Add helper functions --- py/orbit/envelope/envelope.py | 109 ++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 2aa07783..47cf1e36 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,10 +1,13 @@ import math import numpy as np +import scipy.special from ..core.bunch import SyncParticle from ..lattice import AccNode from ..lattice import AccLattice +from ..utils.consts import speed_of_light +from ..utils.consts import charge_electron from .matrix import MatrixFactory from .utils import gen_dist @@ -19,15 +22,15 @@ AFTER = AccNode.AFTER -def get_perveance_2d(mass: float, kin_energy: float, line_density: float) -> float: +def calc_perveance_2d(gamma: float, line_density: float) -> float: classical_proton_radius = 1.534697049469832e-18 # [m] - gamma = 1.0 + (kin_energy / mass) # Lorentz factor - beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) # velocity/speed_of_light + beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) -def get_perveance_3d(): - raise NotImplementedError() +def calc_perveance_3d(gamma: float, mass: float, total_charge: float) -> float: + bg2 = gamma**2 - 1.0 + return total_charge * (1.0e-7 * speed_of_light**2) * (1.0 / (gamma * bg2)) * abs(charge_electron) / mass def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: @@ -40,6 +43,54 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A +def build_sc_matrix_2d( + cov_xx: float, + cov_yy: float, + perveance: float, + length: float, +) -> np.ndarray: + """Return 7 x 7 space charge matrix for upright 2D ellipsoid.""" + r_x = 2.0 * math.sqrt(cov_xx) + r_y = 2.0 * math.sqrt(cov_yy) + kappa_x = 2.0 * perveance / (r_x * (r_x + r_y)) + kappa_y = 2.0 * perveance / (r_y * (r_x + r_y)) + + matrix = np.identity(7) + matrix[1, 0] = kappa_x * length + matrix[3, 2] = kappa_y * length + return matrix + + +def build_sc_matrix_3d( + cov_xx: float, + cov_yy: float, + cov_zz: float, + perveance: float, + length: float, + gamma: float, +) -> np.ndarray: + """Return 7 x 7 space charge matrix for upright 3D ellipsoid.""" + + cov_xx = cov_xx + cov_yy = cov_yy + cov_zz = cov_zz * gamma * gamma + + scale_factor = 5.0 ** 1.5 + RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) / scale_factor + RDy = scipy.special.elliprd(cov_zz, cov_xx, cov_yy) / scale_factor + RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) / scale_factor + + kx = gamma * length * perveance * RDx + ky = gamma * length * perveance * RDy + kz = gamma * length * perveance * RDz + + matrix = np.identity(7) + matrix[1, 0] = kx + matrix[3, 2] = ky + matrix[5, 4] = kz + return matrix + + class Envelope: """Represents beam envelope and centroid. @@ -90,11 +141,21 @@ def set_intensity(self, intensity: float) -> None: self.intensity = intensity cov_matrix = self.cov() length = 4.0 * math.sqrt(cov_matrix[4, 4]) # assume uniform density - self.perveance_2d = get_perveance_2d( - mass=self.sync_part.mass(), - kin_energy=self.sync_part.kinEnergy(), + self.perveance_2d = calc_perveance_2d( + gamma=self.gamma(), line_density=(self.intensity / length), ) + self.perveance_3d = calc_perveance_3d( + gamma=self.gamma(), + mass=self.mass(), + total_charge=(self.intensity * charge_electron), + ) + + def gamma(self) -> float: + return self.sync_part.gamma() + + def mass(self) -> float: + return self.sync_part.mass() def centroid(self) -> np.ndarray: return self.matrix[0:6, 6] @@ -123,14 +184,12 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: eig_res = np.linalg.eig(cov_matrix_proj) - rx, ry = 2.0 * np.sqrt(eig_res.eigenvalues) - - kappa_x = 2.0 * self.perveance_2d / (rx * (rx + ry)) - kappa_y = 2.0 * self.perveance_2d / (ry * (rx + ry)) - - M = np.identity(7) - M[1, 0] = kappa_x * length - M[3, 2] = kappa_y * length + M = build_sc_matrix_2d( + cov_xx=eig_res.eigenvalues[0], + cov_yy=eig_res.eigenvalues[1], + perveance=self.perveance_2d, + length=length + ) A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) @@ -150,16 +209,14 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: eig_res = np.linalg.eig(cov_matrix_proj) - cov_xx, cov_yy, cov_zz = np.sqrt(eig_res.eigenvalues) - - kappa_x = ... - kappa_y = ... - kappa_z = ... - - M = np.identity(7) - M[1, 0] = kappa_x * length - M[3, 2] = kappa_y * length - M[5, 4] = kappa_z * length + M = build_sc_matrix_3d( + cov_xx=eig_res.eigenvalues[0], + cov_yy=eig_res.eigenvalues[1], + cov_zz=eig_res.eigenvalues[2], + perveance=self.perveance_3d, + length=length, + gamma=self.gamma(), + ) A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) From 6775b0c120fcc9497429b6aca2f8b22a38c3c01b Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 18:23:38 -0400 Subject: [PATCH 040/183] Set perveance_3d to 0 --- py/orbit/envelope/envelope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 47cf1e36..a5638448 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -134,7 +134,7 @@ def __init__( self.intensity = 0.0 self.perveance_2d = 0.0 - self.perveance_3d = None + self.perveance_3d = 0.0 self.set_intensity(intensity) def set_intensity(self, intensity: float) -> None: From f918721ed3f0f6bf4615113f8c337dce98c9baa3 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 18:57:29 -0400 Subject: [PATCH 041/183] Keep trying to implement 3D space charge --- py/orbit/envelope/envelope.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index a5638448..2ceb3070 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -67,27 +67,23 @@ def build_sc_matrix_3d( cov_zz: float, perveance: float, length: float, - gamma: float, ) -> np.ndarray: """Return 7 x 7 space charge matrix for upright 3D ellipsoid.""" - - cov_xx = cov_xx - cov_yy = cov_yy - cov_zz = cov_zz * gamma * gamma scale_factor = 5.0 ** 1.5 RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) / scale_factor RDy = scipy.special.elliprd(cov_zz, cov_xx, cov_yy) / scale_factor RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) / scale_factor - kx = gamma * length * perveance * RDx - ky = gamma * length * perveance * RDy - kz = gamma * length * perveance * RDz + kappa_x = perveance * RDx + kappa_y = perveance * RDy + kappa_z = perveance * RDz matrix = np.identity(7) - matrix[1, 0] = kx - matrix[3, 2] = ky - matrix[5, 4] = kz + matrix[1, 0] = kappa_x * length + matrix[3, 2] = kappa_y * length + matrix[5, 4] = kappa_z * length + return matrix @@ -178,7 +174,6 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: centroid = self.centroid() - cov_matrix = self.cov() cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) @@ -202,8 +197,9 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - centroid = self.centroid() + # Not working! + centroid = self.centroid() cov_matrix = self.cov() cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) @@ -212,10 +208,9 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M = build_sc_matrix_3d( cov_xx=eig_res.eigenvalues[0], cov_yy=eig_res.eigenvalues[1], - cov_zz=eig_res.eigenvalues[2], + cov_zz=eig_res.eigenvalues[2] / self.gamma()**2, perveance=self.perveance_3d, length=length, - gamma=self.gamma(), ) A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) @@ -225,7 +220,9 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: T[2, -1] = centroid[2] T[4, -1] = centroid[4] - V = np.matmul(T, A) + L = np.identity(7) # to do: lorentz + + V = np.linalg.multi_dot([L, T, A]) V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) From 6b876e78d43991a9028230661213846ebeabe27d Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 18:58:13 -0400 Subject: [PATCH 042/183] Add 3D drift test --- .../{test_env_fodo.py => test_env_2d_fodo.py} | 0 examples/Envelope/test_env_3d_drift.py | 279 ++++++++++++++++++ 2 files changed, 279 insertions(+) rename examples/Envelope/{test_env_fodo.py => test_env_2d_fodo.py} (100%) create mode 100644 examples/Envelope/test_env_3d_drift.py diff --git a/examples/Envelope/test_env_fodo.py b/examples/Envelope/test_env_2d_fodo.py similarity index 100% rename from examples/Envelope/test_env_fodo.py rename to examples/Envelope/test_env_2d_fodo.py diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py new file mode 100644 index 00000000..ea85873d --- /dev/null +++ b/examples/Envelope/test_env_3d_drift.py @@ -0,0 +1,279 @@ +"""Test 3D envelope tracker in drift.""" + +import argparse +import copy +import math +import os +import pathlib + +import numpy as np +import matplotlib.pyplot as plt + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.spacecharge import SpaceChargeCalc3D +from orbit.bunch_utils import collect_bunch +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.lattice import AccLattice +from orbit.lattice import AccNode +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.space_charge.sc3d import setSC3DAccNodes +from orbit.teapot import DriftTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import TEAPOT_Lattice +from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.utils.consts import mass_proton + +from plot import plot_rms_ellipse +from plot import plot_corner +from utils import gen_dist +from utils import build_rotation_matrix_xy +from utils import project_cov_matrix + +plt.style.use("style.mplstyle") + + +# Parse arguments +# ------------------------------------------------------------------------------ + +parser = argparse.ArgumentParser() +parser.add_argument("--kin-energy", type=float, default=0.025) +parser.add_argument("--intensity", type=float, default=4e9) + +parser.add_argument("--xrms", type=float, default=0.010) +parser.add_argument("--yrms", type=float, default=0.010) +parser.add_argument("--zrms", type=float, default=0.010) + +parser.add_argument("--nslice", type=int, default=10) +parser.add_argument("--length", type=float, default=0.1) +parser.add_argument("--turns", type=int, default=20) + +parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--sc", type=int, default=0) +args = parser.parse_args() + + +# Setup +# ------------------------------------------------------------------------------ + +path = pathlib.Path(__file__) +output_dir = os.path.join("outputs", path.stem) +os.makedirs(output_dir, exist_ok=True) + + +# Create lattice +# ------------------------------------------------------------------------------ + +node = DriftTEAPOT(length=args.length) +node.setLength(args.length) +node.setnParts(args.nslice) + +lattice = TEAPOT_Lattice() +lattice.addNode(node) +lattice.initialize() + + +# Create envelope +# ------------------------------------------------------------------------------ + +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(args.kin_energy) + +cov_matrix_init = np.zeros((6, 6)) +cov_matrix_init[0, 0] = 0.010 ** 2 +cov_matrix_init[2, 2] = 0.010 ** 2 +cov_matrix_init[4, 4] = (0.010 / sync_part.gamma()) ** 2 + +centroid_init = np.zeros(6) + +envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, +) + + +# Track envelope +# ------------------------------------------------------------------------------ + +print("TRACK ENVELOPE") + +tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) + +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) + + cov_matrix = envelope.cov() + centroid = envelope.centroid() + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * centroid[0] + yavg = 1000.0 * centroid[2] + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + +histories = {} +histories["envelope"] = copy.deepcopy(history) + + +# Track bunch +# ------------------------------------------------------------------------------ + +print("TRACK BUNCH") + +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, (0, 2, 4)] = gen_dist(args.nparts, cov_matrix=np.eye(3), name="waterbag") +bunch_coords[:, 0] *= args.xrms +bunch_coords[:, 2] *= args.yrms +bunch_coords[:, 4] *= args.zrms / sync_part.gamma() + +for (x, xp, y, yp, z, dE) in bunch_coords: + bunch.addParticle(x, xp, y, yp, z, dE) + +size_global = bunch.getSizeGlobal() +bunch.macroSize(args.intensity / size_global) + +if args.sc: + sc_calc = SpaceChargeCalc3D(64, 64, 64) + sc_min_path_length = 0.01 + sc_nodes = setSC3DAccNodes(lattice, sc_min_path_length, sc_calc) + + +# Track bunch through lattice. +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * twiss_calc.getAverage(0) + yavg = 1000.0 * twiss_calc.getAverage(2) + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + +histories["bunch"] = copy.deepcopy(history) + + +# Analysis +# ------------------------------------------------------------------------------ + +for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + +# Print errors +for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + +# Plot rms bunch sizes +for key in ["xrms", "yrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + +# Plot centroids +for key in ["xavg", "yavg"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel("AVG [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + +# Collect bunch/envelope data on final turn. +particles = collect_bunch(bunch)["coords"] +particles[:, :4] *= 1000.0 + +env_cov_matrix = envelope.cov() +env_cov_matrix[:4, :4] *= 1000.0**2 + +env_centroid = envelope.centroid() +env_centroid[:4] *= 1000.0 + +xmax = 4.0 * np.std(particles, axis=0) +limits = list(zip(-xmax, xmax)) +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + + +# Plot x-x' +fig, ax = plt.subplots(figsize=(4, 4)) +ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) +plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, +) +ax.set_xlabel(labels[0]) +ax.set_ylabel(labels[1]) +plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) +plt.close() + +# Plot corner +fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, +) +for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) +plt.savefig(os.path.join(output_dir, "fig_dist_corner")) +plt.close() From 6def869642833efc6381f71fc4cb392500174f53 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Mon, 27 Apr 2026 19:01:02 -0400 Subject: [PATCH 043/183] Add comment --- py/orbit/envelope/envelope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 2ceb3070..aa6a04b0 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -208,8 +208,8 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M = build_sc_matrix_3d( cov_xx=eig_res.eigenvalues[0], cov_yy=eig_res.eigenvalues[1], - cov_zz=eig_res.eigenvalues[2] / self.gamma()**2, - perveance=self.perveance_3d, + cov_zz=eig_res.eigenvalues[2], # gamma here? but this may not point along z + perveance=self.perveance_3d, length=length, ) From d43cbf3f242f53bc46c20c471b9a56f293956064 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Tue, 28 Apr 2026 08:06:42 -0400 Subject: [PATCH 044/183] Clarify attempt to calculate 3D sc matrix --- py/orbit/envelope/envelope.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index aa6a04b0..c97485b1 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -143,7 +143,7 @@ def set_intensity(self, intensity: float) -> None: ) self.perveance_3d = calc_perveance_3d( gamma=self.gamma(), - mass=self.mass(), + mass=(self.mass() * 1e9), total_charge=(self.intensity * charge_electron), ) @@ -197,10 +197,23 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - # Not working! + # TEMP + # + # Calculate transfer matrix M from covariance matrix S in normalized + # coordinates. In the normalized coordinates, the beam is at rest with + # zero mean and diagonal x-y-z covariance matrix. + # + # The matrix V to get back to lab-frame coordinates is then + # V = LTA, where A is from the x-y-z covariance eigenvectors, T is a + # translation to the x-y-z centroid, and L is the Lorentz boost that + # scales z by 1 / gamma. centroid = self.centroid() + centroid[4] *= self.gamma() + cov_matrix = self.cov() + cov_matrix[4, 4] *= self.gamma()**2 + cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) eig_res = np.linalg.eig(cov_matrix_proj) @@ -208,7 +221,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M = build_sc_matrix_3d( cov_xx=eig_res.eigenvalues[0], cov_yy=eig_res.eigenvalues[1], - cov_zz=eig_res.eigenvalues[2], # gamma here? but this may not point along z + cov_zz=eig_res.eigenvalues[2], perveance=self.perveance_3d, length=length, ) @@ -216,11 +229,11 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) T = np.identity(7) - T[0, -1] = centroid[0] - T[2, -1] = centroid[2] - T[4, -1] = centroid[4] + for i in (0, 2, 4): + T[i, -1] = centroid[i] - L = np.identity(7) # to do: lorentz + L = np.identity(7) + L[4, 4] = 1.0 / self.gamma() V = np.linalg.multi_dot([L, T, A]) V_inv = np.linalg.inv(V) From b75520bb8be09cd70d02f531f099017a6e424144 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 30 Apr 2026 10:30:07 -0400 Subject: [PATCH 045/183] Add comment --- py/orbit/envelope/envelope.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index c97485b1..7a2d2e67 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -83,6 +83,9 @@ def build_sc_matrix_3d( matrix[1, 0] = kappa_x * length matrix[3, 2] = kappa_y * length matrix[5, 4] = kappa_z * length + + # PyORBIT uses dE, not z', so need to calculate energy + # kick from q * E_z * length. return matrix From db2212aaecf3a011b67a9a3676b9513c5d718e68 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 13:48:24 -0400 Subject: [PATCH 046/183] Correct formula for 3D space charge matrix --- examples/Envelope/test_env_2d_fodo.py | 31 ++--- examples/Envelope/test_env_3d_drift.py | 66 +++------- py/orbit/envelope/envelope.py | 167 ++++++++++--------------- 3 files changed, 99 insertions(+), 165 deletions(-) diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 08c7ed7b..296d83fe 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -42,14 +42,12 @@ parser.add_argument("--kin-energy", type=float, default=0.025) parser.add_argument("--intensity", type=float, default=5e10) -parser.add_argument( - "--dist-name", type=str, default="kv", choices=["kv", "waterbag", "gauss"] -) -parser.add_argument("--dist-mismatch-x", type=float, default=0.0) -parser.add_argument("--dist-mismatch-y", type=float, default=0.0) -parser.add_argument("--dist-offset-x", type=float, default=0.0) -parser.add_argument("--dist-offset-y", type=float, default=0.0) -parser.add_argument("--dist-tilt", action="store_true") +parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) +parser.add_argument("--mismatch-x", type=float, default=0.0) +parser.add_argument("--mismatch-y", type=float, default=0.0) +parser.add_argument("--offset-x", type=float, default=0.0) +parser.add_argument("--offset-y", type=float, default=0.0) +parser.add_argument("--tilt", action="store_true") parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--kq", type=float, default=0.25) @@ -120,20 +118,20 @@ cov_matrix[5, 5] = 0.0 # Tilt -if args.dist_tilt: +if args.tilt: rot_matrix = np.identity(6) rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(0.15 * math.pi)) cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) # Mismatch -cov_matrix[0, 0] *= (1.0 + args.dist_mismatch_x) ** 2 -cov_matrix[2, 2] *= (1.0 + args.dist_mismatch_y) ** 2 +cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 +cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 cov_matrix_init = np.copy(cov_matrix) # Offset centroid_init = np.zeros(6) -centroid_init[0] += args.dist_offset_x -centroid_init[2] += args.dist_offset_y +centroid_init[0] += args.offset_x +centroid_init[2] += args.offset_y # Create envelope envelope = Envelope( @@ -182,19 +180,16 @@ print("TRACK BUNCH") -# Generate particles from KV distribution + uniform longitudinal distribution. rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist(args.nparts, cov_matrix_init[0:4, 0:4], args.dist_name) +bunch_coords[:, :4] = gen_dist(n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist) bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) bunch_coords += centroid_init[None, :6] for i in range(bunch_coords.shape[0]): bunch.addParticle(*bunch_coords[i]) - -# Add space charge nodes to lattice. if args.sc: sc_calc = SpaceChargeCalc2p5D(128, 128, 1) sc_path_length_min = 1.00e-06 @@ -203,8 +198,6 @@ bunch_size = bunch.getSizeGlobal() bunch.macroSize(args.intensity / bunch_size) - -# Track bunch through lattice. history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): if turn > 0: diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index ea85873d..80adaef5 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -39,7 +39,7 @@ parser = argparse.ArgumentParser() parser.add_argument("--kin-energy", type=float, default=0.025) -parser.add_argument("--intensity", type=float, default=4e9) +parser.add_argument("--intensity", type=float, default=1e11) parser.add_argument("--xrms", type=float, default=0.010) parser.add_argument("--yrms", type=float, default=0.010) @@ -48,8 +48,9 @@ parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--length", type=float, default=0.1) parser.add_argument("--turns", type=int, default=20) +parser.add_argument("--sc-grid", type=int, default=128) -parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--nparts", type=int, default=500_000) parser.add_argument("--sc", type=int, default=0) args = parser.parse_args() @@ -83,9 +84,9 @@ sync_part.kinEnergy(args.kin_energy) cov_matrix_init = np.zeros((6, 6)) -cov_matrix_init[0, 0] = 0.010 ** 2 -cov_matrix_init[2, 2] = 0.010 ** 2 -cov_matrix_init[4, 4] = (0.010 / sync_part.gamma()) ** 2 +cov_matrix_init[0, 0] = args.xrms ** 2 +cov_matrix_init[2, 2] = args.yrms ** 2 +cov_matrix_init[4, 4] = (args.zrms / sync_part.gamma()) ** 2 centroid_init = np.zeros(6) @@ -96,7 +97,6 @@ intensity=args.intensity, ) - # Track envelope # ------------------------------------------------------------------------------ @@ -104,7 +104,7 @@ tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +history = {"xrms": [], "yrms": [], "zrms": []} for turn in range(args.turns): if turn > 0: tracker.track(envelope) @@ -114,17 +114,13 @@ xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * centroid[0] - yavg = 1000.0 * centroid[2] - - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() history["xrms"].append(xrms) history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") histories = {} histories["envelope"] = copy.deepcopy(history) @@ -148,13 +144,11 @@ bunch.macroSize(args.intensity / size_global) if args.sc: - sc_calc = SpaceChargeCalc3D(64, 64, 64) - sc_min_path_length = 0.01 - sc_nodes = setSC3DAccNodes(lattice, sc_min_path_length, sc_calc) + sc_calc = SpaceChargeCalc3D(args.sc_grid, args.sc_grid, args.sc_grid) + sc_path_length_min = 0.01 + sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) - -# Track bunch through lattice. -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +history = {"xrms": [], "yrms": [], "zrms": []} for turn in range(args.turns): if turn > 0: lattice.trackBunch(bunch) @@ -170,17 +164,13 @@ xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * twiss_calc.getAverage(0) - yavg = 1000.0 * twiss_calc.getAverage(2) - - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * bunch.getSyncParticle().gamma() history["xrms"].append(xrms) history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") histories["bunch"] = copy.deepcopy(history) @@ -200,7 +190,7 @@ print("avg_abs_delta:", np.mean(np.abs(deltas))) # Plot rms bunch sizes -for key in ["xrms", "yrms"]: +for key in ["xrms", "yrms", "zrms"]: fig, ax = plt.subplots(figsize=(5, 3)) for i, model in enumerate(["envelope", "bunch"]): color = ["black", "red"][i] @@ -213,21 +203,6 @@ plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() -# Plot centroids -for key in ["xavg", "yavg"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(-5.0, 5.0) - ax.set_xlabel("Turn") - ax.set_ylabel("AVG [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close() - - # Collect bunch/envelope data on final turn. particles = collect_bunch(bunch)["coords"] particles[:, :4] *= 1000.0 @@ -242,7 +217,6 @@ limits = list(zip(-xmax, xmax)) labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] - # Plot x-x' fig, ax = plt.subplots(figsize=(4, 4)) ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 7a2d2e67..e0cfab6f 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -21,16 +21,7 @@ BEFORE = AccNode.BEFORE AFTER = AccNode.AFTER - -def calc_perveance_2d(gamma: float, line_density: float) -> float: - classical_proton_radius = 1.534697049469832e-18 # [m] - beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) - return (2.0 * classical_proton_radius * line_density) / (beta**2 * gamma**3) - - -def calc_perveance_3d(gamma: float, mass: float, total_charge: float) -> float: - bg2 = gamma**2 - 1.0 - return total_charge * (1.0e-7 * speed_of_light**2) * (1.0 / (gamma * bg2)) * abs(charge_electron) / mass +CLASSICAL_PROTON_RADIUS = 1.534697049469832e-18 # [m] def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: @@ -43,53 +34,6 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A -def build_sc_matrix_2d( - cov_xx: float, - cov_yy: float, - perveance: float, - length: float, -) -> np.ndarray: - """Return 7 x 7 space charge matrix for upright 2D ellipsoid.""" - r_x = 2.0 * math.sqrt(cov_xx) - r_y = 2.0 * math.sqrt(cov_yy) - kappa_x = 2.0 * perveance / (r_x * (r_x + r_y)) - kappa_y = 2.0 * perveance / (r_y * (r_x + r_y)) - - matrix = np.identity(7) - matrix[1, 0] = kappa_x * length - matrix[3, 2] = kappa_y * length - return matrix - - -def build_sc_matrix_3d( - cov_xx: float, - cov_yy: float, - cov_zz: float, - perveance: float, - length: float, -) -> np.ndarray: - """Return 7 x 7 space charge matrix for upright 3D ellipsoid.""" - - scale_factor = 5.0 ** 1.5 - RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) / scale_factor - RDy = scipy.special.elliprd(cov_zz, cov_xx, cov_yy) / scale_factor - RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) / scale_factor - - kappa_x = perveance * RDx - kappa_y = perveance * RDy - kappa_z = perveance * RDz - - matrix = np.identity(7) - matrix[1, 0] = kappa_x * length - matrix[3, 2] = kappa_y * length - matrix[5, 4] = kappa_z * length - - # PyORBIT uses dE, not z', so need to calculate energy - # kick from q * E_z * length. - - return matrix - - class Envelope: """Represents beam envelope and centroid. @@ -137,33 +81,31 @@ def __init__( self.set_intensity(intensity) def set_intensity(self, intensity: float) -> None: + factor = 2.0 * CLASSICAL_PROTON_RADIUS / (self.beta()**2 * self.gamma()**3) + bunch_length = 4.0 * self.rms(axis=4) + self.intensity = intensity - cov_matrix = self.cov() - length = 4.0 * math.sqrt(cov_matrix[4, 4]) # assume uniform density - self.perveance_2d = calc_perveance_2d( - gamma=self.gamma(), - line_density=(self.intensity / length), - ) - self.perveance_3d = calc_perveance_3d( - gamma=self.gamma(), - mass=(self.mass() * 1e9), - total_charge=(self.intensity * charge_electron), - ) + self.perveance_3d = factor * intensity + self.perveance_2d = factor * intensity / bunch_length def gamma(self) -> float: return self.sync_part.gamma() + + def beta(self) -> float: + return self.sync_part.beta() def mass(self) -> float: return self.sync_part.mass() def centroid(self) -> np.ndarray: - return self.matrix[0:6, 6] + return np.copy(self.matrix[0:6, 6]) def cov(self) -> np.ndarray: - return self.matrix[0:6, 0:6] + return np.copy(self.matrix[0:6, 0:6]) - def rms(self) -> np.ndarray: - return np.sqrt(np.diag(self.cov())) + def rms(self, axis: int = None) -> float | np.ndarray: + rms_arr = np.sqrt(np.diag(self.cov())) + return rms_arr[axis] def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) @@ -180,16 +122,21 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: cov_matrix = self.cov() cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) - eig_res = np.linalg.eig(cov_matrix_proj) + cov_eig_res = np.linalg.eig(cov_matrix_proj) + cov_eig_vals = cov_eig_res.eigenvalues + cov_eig_vecs = cov_eig_res.eigenvectors - M = build_sc_matrix_2d( - cov_xx=eig_res.eigenvalues[0], - cov_yy=eig_res.eigenvalues[1], - perveance=self.perveance_2d, - length=length - ) + rx = 2.0 * math.sqrt(cov_eig_vals[0]) + ry = 2.0 * math.sqrt(cov_eig_vals[1]) - A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) + kappa_x = 2.0 * self.perveance_2d / (rx * (rx + ry)) + kappa_y = 2.0 * self.perveance_2d / (ry * (rx + ry)) + + M = np.identity(7) + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + + A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) T = np.identity(7) T[0, -1] = centroid[0] @@ -200,37 +147,54 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - # TEMP - # - # Calculate transfer matrix M from covariance matrix S in normalized - # coordinates. In the normalized coordinates, the beam is at rest with - # zero mean and diagonal x-y-z covariance matrix. - # - # The matrix V to get back to lab-frame coordinates is then - # V = LTA, where A is from the x-y-z covariance eigenvectors, T is a - # translation to the x-y-z centroid, and L is the Lorentz boost that - # scales z by 1 / gamma. - + # Get centroid in rest frame. centroid = self.centroid() centroid[4] *= self.gamma() + # Get covariance matrix in rest frame. cov_matrix = self.cov() cov_matrix[4, 4] *= self.gamma()**2 + # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) - + + # Calculate eigenvectors of x-y-z covariance matrix. eig_res = np.linalg.eig(cov_matrix_proj) - M = build_sc_matrix_3d( - cov_xx=eig_res.eigenvalues[0], - cov_yy=eig_res.eigenvalues[1], - cov_zz=eig_res.eigenvalues[2], - perveance=self.perveance_3d, - length=length, - ) + # Extract diagonal x-y-z covariance matrix elements in diagonalized frame. + cov_xx = eig_res.eigenvalues[0] + cov_yy = eig_res.eigenvalues[1] + cov_zz = eig_res.eigenvalues[2] + + # Compute linear matrix elements k_{x, y, z}. + # x' -> kappa_x * ds * x' + # y' -> kappa_y * ds * y' + # z' -> kappa_z * ds * z' + factor_x = 0.5 * self.perveance_3d / ((5.0 * cov_xx) ** 1.5) + factor_y = 0.5 * self.perveance_3d / ((5.0 * cov_yy) ** 1.5) + factor_z = 0.5 * self.perveance_3d / ((5.0 * cov_zz) ** 1.5) + + RDx = scipy.special.elliprd((cov_yy / cov_xx), (cov_zz / cov_xx), 1.0) + RDy = scipy.special.elliprd((cov_xx / cov_yy), (cov_zz / cov_yy), 1.0) + RDz = scipy.special.elliprd((cov_xx / cov_zz), (cov_yy / cov_zz), 1.0) + + kappa_x = factor_x * RDx # [1 / m] + kappa_y = factor_y * RDy # [1 / m] + kappa_z = factor_z * RDz # [1 / m] + + # Switch from z' to dE [GeV] + # z' = (dp / p) / gamma^2 + # z' = (dE / E) / (gamma^2 * beta^2) + # z' = dE / (gamma^3 * beta^2 * m & c^2) + kappa_z *= self.gamma()**3 * self.beta()**2 * self.mass() # [GeV / m] + + M = np.identity(7) + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + M[5, 4] = kappa_z * length A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) - + T = np.identity(7) for i in (0, 2, 4): T[i, -1] = centroid[i] @@ -273,6 +237,9 @@ def track(self, envelope: Envelope) -> None: else: raise ValueError(f"Invalid space charge model: {self.space_charge}") + # print("debug space charge matrix") + # print(matrix) + envelope.apply_transfer_matrix(matrix) # Main node part From 68f5d63e3cdf8e213dc51a81055adc4ee18e0ef1 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 17:10:48 -0400 Subject: [PATCH 047/183] Update matrices --- examples/Envelope/test_env_2d_fodo.py | 4 +- py/orbit/envelope/envelope.py | 49 +-- py/orbit/envelope/matrix.py | 49 ++- .../.ipynb_checkpoints/sc1DNode-checkpoint.py | 304 ++++++++++++++++++ 4 files changed, 356 insertions(+), 50 deletions(-) create mode 100644 py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 296d83fe..379d426b 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -47,7 +47,7 @@ parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) parser.add_argument("--offset-y", type=float, default=0.0) -parser.add_argument("--tilt", action="store_true") +parser.add_argument("--tilt", type=float, default=0) parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--kq", type=float, default=0.25) @@ -120,7 +120,7 @@ # Tilt if args.tilt: rot_matrix = np.identity(6) - rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(0.15 * math.pi)) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) # Mismatch diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e0cfab6f..9465c371 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -81,12 +81,8 @@ def __init__( self.set_intensity(intensity) def set_intensity(self, intensity: float) -> None: - factor = 2.0 * CLASSICAL_PROTON_RADIUS / (self.beta()**2 * self.gamma()**3) - bunch_length = 4.0 * self.rms(axis=4) - self.intensity = intensity - self.perveance_3d = factor * intensity - self.perveance_2d = factor * intensity / bunch_length + self.perveance = 2.0 * intensity * CLASSICAL_PROTON_RADIUS / (self.beta()**2 * self.gamma()**3) def gamma(self) -> float: return self.sync_part.gamma() @@ -129,8 +125,11 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: rx = 2.0 * math.sqrt(cov_eig_vals[0]) ry = 2.0 * math.sqrt(cov_eig_vals[1]) - kappa_x = 2.0 * self.perveance_2d / (rx * (rx + ry)) - kappa_y = 2.0 * self.perveance_2d / (ry * (rx + ry)) + bunch_length = 4.0 * self.rms(axis=4) + perveance = self.perveance / bunch_length + + kappa_x = 2.0 * perveance / (rx * (rx + ry)) + kappa_y = 2.0 * perveance / (ry * (rx + ry)) M = np.identity(7) M[1, 0] = kappa_x * length @@ -147,47 +146,33 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - # Get centroid in rest frame. centroid = self.centroid() centroid[4] *= self.gamma() - # Get covariance matrix in rest frame. cov_matrix = self.cov() cov_matrix[4, 4] *= self.gamma()**2 - - # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) - # Calculate eigenvectors of x-y-z covariance matrix. eig_res = np.linalg.eig(cov_matrix_proj) - # Extract diagonal x-y-z covariance matrix elements in diagonalized frame. cov_xx = eig_res.eigenvalues[0] cov_yy = eig_res.eigenvalues[1] cov_zz = eig_res.eigenvalues[2] # Compute linear matrix elements k_{x, y, z}. - # x' -> kappa_x * ds * x' - # y' -> kappa_y * ds * y' - # z' -> kappa_z * ds * z' - factor_x = 0.5 * self.perveance_3d / ((5.0 * cov_xx) ** 1.5) - factor_y = 0.5 * self.perveance_3d / ((5.0 * cov_yy) ** 1.5) - factor_z = 0.5 * self.perveance_3d / ((5.0 * cov_zz) ** 1.5) - - RDx = scipy.special.elliprd((cov_yy / cov_xx), (cov_zz / cov_xx), 1.0) - RDy = scipy.special.elliprd((cov_xx / cov_yy), (cov_zz / cov_yy), 1.0) - RDz = scipy.special.elliprd((cov_xx / cov_zz), (cov_yy / cov_zz), 1.0) - - kappa_x = factor_x * RDx # [1 / m] - kappa_y = factor_y * RDy # [1 / m] - kappa_z = factor_z * RDz # [1 / m] - - # Switch from z' to dE [GeV] - # z' = (dp / p) / gamma^2 - # z' = (dE / E) / (gamma^2 * beta^2) - # z' = dE / (gamma^3 * beta^2 * m & c^2) + RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) + RDy = scipy.special.elliprd(cov_xx, cov_zz, cov_yy) + RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) + + factor = 0.5 * self.perveance * ((1.0 / 5.0) ** 1.5) + kappa_x = factor * RDx # [1 / m] + kappa_y = factor * RDy # [1 / m] + kappa_z = factor * RDz # [1 / m] kappa_z *= self.gamma()**3 * self.beta()**2 * self.mass() # [GeV / m] + # TEMP + # kappa_z *= 19.84 + M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index b9086bc4..88a81815 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -17,11 +17,14 @@ from ..teapot import TurnCounterTEAPOT from .utils import proj_cov_matrix +from ..utils import speed_of_light class MatrixFactory: - """Factory for 7x7 transfer matrices.""" + """Factory for 7 x 7 transfer matrices. + Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] + """ def __init__(self) -> None: self.ignore_node_types = [ ApertureTEAPOT, @@ -31,14 +34,18 @@ def __init__(self) -> None: TurnCounterTEAPOT, ] - def drift(self, length: float, gamma: float) -> np.ndarray: + def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: + dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) + matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length - matrix[4, 5] = length / gamma**2 + matrix[4, 5] = length / sync_part.gamma()**2 + matrix[4, 5] *= dp_p_coef return matrix - def quad(self, length: float, kq: float) -> np.ndarray: + def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) sqrt_abs_kq = math.sqrt(abs(kq)) matrix = np.identity(7) @@ -68,25 +75,38 @@ def quad(self, length: float, kq: float) -> np.ndarray: matrix[2, 3] = +sy / sqrt_abs_kq matrix[3, 2] = -sy * sqrt_abs_kq matrix[3, 3] = cy + + matrix[4, 5] = length / sync_part.gamma()**2 + matrix[4, 5] *= dp_p_coef return matrix - def bend(self, length: float, theta: float, gamma: float) -> np.ndarray: - rho = length / theta + def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: + if length <= 0: + return np.identity(7) + + v = speed_of_light * sync_part.beta() + sync_part.setTime(sync_part.getTime() + length / v) + + dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) + rho = length / theta cx = math.cos(theta) sx = math.sin(theta) matrix = np.identity(7) matrix[0, 0] = cx - matrix[0, 1] = sx * rho - matrix[0, 5] = (1.0 - cx) * rho + matrix[0, 1] = rho * sx + matrix[0, 5] = rho * (1.0 - cx) matrix[1, 0] = -sx / rho matrix[1, 1] = cx matrix[1, 5] = sx + matrix[2, 3] = length + matrix[4, 0] = -sx - matrix[4, 1] = -(1.0 - cx) * rho - matrix[4, 5] = (length / gamma**2) - rho * (theta - sx) + matrix[4, 1] = -rho * (1.0 - cx) + matrix[4, 5] = -length * sync_part.beta()**2 + rho * sx + matrix[4, 5] *= dp_p_coef return matrix def tilt(self, angle: float) -> np.ndarray: @@ -114,11 +134,8 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) - gamma = sync_part.gamma() - return self.drift(length=length, gamma=gamma) - + return self.drift(length=length, sync_part=sync_part) elif type(node) is QuadTEAPOT: - nparts = node.getnParts() length = node.getLength(part_index) scale = 1.0 @@ -126,14 +143,14 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) scale = node.waveform.getStrength() kq = scale * node.getParam("kq") - return self.quad(length=length, kq=kq) + return self.quad(length=length, kq=kq, sync_part=sync_part) elif type(node) is BendTEAPOT: nparts = node.getnParts() length = node.getLength(part_index) theta = node.getParam("theta") / (nparts - 1) gamma = sync_part.gamma() - return self.bend(length=length, theta=theta, gamma=gamma) + return self.bend(length=length, theta=theta, sync_part=sync_part) elif type(node) is KickTEAPOT: nparts = node.getnParts() diff --git a/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py b/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py new file mode 100644 index 00000000..0bf968e1 --- /dev/null +++ b/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py @@ -0,0 +1,304 @@ +""" +Module. Includes classes for 1D longidutinal space charge accelerator nodes. +""" + +import sys +import os +import math + +from orbit.core.bunch import Bunch +from orbit.core.spacecharge import LSpaceChargeCalc +from orbit.lattice import AccLattice +from orbit.lattice import AccNode +from orbit.lattice import AccActionsContainer +from orbit.lattice import AccNodeBunchTracker +from orbit.utils import consts +from orbit.utils import orbitFinalize +from orbit.teapot import DriftTEAPOT + + +class SC1D_AccNode(DriftTEAPOT): + """Longitudinal space charge node.""" + + def __init__( + self, + b_a: float, + phase_length: float, + nmacros_min: float, + use_sc: float, + nbins: float, + nmodes: int = None, + use_grad: bool = False, + name="long sc node", + ) -> None: + """ + Constructor. Creates the SC1D-teapot element. + """ + DriftTEAPOT.__init__(self, name) + self.lspacecharge = LSpaceChargeCalc(b_a, phase_length, nmacros_min, use_sc, nbins) + self.setNumModes(nmodes) + # self.setUseGrad(use_grad) + self.setType("long sc node") + self.setLength(0.0) + + def setUseGrad(self, use_grad: bool) -> None: + """Sets whether to use gradient-based solver instead of impedance solver.""" + self.lspacecharge.setUseGrad(int(use_grad)) + + def setNumModes(self, n: int) -> None: + """Sets number of FFT modes used to calculate energy kick.""" + self.lspacecharge.setNumModes(n) + + def trackBunch(self, bunch: Bunch) -> None: + """ + The SC1D-teapot class implementation of the + AccNodeBunchTracker class trackBunch(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + self.lspacecharge.trackBunch(bunch) # track method goes here + + def track(self, params_dict: dict) -> None: + """ + The SC1D-teapot class implementation of the + AccNodeBunchTracker class track(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + bunch = params_dict["bunch"] + self.lspacecharge.trackBunch(bunch) + + def assignImpedance(self, py_cmplx_arr: list[float]) -> None: + self.lspacecharge.assignImpedance(py_cmplx_arr) + + +class FreqDep_SC1D_AccNode(DriftTEAPOT): + """Longitudinal space charge node (frequency-dependent).""" + + def __init__( + self, + b_a: float, + phase_length: float, + nmacros_min: int, + use_sc: int, + nbins: int, + bunch: Bunch, + imp_dict: dict, + name: str = "freq. dep. long sc node", + ) -> None: + """ + Constructor. Creates the FreqDep_SC1D-teapot element. + """ + DriftTEAPOT.__init__(self, name) + self.lspacecharge = LSpaceChargeCalc( + b_a, phase_length, nmacros_min, use_sc, nbins + ) + self.setType("freq. dep. long sc node") + self.setLength(0.0) + self.phase_length = phase_length + self.nbins = nbins + self.localDict = imp_dict + self.freq_tuple = self.localDict["freqs"] + self.freq_range = len(self.freq_tuple) - 1 + self.z_tuple = self.localDict["z_imp"] + self.c = consts.speed_of_light + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins // 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + + def trackBunch(self, bunch: Bunch) -> None: + """ + The FreqDep_SC1D-teapot class implementation of + the AccNodeBunchTracker class track(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins // 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + self.lspacecharge.trackBunch(bunch) + + def track(self, params_dict: dict) -> None: + """ + The FreqDep_SC1D-teapot class implementation of + the AccNodeBunchTracker class track(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + bunch = params_dict["bunch"] + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins // 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + self.lspacecharge.trackBunch(bunch) + + +class BetFreqDep_SC1D_AccNode(DriftTEAPOT): + """Longitudinal space charge node (frequency- and velocity-dependent).""" + + def __init__( + self, + b_a: float, + phase_length: float, + nmacros_min: float, + use_sc: int, + nbins: int, + bunch: Bunch, + imp_dict: dict, + name: str = "freq. dep. long sc node", + ) -> None: + """ + Constructor. Creates the BetFreqDep_SC1D-teapot element. + """ + DriftTEAPOT.__init__(self, name) + self.lspacecharge = LSpaceChargeCalc( + b_a, phase_length, nmacros_min, use_sc, nbins + ) + self.setType("beta-freq. dep. long sc node") + self.setLength(0.0) + self.phase_length = phase_length + self.nbins = nbins + self.localDict = imp_dict + self.bet_tuple = self.localDict["betas"] + self.bet_range = len(self.bet_tuple) - 1 + self.freq_tuple = self.localDict["freqs"] + self.freq_range = len(self.freq_tuple) - 1 + self.z_bf = self.localDict["z_imp"] + self.c = consts.speed_of_light + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins / 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = bilinterp( + BetaRel, + freq_mode, + self.bet_range, + self.freq_range, + self.bet_tuple, + self.freq_tuple, + self.z_bf, + ) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + + def trackBunch(self, bunch: Bunch) -> None: + """ + The BetFreqDep_SC1D-teapot class implementation of + the AccNodeBunchTracker class track(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins / 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = bilinterp( + BetaRel, + freq_mode, + self.bet_range, + self.freq_range, + self.bet_tuple, + self.freq_tuple, + self.z_bf, + ) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + self.lspacecharge.trackBunch(bunch) + + def track(self, params_dict: dict) -> None: + """ + The BetFreqDep_SC1D-teapot class implementation of + the AccNodeBunchTracker class track(probe) method. + """ + length = self.getLength(self.getActivePartIndex()) + bunch = params_dict["bunch"] + BetaRel = bunch.getSyncParticle().beta() + Freq0 = (BetaRel * self.c) / self.phase_length + Z = [] + for n in range(self.nbins / 2 - 1): + freq_mode = Freq0 * (n + 1) + z_mode = bilinterp( + BetaRel, + freq_mode, + self.bet_range, + self.freq_range, + self.bet_tuple, + self.freq_tuple, + self.z_bf, + ) + Z.append(z_mode) + self.lspacecharge.assignImpedance(Z) + self.lspacecharge.trackBunch(bunch) + + +def interp(x: float, n_tuple: int, x_tuple: list[float], y_tuple: list[float]) -> float: + """ + Linear interpolation: Given n-tuple + 1 points, + x_tuple and y_tuple, routine finds y = y_tuple + at x in x_tuple. Assumes x_tuple is increasing array. + """ + if x < x_tuple[0]: + y = y_tuple[0] + return y + if x > x_tuple[n_tuple]: + y = y_tuple[n_tuple] + return y + dxp = x - x_tuple[0] + for n in range(n_tuple): + dxm = dxp + dxp = x - x_tuple[n + 1] + dxmp = dxm * dxp + if dxmp <= 0: + break + y = (-dxp * y_tuple[n] + dxm * y_tuple[n + 1]) / (dxm - dxp) + return y + + +def bilinterp( + x: float, + y: float, + nx_tuple: int, + ny_tuple: int, + x_tuple: list[float], + y_tuple: list[float], + fxy: list[list[float]], +) -> float: + """ + Bilinear interpolation: Given nx-tuple + 1 x-points, + ny-tuple + 1 y-points, x_tuple and y_tuple, + routine finds f(x, y) = fxy at (x, y) in (x_tuple, y_tuple). + Assumes x_tuple and y_tuple are increasing arrays. + """ + f_tuple = [] + if x < x_tuple[0]: + for ny in range(ny_tuple + 1): + vf = fxy[0][ny] + f_tuple.append(vf) + elif x > x_tuple[nx_tuple]: + for ny in range(ny_tuple + 1): + vf = fxy[x_tuple][ny] + f_tuple.append(vf) + else: + dxp = x - x_tuple[0] + for nx in range(nx_tuple): + dxm = dxp + dxp = x - x_tuple[nx + 1] + dxmp = dxm * dxp + if dxmp <= 0: + break + for ny in range(ny_tuple + 1): + vf = (-dxp * fxy[nx][ny] + dxm * fxy[nx + 1][ny]) / (dxm - dxp) + f_tuple.append(vf) + f = interp(y, ny_tuple, y_tuple, f_tuple) + return f From 9f268e22b036624052d470199a5c255af0a8015a Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 17:17:58 -0400 Subject: [PATCH 048/183] Fix plotting --- examples/Envelope/test_env_3d_drift.py | 22 ++++++++-------------- py/orbit/envelope/envelope.py | 4 ---- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 80adaef5..0a2eadb8 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -15,20 +15,14 @@ from orbit.bunch_utils import collect_bunch from orbit.envelope import Envelope from orbit.envelope import EnvelopeTracker -from orbit.lattice import AccLattice -from orbit.lattice import AccNode -from orbit.core.spacecharge import SpaceChargeCalc2p5D from orbit.space_charge.sc3d import setSC3DAccNodes from orbit.teapot import DriftTEAPOT -from orbit.teapot import QuadTEAPOT from orbit.teapot import TEAPOT_Lattice -from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.utils.consts import mass_proton from plot import plot_rms_ellipse from plot import plot_corner from utils import gen_dist -from utils import build_rotation_matrix_xy from utils import project_cov_matrix plt.style.use("style.mplstyle") @@ -48,9 +42,9 @@ parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--length", type=float, default=0.1) parser.add_argument("--turns", type=int, default=20) -parser.add_argument("--sc-grid", type=int, default=128) +parser.add_argument("--sc-grid", type=int, default=64) -parser.add_argument("--nparts", type=int, default=500_000) +parser.add_argument("--nparts", type=int, default=100_000) parser.add_argument("--sc", type=int, default=0) args = parser.parse_args() @@ -196,26 +190,26 @@ color = ["black", "red"][i] lw = [None, 0][i] ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_ylim(0.0, ax.get_ylim()[1]) ax.set_xlabel("Turn") ax.set_ylabel("RMS [mm]") - ax.legend(loc="upper right") + ax.legend(loc="upper left") plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() # Collect bunch/envelope data on final turn. particles = collect_bunch(bunch)["coords"] -particles[:, :4] *= 1000.0 +particles *= 1e3 env_cov_matrix = envelope.cov() -env_cov_matrix[:4, :4] *= 1000.0**2 +env_cov_matrix *= 1e6 env_centroid = envelope.centroid() -env_centroid[:4] *= 1000.0 +env_centroid *= 1e3 xmax = 4.0 * np.std(particles, axis=0) limits = list(zip(-xmax, xmax)) -labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] # Plot x-x' fig, ax = plt.subplots(figsize=(4, 4)) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 9465c371..73b146c7 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -159,7 +159,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: cov_yy = eig_res.eigenvalues[1] cov_zz = eig_res.eigenvalues[2] - # Compute linear matrix elements k_{x, y, z}. RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) RDy = scipy.special.elliprd(cov_xx, cov_zz, cov_yy) RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) @@ -170,9 +169,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: kappa_z = factor * RDz # [1 / m] kappa_z *= self.gamma()**3 * self.beta()**2 * self.mass() # [GeV / m] - # TEMP - # kappa_z *= 19.84 - M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length From 66710864012b51447e2ff81977839ee7585bb991 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 17:18:59 -0400 Subject: [PATCH 049/183] Format --- examples/Envelope/plot.py | 2 +- examples/Envelope/test_env_2d_fodo.py | 8 +++- examples/Envelope/test_env_3d_drift.py | 10 +++-- py/orbit/envelope/envelope.py | 58 +++++++++++++++++--------- py/orbit/envelope/matrix.py | 14 ++++--- py/orbit/envelope/utils.py | 1 - 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py index 0a088a66..99a77f80 100644 --- a/examples/Envelope/plot.py +++ b/examples/Envelope/plot.py @@ -5,7 +5,7 @@ def calc_rms_ellipse_params(cov_matrix: np.ndarray) -> tuple[float, float, float]: """Return rms ellipse dimensions and orientation.""" - (i, j) = (0, 1) + i, j = (0, 1) sii = cov_matrix[i, i] sjj = cov_matrix[j, j] diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 379d426b..e1734515 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -42,7 +42,9 @@ parser.add_argument("--kin-energy", type=float, default=0.025) parser.add_argument("--intensity", type=float, default=5e10) -parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) +parser.add_argument( + "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] +) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) @@ -183,7 +185,9 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist(n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist) +bunch_coords[:, :4] = gen_dist( + n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist +) bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) bunch_coords += centroid_init[None, :6] diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 0a2eadb8..2b9e2542 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -78,8 +78,8 @@ sync_part.kinEnergy(args.kin_energy) cov_matrix_init = np.zeros((6, 6)) -cov_matrix_init[0, 0] = args.xrms ** 2 -cov_matrix_init[2, 2] = args.yrms ** 2 +cov_matrix_init[0, 0] = args.xrms**2 +cov_matrix_init[2, 2] = args.yrms**2 cov_matrix_init[4, 4] = (args.zrms / sync_part.gamma()) ** 2 centroid_init = np.zeros(6) @@ -126,12 +126,14 @@ print("TRACK BUNCH") bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, (0, 2, 4)] = gen_dist(args.nparts, cov_matrix=np.eye(3), name="waterbag") +bunch_coords[:, (0, 2, 4)] = gen_dist( + args.nparts, cov_matrix=np.eye(3), name="waterbag" +) bunch_coords[:, 0] *= args.xrms bunch_coords[:, 2] *= args.yrms bunch_coords[:, 4] *= args.zrms / sync_part.gamma() -for (x, xp, y, yp, z, dE) in bunch_coords: +for x, xp, y, yp, z, dE in bunch_coords: bunch.addParticle(x, xp, y, yp, z, dE) size_global = bunch.getSizeGlobal() diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 73b146c7..951ce9e6 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -13,7 +13,6 @@ from .utils import gen_dist from .utils import proj_cov_matrix - ENTRANCE = AccNode.ENTRANCE BODY = AccNode.BODY EXIT = AccNode.EXIT @@ -41,19 +40,20 @@ class Envelope: matrix: 7 x 7 covariance matrix for augmented phase space vector. Define the phase space vector X = [x, x', y, y', z, dE]^T and - augmented vector Y = [x, x', y, y', z, dE, 1]. + augmented vector Y = [x, x', y, y', z, dE, 1]. Let X evolve according to X -> MX + U, where M is a 6 x 6 transfer matrix and U is 6 x 1 "driving" vector. The augmented vector Y evolves according to Y -> NY, where N = [[M, U], [0, 1]] is a 7 x 7 matrix. - We track the 7 x 7 covariance matrix of Y: - + We track the 7 x 7 covariance matrix of Y: + R = = [[, ], [, 1]], - which contains both the phase space covariance matrix and centroid vector. + which contains both the phase space covariance matrix and centroid vector. R evolves according to R -> N R N^T. """ + def __init__( self, sync_part: SyncParticle, @@ -82,14 +82,19 @@ def __init__( def set_intensity(self, intensity: float) -> None: self.intensity = intensity - self.perveance = 2.0 * intensity * CLASSICAL_PROTON_RADIUS / (self.beta()**2 * self.gamma()**3) + self.perveance = ( + 2.0 + * intensity + * CLASSICAL_PROTON_RADIUS + / (self.beta() ** 2 * self.gamma() ** 3) + ) def gamma(self) -> float: return self.sync_part.gamma() def beta(self) -> float: return self.sync_part.beta() - + def mass(self) -> float: return self.sync_part.mass() @@ -104,7 +109,9 @@ def rms(self, axis: int = None) -> float | np.ndarray: return rms_arr[axis] def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: - self.matrix = np.linalg.multi_dot([transfer_matrix, self.matrix, transfer_matrix.T]) + self.matrix = np.linalg.multi_dot( + [transfer_matrix, self.matrix, transfer_matrix.T] + ) def sample(self, n: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, @@ -112,12 +119,12 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles - + def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: centroid = self.centroid() cov_matrix = self.cov() cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) - + cov_eig_res = np.linalg.eig(cov_matrix_proj) cov_eig_vals = cov_eig_res.eigenvalues cov_eig_vecs = cov_eig_res.eigenvectors @@ -136,21 +143,21 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: M[3, 2] = kappa_y * length A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) - + T = np.identity(7) T[0, -1] = centroid[0] - T[2, -1] = centroid[2] + T[2, -1] = centroid[2] V = np.matmul(T, A) V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) - + def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: centroid = self.centroid() centroid[4] *= self.gamma() cov_matrix = self.cov() - cov_matrix[4, 4] *= self.gamma()**2 + cov_matrix[4, 4] *= self.gamma() ** 2 cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) eig_res = np.linalg.eig(cov_matrix_proj) @@ -167,7 +174,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: kappa_x = factor * RDx # [1 / m] kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] - kappa_z *= self.gamma()**3 * self.beta()**2 * self.mass() # [GeV / m] + kappa_z *= self.gamma() ** 3 * self.beta() ** 2 * self.mass() # [GeV / m] M = np.identity(7) M[1, 0] = kappa_x * length @@ -190,6 +197,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: """Tracks envelope through linear lattice with optional linear space charge kicks.""" + def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: self.lattice = lattice self.matrix_factory = MatrixFactory() @@ -199,11 +207,15 @@ def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): # Child nodes before node for child_node in node.getChildNodes(ENTRANCE): - envelope.apply_transfer_matrix(self.matrix_factory(child_node, envelope.sync_part)) + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) for part_index in range(node.getnParts()): # Child nodes before part - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + for child_node in node.getChildNodes( + BODY, part_index, place_in_part=BEFORE + ): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) @@ -216,7 +228,9 @@ def track(self, envelope: Envelope) -> None: elif self.space_charge == "3d": matrix = envelope.sc_transfer_matrix_3d(length) else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") + raise ValueError( + f"Invalid space charge model: {self.space_charge}" + ) # print("debug space charge matrix") # print(matrix) @@ -229,11 +243,15 @@ def track(self, envelope: Envelope) -> None: ) # Child nodes after part - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + for child_node in node.getChildNodes( + BODY, part_index, place_in_part=AFTER + ): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) # Child nodes after node for child_node in node.getChildNodes(EXIT): - envelope.apply_transfer_matrix(self.matrix_factory(child_node, envelope.sync_part)) + envelope.apply_transfer_matrix( + self.matrix_factory(child_node, envelope.sync_part) + ) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 88a81815..b7de01e2 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -16,7 +16,6 @@ from ..teapot import MonitorTEAPOT from ..teapot import TurnCounterTEAPOT -from .utils import proj_cov_matrix from ..utils import speed_of_light @@ -25,6 +24,7 @@ class MatrixFactory: Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] """ + def __init__(self) -> None: self.ignore_node_types = [ ApertureTEAPOT, @@ -40,7 +40,7 @@ def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length - matrix[4, 5] = length / sync_part.gamma()**2 + matrix[4, 5] = length / sync_part.gamma() ** 2 matrix[4, 5] *= dp_p_coef return matrix @@ -76,7 +76,7 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix[3, 2] = -sy * sqrt_abs_kq matrix[3, 3] = cy - matrix[4, 5] = length / sync_part.gamma()**2 + matrix[4, 5] = length / sync_part.gamma() ** 2 matrix[4, 5] *= dp_p_coef return matrix @@ -105,7 +105,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[4, 0] = -sx matrix[4, 1] = -rho * (1.0 - cx) - matrix[4, 5] = -length * sync_part.beta()**2 + rho * sx + matrix[4, 5] = -length * sync_part.beta() ** 2 + rho * sx matrix[4, 5] *= dp_p_coef return matrix @@ -116,7 +116,7 @@ def tilt(self, angle: float) -> np.ndarray: matrix[2, 0] = matrix[3, 1] = -math.sin(angle) matrix[2, 2] = matrix[3, 3] = +math.cos(angle) return matrix - + def translation(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: matrix = np.identity(7) matrix[0, 6] = x @@ -131,7 +131,9 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix[5, 6] = dE return matrix - def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: + def __call__( + self, node: AccNode, sync_part: SyncParticle, part_index: int = 0 + ) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) return self.drift(length=length, sync_part=sync_part) diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index a88fcbae..83d8e847 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -39,7 +39,6 @@ def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: return np.matmul(X, L.T) - def proj_cov_matrix(cov_matrix: np.ndarray, axis: tuple[int, ...]) -> np.ndarray: cov_matrix_proj = np.zeros((len(axis), len(axis))) for i in range(len(axis)): From 2fe1c2c116773ac1ae805e086c687b69b9980696 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 17:33:14 -0400 Subject: [PATCH 050/183] Add comments --- examples/Envelope/test_env_3d_drift.py | 2 +- py/orbit/envelope/envelope.py | 40 +++++++++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 2b9e2542..ce8942a3 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -33,7 +33,7 @@ parser = argparse.ArgumentParser() parser.add_argument("--kin-energy", type=float, default=0.025) -parser.add_argument("--intensity", type=float, default=1e11) +parser.add_argument("--intensity", type=float, default=5e11) parser.add_argument("--xrms", type=float, default=0.010) parser.add_argument("--yrms", type=float, default=0.010) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 951ce9e6..92f8f8fc 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -121,33 +121,45 @@ def sample(self, n: int, dist: str = "kv") -> np.ndarray: return particles def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: + # Extract beam centroid and covariance matrix. centroid = self.centroid() cov_matrix = self.cov() + + # Project covariance matrix onto x-y plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) + # Compute eigenvalues and eigenvectors of x-y covariance matrix. cov_eig_res = np.linalg.eig(cov_matrix_proj) cov_eig_vals = cov_eig_res.eigenvalues cov_eig_vecs = cov_eig_res.eigenvectors + # Compute rms beam sizes in upright frame. rx = 2.0 * math.sqrt(cov_eig_vals[0]) ry = 2.0 * math.sqrt(cov_eig_vals[1]) + # Build transfer matrix in upright frame. bunch_length = 4.0 * self.rms(axis=4) perveance = self.perveance / bunch_length - - kappa_x = 2.0 * perveance / (rx * (rx + ry)) - kappa_y = 2.0 * perveance / (ry * (rx + ry)) + factor = 2.0 * perveance / (rx + ry) + kappa_x = factor / rx + kappa_y = factor / ry M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length + # Build matrix to undo x-y diagonalization. A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) + # Build matrix to translate to centroid. T = np.identity(7) T[0, -1] = centroid[0] T[2, -1] = centroid[2] + # Compute matrix in lab frame. + # x = V u = T A u. + # u -> M u + # x -> V M V^-1 x V = np.matmul(T, A) V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) @@ -160,12 +172,13 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: cov_matrix[4, 4] *= self.gamma() ** 2 cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) - eig_res = np.linalg.eig(cov_matrix_proj) - - cov_xx = eig_res.eigenvalues[0] - cov_yy = eig_res.eigenvalues[1] - cov_zz = eig_res.eigenvalues[2] + # Compute eigenvalues and eigenvectors of x-y covariance matrix. + cov_eig_res = np.linalg.eig(cov_matrix_proj) + cov_eig_vals = cov_eig_res.eigenvalues + cov_eig_vecs = cov_eig_res.eigenvectors + # Build transfer matrix in upright frame. + cov_xx, cov_yy, cov_zz = cov_eig_vals RDx = scipy.special.elliprd(cov_yy, cov_zz, cov_xx) RDy = scipy.special.elliprd(cov_xx, cov_zz, cov_yy) RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) @@ -181,16 +194,23 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M[3, 2] = kappa_y * length M[5, 4] = kappa_z * length - A = build_diag_matrix_from_xyz_eig(eig_res.eigenvectors) + # Build matrix to undo x-y-z diagonalization. + A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) + # Build matrix for translation to centroid. T = np.identity(7) for i in (0, 2, 4): T[i, -1] = centroid[i] + # Build matrix for Lorentz boost (length contraction). L = np.identity(7) L[4, 4] = 1.0 / self.gamma() - V = np.linalg.multi_dot([L, T, A]) + # Compute matrix in lab frame. + # x = L V u = L T A u. + # u -> M u + # x -> V M V^-1 x + V = np.matmul(T, A) V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) From 5a34ee6fdc47c4182b18396371bc4eeb1e99bdc9 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 18:08:56 -0400 Subject: [PATCH 051/183] Delete a few unused lines --- py/orbit/envelope/envelope.py | 4 ++-- py/orbit/envelope/matrix.py | 30 ++++++++++++++---------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 92f8f8fc..7b3b6425 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -218,9 +218,9 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: """Tracks envelope through linear lattice with optional linear space charge kicks.""" - def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: + def __init__(self, lattice: AccLattice, space_charge: str | None = None, ignore_unknown: bool = False) -> None: self.lattice = lattice - self.matrix_factory = MatrixFactory() + self.matrix_factory = MatrixFactory(ignore_unknown=ignore_unknown) self.space_charge = space_charge def track(self, envelope: Envelope) -> None: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index b7de01e2..3cbf8438 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -19,13 +19,16 @@ from ..utils import speed_of_light +def get_dp_p_coeff(sync_part: SyncParticle) -> float: + return 1.0 / (sync_part.momentum() * sync_part.beta()) + + class MatrixFactory: """Factory for 7 x 7 transfer matrices. Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] """ - - def __init__(self) -> None: + def __init__(self, ignore_unknown: bool = False) -> None: self.ignore_node_types = [ ApertureTEAPOT, BunchWrapTEAPOT, @@ -33,19 +36,17 @@ def __init__(self) -> None: MonitorTEAPOT, TurnCounterTEAPOT, ] + self.ignore_unknown = ignore_unknown def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: - dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) - matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix[4, 5] *= dp_p_coef + matrix[4, 5] *= get_dp_p_coeff(sync_part) return matrix def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: - dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) sqrt_abs_kq = math.sqrt(abs(kq)) matrix = np.identity(7) @@ -77,7 +78,7 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix[3, 3] = cy matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix[4, 5] *= dp_p_coef + matrix[4, 5] *= get_dp_p_coeff(sync_part) return matrix def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: @@ -85,9 +86,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr return np.identity(7) v = speed_of_light * sync_part.beta() - sync_part.setTime(sync_part.getTime() + length / v) - - dp_p_coef = 1.0 / (sync_part.momentum() * sync_part.beta()) + sync_part.time(sync_part.time() + length / v) rho = length / theta cx = math.cos(theta) @@ -106,7 +105,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[4, 0] = -sx matrix[4, 1] = -rho * (1.0 - cx) matrix[4, 5] = -length * sync_part.beta() ** 2 + rho * sx - matrix[4, 5] *= dp_p_coef + matrix[4, 5] *= get_dp_p_coeff(sync_part) return matrix def tilt(self, angle: float) -> np.ndarray: @@ -131,9 +130,7 @@ def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix[5, 6] = dE return matrix - def __call__( - self, node: AccNode, sync_part: SyncParticle, part_index: int = 0 - ) -> np.ndarray: + def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) return self.drift(length=length, sync_part=sync_part) @@ -151,7 +148,6 @@ def __call__( nparts = node.getnParts() length = node.getLength(part_index) theta = node.getParam("theta") / (nparts - 1) - gamma = sync_part.gamma() return self.bend(length=length, theta=theta, sync_part=sync_part) elif type(node) is KickTEAPOT: @@ -174,4 +170,6 @@ def __call__( return np.identity(7) else: - raise NotImplementedError("Unsupported node type: {}".format(type(node))) + if self.ignore_unknown: + return self.drift(length=node.getLength(), sync_part=sync_part) + raise NotImplementedError("Unsupported node type: {}. Set `ignore_unknown=True` to replace with drift.".format(type(node))) From 1537400bf7934451a1d15975174ed7c436a17907 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 18:09:11 -0400 Subject: [PATCH 052/183] Add test_env_sns_ring --- examples/Envelope/inputs/sns_ring.lat | 768 +++++++++++++++++++++++++ examples/Envelope/test_env_2d_fodo.py | 2 +- examples/Envelope/test_env_sns_ring.py | 325 +++++++++++ 3 files changed, 1094 insertions(+), 1 deletion(-) create mode 100644 examples/Envelope/inputs/sns_ring.lat create mode 100644 examples/Envelope/test_env_sns_ring.py diff --git a/examples/Envelope/inputs/sns_ring.lat b/examples/Envelope/inputs/sns_ring.lat new file mode 100644 index 00000000..cbfb8df1 --- /dev/null +++ b/examples/Envelope/inputs/sns_ring.lat @@ -0,0 +1,768 @@ +kvx12 := kdc; +kdc = -3.412580596; +kta11c12 := 0; +brho := 1e9*(pc/c); +pc := sqrt(ek*(ek+2*e0)); +ek := 0.8; +e0 := 0.93827231; +c := 299792458; +khx13 := kfc; +kfc = 3.161218767; +kta10b13 := 0; +kdhta13 := 0; +kdvta13 := 0; +vkck12 := 0; +hkck12 := 0; +vkck13 := 0; +hkck13 := 0; +kdvtb1 := 0; +ksc_b01 := 0; +kssb1_9 = 0; +kvx1 := kdee; +kdee = -2.579970923; +ktb1_9 := 0; +ksv1 := 0; +ksc_b02 := 0; +kssb2_8 = 0; +khx2 := kf; +kf = 3.388342139; +ktb2d8 := 0; +ksh2 := 0; +kdvtb3 := 0; +ksc_b03 := 0; +kvx3 := kd; +kd = -3.731394588; +ktb357 := 0; +ksv3 := chrm3; +chrm3 := 0; +kdhtb4 := 0; +khx4 := kf26; +kf26 := kf*(lf/lf26); +lf := 0.25; +lf26 := 0.2705; +kta4b6 := 0; +ksh4 := chrm4; +chrm4 := 0; +ksv5 := chrm5; +chrm5 := 0; +kvx5 := kd; +kdvtb5 := 0; +ksc_b05 := 0; +ksh6 := chrm6; +chrm6 := 0; +khx6 := kf26; +kdhtb6 := 0; +ksv7 := chrm7; +chrm7 := chrm3; +kvx7 := kd; +kdvtb7 := 0; +ksc_b07 := 0; +ko_b08 := 0; +khx8 := kf; +kdhtb8 := 0; +ksc_b08 := 0; +ko_b09 := 0; +kvx9 := kdee; +kdvtb9 := 0; +ksc_b09 := 0; +kdhtb10 := 0; +kdvtb10 := 0; +khx10 := kfc; +kvx11 := kdc; +ktb11d12 := 0; +kdhtb13 := 0; +kdvtb13 := 0; +kdvtc1 := 0; +ksc_c01 := 0; +kssc1_9 = 0; +ktc1_9 := 0; +kdhtc2 := 0; +kssc2_8 = 0; +ksc_c02 := 0; +kta2c8 := 0; +kdvtc3 := 0; +ksc_c03 := 0; +ktc357 := 0; +kdhtc4 := 0; +ktc4d6 := 0; +kdvtc5 := 0; +ksc_c05 := 0; +kdhtc6 := 0; +kdvtc7 := 0; +ksc_c07 := 0; +ko_c08 := 0; +kdhtc8 := 0; +ksc_c08 := 0; +ko_c09 := 0; +kdvtc9 := 0; +ksc_c09 := 0; +kdhtc10 := 0; +kdvtc10 := 0; +ktc10d13 := 0; +kdhtc13 := 0; +kdvtc13 := 0; +ksisol := 1.22174*kssol; +kssol := solfield/brho; +solfield = 0; +kdvtd1 := 0; +ksc_d01 := 0; +kssd1_9 = 0; +ktd1_9 := 0; +kdhtd2 := 0; +ksc_d02 := 0; +kssd2_8 = 0; +kdvtd3 := 0; +ksc_d03 := 0; +ktd357 := 0; +lsv3 := lsxt; +lsxt := 0.317; +kdhtd4 := 0; +kdvtd5 := 0; +ksc_d05 := 0; +kdhtd6 := 0; +kdvtd7 := 0; +ksc_d07 := 0; +ko_d08 := 0; +kdhtd8 := 0; +ksc_d08 := 0; +ko_d09 := 0; +kdvtd9 := 0; +ksc_d09 := 0; +kdhtd10 := 0; +kdvtd10 := 0; +kdhtd13 := 0; +kdvtd13 := 0; +kdvta1 := 0; +ksc_a01 := 0; +kssa1_9 = 0; +kta1_9 := 0; +kdhta2 := 0; +ksc_a02 := 0; +kssa2_8 = 0; +kdvta3 := 0; +ksc_a03 := 0; +kta357 := 0; +kdhta4 := 0; +kdvta5 := 0; +kdhta6 := 0; +kdvta7 := 0; +ksc_a07 := 0; +ko_a08 := 0; +kdhta8 := 0; +ksc_a08 := 0; +ko_a09 := 0; +kdvta9 := 0; +ksc_a09 := 0; +hkck10 := 0; +vkck10 := 0; +hkck11 := 0; +vkck11 := 0; +kdhta10 := 0; +kdvta10 := 0; +injm1: marker; +dh_a12: sbend,l:= 0.9903829659,angle:= 0.0436,e1:= -0.003,e2:= 0.0466; +dh_a13: sbend,l:= 0.8903221964,angle:= -0.0466,e1:= -0.0466,e2:= -6.938893904e-18; +qtv_a12: quadrupole,l:= 0.533,k1:=( kvx12 + kta11c12 ) / brho ; +injm4: marker; +qth_a13: quadrupole,l:= 0.673,k1:=( khx13 + kta10b13 ) / brho ; +bpm_a13: monitor; +dchv_a13: kicker,l:= 0,hkick:=kdhta13 ,vkick:=kdvta13 ; +ikickv_a12: vkicker,l:= 0.428,kick:=vkck12 ; +ikickh_a12: hkicker,l:= 0.428,kick:=hkck12 ; +ikickv_a13: vkicker,l:= 0.839,kick:=vkck13 ; +ikickh_a13: hkicker,l:= 0.839,kick:=hkck13 ; +injm2: marker; +dmcv_b01: vkicker,l:= 0,kick:=kdvtb1 ; +qsc_b01: multipole,knl:={ 0,ksc_b01 }; +ssxc_b01: multipole,ksl:={ 0, 0,kssb1_9 }; +bpm_b01: monitor; +qtv_b01: quadrupole,l:= 0.5,k1:=( kvx1 + ktb1_9 ) / brho ; +scv_b01: sextupole,l:= 0.354,k2:=ksv1 ; +dh_b01: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_b02: hkicker,l:= 0,kick:= 0; +qsc_b02: multipole,knl:={ 0,ksc_b02 }; +ssxc_b02: multipole,ksl:={ 0, 0,kssb2_8 }; +bpm_b02: monitor; +qth_b02: quadrupole,l:= 0.5,k1:=( khx2 + ktb2d8 ) / brho ; +sch_b02: sextupole,l:= 0.354,k2:=ksh2 ; +dh_b02: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmcv_b03: vkicker,l:= 0,kick:=kdvtb3 ; +qsc_b03: multipole,knl:={ 0,ksc_b03 }; +bpm_b03: monitor; +qtv_b03: quadrupole,l:= 0.5,k1:=( kvx3 + ktb357 ) / brho ; +sv_b03: sextupole,l:= 0.317,k2:=ksv3 ; +dh_b03: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_b04: hkicker,l:= 0,kick:=kdhtb4 ; +bpm_b04: monitor; +qth_b04: quadrupole,l:= 0.541,k1:=( khx4 - kta4b6 ) / brho ; +sh_b04: sextupole,l:= 0.33,k2:=ksh4 ; +dh_b04: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_b05: sextupole,l:= 0.317,k2:=ksv5 ; +qtv_b05: quadrupole,l:= 0.5,k1:=( kvx5 + ktb357 ) / brho ; +bpm_b05: monitor; +dmcv_b05: vkicker,l:= 0,kick:=kdvtb5 ; +qsc_b05: multipole,knl:={ 0,ksc_b05 }; +dh_b06: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sh_b06: sextupole,l:= 0.33,k2:=ksh6 ; +qth_b06: quadrupole,l:= 0.541,k1:=( khx6 - kta4b6 ) / brho ; +bpm_b06: monitor; +dmch_b06: hkicker,l:= 0,kick:=kdhtb6 ; +dh_b07: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_b07: sextupole,l:= 0.317,k2:=ksv7 ; +qtv_b07: quadrupole,l:= 0.5,k1:=( kvx7 + ktb357 ) / brho ; +bpm_b07: monitor; +dmcv_b07: vkicker,l:= 0,kick:=kdvtb7 ; +qsc_b07: multipole,knl:={ 0,ksc_b07 }; +dh_b08: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_b08: octupole,l:= 0.33,k3:=ko_b08 ; +qth_b08: quadrupole,l:= 0.5,k1:=( khx8 + ktb2d8 ) / brho ; +bpm_b08: monitor; +dmch_b08: hkicker,l:= 0,kick:=kdhtb8 ; +qsc_b08: multipole,knl:={ 0,ksc_b08 }; +ssxc_b08: multipole,ksl:={ 0, 0,kssb2_8 }; +dh_b09: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_b09: octupole,l:= 0.33,k3:=ko_b09 ; +qtv_b09: quadrupole,l:= 0.5,k1:=( kvx9 + ktb1_9 ) / brho ; +bpm_b09: monitor; +dmcv_b09: vkicker,l:= 0,kick:=kdvtb9 ; +qsc_b09: multipole,knl:={ 0,ksc_b09 }; +ssxc_b09: multipole,ksl:={ 0, 0,kssb1_9 }; +dchv_b10: kicker,l:= 0,hkick:=kdhtb10 ,vkick:=kdvtb10 ; +bpm_b10: monitor; +qth_b10: quadrupole,l:= 0.673,k1:=( khx10 - kta10b13 ) / brho ; +qtv_b11: quadrupole,l:= 0.533,k1:=( kvx11 + ktb11d12 ) / brho ; +qtv_b12: quadrupole,l:= 0.533,k1:=( kvx12 + ktb11d12 ) / brho ; +qth_b13: quadrupole,l:= 0.673,k1:=( khx13 - kta10b13 ) / brho ; +bpm_b13: monitor; +dchv_b13: kicker,l:= 0,hkick:=kdhtb13 ,vkick:=kdvtb13 ; +dampkicker1: marker; +dampkicker2: marker; +qmmkicker: marker; +tunekicker: marker; +dmcv_c01: vkicker,l:= 0,kick:=kdvtc1 ; +qsc_c01: multipole,knl:={ 0,ksc_c01 }; +ssxc_c01: multipole,ksl:={ 0, 0,kssc1_9 }; +bpm_c01: monitor; +qtv_c01: quadrupole,l:= 0.5,k1:=( kvx1 + ktc1_9 ) / brho ; +scv_c01: sextupole,l:= 0.354,k2:=ksv1 ; +dh_c01: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_c02: hkicker,l:= 0,kick:=kdhtc2 ; +ssxc_c02: multipole,ksl:={ 0, 0,kssc2_8 }; +qsc_c02: multipole,knl:={ 0,ksc_c02 }; +bpm_c02: monitor; +qth_c02: quadrupole,l:= 0.5,k1:=( khx2 + kta2c8 ) / brho ; +sch_c02: sextupole,l:= 0.354,k2:=ksh2 ; +dh_c02: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmcv_c03: vkicker,l:= 0,kick:=kdvtc3 ; +qsc_c03: multipole,knl:={ 0,ksc_c03 }; +bpm_c03: monitor; +qtv_c03: quadrupole,l:= 0.5,k1:=( kvx3 + ktc357 ) / brho ; +sv_c03: sextupole,l:= 0.317,k2:=ksv3 ; +dh_c03: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_c04: hkicker,l:= 0,kick:=kdhtc4 ; +bpm_c04: monitor; +qth_c04: quadrupole,l:= 0.541,k1:=( khx4 + ktc4d6 ) / brho ; +sh_c04: sextupole,l:= 0.33,k2:=ksh4 ; +dh_c04: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_c05: sextupole,l:= 0.317,k2:=ksv5 ; +qtv_c05: quadrupole,l:= 0.5,k1:=( kvx5 + ktc357 ) / brho ; +bpm_c05: monitor; +dmcv_c05: vkicker,l:= 0,kick:=kdvtc5 ; +qsc_c05: multipole,knl:={ 0,ksc_c05 }; +dh_c06: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sh_c06: sextupole,l:= 0.33,k2:=ksh6 ; +qth_c06: quadrupole,l:= 0.541,k1:=( khx6 + ktc4d6 ) / brho ; +bpm_c06: monitor; +dmch_c06: hkicker,l:= 0,kick:=kdhtc6 ; +dh_c07: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_c07: sextupole,l:= 0.317,k2:=ksv7 ; +qtv_c07: quadrupole,l:= 0.5,k1:=( kvx7 + ktc357 ) / brho ; +bpm_c07: monitor; +dmcv_c07: vkicker,l:= 0,kick:=kdvtc7 ; +qsc_c07: multipole,knl:={ 0,ksc_c07 }; +dh_c08: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_c08: octupole,l:= 0.33,k3:=ko_c08 ; +qth_c08: quadrupole,l:= 0.5,k1:=( khx8 + kta2c8 ) / brho ; +bpm_c08: monitor; +dmch_c08: hkicker,l:= 0,kick:=kdhtc8 ; +qsc_c08: multipole,knl:={ 0,ksc_c08 }; +ssxc_c08: multipole,ksl:={ 0,kssc2_8 }; +dh_c09: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_c09: octupole,l:= 0.33,k3:=ko_c09 ; +qtv_c09: quadrupole,l:= 0.5,k1:=( kvx9 + ktc1_9 ) / brho ; +bpm_c09: monitor; +dmcv_c09: vkicker,l:= 0,kick:=kdvtc9 ; +qsc_c09: multipole,knl:={ 0,ksc_c09 }; +ssxc_c09: multipole,ksl:={ 0, 0,kssc1_9 }; +ekick01: vkicker,l:= 0.4; +ekick02: vkicker,l:= 0.4; +ekick03: vkicker,l:= 0.4; +ekick04: vkicker,l:= 0.505; +ekick05: vkicker,l:= 0.505; +ekick06: vkicker,l:= 0.505; +ekick07: vkicker,l:= 0.505; +dchv_c10: kicker,l:= 0,hkick:=kdhtc10 ,vkick:=kdvtc10 ; +bpm_c10: monitor; +qth_c10: quadrupole,l:= 0.673,k1:=( khx10 + ktc10d13 ) / brho ; +qtv_c11: quadrupole,l:= 0.533,k1:=( kvx11 - kta11c12 ) / brho ; +ekick08: vkicker,l:= 0.4275; +ekick09: vkicker,l:= 0.4275; +ekick10: vkicker,l:= 0.4275; +ekick11: vkicker,l:= 0.4275; +ekick12: vkicker,l:= 0.39; +ekick13: vkicker,l:= 0.39; +ekick14: vkicker,l:= 0.39; +qtv_c12: quadrupole,l:= 0.533,k1:=( kvx12 - kta11c12 ) / brho ; +qth_c13: quadrupole,l:= 0.673,k1:=( khx13 + ktc10d13 ) / brho ; +bpm_c13: monitor; +dchv_c13: kicker,l:= 0,hkick:=kdhtc13 ,vkick:=kdvtc13 ; +scbdsol_c13a: solenoid,l:= 1.22174,ks:= 0,ksi:=ksisol ; +scbdsol_c13b: solenoid,l:= 1.22174,ks:= 0,ksi:=ksisol ; +dmcv_d01: vkicker,l:= 0,kick:=kdvtd1 ; +qsc_d01: multipole,knl:={ 0,ksc_d01 }; +ssxc_d01: multipole,ksl:={ 0,kssd1_9 }; +bpm_d01: monitor; +qtv_d01: quadrupole,l:= 0.5,k1:=( kvx1 + ktd1_9 ) / brho ; +scv_d01: sextupole,l:= 0.354,k2:=ksv1 ; +dh_d01: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_d02: hkicker,l:= 0,kick:=kdhtd2 ; +qsc_d02: multipole,knl:={ 0,ksc_d02 }; +ssxc_d02: multipole,ksl:={ 0, 0,kssd2_8 }; +bpm_d02: monitor; +qth_d02: quadrupole,l:= 0.5,k1:=( khx2 + ktb2d8 ) / brho ; +sch_d02: sextupole,l:= 0.354,k2:=ksh2 ; +dh_d02: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmcv_d03: vkicker,l:= 0,kick:=kdvtd3 ; +qsc_d03: multipole,knl:={ 0,ksc_d03 }; +bpm_d03: monitor; +qtv_d03: quadrupole,l:= 0.5,k1:=( kvx3 + ktd357 ) / brho ; +sv_d03: sextupole,l:=lsv3 ,k2:=ksv3 ; +dh_d03: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_d04: hkicker,l:= 0,kick:=kdhtd4 ; +bpm_d04: monitor; +qth_d04: quadrupole,l:= 0.541,k1:=( khx4 - ktc4d6 ) / brho ; +sh_d04: sextupole,l:= 0.33,k2:=ksh4 ; +dh_d04: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_d05: sextupole,l:= 0.317,k2:=ksv5 ; +qtv_d05: quadrupole,l:= 0.5,k1:=( kvx5 + ktd357 ) / brho ; +bpm_d05: monitor; +dmcv_d05: vkicker,l:= 0,kick:=kdvtd5 ; +qsc_d05: multipole,knl:={ 0,ksc_d05 }; +dh_d06: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sh_d06: sextupole,l:= 0.33,k2:=ksh6 ; +qth_d06: quadrupole,l:= 0.541,k1:=( khx6 - ktc4d6 ) / brho ; +bpm_d06: monitor; +dmch_d06: hkicker,l:= 0,kick:=kdhtd6 ; +dh_d07: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_d07: sextupole,l:= 0.317,k2:=ksv7 ; +qtv_d07: quadrupole,l:= 0.5,k1:=( kvx7 + ktd357 ) / brho ; +bpm_d07: monitor; +dmcv_d07: vkicker,l:= 0,kick:=kdvtd7 ; +qsc_d07: multipole,knl:={ 0,ksc_d07 }; +dh_d08: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_d08: octupole,l:= 0.33,k3:=ko_d08 ; +qth_d08: quadrupole,l:= 0.5,k1:=( khx8 + ktb2d8 ) / brho ; +bpm_d08: monitor; +dmch_d08: hkicker,l:= 0,kick:=kdhtd8 ; +qsc_d08: multipole,knl:={ 0,ksc_d08 }; +ssxc_d08: multipole,ksl:={ 0, 0,kssd2_8 }; +dh_d09: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_d09: octupole,l:= 0.33,k3:=ko_d09 ; +qtv_d09: quadrupole,l:= 0.5,k1:=( kvx9 + ktd1_9 ) / brho ; +bpm_d09: monitor; +dmcv_d09: vkicker,l:= 0,kick:=kdvtd9 ; +qsc_d09: multipole,knl:={ 0,ksc_d09 }; +ssxc_d09: multipole,ksl:={ 0, 0,kssd1_9 }; +tunepickup: marker; +qmmpickup: marker; +wcm: marker; +bcm: marker; +dchv_d10: kicker,l:= 0,hkick:=kdhtd10 ,vkick:=kdvtd10 ; +bpm_d10: monitor; +qth_d10: quadrupole,l:= 0.673,k1:=( khx10 - ktc10d13 ) / brho ; +qtv_d11: quadrupole,l:= 0.533,k1:=( kvx11 - ktb11d12 ) / brho ; +cav_01: rfcavity,l:= 2.1466,volt:= 0.0133,harmon:= 1; +cav_02: rfcavity,l:= 2.1466,volt:= 0.0133,harmon:= 1; +cav_03: rfcavity,l:= 2.1466,volt:= 0.0133,harmon:= 1; +cav_04: rfcavity,l:= 2.1466,volt:= -0.02,harmon:= 2; +qtv_d12: quadrupole,l:= 0.533,k1:=( kvx12 - ktb11d12 ) / brho ; +qth_d13: quadrupole,l:= 0.673,k1:=( khx13 - ktc10d13 ) / brho ; +bpm_d13: monitor; +dchv_d13: kicker,l:= 0,hkick:=kdhtd13 ,vkick:=kdvtd13 ; +haloscanner1: marker; +wirescanner1: marker; +ipm1: marker; +ipm2: marker; +wirescanner2: marker; +haloscanner2: marker; +dmcv_a01: vkicker,l:= 0,kick:=kdvta1 ; +qsc_a01: multipole,knl:={ 0,ksc_a01 }; +ssxc_a01: multipole,ksl:={ 0, 0,kssa1_9 }; +bpm_a01: monitor; +qtv_a01: quadrupole,l:= 0.5,k1:=( kvx1 + kta1_9 ) / brho ; +scv_a01: sextupole,l:= 0.354,k2:=ksv1 ; +dh_a01: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_a02: hkicker,l:= 0,kick:=kdhta2 ; +qsc_a02: multipole,knl:={ 0,ksc_a02 }; +ssxc_a02: multipole,ksl:={ 0, 0,kssa2_8 }; +bpm_a02: monitor; +qth_a02: quadrupole,l:= 0.5,k1:=( khx2 + kta2c8 ) / brho ; +sch_a02: sextupole,l:= 0.354,k2:=ksh2 ; +dh_a02: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmcv_a03: vkicker,l:= 0,kick:=kdvta3 ; +qsc_a03: multipole,knl:={ 0,ksc_a03 }; +bpm_a03: monitor; +qtv_a03: quadrupole,l:= 0.5,k1:=( kvx3 + kta357 ) / brho ; +sv_a03: sextupole,l:= 0.317,k2:=ksv3 ; +dh_a03: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +dmch_a04: hkicker,l:= 0,kick:=kdhta4 ; +bpm_a04: monitor; +qth_a04: quadrupole,l:= 0.541,k1:=( khx4 + kta4b6 ) / brho ; +sh_a04: sextupole,l:= 0.33,k2:=ksh4 ; +dh_a04: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_a05: sextupole,l:= 0.317,k2:=ksv5 ; +qtv_a05: quadrupole,l:= 0.5,k1:=( kvx5 + kta357 ) / brho ; +bpm_a05: monitor; +dmcv_a05: vkicker,l:= 0,kick:=kdvta5 ; +qsc_a05: multipole,knl:={ 0}; +dh_a06: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sh_a06: sextupole,l:= 0.33,k2:=ksh6 ; +qth_a06: quadrupole,l:= 0.541,k1:=( khx6 + kta4b6 ) / brho ; +bpm_a06: monitor; +dmch_a06: hkicker,l:= 0,kick:=kdhta6 ; +dh_a07: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +sv_a07: sextupole,l:= 0.317,k2:=ksv7 ; +qtv_a07: quadrupole,l:= 0.5,k1:=( kvx7 + kta357 ) / brho ; +bpm_a07: monitor; +dmcv_a07: vkicker,l:= 0,kick:=kdvta7 ; +qsc_a07: multipole,knl:={ 0,ksc_a07 }; +dh_a08: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_a08: octupole,l:= 0.33,k3:=ko_a08 ; +qth_a08: quadrupole,l:= 0.5,k1:=( khx8 + kta2c8 ) / brho ; +bpm_a08: monitor; +dmch_a08: hkicker,l:= 0,kick:=kdhta8 ; +qsc_a08: multipole,knl:={ 0,ksc_a08 }; +ssxc_a08: multipole,ksl:={ 0, 0,kssa2_8 }; +dh_a09: sbend,l:= 1.4407,angle:= 0.1963495408,e1:= 0,e2:= 0; +oct_a09: octupole,l:= 0.33,k3:=ko_a09 ; +qtv_a09: quadrupole,l:= 0.5,k1:=( kvx9 + kta1_9 ) / brho ; +bpm_a09: monitor; +dmcv_a09: vkicker,l:= 0,kick:=kdvta9 ; +qsc_a09: multipole,knl:={ 0,ksc_a09 }; +ssxc_a09: multipole,ksl:={ 0, 0,kssa1_9 }; +ikickh_a10: hkicker,l:= 0.839,kick:=hkck10 ; +ikickv_a10: vkicker,l:= 0.839,kick:=vkck10 ; +ikickh_a11: hkicker,l:= 0.428,kick:=hkck11 ; +ikickv_a11: vkicker,l:= 0.428,kick:=vkck11 ; +dchv_a10: kicker,l:= 0,hkick:=kdhta10 ,vkick:=kdvta10 ; +bpm_a10: monitor; +qth_a10: quadrupole,l:= 0.673,k1:=( khx10 + kta10b13 ) / brho ; +injm3: marker; +qtv_a11: quadrupole,l:= 0.533,k1:=( kvx11 + kta11c12 ) / brho ; +dh_a10: sbend,l:= 0.8632537742,angle:= -0.042,e1:= 0,e2:= -0.042; +dh_a11: sbend,l:= 0.8722394086,angle:= 0.045,e1:= 0.042,e2:= 0.003; +rnginjsol: sequence, l = 248.0098418; +injm1, at = 0; +dh_a12, at = 1.378195456; +dh_a13, at = 3.408731523; +qtv_a12, at = 7.004892621; +injm4, at = 7.693392621; +qth_a13, at = 8.029892621; +bpm_a13, at = 8.547265121; +dchv_a13, at = 8.683688621; +ikickv_a12, at = 10.59989262; +ikickh_a12, at = 11.13989262; +ikickv_a13, at = 12.66989262; +ikickh_a13, at = 13.82989262; +injm2, at = 14.24939262; +dmcv_b01, at = 14.92668062; +qsc_b01, at = 14.92668062; +ssxc_b01, at = 14.92668062; +bpm_b01, at = 15.12176002; +qtv_b01, at = 15.47989262; +scv_b01, at = 16.03310462; +dh_b01, at = 17.47998825; +dmch_b02, at = 18.92687188; +qsc_b02, at = 18.92687188; +ssxc_b02, at = 18.92687188; +bpm_b02, at = 19.11879438; +bpm_b02, at = 19.23008388; +qth_b02, at = 19.48008388; +sch_b02, at = 20.03329588; +dh_b02, at = 21.4801795; +dmcv_b03, at = 22.92706313; +qsc_b03, at = 22.92706313; +bpm_b03, at = 23.11546513; +qtv_b03, at = 23.48027513; +sv_b03, at = 24.01443713; +dh_b03, at = 25.48037076; +dmch_b04, at = 26.89474238; +bpm_b04, at = 27.11860888; +qth_b04, at = 27.48046638; +sh_b04, at = 28.08524038; +dh_b04, at = 29.48056201; +sv_b05, at = 30.94649564; +qtv_b05, at = 31.48065764; +bpm_b05, at = 31.84546764; +dmcv_b05, at = 32.03386964; +qsc_b05, at = 32.03386964; +dh_b06, at = 33.48075326; +sh_b06, at = 34.87607489; +qth_b06, at = 35.48084889; +bpm_b06, at = 35.84270639; +dmch_b06, at = 36.06657289; +dh_b07, at = 37.48094452; +sv_b07, at = 38.94687815; +qtv_b07, at = 39.48104015; +bpm_b07, at = 39.84585015; +dmcv_b07, at = 40.03425215; +qsc_b07, at = 40.03425215; +dh_b08, at = 41.48113577; +oct_b08, at = 42.9280194; +qth_b08, at = 43.4812314; +bpm_b08, at = 43.8425209; +dmch_b08, at = 44.0344434; +qsc_b08, at = 44.0344434; +ssxc_b08, at = 44.0344434; +dh_b09, at = 45.48132703; +oct_b09, at = 46.92821065; +qtv_b09, at = 47.48142265; +bpm_b09, at = 47.83955525; +dmcv_b09, at = 48.03463465; +qsc_b09, at = 48.03463465; +ssxc_b09, at = 48.03463465; +dchv_b10, at = 54.27762665; +bpm_b10, at = 54.41405015; +qth_b10, at = 54.93142265; +qtv_b11, at = 55.95642265; +qtv_b12, at = 69.00642265; +qth_b13, at = 70.03142265; +bpm_b13, at = 70.54879515; +dchv_b13, at = 70.68521865; +dampkicker1, at = 73.19024865; +dampkicker2, at = 73.69024765; +qmmkicker, at = 74.31523965; +tunekicker, at = 75.44023065; +dmcv_c01, at = 76.92821065; +qsc_c01, at = 76.92821065; +ssxc_c01, at = 76.92821065; +bpm_c01, at = 77.12329005; +qtv_c01, at = 77.48142265; +scv_c01, at = 78.03463465; +dh_c01, at = 79.48151828; +dmch_c02, at = 80.92840191; +ssxc_c02, at = 80.92840191; +qsc_c02, at = 80.92840191; +bpm_c02, at = 81.12032441; +qth_c02, at = 81.48161391; +sch_c02, at = 82.03482591; +dh_c02, at = 83.48170954; +dmcv_c03, at = 84.92859316; +qsc_c03, at = 84.92859316; +bpm_c03, at = 85.11699516; +qtv_c03, at = 85.48180516; +sv_c03, at = 86.01596716; +dh_c03, at = 87.48190079; +dmch_c04, at = 88.89627242; +bpm_c04, at = 89.12013892; +qth_c04, at = 89.48199642; +sh_c04, at = 90.08677042; +dh_c04, at = 91.48209204; +sv_c05, at = 92.94802567; +qtv_c05, at = 93.48218767; +bpm_c05, at = 93.84699767; +dmcv_c05, at = 94.03539967; +qsc_c05, at = 94.03539967; +dh_c06, at = 95.4822833; +sh_c06, at = 96.87760493; +qth_c06, at = 97.48237893; +bpm_c06, at = 97.84423643; +dmch_c06, at = 98.06810293; +dh_c07, at = 99.48247455; +sv_c07, at = 100.9484082; +qtv_c07, at = 101.4825702; +bpm_c07, at = 101.8473802; +dmcv_c07, at = 102.0357822; +qsc_c07, at = 102.0357822; +dh_c08, at = 103.4826658; +oct_c08, at = 104.9295494; +qth_c08, at = 105.4827614; +bpm_c08, at = 105.8440509; +dmch_c08, at = 106.0359734; +qsc_c08, at = 106.0359734; +ssxc_c08, at = 106.0359734; +dh_c09, at = 107.4828571; +oct_c09, at = 108.9297407; +qtv_c09, at = 109.4829527; +bpm_c09, at = 109.8410853; +dmcv_c09, at = 110.0361647; +qsc_c09, at = 110.0361647; +ssxc_c09, at = 110.0361647; +ekick01, at = 112.3299527; +ekick02, at = 112.8099527; +ekick03, at = 113.2899527; +ekick04, at = 113.8229527; +ekick05, at = 114.4079527; +ekick06, at = 114.9929527; +ekick07, at = 115.5779527; +dchv_c10, at = 116.2791567; +bpm_c10, at = 116.4155802; +qth_c10, at = 116.9329527; +qtv_c11, at = 117.9579527; +ekick08, at = 118.9059527; +ekick09, at = 119.4149527; +ekick10, at = 119.9249527; +ekick11, at = 120.4309527; +ekick12, at = 120.9202527; +ekick13, at = 121.3895527; +ekick14, at = 121.8606527; +qtv_c12, at = 131.0079527; +qth_c13, at = 132.0329527; +bpm_c13, at = 132.5503252; +dchv_c13, at = 132.6867487; +scbdsol_c13a, at = 134.3897627; +scbdsol_c13b, at = 138.0255227; +dmcv_d01, at = 138.9297407; +qsc_d01, at = 138.9297407; +ssxc_d01, at = 138.9297407; +bpm_d01, at = 139.1248201; +qtv_d01, at = 139.4829527; +scv_d01, at = 140.0361647; +dh_d01, at = 141.4830483; +dmch_d02, at = 142.9299319; +qsc_d02, at = 142.9299319; +ssxc_d02, at = 142.9299319; +bpm_d02, at = 143.1218544; +qth_d02, at = 143.4831439; +sch_d02, at = 144.0363559; +dh_d02, at = 145.4832396; +dmcv_d03, at = 146.9301232; +qsc_d03, at = 146.9301232; +bpm_d03, at = 147.1185252; +qtv_d03, at = 147.4833352; +sv_d03, at = 148.0174972; +dh_d03, at = 149.4834308; +dmch_d04, at = 150.8978024; +bpm_d04, at = 151.1216689; +qth_d04, at = 151.4835264; +sh_d04, at = 152.0883004; +dh_d04, at = 153.4836221; +sv_d05, at = 154.9495557; +qtv_d05, at = 155.4837177; +bpm_d05, at = 155.8485277; +dmcv_d05, at = 156.0369297; +qsc_d05, at = 156.0369297; +dh_d06, at = 157.4838133; +sh_d06, at = 158.879135; +qth_d06, at = 159.483909; +bpm_d06, at = 159.8457665; +dmch_d06, at = 160.069633; +dh_d07, at = 161.4840046; +sv_d07, at = 162.9499382; +qtv_d07, at = 163.4841002; +bpm_d07, at = 163.8489102; +dmcv_d07, at = 164.0373122; +qsc_d07, at = 164.0373122; +dh_d08, at = 165.4841958; +oct_d08, at = 166.9310795; +qth_d08, at = 167.4842915; +bpm_d08, at = 167.845581; +dmch_d08, at = 168.0375035; +qsc_d08, at = 168.0375035; +ssxc_d08, at = 168.0375035; +dh_d09, at = 169.4843871; +oct_d09, at = 170.9312707; +qtv_d09, at = 171.4844827; +bpm_d09, at = 171.8426153; +dmcv_d09, at = 172.0376947; +qsc_d09, at = 172.0376947; +ssxc_d09, at = 172.0376947; +tunepickup, at = 173.5472277; +qmmpickup, at = 174.6722197; +wcm, at = 175.7778927; +bcm, at = 176.7642687; +dchv_d10, at = 178.2806867; +bpm_d10, at = 178.4171102; +qth_d10, at = 178.9344827; +qtv_d11, at = 179.9594827; +cav_01, at = 183.0386827; +cav_02, at = 185.3358827; +cav_03, at = 187.6330827; +cav_04, at = 189.9302827; +qtv_d12, at = 193.0094827; +qth_d13, at = 194.0344827; +bpm_d13, at = 194.5518552; +dchv_d13, at = 194.6882787; +haloscanner1, at = 196.4528147; +wirescanner1, at = 196.6309197; +ipm1, at = 197.0093417; +ipm2, at = 199.3134267; +wirescanner2, at = 199.6918487; +haloscanner2, at = 199.8699527; +dmcv_a01, at = 200.9312707; +qsc_a01, at = 200.9312707; +ssxc_a01, at = 200.9312707; +bpm_a01, at = 201.1263501; +qtv_a01, at = 201.4844827; +scv_a01, at = 202.0376947; +dh_a01, at = 203.4845783; +dmch_a02, at = 204.931462; +qsc_a02, at = 204.931462; +ssxc_a02, at = 204.931462; +bpm_a02, at = 205.1233845; +qth_a02, at = 205.484674; +sch_a02, at = 206.037886; +dh_a02, at = 207.4847696; +dmcv_a03, at = 208.9316532; +qsc_a03, at = 208.9316532; +bpm_a03, at = 209.1200552; +qtv_a03, at = 209.4848652; +sv_a03, at = 210.0190272; +dh_a03, at = 211.4849609; +dmch_a04, at = 212.8993325; +bpm_a04, at = 213.123199; +qth_a04, at = 213.4850565; +sh_a04, at = 214.0898305; +dh_a04, at = 215.4851521; +sv_a05, at = 216.9510857; +qtv_a05, at = 217.4852477; +bpm_a05, at = 217.8500577; +dmcv_a05, at = 218.0384597; +qsc_a05, at = 218.0384597; +dh_a06, at = 219.4853434; +sh_a06, at = 220.880665; +qth_a06, at = 221.485439; +bpm_a06, at = 221.8472965; +dmch_a06, at = 222.071163; +dh_a07, at = 223.4855346; +sv_a07, at = 224.9514682; +qtv_a07, at = 225.4856302; +bpm_a07, at = 225.8504402; +dmcv_a07, at = 226.0388422; +qsc_a07, at = 226.0388422; +dh_a08, at = 227.4857259; +oct_a08, at = 228.9326095; +qth_a08, at = 229.4858215; +bpm_a08, at = 229.847111; +dmch_a08, at = 230.0390335; +qsc_a08, at = 230.0390335; +ssxc_a08, at = 230.0390335; +dh_a09, at = 231.4859171; +oct_a09, at = 232.9328008; +qtv_a09, at = 233.4860128; +bpm_a09, at = 233.8441454; +dmcv_a09, at = 234.0392248; +qsc_a09, at = 234.0392248; +ssxc_a09, at = 234.0392248; +ikickh_a10, at = 235.1360128; +ikickv_a10, at = 236.2960128; +ikickh_a11, at = 237.8260128; +ikickv_a11, at = 238.3660128; +dchv_a10, at = 240.2822168; +bpm_a10, at = 240.4186403; +qth_a10, at = 240.9360128; +injm3, at = 241.2725128; +qtv_a11, at = 241.9610128; +dh_a10, at = 245.1911396; +dh_a11, at = 247.5737221; +endsequence; diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index e1734515..d5234b41 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -1,4 +1,4 @@ -"""Test envelope tracker in FODO lattice.""" +"""Test 2D envelope tracker in FODO lattice.""" import argparse import copy diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py new file mode 100644 index 00000000..fe8e6d26 --- /dev/null +++ b/examples/Envelope/test_env_sns_ring.py @@ -0,0 +1,325 @@ +"""Test envelope tracker in SNS ring.""" + +import argparse +import copy +import math +import os +import pathlib + +import numpy as np +import matplotlib.pyplot as plt + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.bunch_utils import collect_bunch +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.space_charge.sc2p5d import setSC2p5DAccNodes +from orbit.teapot import TEAPOT_Ring +from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.utils.consts import mass_proton + +from plot import plot_rms_ellipse +from plot import plot_corner +from utils import gen_dist +from utils import build_rotation_matrix_xy +from utils import project_cov_matrix + +plt.style.use("style.mplstyle") + + +# Parse arguments +# ------------------------------------------------------------------------------ + +parser = argparse.ArgumentParser() +parser.add_argument("--bunch-length", type=float, default=120.0) +parser.add_argument("--kin-energy", type=float, default=1.300) +parser.add_argument("--intensity", type=float, default=5e14) + +parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) +parser.add_argument("--mismatch-x", type=float, default=0.0) +parser.add_argument("--mismatch-y", type=float, default=0.0) +parser.add_argument("--offset-x", type=float, default=0.0) +parser.add_argument("--offset-y", type=float, default=0.0) +parser.add_argument("--tilt", type=float, default=0) + +parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--turns", type=int, default=25) +parser.add_argument("--sol", type=int, default=0) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--sc-grid", type=int, default=64) + +parser.add_argument("--ignore-unknown", type=int, default=0) +args = parser.parse_args() + + +# Setup +# ------------------------------------------------------------------------------ + +path = pathlib.Path(__file__) +output_dir = os.path.join("outputs", path.stem) +os.makedirs(output_dir, exist_ok=True) + + +# Create lattice +# ------------------------------------------------------------------------------ + +lattice = TEAPOT_Ring() +lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") +lattice.initialize() + +for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass + +if args.sol: + for name in ["scbdsol_c13a", "scbdsol_c13b"]: + node = lattice.getNodeForName(name) + node.setParam("B", 0.15) + + +# Create envelope +# ------------------------------------------------------------------------------ + +# Create bunch +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(args.kin_energy) + +# Find periodic lattice parameters +matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) +matrix_lattice_params = matrix_lattice.getRingParametersDict() +alpha_x = matrix_lattice_params["alpha x"] +alpha_y = matrix_lattice_params["alpha y"] +beta_x = matrix_lattice_params["beta x [m]"] +beta_y = matrix_lattice_params["beta y [m]"] +eps_x = 25.0e-06 +eps_y = eps_x + +# Generate covariance matrix +cov_matrix = np.zeros((6, 6)) +cov_matrix[0, 0] = eps_x * beta_x +cov_matrix[2, 2] = eps_y * beta_y +cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x +cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y +cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x +cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y +cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 +cov_matrix[5, 5] = 0.0 + +# Tilt +if args.tilt: + rot_matrix = np.identity(6) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) + cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) + +# Mismatch +cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 +cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 +cov_matrix_init = np.copy(cov_matrix) + +# Offset +centroid_init = np.zeros(6) +centroid_init[0] += args.offset_x +centroid_init[2] += args.offset_y + +# Create envelope +envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, +) + + +# Track envelope +# ------------------------------------------------------------------------------ + +print("TRACK ENVELOPE") + +tracker = EnvelopeTracker( + lattice, + ignore_unknown=args.ignore_unknown, + space_charge=("2d" if args.sc else None), +) + +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) + + cov_matrix = envelope.cov() + centroid = envelope.centroid() + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * centroid[0] + yavg = 1000.0 * centroid[2] + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + +histories = {} +histories["envelope"] = copy.deepcopy(history) + + +# Track bunch +# ------------------------------------------------------------------------------ + +print("TRACK BUNCH") + +rng = np.random.default_rng() + +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, :4] = gen_dist( + n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist +) +bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) +bunch_coords += centroid_init[None, :6] + +for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + +if args.sc: + sc_calc = SpaceChargeCalc2p5D(128, 128, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + +history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} +for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * twiss_calc.getAverage(0) + yavg = 1000.0 * twiss_calc.getAverage(2) + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + +histories["bunch"] = copy.deepcopy(history) + + +# Analysis +# ------------------------------------------------------------------------------ + +for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + +# Print errors +for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + +# Plot rms bunch sizes +for key in ["xrms", "yrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + +# Plot centroids +for key in ["xavg", "yavg"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel("AVG [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + +# Collect bunch/envelope data on final turn. +particles = collect_bunch(bunch)["coords"] +particles[:, :4] *= 1000.0 + +env_cov_matrix = envelope.cov() +env_cov_matrix[:4, :4] *= 1000.0**2 + +env_centroid = envelope.centroid() +env_centroid[:4] *= 1000.0 + +xmax = 4.0 * np.std(particles, axis=0) +limits = list(zip(-xmax, xmax)) +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + + +# Plot x-x' +fig, ax = plt.subplots(figsize=(4, 4)) +ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) +plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, +) +ax.set_xlabel(labels[0]) +ax.set_ylabel(labels[1]) +plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) +plt.close() + +# Plot corner +fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, +) +for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) +plt.savefig(os.path.join(output_dir, "fig_dist_corner")) +plt.close() From edec43d3d9e8ac6bdc91cf9c8b3d032a4927d6b5 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:01:24 -0400 Subject: [PATCH 053/183] Add options for handling matrix elements without known matrix --- py/orbit/envelope/envelope.py | 9 +++++++-- py/orbit/envelope/matrix.py | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 7b3b6425..fa3955a7 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -218,9 +218,14 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: """Tracks envelope through linear lattice with optional linear space charge kicks.""" - def __init__(self, lattice: AccLattice, space_charge: str | None = None, ignore_unknown: bool = False) -> None: + def __init__( + self, + lattice: AccLattice, + space_charge: str | None = None, + handle_unkown: str | None = None, + ) -> None: self.lattice = lattice - self.matrix_factory = MatrixFactory(ignore_unknown=ignore_unknown) + self.matrix_factory = MatrixFactory(handle_unkown=handle_unkown) self.space_charge = space_charge def track(self, envelope: Envelope) -> None: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 3cbf8438..630795f9 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -28,7 +28,7 @@ class MatrixFactory: Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] """ - def __init__(self, ignore_unknown: bool = False) -> None: + def __init__(self, handle_unkown: bool = False) -> None: self.ignore_node_types = [ ApertureTEAPOT, BunchWrapTEAPOT, @@ -36,7 +36,7 @@ def __init__(self, ignore_unknown: bool = False) -> None: MonitorTEAPOT, TurnCounterTEAPOT, ] - self.ignore_unknown = ignore_unknown + self.handle_unkown = handle_unkown def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix = np.identity(7) @@ -170,6 +170,8 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) return np.identity(7) else: - if self.ignore_unknown: + if self.handle_unkown == "drift": return self.drift(length=node.getLength(), sync_part=sync_part) + elif self.handle_unknown == "fit": + raise NotImplementedError() raise NotImplementedError("Unsupported node type: {}. Set `ignore_unknown=True` to replace with drift.".format(type(node))) From 3c0c1b8cf3f3f8ed186c4327732595d12e7727a1 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:01:50 -0400 Subject: [PATCH 054/183] Add tests Dipole is incorrect --- examples/Envelope/test_env.py | 132 +++++++++++++++++++++++++ examples/Envelope/test_env_2d_fodo.py | 4 +- examples/Envelope/test_env_3d_drift.py | 4 +- examples/Envelope/test_env_sns_ring.py | 4 +- 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 examples/Envelope/test_env.py diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py new file mode 100644 index 00000000..a5badbee --- /dev/null +++ b/examples/Envelope/test_env.py @@ -0,0 +1,132 @@ +import numpy as np + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.lattice import AccNode +from orbit.lattice import AccLattice +from orbit.teapot import QuadTEAPOT +from orbit.teapot import BendTEAPOT +from orbit.teapot import DriftTEAPOT +from orbit.teapot import TEAPOT_Lattice +from orbit.utils.consts import mass_proton + +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker + + +def calc_bunch_cov(bunch: Bunch) -> np.ndarray: + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + return cov_matrix + + +def track_and_compare( + nodes: list[AccNode], + kin_energy: float, + cov_matrix: np.ndarray, + nparts: int = 100_000, +) -> dict: + + cov_scale = 1e6 + + data = {} + for k1 in ["env", "bunch"]: + data[k1] = {} + for k2 in ["rms", "cov"]: + data[k1][k2] = {} + for k3 in ["env", "bunch"]: + data[k1][k2][k3] = {} + + lattice = TEAPOT_Lattice() + for node in nodes: + lattice.addNode(node) + lattice.initialize() + for node in lattice.getNodes(): + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + + bunch = Bunch() + bunch.mass(mass_proton) + + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(kin_energy) + + envelope = Envelope(sync_part=sync_part, cov_matrix=cov_matrix) + + envelope_tracker = EnvelopeTracker(lattice=lattice) + + data["env"]["cov"]["in"] = cov_scale * envelope.cov() + envelope_tracker.track(envelope) + data["env"]["cov"]["out"] = cov_scale * envelope.cov() + + particles = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=nparts) + for x in particles: + bunch.addParticle(*x) + + data["bunch"]["cov"]["in"] = cov_scale * calc_bunch_cov(bunch) + lattice.trackBunch(bunch) + data["bunch"]["cov"]["out"] = cov_scale * calc_bunch_cov(bunch) + + for mode in ["env", "bunch"]: + for loc in ["in", "out"]: + data[mode]["rms"][loc] = np.sqrt(np.diag(data[mode]["cov"][loc])) + + dims = ["x", "xp", "y", "yp", "z", "dE"] + for key in ["in", "out"]: + print(key.upper()) + for i in range(6): + print(" rms {}:".format(dims[i])) + print(" env: {}".format(data["env"]["rms"][key][i])) + print(" bunch: {}".format(data["bunch"]["rms"][key][i])) + + for key in ["in", "out"]: + assert np.all(np.isclose(data["env"]["cov"][key], data["bunch"]["cov"][key])) + + +def make_default_cov_matrix(scale: float = 0.001) -> np.ndarray: + cov_matrix = np.zeros((6, 6)) + cov_matrix[0, 0] = scale ** 2 + cov_matrix[2, 2] = scale ** 2 + return cov_matrix + + +def test_drift(kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None): + nodes = [ + DriftTEAPOT(length=1.0), + ] + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare(nodes, kin_energy, cov_matrix) + +def test_quad(kin_energy: float = 0.0025, length: float = 1.0, kq: float = 1.0, cov_matrix: np.ndarray = None): + nodes = [ + QuadTEAPOT(length=length, kq=kq), + ] + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare(nodes, kin_energy, cov_matrix) + + +def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None): + nodes = [ + BendTEAPOT(length=length, theta=np.radians(theta)) + ] + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare(nodes, kin_energy, cov_matrix) + + +if __name__ == "__main__": + functions = [ + # test_drift, + # test_quad, + test_dipole, + ] + for function in functions: + function() diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index d5234b41..8184f5bf 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -39,8 +39,8 @@ parser = argparse.ArgumentParser() parser.add_argument("--zrms", type=float, default=5.0) -parser.add_argument("--kin-energy", type=float, default=0.025) -parser.add_argument("--intensity", type=float, default=5e10) +parser.add_argument("--kin-energy", type=float, default=0.0025) +parser.add_argument("--intensity", type=float, default=5e9) parser.add_argument( "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index ce8942a3..5e344249 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -32,8 +32,8 @@ # ------------------------------------------------------------------------------ parser = argparse.ArgumentParser() -parser.add_argument("--kin-energy", type=float, default=0.025) -parser.add_argument("--intensity", type=float, default=5e11) +parser.add_argument("--kin-energy", type=float, default=0.0025) +parser.add_argument("--intensity", type=float, default=5e10) parser.add_argument("--xrms", type=float, default=0.010) parser.add_argument("--yrms", type=float, default=0.010) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index fe8e6d26..46dc69e8 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -51,7 +51,7 @@ parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) -parser.add_argument("--ignore-unknown", type=int, default=0) +parser.add_argument("--handle-unknown", type=str, default=None) args = parser.parse_args() @@ -145,7 +145,7 @@ tracker = EnvelopeTracker( lattice, - ignore_unknown=args.ignore_unknown, + handle_unkown=args.handle_unkown, space_charge=("2d" if args.sc else None), ) From 328af0b75dcd8fc3fbb6d52629364e60fc56fa62 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:04:48 -0400 Subject: [PATCH 055/183] Typo --- py/orbit/envelope/envelope.py | 4 ++-- py/orbit/envelope/matrix.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index fa3955a7..0d03a0d0 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -222,10 +222,10 @@ def __init__( self, lattice: AccLattice, space_charge: str | None = None, - handle_unkown: str | None = None, + handle_unknown: str | None = None, ) -> None: self.lattice = lattice - self.matrix_factory = MatrixFactory(handle_unkown=handle_unkown) + self.matrix_factory = MatrixFactory(handle_unknown=handle_unknown) self.space_charge = space_charge def track(self, envelope: Envelope) -> None: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 630795f9..eb8cc1b5 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -28,7 +28,7 @@ class MatrixFactory: Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] """ - def __init__(self, handle_unkown: bool = False) -> None: + def __init__(self, handle_unknown: bool = False) -> None: self.ignore_node_types = [ ApertureTEAPOT, BunchWrapTEAPOT, @@ -36,14 +36,16 @@ def __init__(self, handle_unkown: bool = False) -> None: MonitorTEAPOT, TurnCounterTEAPOT, ] - self.handle_unkown = handle_unkown + self.handle_unknown = handle_unknown def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length matrix[4, 5] = length / sync_part.gamma() ** 2 + matrix[4, 5] *= get_dp_p_coeff(sync_part) + matrix[5, 4] /= get_dp_p_coeff(sync_part) return matrix def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: @@ -78,7 +80,9 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix[3, 3] = cy matrix[4, 5] = length / sync_part.gamma() ** 2 + matrix[4, 5] *= get_dp_p_coeff(sync_part) + matrix[5, 4] /= get_dp_p_coeff(sync_part) return matrix def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: @@ -105,7 +109,9 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[4, 0] = -sx matrix[4, 1] = -rho * (1.0 - cx) matrix[4, 5] = -length * sync_part.beta() ** 2 + rho * sx + matrix[4, 5] *= get_dp_p_coeff(sync_part) + matrix[5, 4] /= get_dp_p_coeff(sync_part) return matrix def tilt(self, angle: float) -> np.ndarray: From 391acb0ac6de752ccb5c6e59bc43d52746e89817 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:05:22 -0400 Subject: [PATCH 056/183] Fix type --- py/orbit/envelope/matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index eb8cc1b5..a34f4cb0 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -28,7 +28,7 @@ class MatrixFactory: Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] """ - def __init__(self, handle_unknown: bool = False) -> None: + def __init__(self, handle_unknown: str | None = None) -> None: self.ignore_node_types = [ ApertureTEAPOT, BunchWrapTEAPOT, From 6921eda43ff80361d02ebfe1c0a192e12cf55f1d Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:15:08 -0400 Subject: [PATCH 057/183] Check dipole matrix --- py/orbit/envelope/matrix.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index a34f4cb0..acb9d62c 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -44,6 +44,7 @@ def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix[2, 3] = length matrix[4, 5] = length / sync_part.gamma() ** 2 + # Matrix above is for dp_p; switch to dE. matrix[4, 5] *= get_dp_p_coeff(sync_part) matrix[5, 4] /= get_dp_p_coeff(sync_part) return matrix @@ -80,7 +81,6 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix[3, 3] = cy matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix[4, 5] *= get_dp_p_coeff(sync_part) matrix[5, 4] /= get_dp_p_coeff(sync_part) return matrix @@ -96,6 +96,12 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr cx = math.cos(theta) sx = math.sin(theta) + betasq = sync_part.beta() ** 2 + + rho = length / theta + cx = math.cos(theta) + sx = math.sin(theta) + matrix = np.identity(7) matrix[0, 0] = cx matrix[0, 1] = rho * sx @@ -103,12 +109,10 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[1, 0] = -sx / rho matrix[1, 1] = cx matrix[1, 5] = sx - matrix[2, 3] = length - matrix[4, 0] = -sx matrix[4, 1] = -rho * (1.0 - cx) - matrix[4, 5] = -length * sync_part.beta() ** 2 + rho * sx + matrix[4, 5] = -betasq * length + rho * sx matrix[4, 5] *= get_dp_p_coeff(sync_part) matrix[5, 4] /= get_dp_p_coeff(sync_part) From b9bc1bf4b8e2a7c8a1e10267fe85a2d2099d2ae6 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:15:26 -0400 Subject: [PATCH 058/183] Update tests_sns_ring.py --- examples/Envelope/test_env_sns_ring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 46dc69e8..4daadbeb 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -145,7 +145,7 @@ tracker = EnvelopeTracker( lattice, - handle_unkown=args.handle_unkown, + handle_unknown=args.handle_unknown, space_charge=("2d" if args.sc else None), ) From 5834bc21ce335d001fcd13097253c83fb1562835 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:43:44 -0400 Subject: [PATCH 059/183] Fix dipole matrix calculation for parts --- py/orbit/envelope/envelope.py | 18 ++---------------- py/orbit/envelope/matrix.py | 9 ++++----- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 0d03a0d0..cce9aa0c 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -230,22 +230,17 @@ def __init__( def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): - # Child nodes before node for child_node in node.getChildNodes(ENTRANCE): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) for part_index in range(node.getnParts()): - # Child nodes before part - for child_node in node.getChildNodes( - BODY, part_index, place_in_part=BEFORE - ): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) - # Space charge if self.space_charge: length = node.getLength(part_index) if self.space_charge == "2d": @@ -256,26 +251,17 @@ def track(self, envelope: Envelope) -> None: raise ValueError( f"Invalid space charge model: {self.space_charge}" ) - - # print("debug space charge matrix") - # print(matrix) - envelope.apply_transfer_matrix(matrix) - # Main node part envelope.apply_transfer_matrix( self.matrix_factory(node, envelope.sync_part, part_index) ) - # Child nodes after part - for child_node in node.getChildNodes( - BODY, part_index, place_in_part=AFTER - ): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) ) - # Child nodes after node for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix( self.matrix_factory(child_node, envelope.sync_part) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index acb9d62c..48cb7964 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -155,9 +155,8 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) return self.quad(length=length, kq=kq, sync_part=sync_part) elif type(node) is BendTEAPOT: - nparts = node.getnParts() length = node.getLength(part_index) - theta = node.getParam("theta") / (nparts - 1) + theta = node.getParam("theta") / node.getnParts() return self.bend(length=length, theta=theta, sync_part=sync_part) elif type(node) is KickTEAPOT: @@ -167,9 +166,9 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) if node.waveform: scale = node.waveform.getStrength() - kx = scale * node.getParam("kx") / (nparts - 1) - ky = scale * node.getParam("ky") / (nparts - 1) - dE = node.getParam("dE") / (nparts - 1) + kx = scale * node.getParam("kx") / nparts + ky = scale * node.getParam("ky") / nparts + dE = node.getParam("dE") / nparts return self.kick(kx, ky, dE) elif type(node) is TiltTEAPOT: From a2ad2ac8d2086b6fb420e113133c317f61a94a04 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:44:46 -0400 Subject: [PATCH 060/183] Typo --- py/orbit/envelope/matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 48cb7964..a301a572 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -179,7 +179,7 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) return np.identity(7) else: - if self.handle_unkown == "drift": + if self.handle_unknown == "drift": return self.drift(length=node.getLength(), sync_part=sync_part) elif self.handle_unknown == "fit": raise NotImplementedError() From 789f70c61ade0174890d2000895bcedf7cb4faa2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 1 May 2026 19:49:05 -0400 Subject: [PATCH 061/183] Test sns ring --- examples/Envelope/test_env.py | 102 +++++++++++++++++++------ examples/Envelope/test_env_sns_ring.py | 2 +- py/orbit/envelope/matrix.py | 2 +- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index a5badbee..3cb46a2d 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -2,6 +2,7 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis +from orbit.bunch_utils import collect_bunch from orbit.lattice import AccNode from orbit.lattice import AccLattice from orbit.teapot import QuadTEAPOT @@ -9,21 +10,34 @@ from orbit.teapot import DriftTEAPOT from orbit.teapot import TEAPOT_Lattice from orbit.utils.consts import mass_proton - from orbit.envelope import Envelope from orbit.envelope import EnvelopeTracker def calc_bunch_cov(bunch: Bunch) -> np.ndarray: - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + coords = collect_bunch(bunch)["coords"] + return np.cov(coords.T) - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] - return cov_matrix + # twiss_calc = BunchTwissAnalysis() + # twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + # + # cov_matrix = np.zeros((6, 6)) + # for i in range(6): + # for j in range(i + 1): + # cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + # cov_matrix[j, i] = cov_matrix[i, j] + # return cov_matrix + + +def make_lattice(nodes: list[AccNode]) -> AccLattice: + lattice = TEAPOT_Lattice() + for node in nodes: + lattice.addNode(node) + lattice.initialize() + for node in lattice.getNodes(): + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + return lattice def track_and_compare( @@ -43,13 +57,7 @@ def track_and_compare( for k3 in ["env", "bunch"]: data[k1][k2][k3] = {} - lattice = TEAPOT_Lattice() - for node in nodes: - lattice.addNode(node) - lattice.initialize() - for node in lattice.getNodes(): - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) + lattice = make_lattice(nodes) bunch = Bunch() bunch.mass(mass_proton) @@ -122,11 +130,59 @@ def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = track_and_compare(nodes, kin_energy, cov_matrix) +def test_dipole_matrix(): + from orbit.envelope.matrix import MatrixFactory + + bunch = Bunch() + bunch.mass(mass_proton) + + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(0.0025) + + node = BendTEAPOT(length=1.0, theta=np.radians(20.0)) + + matrix_factory = MatrixFactory() + matrix = matrix_factory.bend(length=node.getLength(), theta=node.getParam("theta"), sync_part=sync_part) + + print(np.round(matrix, 2)) + + x = np.zeros(6) + x[1] = 0.001 + x[1] = 0.001 + + bunch.addParticle(*x) + + node.trackBunch(bunch) + + x_out = [bunch.x(0), bunch.xp(0), bunch.y(0), bunch.yp(0), bunch.z(0), bunch.dE(0)] + x_out = np.array(x_out) + + print(x) + print(x_out) + print(np.matmul(matrix[:6, :6], x)) + + cov_matrix = make_default_cov_matrix() + envelope = Envelope(sync_part=bunch.getSyncParticle(), cov_matrix=cov_matrix) + envelope.apply_transfer_matrix(matrix) + print(np.round(1e6 * envelope.cov(), 2)) + + particles_in = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=100_000) + particles_out = np.matmul(particles_in, matrix[:6, :6].T) + print(np.round(1e6 * np.cov(particles_out.T), 2)) + + bunch.deleteAllParticles() + for x in particles_in: + bunch.addParticle(*x) + node.trackBunch(bunch) + particles_out = collect_bunch(bunch)["coords"] + print(np.round(1e6 * np.cov(particles_out.T), 2)) + + if __name__ == "__main__": - functions = [ - # test_drift, - # test_quad, - test_dipole, - ] - for function in functions: - function() + # test_drift() + # test_quad() + test_dipole() + # test_dipole_matrix() + + + diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 4daadbeb..f5ef19ef 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -51,7 +51,7 @@ parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) -parser.add_argument("--handle-unknown", type=str, default=None) +parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) args = parser.parse_args() diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index a301a572..6196262a 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -183,4 +183,4 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) return self.drift(length=node.getLength(), sync_part=sync_part) elif self.handle_unknown == "fit": raise NotImplementedError() - raise NotImplementedError("Unsupported node type: {}. Set `ignore_unknown=True` to replace with drift.".format(type(node))) + raise NotImplementedError("Unsupported node type: {}. See `handle_unknown` attribute.".format(type(node))) From 64e83f44edf24771b3b2a735fd5c9397e1e1617b Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Tue, 12 May 2026 11:03:51 -0400 Subject: [PATCH 062/183] Scale last column of matrix by dp_p coeff --- py/orbit/envelope/matrix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 6196262a..8f555b47 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -114,8 +114,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[4, 1] = -rho * (1.0 - cx) matrix[4, 5] = -betasq * length + rho * sx - matrix[4, 5] *= get_dp_p_coeff(sync_part) - matrix[5, 4] /= get_dp_p_coeff(sync_part) + matrix[:, 5] *= get_dp_p_coeff(sync_part) return matrix def tilt(self, angle: float) -> np.ndarray: From 889391f234a912e6f9c54a36cdd9bd4a02165e5a Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 10 Jun 2026 12:41:07 -0400 Subject: [PATCH 063/183] Update tests --- examples/Envelope/test_env.py | 86 +++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 3cb46a2d..d837db36 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -15,18 +15,18 @@ def calc_bunch_cov(bunch: Bunch) -> np.ndarray: - coords = collect_bunch(bunch)["coords"] - return np.cov(coords.T) + twiss_calc = BunchTwissAnalysis() + twiss_calc.analyzeBunch(bunch) - # twiss_calc = BunchTwissAnalysis() - # twiss_calc.computeBunchMoments(bunch, 2, 0, 0) - # - # cov_matrix = np.zeros((6, 6)) - # for i in range(6): - # for j in range(i + 1): - # cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - # cov_matrix[j, i] = cov_matrix[i, j] - # return cov_matrix + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + return cov_matrix + + # coords = collect_bunch(bunch)["coords"] + # return np.cov(coords.T) def make_lattice(nodes: list[AccNode]) -> AccLattice: @@ -40,13 +40,15 @@ def make_lattice(nodes: list[AccNode]) -> AccLattice: return lattice -def track_and_compare( - nodes: list[AccNode], +def track_and_compare_rms( + lattice: AccLattice, kin_energy: float, cov_matrix: np.ndarray, nparts: int = 100_000, + rtol: float = 1e-3, + atol: float = 1e-3, ) -> dict: - + """Track bunch/envelope and compare rms beam sizes. """ cov_scale = 1e6 data = {} @@ -57,34 +59,37 @@ def track_and_compare( for k3 in ["env", "bunch"]: data[k1][k2][k3] = {} - lattice = make_lattice(nodes) - + # Initialize bunch bunch = Bunch() bunch.mass(mass_proton) + bunch.getSyncParticle().kinEnergy(kin_energy) - sync_part = bunch.getSyncParticle() - sync_part.kinEnergy(kin_energy) - - envelope = Envelope(sync_part=sync_part, cov_matrix=cov_matrix) - - envelope_tracker = EnvelopeTracker(lattice=lattice) - - data["env"]["cov"]["in"] = cov_scale * envelope.cov() - envelope_tracker.track(envelope) - data["env"]["cov"]["out"] = cov_scale * envelope.cov() - + # Track bunch particles = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=nparts) for x in particles: bunch.addParticle(*x) + # Covariance matrix calculated from particles will be slightly different. + cov_matrix = calc_bunch_cov(bunch) + data["bunch"]["cov"]["in"] = cov_scale * calc_bunch_cov(bunch) lattice.trackBunch(bunch) data["bunch"]["cov"]["out"] = cov_scale * calc_bunch_cov(bunch) + # Track envelope + envelope = Envelope(sync_part=bunch.getSyncParticle(), cov_matrix=cov_matrix) + envelope_tracker = EnvelopeTracker(lattice=lattice) + + data["env"]["cov"]["in"] = cov_scale * envelope.cov() + envelope_tracker.track(envelope) + data["env"]["cov"]["out"] = cov_scale * envelope.cov() + + # Calculate rms sizes for mode in ["env", "bunch"]: for loc in ["in", "out"]: data[mode]["rms"][loc] = np.sqrt(np.diag(data[mode]["cov"][loc])) + # Print dims = ["x", "xp", "y", "yp", "z", "dE"] for key in ["in", "out"]: print(key.upper()) @@ -94,40 +99,53 @@ def track_and_compare( print(" bunch: {}".format(data["bunch"]["rms"][key][i])) for key in ["in", "out"]: - assert np.all(np.isclose(data["env"]["cov"][key], data["bunch"]["cov"][key])) + assert np.all( + np.isclose( + data["env"]["cov"][key], + data["bunch"]["cov"][key], + rtol=rtol, + atol=atol, + ) + ) def make_default_cov_matrix(scale: float = 0.001) -> np.ndarray: cov_matrix = np.zeros((6, 6)) cov_matrix[0, 0] = scale ** 2 + cov_matrix[1, 1] = scale ** 2 cov_matrix[2, 2] = scale ** 2 + cov_matrix[3, 3] = scale ** 2 return cov_matrix def test_drift(kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None): nodes = [ - DriftTEAPOT(length=1.0), + DriftTEAPOT(length=length), ] + lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() - track_and_compare(nodes, kin_energy, cov_matrix) + track_and_compare_rms(lattice, kin_energy, cov_matrix) + def test_quad(kin_energy: float = 0.0025, length: float = 1.0, kq: float = 1.0, cov_matrix: np.ndarray = None): nodes = [ QuadTEAPOT(length=length, kq=kq), ] + lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() - track_and_compare(nodes, kin_energy, cov_matrix) + track_and_compare_rms(lattice, kin_energy, cov_matrix) def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None): nodes = [ BendTEAPOT(length=length, theta=np.radians(theta)) ] + lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() - track_and_compare(nodes, kin_energy, cov_matrix) + track_and_compare_rms(lattice, kin_energy, cov_matrix) def test_dipole_matrix(): @@ -180,8 +198,8 @@ def test_dipole_matrix(): if __name__ == "__main__": # test_drift() - # test_quad() - test_dipole() + test_quad() + # test_dipole() # test_dipole_matrix() From 9882d717faca81ea889861a38c72f69b0446d664 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 10 Jun 2026 12:50:19 -0400 Subject: [PATCH 064/183] Modify atol and rtol in np.isclose --- examples/Envelope/test_env.py | 95 ++++++++++------------------------- 1 file changed, 26 insertions(+), 69 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index d837db36..5c406aad 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -25,9 +25,6 @@ def calc_bunch_cov(bunch: Bunch) -> np.ndarray: cov_matrix[j, i] = cov_matrix[i, j] return cov_matrix - # coords = collect_bunch(bunch)["coords"] - # return np.cov(coords.T) - def make_lattice(nodes: list[AccNode]) -> AccLattice: lattice = TEAPOT_Lattice() @@ -45,10 +42,21 @@ def track_and_compare_rms( kin_energy: float, cov_matrix: np.ndarray, nparts: int = 100_000, - rtol: float = 1e-3, - atol: float = 1e-3, + rtol: float = 1e-5, + atol: float = 1e-4, + verbose: int = 1, ) -> dict: - """Track bunch/envelope and compare rms beam sizes. """ + """Track bunch/envelope and compare rms beam sizes. + + Args: + lattice: Accelerator lattice. + kin_energy: Synchronous particle kinetic energy [GeV]. + cov_matrix: 6 x 6 covariance matrix. + nparts: Number of particles in bunch. + rtol/atol: Relative/absolute tolerance on rms beam sizes (bunch vs. envelope). + Units are [mm, mrad]. + verbose: Whether to print results. + """ cov_scale = 1e6 data = {} @@ -84,19 +92,19 @@ def track_and_compare_rms( envelope_tracker.track(envelope) data["env"]["cov"]["out"] = cov_scale * envelope.cov() - # Calculate rms sizes + # Compare for mode in ["env", "bunch"]: for loc in ["in", "out"]: data[mode]["rms"][loc] = np.sqrt(np.diag(data[mode]["cov"][loc])) - # Print - dims = ["x", "xp", "y", "yp", "z", "dE"] - for key in ["in", "out"]: - print(key.upper()) - for i in range(6): - print(" rms {}:".format(dims[i])) - print(" env: {}".format(data["env"]["rms"][key][i])) - print(" bunch: {}".format(data["bunch"]["rms"][key][i])) + if verbose: + dims = ["x", "xp", "y", "yp", "z", "dE"] + for key in ["in", "out"]: + print(key.upper()) + for i in range(6): + print(" rms {}:".format(dims[i])) + print(" env: {}".format(data["env"]["rms"][key][i])) + print(" bunch: {}".format(data["bunch"]["rms"][key][i])) for key in ["in", "out"]: assert np.all( @@ -110,6 +118,7 @@ def track_and_compare_rms( def make_default_cov_matrix(scale: float = 0.001) -> np.ndarray: + """Isotropic covariance matrix in 4D phase space.""" cov_matrix = np.zeros((6, 6)) cov_matrix[0, 0] = scale ** 2 cov_matrix[1, 1] = scale ** 2 @@ -148,59 +157,7 @@ def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_dipole_matrix(): - from orbit.envelope.matrix import MatrixFactory - - bunch = Bunch() - bunch.mass(mass_proton) - - sync_part = bunch.getSyncParticle() - sync_part.kinEnergy(0.0025) - - node = BendTEAPOT(length=1.0, theta=np.radians(20.0)) - - matrix_factory = MatrixFactory() - matrix = matrix_factory.bend(length=node.getLength(), theta=node.getParam("theta"), sync_part=sync_part) - - print(np.round(matrix, 2)) - - x = np.zeros(6) - x[1] = 0.001 - x[1] = 0.001 - - bunch.addParticle(*x) - - node.trackBunch(bunch) - - x_out = [bunch.x(0), bunch.xp(0), bunch.y(0), bunch.yp(0), bunch.z(0), bunch.dE(0)] - x_out = np.array(x_out) - - print(x) - print(x_out) - print(np.matmul(matrix[:6, :6], x)) - - cov_matrix = make_default_cov_matrix() - envelope = Envelope(sync_part=bunch.getSyncParticle(), cov_matrix=cov_matrix) - envelope.apply_transfer_matrix(matrix) - print(np.round(1e6 * envelope.cov(), 2)) - - particles_in = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=100_000) - particles_out = np.matmul(particles_in, matrix[:6, :6].T) - print(np.round(1e6 * np.cov(particles_out.T), 2)) - - bunch.deleteAllParticles() - for x in particles_in: - bunch.addParticle(*x) - node.trackBunch(bunch) - particles_out = collect_bunch(bunch)["coords"] - print(np.round(1e6 * np.cov(particles_out.T), 2)) - - if __name__ == "__main__": - # test_drift() + test_drift() test_quad() - # test_dipole() - # test_dipole_matrix() - - - + test_dipole() \ No newline at end of file From 7c30aa2e9d3259b5ac4712ebb49c2d671ef4e3c4 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 10 Jun 2026 12:59:22 -0400 Subject: [PATCH 065/183] Add energy spread and rms z to cov matrix tests --- examples/Envelope/test_env.py | 43 +++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 5c406aad..6dd0161c 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -42,7 +42,7 @@ def track_and_compare_rms( kin_energy: float, cov_matrix: np.ndarray, nparts: int = 100_000, - rtol: float = 1e-5, + rtol: float = 1e-3, atol: float = 1e-4, verbose: int = 1, ) -> dict: @@ -109,25 +109,26 @@ def track_and_compare_rms( for key in ["in", "out"]: assert np.all( np.isclose( - data["env"]["cov"][key], - data["bunch"]["cov"][key], - rtol=rtol, - atol=atol, + data["env"]["cov"][key], data["bunch"]["cov"][key], rtol=rtol, atol=atol ) ) -def make_default_cov_matrix(scale: float = 0.001) -> np.ndarray: +def make_default_cov_matrix() -> np.ndarray: """Isotropic covariance matrix in 4D phase space.""" cov_matrix = np.zeros((6, 6)) - cov_matrix[0, 0] = scale ** 2 - cov_matrix[1, 1] = scale ** 2 - cov_matrix[2, 2] = scale ** 2 - cov_matrix[3, 3] = scale ** 2 + cov_matrix[0, 0] = 0.001**2 + cov_matrix[1, 1] = 0.001**2 + cov_matrix[2, 2] = 0.001**2 + cov_matrix[3, 3] = 0.001**2 + cov_matrix[4, 4] = 0.001**2 + cov_matrix[5, 5] = 0.00001**2 return cov_matrix -def test_drift(kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None): +def test_drift( + kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None +): nodes = [ DriftTEAPOT(length=length), ] @@ -137,7 +138,12 @@ def test_drift(kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.n track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_quad(kin_energy: float = 0.0025, length: float = 1.0, kq: float = 1.0, cov_matrix: np.ndarray = None): +def test_quad( + kin_energy: float = 0.0025, + length: float = 1.0, + kq: float = 1.0, + cov_matrix: np.ndarray = None, +): nodes = [ QuadTEAPOT(length=length, kq=kq), ] @@ -147,10 +153,13 @@ def test_quad(kin_energy: float = 0.0025, length: float = 1.0, kq: float = 1.0, track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None): - nodes = [ - BendTEAPOT(length=length, theta=np.radians(theta)) - ] +def test_dipole( + kin_energy: float = 0.0025, + length: float = 1.0, + theta: float = 20.0, + cov_matrix: np.ndarray = None, +): + nodes = [BendTEAPOT(length=length, theta=np.radians(theta))] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -160,4 +169,4 @@ def test_dipole(kin_energy: float = 0.0025, length: float = 1.0, theta: float = if __name__ == "__main__": test_drift() test_quad() - test_dipole() \ No newline at end of file + test_dipole() From 4897409c2b80929a9ed0f1375a3cea43cb9af404 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 13:30:06 -0400 Subject: [PATCH 066/183] Fix dp/p to dE correction in transfer matrices --- examples/Envelope/test_env.py | 25 ++++++++++++----------- py/orbit/envelope/matrix.py | 37 +++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 6dd0161c..d2f0814b 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -2,7 +2,6 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis -from orbit.bunch_utils import collect_bunch from orbit.lattice import AccNode from orbit.lattice import AccLattice from orbit.teapot import QuadTEAPOT @@ -42,8 +41,8 @@ def track_and_compare_rms( kin_energy: float, cov_matrix: np.ndarray, nparts: int = 100_000, - rtol: float = 1e-3, - atol: float = 1e-4, + rtol: float = 1e-5, + atol: float = 1e12, verbose: int = 1, ) -> dict: """Track bunch/envelope and compare rms beam sizes. @@ -114,16 +113,15 @@ def track_and_compare_rms( ) -def make_default_cov_matrix() -> np.ndarray: - """Isotropic covariance matrix in 4D phase space.""" - cov_matrix = np.zeros((6, 6)) - cov_matrix[0, 0] = 0.001**2 - cov_matrix[1, 1] = 0.001**2 - cov_matrix[2, 2] = 0.001**2 - cov_matrix[3, 3] = 0.001**2 - cov_matrix[4, 4] = 0.001**2 - cov_matrix[5, 5] = 0.00001**2 - return cov_matrix +def make_default_cov_matrix( + rms_x: float = 0.001, + rms_xp: float = 0.001, + rms_y: float = 0.001, + rms_yp: float = 0.001, + rms_z: float = 0.001, + rms_dE: float = 0.00001, +) -> np.ndarray: + return np.diag(np.square([rms_x, rms_xp, rms_y, rms_yp, rms_z, rms_dE])) def test_drift( @@ -170,3 +168,4 @@ def test_dipole( test_drift() test_quad() test_dipole() + diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 8f555b47..292b07fb 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -20,7 +20,26 @@ def get_dp_p_coeff(sync_part: SyncParticle) -> float: - return 1.0 / (sync_part.momentum() * sync_part.beta()) + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() # GeV + + # dE/E = (beta^2) * dp/p + # dE = (beta^2 * E) * dp/p + # dE = (beta^2 * gamma * m * c^2) * dp/p + return 1.0 / (beta**2 * gamma * rest_energy) + + +def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + # v = [x, x', y, y', z, dp/p] + # w = [x, x', y, y', z, dE] + # v = A w + # v -> M v + # w -> A M A^-1 + dp_p_coeff = get_dp_p_coeff(sync_part) + matrix[:5, 5] *= dp_p_coeff + matrix[5, :5] /= dp_p_coeff + return matrix class MatrixFactory: @@ -43,10 +62,7 @@ def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix[0, 1] = length matrix[2, 3] = length matrix[4, 5] = length / sync_part.gamma() ** 2 - - # Matrix above is for dp_p; switch to dE. - matrix[4, 5] *= get_dp_p_coeff(sync_part) - matrix[5, 4] /= get_dp_p_coeff(sync_part) + matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: @@ -81,8 +97,7 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix[3, 3] = cy matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix[4, 5] *= get_dp_p_coeff(sync_part) - matrix[5, 4] /= get_dp_p_coeff(sync_part) + matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: @@ -92,10 +107,6 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr v = speed_of_light * sync_part.beta() sync_part.time(sync_part.time() + length / v) - rho = length / theta - cx = math.cos(theta) - sx = math.sin(theta) - betasq = sync_part.beta() ** 2 rho = length / theta @@ -113,8 +124,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix[4, 0] = -sx matrix[4, 1] = -rho * (1.0 - cx) matrix[4, 5] = -betasq * length + rho * sx - - matrix[:, 5] *= get_dp_p_coeff(sync_part) + matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix def tilt(self, angle: float) -> np.ndarray: @@ -143,6 +153,7 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) if type(node) is DriftTEAPOT: length = node.getLength(part_index) return self.drift(length=length, sync_part=sync_part) + elif type(node) is QuadTEAPOT: length = node.getLength(part_index) From 1f73d3944fd24c5a43ed4b75a573de1108c3bd78 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 14:05:18 -0400 Subject: [PATCH 067/183] Fix kick matrix Drift then kick (node has a length param) --- examples/Envelope/test_env.py | 44 +++++++++++++++++++++++++++-------- py/orbit/envelope/matrix.py | 44 +++++++++++++++++++++++++---------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index d2f0814b..73135922 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -4,7 +4,7 @@ from orbit.core.bunch import BunchTwissAnalysis from orbit.lattice import AccNode from orbit.lattice import AccLattice -from orbit.teapot import QuadTEAPOT +from orbit.teapot import QuadTEAPOT, KickTEAPOT, TiltTEAPOT from orbit.teapot import BendTEAPOT from orbit.teapot import DriftTEAPOT from orbit.teapot import TEAPOT_Lattice @@ -126,10 +126,8 @@ def make_default_cov_matrix( def test_drift( kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None -): - nodes = [ - DriftTEAPOT(length=length), - ] +) -> None: + nodes = [DriftTEAPOT(length=length)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -141,10 +139,8 @@ def test_quad( length: float = 1.0, kq: float = 1.0, cov_matrix: np.ndarray = None, -): - nodes = [ - QuadTEAPOT(length=length, kq=kq), - ] +) -> None: + nodes = [QuadTEAPOT(length=length, kq=kq)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -156,7 +152,7 @@ def test_dipole( length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None, -): +) -> None: nodes = [BendTEAPOT(length=length, theta=np.radians(theta))] lattice = make_lattice(nodes) if cov_matrix is None: @@ -164,8 +160,36 @@ def test_dipole( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_kick( + kin_energy: float = 0.0025, + length: float = 0.1, + kx: float = 0.001, + ky: float = 0.001, + dE: float = 0.001, + cov_matrix: np.ndarray = None, +) -> None: + nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length)] + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + +def test_tilt( + kin_energy: float = 0.0025, + angle: float = 0.25 * np.pi, + cov_matrix: np.ndarray = None, +) -> None: + nodes = [TiltTEAPOT(angle=angle)] + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + if __name__ == "__main__": test_drift() test_quad() test_dipole() + test_kick() diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 292b07fb..96c11a77 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -39,8 +39,17 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np dp_p_coeff = get_dp_p_coeff(sync_part) matrix[:5, 5] *= dp_p_coeff matrix[5, :5] /= dp_p_coeff + matrix[5, 6] /= dp_p_coeff return matrix + # scale = np.identity(7) + # scale[5, 5] = dp_p_coeff + # + # scale_inv = np.identity(7) + # scale_inv[5, 5] = 1.0 / dp_p_coeff + # + # return np.linalg.multi_dot([scale, matrix, scale_inv]) + class MatrixFactory: """Factory for 7 x 7 transfer matrices. @@ -57,7 +66,7 @@ def __init__(self, handle_unknown: str | None = None) -> None: ] self.handle_unknown = handle_unknown - def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: + def drift_matrix(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix = np.identity(7) matrix[0, 1] = length matrix[2, 3] = length @@ -65,7 +74,7 @@ def drift(self, length: float, sync_part: SyncParticle) -> np.ndarray: matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix - def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + def quad_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: sqrt_abs_kq = math.sqrt(abs(kq)) matrix = np.identity(7) @@ -100,7 +109,7 @@ def quad(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix - def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: + def bend_matrix(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: if length <= 0: return np.identity(7) @@ -127,7 +136,7 @@ def bend(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarr matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) return matrix - def tilt(self, angle: float) -> np.ndarray: + def tilt_matrix(self, angle: float) -> np.ndarray: matrix = np.identity(7) matrix[0, 0] = matrix[1, 1] = +math.cos(angle) matrix[0, 2] = matrix[1, 3] = +math.sin(angle) @@ -135,24 +144,30 @@ def tilt(self, angle: float) -> np.ndarray: matrix[2, 2] = matrix[3, 3] = +math.cos(angle) return matrix - def translation(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: + def translation_matrix(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: matrix = np.identity(7) matrix[0, 6] = x matrix[2, 6] = y matrix[4, 6] = z return matrix - def kick(self, kx: float, ky: float, dE: float) -> np.ndarray: + def kick_matrix(self, kx: float, ky: float, dE: float) -> np.ndarray: matrix = np.identity(7) matrix[1, 6] = kx matrix[3, 6] = ky matrix[5, 6] = dE return matrix + def solenoid_matrix(self, length: float, B: float, sync_part: SyncParticle) -> np.ndarray: + raise NotImplementedError() + + def cf_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + raise NotImplementedError() + def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: if type(node) is DriftTEAPOT: length = node.getLength(part_index) - return self.drift(length=length, sync_part=sync_part) + return self.drift_matrix(length=length, sync_part=sync_part) elif type(node) is QuadTEAPOT: length = node.getLength(part_index) @@ -162,14 +177,15 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) scale = node.waveform.getStrength() kq = scale * node.getParam("kq") - return self.quad(length=length, kq=kq, sync_part=sync_part) + return self.quad_matrix(length=length, kq=kq, sync_part=sync_part) elif type(node) is BendTEAPOT: length = node.getLength(part_index) theta = node.getParam("theta") / node.getnParts() - return self.bend(length=length, theta=theta, sync_part=sync_part) + return self.bend_matrix(length=length, theta=theta, sync_part=sync_part) elif type(node) is KickTEAPOT: + length = node.getLength(part_index) nparts = node.getnParts() scale = 1.0 @@ -179,18 +195,22 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) kx = scale * node.getParam("kx") / nparts ky = scale * node.getParam("ky") / nparts dE = node.getParam("dE") / nparts - return self.kick(kx, ky, dE) + + return np.matmul( + self.kick_matrix(kx=kx, ky=ky, dE=dE), + self.drift_matrix(length=length, sync_part=sync_part) + ) elif type(node) is TiltTEAPOT: angle = node.getTiltAngle() - return self.tilt(angle) + return self.tilt_matrix(angle) elif type(node) in self.ignore_node_types: return np.identity(7) else: if self.handle_unknown == "drift": - return self.drift(length=node.getLength(), sync_part=sync_part) + return self.drift_matrix(length=node.getLength(), sync_part=sync_part) elif self.handle_unknown == "fit": raise NotImplementedError() raise NotImplementedError("Unsupported node type: {}. See `handle_unknown` attribute.".format(type(node))) From f674b12a5cca9a5395b740c1f21dcf6cc10e1e1c Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 14:28:13 -0400 Subject: [PATCH 068/183] Fix atol/rtol in tests Comparison is |a - b| <= atol + rtol * |b| --- examples/Envelope/test_env.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 73135922..7445f9af 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -42,7 +42,7 @@ def track_and_compare_rms( cov_matrix: np.ndarray, nparts: int = 100_000, rtol: float = 1e-5, - atol: float = 1e12, + atol: float = 0, verbose: int = 1, ) -> dict: """Track bunch/envelope and compare rms beam sizes. @@ -165,7 +165,7 @@ def test_kick( length: float = 0.1, kx: float = 0.001, ky: float = 0.001, - dE: float = 0.001, + dE: float = 0.0001, cov_matrix: np.ndarray = None, ) -> None: nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length)] @@ -188,8 +188,8 @@ def test_tilt( if __name__ == "__main__": - test_drift() - test_quad() - test_dipole() + # test_drift() + # test_quad() + # test_dipole() test_kick() From ae9f605e30f5f845ffdcbcc7bb2f61259bb458a6 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 17:55:02 -0400 Subject: [PATCH 069/183] Fix confusion on covariance vs. moments matrix --- examples/Envelope/test_env.py | 47 +-- examples/Envelope/test_env_2d_fodo.py | 466 ++++++++++++++------------ py/orbit/envelope/envelope.py | 42 ++- py/orbit/envelope/matrix.py | 28 +- 4 files changed, 307 insertions(+), 276 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 7445f9af..625f6b52 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -41,8 +41,6 @@ def track_and_compare_rms( kin_energy: float, cov_matrix: np.ndarray, nparts: int = 100_000, - rtol: float = 1e-5, - atol: float = 0, verbose: int = 1, ) -> dict: """Track bunch/envelope and compare rms beam sizes. @@ -52,8 +50,7 @@ def track_and_compare_rms( kin_energy: Synchronous particle kinetic energy [GeV]. cov_matrix: 6 x 6 covariance matrix. nparts: Number of particles in bunch. - rtol/atol: Relative/absolute tolerance on rms beam sizes (bunch vs. envelope). - Units are [mm, mrad]. + rtol: Relative tolerance on rms beam sizes (bunch vs. envelope). verbose: Whether to print results. """ cov_scale = 1e6 @@ -105,21 +102,29 @@ def track_and_compare_rms( print(" env: {}".format(data["env"]["rms"][key][i])) print(" bunch: {}".format(data["bunch"]["rms"][key][i])) - for key in ["in", "out"]: - assert np.all( - np.isclose( - data["env"]["cov"][key], data["bunch"]["cov"][key], rtol=rtol, atol=atol - ) + assert np.all(np.isclose(data["env"]["cov"]["in"], data["bunch"]["cov"]["in"])) + + atol = np.ones(6) + atol[0:4] = 1e-3 # [mm mrad] + atol[4] = 1e-3 # [mm] + atol[5] = 1e-3 # [MeV] + + for i in range(6): + assert np.isclose( + data["env"]["rms"]["out"][i], + data["bunch"]["rms"]["out"][i], + atol=atol[i], + rtol=0, ) def make_default_cov_matrix( - rms_x: float = 0.001, - rms_xp: float = 0.001, - rms_y: float = 0.001, - rms_yp: float = 0.001, - rms_z: float = 0.001, - rms_dE: float = 0.00001, + rms_x: float = 1e-3, + rms_xp: float = 1e-3, + rms_y: float = 1e-3, + rms_yp: float = 1e-3, + rms_z: float = 1e-3, + rms_dE: float = 1e-5, ) -> np.ndarray: return np.diag(np.square([rms_x, rms_xp, rms_y, rms_yp, rms_z, rms_dE])) @@ -162,10 +167,10 @@ def test_dipole( def test_kick( kin_energy: float = 0.0025, - length: float = 0.1, + length: float = 1.0, kx: float = 0.001, ky: float = 0.001, - dE: float = 0.0001, + dE: float = 0.00001, cov_matrix: np.ndarray = None, ) -> None: nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length)] @@ -188,8 +193,8 @@ def test_tilt( if __name__ == "__main__": - # test_drift() - # test_quad() - # test_dipole() - test_kick() + test_drift() + test_quad() + test_dipole() + # test_kick() diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 8184f5bf..8eb3de58 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -60,268 +60,294 @@ args = parser.parse_args() -# Setup -# ------------------------------------------------------------------------------ +def main(args: argparse.Namespace) -> None: -path = pathlib.Path(__file__) -output_dir = os.path.join("outputs", path.stem) -os.makedirs(output_dir, exist_ok=True) + # Setup + # ------------------------------------------------------------------------------ + path = pathlib.Path(__file__) + output_dir = os.path.join("outputs", path.stem) + os.makedirs(output_dir, exist_ok=True) -# Create lattice -# ------------------------------------------------------------------------------ -nodes = [ - QuadTEAPOT(length=0.5, kq=+args.kq), - DriftTEAPOT(length=1.0), - QuadTEAPOT(length=1.0, kq=-args.kq), - DriftTEAPOT(length=1.0), - QuadTEAPOT(length=0.5, kq=+args.kq), -] + # Create lattice + # ------------------------------------------------------------------------------ -lattice = TEAPOT_Lattice() -for node in nodes: - node.setnParts(args.nslice) - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) - lattice.addNode(node) + nodes = [ + QuadTEAPOT(length=0.5, kq=+args.kq), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=1.0, kq=-args.kq), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=0.5, kq=+args.kq), + ] -lattice.initialize() + lattice = TEAPOT_Lattice() + for node in nodes: + node.setnParts(args.nslice) + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + lattice.addNode(node) + lattice.initialize() -# Create envelope -# ------------------------------------------------------------------------------ -# Create bunch -bunch = Bunch() -bunch.mass(mass_proton) -sync_part = bunch.getSyncParticle() -sync_part.kinEnergy(args.kin_energy) - -# Find periodic lattice parameters -matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) -matrix_lattice_params = matrix_lattice.getRingParametersDict() -alpha_x = matrix_lattice_params["alpha x"] -alpha_y = matrix_lattice_params["alpha y"] -beta_x = matrix_lattice_params["beta x [m]"] -beta_y = matrix_lattice_params["beta y [m]"] -eps_x = 0.25e-06 -eps_y = eps_x - -# Generate covariance matrix -cov_matrix = np.zeros((6, 6)) -cov_matrix[0, 0] = eps_x * beta_x -cov_matrix[2, 2] = eps_y * beta_y -cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x -cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y -cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x -cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y -cov_matrix[4, 4] = args.zrms**2 -cov_matrix[5, 5] = 0.0 - -# Tilt -if args.tilt: - rot_matrix = np.identity(6) - rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) - cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) - -# Mismatch -cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 -cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 -cov_matrix_init = np.copy(cov_matrix) - -# Offset -centroid_init = np.zeros(6) -centroid_init[0] += args.offset_x -centroid_init[2] += args.offset_y - -# Create envelope -envelope = Envelope( - sync_part=sync_part, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, -) + # Create envelope + # ------------------------------------------------------------------------------ + # Create bunch + bunch = Bunch() + bunch.mass(mass_proton) + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(args.kin_energy) -# Track envelope -# ------------------------------------------------------------------------------ + # Find periodic lattice parameters + matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) + matrix_lattice_params = matrix_lattice.getRingParametersDict() + alpha_x = matrix_lattice_params["alpha x"] + alpha_y = matrix_lattice_params["alpha y"] + beta_x = matrix_lattice_params["beta x [m]"] + beta_y = matrix_lattice_params["beta y [m]"] + eps_x = 0.25e-06 + eps_y = eps_x -print("TRACK ENVELOPE") + # Generate covariance matrix + cov_matrix = np.zeros((6, 6)) + cov_matrix[0, 0] = eps_x * beta_x + cov_matrix[2, 2] = eps_y * beta_y + cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x + cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y + cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x + cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y + cov_matrix[4, 4] = args.zrms**2 + cov_matrix[5, 5] = 0.0 + + # Tilt + if args.tilt: + rot_matrix = np.identity(6) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) + cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) + + # Mismatch + cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 + cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 + cov_matrix_init = np.copy(cov_matrix) + + # Offset + centroid_init = np.zeros(6) + centroid_init[0] += args.offset_x + centroid_init[2] += args.offset_y + + # Create envelope + envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, + ) -tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} -for turn in range(args.turns): - if turn > 0: - tracker.track(envelope) + # Track envelope + # ------------------------------------------------------------------------------ - cov_matrix = envelope.cov() - centroid = envelope.centroid() + print("TRACK ENVELOPE") - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * centroid[0] - yavg = 1000.0 * centroid[2] + tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) + cov_matrix = envelope.cov() + centroid = envelope.centroid() -histories = {} -histories["envelope"] = copy.deepcopy(history) + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * centroid[0] + yavg = 1000.0 * centroid[2] + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) -# Track bunch -# ------------------------------------------------------------------------------ + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) -print("TRACK BUNCH") + histories = {} + histories["envelope"] = copy.deepcopy(history) -rng = np.random.default_rng() -bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist -) -bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) -bunch_coords += centroid_init[None, :6] + # Track bunch + # ------------------------------------------------------------------------------ -for i in range(bunch_coords.shape[0]): - bunch.addParticle(*bunch_coords[i]) + print("TRACK BUNCH") -if args.sc: - sc_calc = SpaceChargeCalc2p5D(128, 128, 1) - sc_path_length_min = 1.00e-06 - sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + rng = np.random.default_rng() - bunch_size = bunch.getSizeGlobal() - bunch.macroSize(args.intensity / bunch_size) + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, :4] = gen_dist( + n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist + ) + bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) + bunch_coords += centroid_init[None, :6] -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} -for turn in range(args.turns): - if turn > 0: - lattice.trackBunch(bunch) + for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + if args.sc: + sc_calc = SpaceChargeCalc2p5D(128, 128, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * twiss_calc.getAverage(0) - yavg = 1000.0 * twiss_calc.getAverage(2) + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) -histories["bunch"] = copy.deepcopy(history) + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * twiss_calc.getAverage(0) + yavg = 1000.0 * twiss_calc.getAverage(2) -# Analysis -# ------------------------------------------------------------------------------ + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) -for history in histories.values(): - for key in history: - history[key] = np.array(history[key]) - -# Print errors -for key in histories["envelope"]: - deltas = histories["bunch"][key] - histories["envelope"][key] - print("key:", key) - print("max_abs_delta:", np.max(np.abs(deltas))) - print("avg_abs_delta:", np.mean(np.abs(deltas))) - -# Plot rms bunch sizes -for key in ["xrms", "yrms"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) - ax.set_xlabel("Turn") - ax.set_ylabel("RMS [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + + histories["bunch"] = copy.deepcopy(history) + + + # Analysis + # ------------------------------------------------------------------------------ + + for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + + # Print errors + for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + + # Plot rms bunch sizes + for key in ["xrms", "yrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + # Plot centroids + for key in ["xavg", "yavg"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel("AVG [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + + # Collect bunch/envelope data on final turn. + particles = collect_bunch(bunch)["coords"] + particles[:, :4] *= 1000.0 + + env_cov_matrix = envelope.cov() + env_cov_matrix[:4, :4] *= 1000.0**2 + + env_centroid = envelope.centroid() + env_centroid[:4] *= 1000.0 + + xmax = 4.0 * np.std(particles, axis=0) + limits = list(zip(-xmax, xmax)) + labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + + + # Plot x-x' + fig, ax = plt.subplots(figsize=(4, 4)) + ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) + plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, + ) + ax.set_xlabel(labels[0]) + ax.set_ylabel(labels[1]) + plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) plt.close() -# Plot centroids -for key in ["xavg", "yavg"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(-5.0, 5.0) - ax.set_xlabel("Turn") - ax.set_ylabel("AVG [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) + # Plot corner + fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, + ) + for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) + plt.savefig(os.path.join(output_dir, "fig_dist_corner")) plt.close() -# Collect bunch/envelope data on final turn. -particles = collect_bunch(bunch)["coords"] -particles[:, :4] *= 1000.0 - -env_cov_matrix = envelope.cov() -env_cov_matrix[:4, :4] *= 1000.0**2 +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--zrms", type=float, default=5.0) + parser.add_argument("--kin-energy", type=float, default=0.0025) + parser.add_argument("--intensity", type=float, default=5e9) -env_centroid = envelope.centroid() -env_centroid[:4] *= 1000.0 + parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) + parser.add_argument("--mismatch-x", type=float, default=0.0) + parser.add_argument("--mismatch-y", type=float, default=0.0) + parser.add_argument("--offset-x", type=float, default=0.0) + parser.add_argument("--offset-y", type=float, default=0.0) + parser.add_argument("--tilt", type=float, default=0) -xmax = 4.0 * np.std(particles, axis=0) -limits = list(zip(-xmax, xmax)) -labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + parser.add_argument("--nslice", type=int, default=10) + parser.add_argument("--kq", type=float, default=0.25) + parser.add_argument("--nparts", type=int, default=100_000) + parser.add_argument("--turns", type=int, default=25) + parser.add_argument("--sc", type=int, default=0) + args = parser.parse_args() -# Plot x-x' -fig, ax = plt.subplots(figsize=(4, 4)) -ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) -plot_rms_ellipse( - env_cov_matrix[0:2, 0:2], - center=(env_centroid[0], env_centroid[1]), - level=2.0, - color="red", - ax=ax, -) -ax.set_xlabel(labels[0]) -ax.set_ylabel(labels[1]) -plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) -plt.close() - -# Plot corner -fig, axs = plot_corner( - particles, - limits=limits, - bins=100, - labels=labels, -) -for i in range(6): - for j in range(i): - env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) - plot_rms_ellipse( - env_cov_matrix_proj, - center=(env_centroid[j], env_centroid[i]), - level=2.0, - color="red", - ax=axs[i, j], - ) -plt.savefig(os.path.join(output_dir, "fig_dist_corner")) -plt.close() + main(args) \ No newline at end of file diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index cce9aa0c..8743dacb 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -46,12 +46,11 @@ class Envelope: and U is 6 x 1 "driving" vector. The augmented vector Y evolves according to Y -> NY, where N = [[M, U], [0, 1]] is a 7 x 7 matrix. - We track the 7 x 7 covariance matrix of Y: - - R = = [[, ], [, 1]], - - which contains both the phase space covariance matrix and centroid vector. - R evolves according to R -> N R N^T. + Let S = = [[, ], [, 1]] = [[R, C], [C^T, 1]]. Here + R = is the matrix of second moments, or "autocorrelation" matrix, + and C = is the mean/centroid vector. (To get the covariance matrix: + <(X - C)(X - C)^T> = - ^T = R - CC^T.) S evolves according + to S -> N S N^T. """ def __init__( @@ -62,22 +61,21 @@ def __init__( intensity: float = 0.0, ) -> None: self.sync_part = sync_part + self.dim = 6 if centroid is None: centroid = np.zeros(6) if cov_matrix is None: cov_matrix = np.eye(6) - - self.matrix = np.zeros((7, 7)) - self.matrix[0:6, 0:6] = cov_matrix - self.matrix[0:6, 6] = centroid - self.matrix[6, 0:6] = centroid - self.matrix[6, 6] = 1.0 + + self.moment_matrix = np.zeros((7, 7)) + self.moment_matrix[:self.dim, :self.dim] = cov_matrix + np.outer(centroid, centroid) + self.moment_matrix[:self.dim, self.dim] = centroid + self.moment_matrix[self.dim, :self.dim] = centroid + self.moment_matrix[self.dim, self.dim] = 1.0 self.intensity = 0.0 - self.perveance_2d = 0.0 - self.perveance_3d = 0.0 self.set_intensity(intensity) def set_intensity(self, intensity: float) -> None: @@ -99,24 +97,24 @@ def mass(self) -> float: return self.sync_part.mass() def centroid(self) -> np.ndarray: - return np.copy(self.matrix[0:6, 6]) + return np.copy(self.moment_matrix[:self.dim, self.dim]) def cov(self) -> np.ndarray: - return np.copy(self.matrix[0:6, 0:6]) + autocorrelation_matrix = self.moment_matrix[:self.dim, :self.dim] + centroid = self.moment_matrix[:self.dim, self.dim] + return autocorrelation_matrix - np.outer(centroid, centroid) def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov())) return rms_arr[axis] - def apply_transfer_matrix(self, transfer_matrix: np.ndarray) -> None: - self.matrix = np.linalg.multi_dot( - [transfer_matrix, self.matrix, transfer_matrix.T] - ) + def apply_transfer_matrix(self, m: np.ndarray) -> None: + self.moment_matrix = np.linalg.multi_dot([m, self.moment_matrix, m.T]) - def sample(self, n: int, dist: str = "kv") -> np.ndarray: + def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, # giving error in cholesky decomposition. - particles = gen_dist(n=n, cov_matrix=self.cov(), name=dist) + particles = gen_dist(n=size, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 96c11a77..098da7dd 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -15,7 +15,7 @@ from ..teapot import FringeFieldTEAPOT from ..teapot import MonitorTEAPOT from ..teapot import TurnCounterTEAPOT - +from ..teapot import MultipoleTEAPOT from ..utils import speed_of_light @@ -146,16 +146,16 @@ def tilt_matrix(self, angle: float) -> np.ndarray: def translation_matrix(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: matrix = np.identity(7) - matrix[0, 6] = x - matrix[2, 6] = y - matrix[4, 6] = z + matrix[0, -1] = x + matrix[2, -1] = y + matrix[4, -1] = z return matrix - def kick_matrix(self, kx: float, ky: float, dE: float) -> np.ndarray: + def kick_matrix(self, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: matrix = np.identity(7) - matrix[1, 6] = kx - matrix[3, 6] = ky - matrix[5, 6] = dE + matrix[1, -1] = kx + matrix[3, -1] = ky + matrix[5, -1] = kE return matrix def solenoid_matrix(self, length: float, B: float, sync_part: SyncParticle) -> np.ndarray: @@ -192,12 +192,12 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) if node.waveform: scale = node.waveform.getStrength() - kx = scale * node.getParam("kx") / nparts - ky = scale * node.getParam("ky") / nparts - dE = node.getParam("dE") / nparts + kx = scale * node.getParam("kx") / (nparts - 1) + ky = scale * node.getParam("ky") / (nparts - 1) + kE = node.getParam("dE") / (nparts - 1) return np.matmul( - self.kick_matrix(kx=kx, ky=ky, dE=dE), + self.kick_matrix(kx=kx, ky=ky, kE=kE), self.drift_matrix(length=length, sync_part=sync_part) ) @@ -211,6 +211,8 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) else: if self.handle_unknown == "drift": return self.drift_matrix(length=node.getLength(), sync_part=sync_part) + elif self.handle_unknown == "fit": raise NotImplementedError() - raise NotImplementedError("Unsupported node type: {}. See `handle_unknown` attribute.".format(type(node))) + + raise NotImplementedError("Unsupported node: {}.".format(node)) From 132729660423f1f9b5f2db546b9259348b7b0df3 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 17:57:47 -0400 Subject: [PATCH 070/183] Clean up script --- examples/Envelope/test_env_3d_drift.py | 402 ++++++++++++------------- 1 file changed, 199 insertions(+), 203 deletions(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 5e344249..fcac7d48 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -28,222 +28,218 @@ plt.style.use("style.mplstyle") -# Parse arguments -# ------------------------------------------------------------------------------ +def main(args: argparse.Namespace) -> None: -parser = argparse.ArgumentParser() -parser.add_argument("--kin-energy", type=float, default=0.0025) -parser.add_argument("--intensity", type=float, default=5e10) + # Setup + # ------------------------------------------------------------------------------ + path = pathlib.Path(__file__) + output_dir = os.path.join("outputs", path.stem) + os.makedirs(output_dir, exist_ok=True) -parser.add_argument("--xrms", type=float, default=0.010) -parser.add_argument("--yrms", type=float, default=0.010) -parser.add_argument("--zrms", type=float, default=0.010) -parser.add_argument("--nslice", type=int, default=10) -parser.add_argument("--length", type=float, default=0.1) -parser.add_argument("--turns", type=int, default=20) -parser.add_argument("--sc-grid", type=int, default=64) + # Create lattice + # ------------------------------------------------------------------------------ + node = DriftTEAPOT(length=args.length) + node.setLength(args.length) + node.setnParts(args.nslice) -parser.add_argument("--nparts", type=int, default=100_000) -parser.add_argument("--sc", type=int, default=0) -args = parser.parse_args() + lattice = TEAPOT_Lattice() + lattice.addNode(node) + lattice.initialize() -# Setup -# ------------------------------------------------------------------------------ + # Create envelope + # ------------------------------------------------------------------------------ + bunch = Bunch() + bunch.mass(mass_proton) + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(args.kin_energy) -path = pathlib.Path(__file__) -output_dir = os.path.join("outputs", path.stem) -os.makedirs(output_dir, exist_ok=True) + cov_matrix_init = np.zeros((6, 6)) + cov_matrix_init[0, 0] = args.xrms**2 + cov_matrix_init[2, 2] = args.yrms**2 + cov_matrix_init[4, 4] = (args.zrms / sync_part.gamma()) ** 2 + centroid_init = np.zeros(6) -# Create lattice -# ------------------------------------------------------------------------------ - -node = DriftTEAPOT(length=args.length) -node.setLength(args.length) -node.setnParts(args.nslice) - -lattice = TEAPOT_Lattice() -lattice.addNode(node) -lattice.initialize() - - -# Create envelope -# ------------------------------------------------------------------------------ - -bunch = Bunch() -bunch.mass(mass_proton) -sync_part = bunch.getSyncParticle() -sync_part.kinEnergy(args.kin_energy) - -cov_matrix_init = np.zeros((6, 6)) -cov_matrix_init[0, 0] = args.xrms**2 -cov_matrix_init[2, 2] = args.yrms**2 -cov_matrix_init[4, 4] = (args.zrms / sync_part.gamma()) ** 2 - -centroid_init = np.zeros(6) - -envelope = Envelope( - sync_part=sync_part, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, -) - -# Track envelope -# ------------------------------------------------------------------------------ - -print("TRACK ENVELOPE") - -tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) - -history = {"xrms": [], "yrms": [], "zrms": []} -for turn in range(args.turns): - if turn > 0: - tracker.track(envelope) - - cov_matrix = envelope.cov() - centroid = envelope.centroid() - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["zrms"].append(zrms) - - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") - -histories = {} -histories["envelope"] = copy.deepcopy(history) - - -# Track bunch -# ------------------------------------------------------------------------------ - -print("TRACK BUNCH") - -bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, (0, 2, 4)] = gen_dist( - args.nparts, cov_matrix=np.eye(3), name="waterbag" -) -bunch_coords[:, 0] *= args.xrms -bunch_coords[:, 2] *= args.yrms -bunch_coords[:, 4] *= args.zrms / sync_part.gamma() + envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, + ) + + # Track envelope + # ------------------------------------------------------------------------------ + print("TRACK ENVELOPE") + + tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) + + history = {"xrms": [], "yrms": [], "zrms": []} + for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) + + cov_matrix = envelope.cov() + centroid = envelope.centroid() + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") + + histories = {} + histories["envelope"] = copy.deepcopy(history) + + + # Track bunch + # ------------------------------------------------------------------------------ + print("TRACK BUNCH") + + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, (0, 2, 4)] = gen_dist( + args.nparts, cov_matrix=np.eye(3), name="waterbag" + ) + bunch_coords[:, 0] *= args.xrms + bunch_coords[:, 2] *= args.yrms + bunch_coords[:, 4] *= args.zrms / sync_part.gamma() + + for x, xp, y, yp, z, dE in bunch_coords: + bunch.addParticle(x, xp, y, yp, z, dE) + + size_global = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / size_global) + + if args.sc: + sc_calc = SpaceChargeCalc3D(args.sc_grid, args.sc_grid, args.sc_grid) + sc_path_length_min = 0.01 + sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) + + history = {"xrms": [], "yrms": [], "zrms": []} + for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * bunch.getSyncParticle().gamma() + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") + + histories["bunch"] = copy.deepcopy(history) + + + # Analysis + # ------------------------------------------------------------------------------ + for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + + # Print errors + for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + + # Plot rms bunch sizes + for key in ["xrms", "yrms", "zrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1]) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper left") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + # Collect bunch/envelope data on final turn. + particles = collect_bunch(bunch)["coords"] + particles *= 1e3 + + env_cov_matrix = envelope.cov() + env_cov_matrix *= 1e6 + + env_centroid = envelope.centroid() + env_centroid *= 1e3 + + xmax = 4.0 * np.std(particles, axis=0) + limits = list(zip(-xmax, xmax)) + labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] + + # Plot x-x' + fig, ax = plt.subplots(figsize=(4, 4)) + ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) + plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, + ) + ax.set_xlabel(labels[0]) + ax.set_ylabel(labels[1]) + plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) + plt.close() -for x, xp, y, yp, z, dE in bunch_coords: - bunch.addParticle(x, xp, y, yp, z, dE) + # Plot corner + fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, + ) + for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) + plt.savefig(os.path.join(output_dir, "fig_dist_corner")) + plt.close() -size_global = bunch.getSizeGlobal() -bunch.macroSize(args.intensity / size_global) -if args.sc: - sc_calc = SpaceChargeCalc3D(args.sc_grid, args.sc_grid, args.sc_grid) - sc_path_length_min = 0.01 - sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--kin-energy", type=float, default=0.0025) + parser.add_argument("--intensity", type=float, default=5e10) -history = {"xrms": [], "yrms": [], "zrms": []} -for turn in range(args.turns): - if turn > 0: - lattice.trackBunch(bunch) + parser.add_argument("--xrms", type=float, default=0.010) + parser.add_argument("--yrms", type=float, default=0.010) + parser.add_argument("--zrms", type=float, default=0.010) - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + parser.add_argument("--nslice", type=int, default=10) + parser.add_argument("--length", type=float, default=0.1) + parser.add_argument("--turns", type=int, default=20) + parser.add_argument("--sc-grid", type=int, default=64) - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * bunch.getSyncParticle().gamma() - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["zrms"].append(zrms) - - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") - -histories["bunch"] = copy.deepcopy(history) - - -# Analysis -# ------------------------------------------------------------------------------ - -for history in histories.values(): - for key in history: - history[key] = np.array(history[key]) - -# Print errors -for key in histories["envelope"]: - deltas = histories["bunch"][key] - histories["envelope"][key] - print("key:", key) - print("max_abs_delta:", np.max(np.abs(deltas))) - print("avg_abs_delta:", np.mean(np.abs(deltas))) - -# Plot rms bunch sizes -for key in ["xrms", "yrms", "zrms"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1]) - ax.set_xlabel("Turn") - ax.set_ylabel("RMS [mm]") - ax.legend(loc="upper left") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close() + parser.add_argument("--nparts", type=int, default=100_000) + parser.add_argument("--sc", type=int, default=0) + args = parser.parse_args() -# Collect bunch/envelope data on final turn. -particles = collect_bunch(bunch)["coords"] -particles *= 1e3 - -env_cov_matrix = envelope.cov() -env_cov_matrix *= 1e6 - -env_centroid = envelope.centroid() -env_centroid *= 1e3 - -xmax = 4.0 * np.std(particles, axis=0) -limits = list(zip(-xmax, xmax)) -labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] - -# Plot x-x' -fig, ax = plt.subplots(figsize=(4, 4)) -ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) -plot_rms_ellipse( - env_cov_matrix[0:2, 0:2], - center=(env_centroid[0], env_centroid[1]), - level=2.0, - color="red", - ax=ax, -) -ax.set_xlabel(labels[0]) -ax.set_ylabel(labels[1]) -plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) -plt.close() - -# Plot corner -fig, axs = plot_corner( - particles, - limits=limits, - bins=100, - labels=labels, -) -for i in range(6): - for j in range(i): - env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) - plot_rms_ellipse( - env_cov_matrix_proj, - center=(env_centroid[j], env_centroid[i]), - level=2.0, - color="red", - ax=axs[i, j], - ) -plt.savefig(os.path.join(output_dir, "fig_dist_corner")) -plt.close() + main(args) \ No newline at end of file From 1a86b992320533023f86da2a84946dc19fcb679f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 17:59:57 -0400 Subject: [PATCH 071/183] Clean up script --- examples/Envelope/test_env_sns_ring.py | 545 +++++++++++++------------ 1 file changed, 275 insertions(+), 270 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index f5ef19ef..ec89f8ac 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -30,296 +30,301 @@ plt.style.use("style.mplstyle") -# Parse arguments -# ------------------------------------------------------------------------------ - -parser = argparse.ArgumentParser() -parser.add_argument("--bunch-length", type=float, default=120.0) -parser.add_argument("--kin-energy", type=float, default=1.300) -parser.add_argument("--intensity", type=float, default=5e14) - -parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) -parser.add_argument("--mismatch-x", type=float, default=0.0) -parser.add_argument("--mismatch-y", type=float, default=0.0) -parser.add_argument("--offset-x", type=float, default=0.0) -parser.add_argument("--offset-y", type=float, default=0.0) -parser.add_argument("--tilt", type=float, default=0) - -parser.add_argument("--nparts", type=int, default=100_000) -parser.add_argument("--turns", type=int, default=25) -parser.add_argument("--sol", type=int, default=0) -parser.add_argument("--sc", type=int, default=0) -parser.add_argument("--sc-grid", type=int, default=64) - -parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) -args = parser.parse_args() - - -# Setup -# ------------------------------------------------------------------------------ - -path = pathlib.Path(__file__) -output_dir = os.path.join("outputs", path.stem) -os.makedirs(output_dir, exist_ok=True) - - -# Create lattice -# ------------------------------------------------------------------------------ - -lattice = TEAPOT_Ring() -lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") -lattice.initialize() - -for node in lattice.getNodes(): - try: - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) - except: - pass - -if args.sol: - for name in ["scbdsol_c13a", "scbdsol_c13b"]: - node = lattice.getNodeForName(name) - node.setParam("B", 0.15) - - -# Create envelope -# ------------------------------------------------------------------------------ - -# Create bunch -bunch = Bunch() -bunch.mass(mass_proton) -sync_part = bunch.getSyncParticle() -sync_part.kinEnergy(args.kin_energy) - -# Find periodic lattice parameters -matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) -matrix_lattice_params = matrix_lattice.getRingParametersDict() -alpha_x = matrix_lattice_params["alpha x"] -alpha_y = matrix_lattice_params["alpha y"] -beta_x = matrix_lattice_params["beta x [m]"] -beta_y = matrix_lattice_params["beta y [m]"] -eps_x = 25.0e-06 -eps_y = eps_x - -# Generate covariance matrix -cov_matrix = np.zeros((6, 6)) -cov_matrix[0, 0] = eps_x * beta_x -cov_matrix[2, 2] = eps_y * beta_y -cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x -cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y -cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x -cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y -cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 -cov_matrix[5, 5] = 0.0 - -# Tilt -if args.tilt: - rot_matrix = np.identity(6) - rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) - cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) - -# Mismatch -cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 -cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 -cov_matrix_init = np.copy(cov_matrix) - -# Offset -centroid_init = np.zeros(6) -centroid_init[0] += args.offset_x -centroid_init[2] += args.offset_y - -# Create envelope -envelope = Envelope( - sync_part=sync_part, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, -) - - -# Track envelope -# ------------------------------------------------------------------------------ - -print("TRACK ENVELOPE") - -tracker = EnvelopeTracker( - lattice, - handle_unknown=args.handle_unknown, - space_charge=("2d" if args.sc else None), -) - -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} -for turn in range(args.turns): - if turn > 0: - tracker.track(envelope) - - cov_matrix = envelope.cov() - centroid = envelope.centroid() - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * centroid[0] - yavg = 1000.0 * centroid[2] - - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" +def main(args: argparse.Namespace) -> None: + + # Setup + # ------------------------------------------------------------------------------ + + path = pathlib.Path(__file__) + output_dir = os.path.join("outputs", path.stem) + os.makedirs(output_dir, exist_ok=True) + + + # Create lattice + # ------------------------------------------------------------------------------ + + lattice = TEAPOT_Ring() + lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") + lattice.initialize() + + for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass + + if args.sol: + for name in ["scbdsol_c13a", "scbdsol_c13b"]: + node = lattice.getNodeForName(name) + node.setParam("B", 0.15) + + + # Create envelope + # ------------------------------------------------------------------------------ + + # Create bunch + bunch = Bunch() + bunch.mass(mass_proton) + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(args.kin_energy) + + # Find periodic lattice parameters + matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) + matrix_lattice_params = matrix_lattice.getRingParametersDict() + alpha_x = matrix_lattice_params["alpha x"] + alpha_y = matrix_lattice_params["alpha y"] + beta_x = matrix_lattice_params["beta x [m]"] + beta_y = matrix_lattice_params["beta y [m]"] + eps_x = 25.0e-06 + eps_y = eps_x + + # Generate covariance matrix + cov_matrix = np.zeros((6, 6)) + cov_matrix[0, 0] = eps_x * beta_x + cov_matrix[2, 2] = eps_y * beta_y + cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x + cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y + cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x + cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y + cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 + cov_matrix[5, 5] = 0.0 + + # Tilt + if args.tilt: + rot_matrix = np.identity(6) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) + cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) + + # Mismatch + cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 + cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 + cov_matrix_init = np.copy(cov_matrix) + + # Offset + centroid_init = np.zeros(6) + centroid_init[0] += args.offset_x + centroid_init[2] += args.offset_y + + # Create envelope + envelope = Envelope( + sync_part=sync_part, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, ) - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) -histories = {} -histories["envelope"] = copy.deepcopy(history) + # Track envelope + # ------------------------------------------------------------------------------ + print("TRACK ENVELOPE") -# Track bunch -# ------------------------------------------------------------------------------ + tracker = EnvelopeTracker( + lattice, + handle_unknown=args.handle_unknown, + space_charge=("2d" if args.sc else None), + ) -print("TRACK BUNCH") + history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) -rng = np.random.default_rng() + cov_matrix = envelope.cov() + centroid = envelope.centroid() -bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist -) -bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) -bunch_coords += centroid_init[None, :6] + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * centroid[0] + yavg = 1000.0 * centroid[2] -for i in range(bunch_coords.shape[0]): - bunch.addParticle(*bunch_coords[i]) + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) -if args.sc: - sc_calc = SpaceChargeCalc2p5D(128, 128, 1) - sc_path_length_min = 1.00e-06 - sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) - bunch_size = bunch.getSizeGlobal() - bunch.macroSize(args.intensity / bunch_size) + histories = {} + histories["envelope"] = copy.deepcopy(history) -history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} -for turn in range(args.turns): - if turn > 0: - lattice.trackBunch(bunch) - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + # Track bunch + # ------------------------------------------------------------------------------ - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] + print("TRACK BUNCH") - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * twiss_calc.getAverage(0) - yavg = 1000.0 * twiss_calc.getAverage(2) + rng = np.random.default_rng() - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, :4] = gen_dist( + n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist ) + bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) + bunch_coords += centroid_init[None, :6] + + for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) - -histories["bunch"] = copy.deepcopy(history) - - -# Analysis -# ------------------------------------------------------------------------------ - -for history in histories.values(): - for key in history: - history[key] = np.array(history[key]) - -# Print errors -for key in histories["envelope"]: - deltas = histories["bunch"][key] - histories["envelope"][key] - print("key:", key) - print("max_abs_delta:", np.max(np.abs(deltas))) - print("avg_abs_delta:", np.mean(np.abs(deltas))) - -# Plot rms bunch sizes -for key in ["xrms", "yrms"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) - ax.set_xlabel("Turn") - ax.set_ylabel("RMS [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) + if args.sc: + sc_calc = SpaceChargeCalc2p5D(128, 128, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + + history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + xavg = 1000.0 * twiss_calc.getAverage(0) + yavg = 1000.0 * twiss_calc.getAverage(2) + + print( + f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" + ) + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["xavg"].append(xavg) + history["yavg"].append(yavg) + + histories["bunch"] = copy.deepcopy(history) + + + # Analysis + # ------------------------------------------------------------------------------ + + for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + + # Print errors + for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + + # Plot rms bunch sizes + for key in ["xrms", "yrms"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel("RMS [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + # Plot centroids + for key in ["xavg", "yavg"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel("AVG [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + + # Collect bunch/envelope data on final turn. + particles = collect_bunch(bunch)["coords"] + particles[:, :4] *= 1000.0 + + env_cov_matrix = envelope.cov() + env_cov_matrix[:4, :4] *= 1000.0**2 + + env_centroid = envelope.centroid() + env_centroid[:4] *= 1000.0 + + xmax = 4.0 * np.std(particles, axis=0) + limits = list(zip(-xmax, xmax)) + labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + + + # Plot x-x' + fig, ax = plt.subplots(figsize=(4, 4)) + ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) + plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, + ) + ax.set_xlabel(labels[0]) + ax.set_ylabel(labels[1]) + plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) plt.close() -# Plot centroids -for key in ["xavg", "yavg"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(-5.0, 5.0) - ax.set_xlabel("Turn") - ax.set_ylabel("AVG [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) + # Plot corner + fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, + ) + for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) + plt.savefig(os.path.join(output_dir, "fig_dist_corner")) plt.close() -# Collect bunch/envelope data on final turn. -particles = collect_bunch(bunch)["coords"] -particles[:, :4] *= 1000.0 - -env_cov_matrix = envelope.cov() -env_cov_matrix[:4, :4] *= 1000.0**2 - -env_centroid = envelope.centroid() -env_centroid[:4] *= 1000.0 - -xmax = 4.0 * np.std(particles, axis=0) -limits = list(zip(-xmax, xmax)) -labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] - - -# Plot x-x' -fig, ax = plt.subplots(figsize=(4, 4)) -ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) -plot_rms_ellipse( - env_cov_matrix[0:2, 0:2], - center=(env_centroid[0], env_centroid[1]), - level=2.0, - color="red", - ax=ax, -) -ax.set_xlabel(labels[0]) -ax.set_ylabel(labels[1]) -plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) -plt.close() - -# Plot corner -fig, axs = plot_corner( - particles, - limits=limits, - bins=100, - labels=labels, -) -for i in range(6): - for j in range(i): - env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) - plot_rms_ellipse( - env_cov_matrix_proj, - center=(env_centroid[j], env_centroid[i]), - level=2.0, - color="red", - ax=axs[i, j], - ) -plt.savefig(os.path.join(output_dir, "fig_dist_corner")) -plt.close() +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--bunch-length", type=float, default=120.0) + parser.add_argument("--kin-energy", type=float, default=1.300) + parser.add_argument("--intensity", type=float, default=5e14) + + parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) + parser.add_argument("--mismatch-x", type=float, default=0.0) + parser.add_argument("--mismatch-y", type=float, default=0.0) + parser.add_argument("--offset-x", type=float, default=0.0) + parser.add_argument("--offset-y", type=float, default=0.0) + parser.add_argument("--tilt", type=float, default=0) + + parser.add_argument("--nparts", type=int, default=100_000) + parser.add_argument("--turns", type=int, default=25) + parser.add_argument("--sol", type=int, default=0) + parser.add_argument("--sc", type=int, default=0) + parser.add_argument("--sc-grid", type=int, default=64) + + parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + main(args) \ No newline at end of file From 74ff3e0d8ebb36f6645e81a8f7cc240af2314335 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 18:05:55 -0400 Subject: [PATCH 072/183] Fix test --- examples/Envelope/test_env.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 625f6b52..97a5273b 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -50,7 +50,6 @@ def track_and_compare_rms( kin_energy: Synchronous particle kinetic energy [GeV]. cov_matrix: 6 x 6 covariance matrix. nparts: Number of particles in bunch. - rtol: Relative tolerance on rms beam sizes (bunch vs. envelope). verbose: Whether to print results. """ cov_scale = 1e6 @@ -110,12 +109,9 @@ def track_and_compare_rms( atol[5] = 1e-3 # [MeV] for i in range(6): - assert np.isclose( - data["env"]["rms"]["out"][i], - data["bunch"]["rms"]["out"][i], - atol=atol[i], - rtol=0, - ) + x = data["env"]["rms"]["out"][i] + y = data["bunch"]["rms"]["out"][i] + assert np.abs(x - y) <= atol[i] def make_default_cov_matrix( @@ -167,7 +163,7 @@ def test_dipole( def test_kick( kin_energy: float = 0.0025, - length: float = 1.0, + length: float = 0.1, kx: float = 0.001, ky: float = 0.001, dE: float = 0.00001, @@ -196,5 +192,5 @@ def test_tilt( test_drift() test_quad() test_dipole() - # test_kick() + test_kick() From 6ca2ada5e1abf368134cb47d1721aefe97fc4c1e Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 18:35:23 -0400 Subject: [PATCH 073/183] Add simplified SNS ring example Keep only quads, drifts, bends --- examples/Envelope/test_env_sns_ring.py | 51 ++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index ec89f8ac..513c1040 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -17,6 +17,7 @@ from orbit.envelope import EnvelopeTracker from orbit.core.spacecharge import SpaceChargeCalc2p5D from orbit.space_charge.sc2p5d import setSC2p5DAccNodes +from orbit.teapot import TEAPOT_Lattice from orbit.teapot import TEAPOT_Ring from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.utils.consts import mass_proton @@ -43,7 +44,7 @@ def main(args: argparse.Namespace) -> None: # Create lattice # ------------------------------------------------------------------------------ - lattice = TEAPOT_Ring() + lattice = TEAPOT_Lattice() lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") lattice.initialize() @@ -59,6 +60,47 @@ def main(args: argparse.Namespace) -> None: node = lattice.getNodeForName(name) node.setParam("B", 0.15) + if args.simple_lattice: + from orbit.teapot import QuadTEAPOT, DriftTEAPOT, BendTEAPOT + + new_nodes = [] + for node in lattice.getNodes(): + new_node = None + if type(node) is DriftTEAPOT: + new_node = DriftTEAPOT(length=node.getLength(), nparts=node.getnParts()) + elif type(node) is QuadTEAPOT: + new_node = QuadTEAPOT(length=node.getLength(), nparts=node.getnParts(), kq=node.getParam("kq")) + elif type(node) is BendTEAPOT: + new_node = BendTEAPOT(length=node.getLength(), nparts=node.getnParts(), theta=node.getParam("theta")) + else: + try: + new_node = DriftTEAPOT(length=node.getLength()) + except: + pass + + if new_node is not None: + new_nodes.append(new_node) + + lattice = TEAPOT_Ring() + for node in new_nodes: + lattice.addNode(node) + lattice.initialize() + + for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass + + for node in lattice.getNodes(): + print(node) + + for node in lattice.getNodes(): + max_length = 1.0 + if node.getLength() > max_length: + node.setnParts(1 + int(node.getLength() / max_length)) + # Create envelope # ------------------------------------------------------------------------------ @@ -79,6 +121,8 @@ def main(args: argparse.Namespace) -> None: eps_x = 25.0e-06 eps_y = eps_x + print(matrix_lattice_params) + # Generate covariance matrix cov_matrix = np.zeros((6, 6)) cov_matrix[0, 0] = eps_x * beta_x @@ -170,7 +214,7 @@ def main(args: argparse.Namespace) -> None: bunch.addParticle(*bunch_coords[i]) if args.sc: - sc_calc = SpaceChargeCalc2p5D(128, 128, 1) + sc_calc = SpaceChargeCalc2p5D(64, 64, 1) sc_path_length_min = 1.00e-06 sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) @@ -306,7 +350,7 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--bunch-length", type=float, default=120.0) parser.add_argument("--kin-energy", type=float, default=1.300) - parser.add_argument("--intensity", type=float, default=5e14) + parser.add_argument("--intensity", type=float, default=2e14) parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) parser.add_argument("--mismatch-x", type=float, default=0.0) @@ -322,6 +366,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--sc-grid", type=int, default=64) parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) + parser.add_argument("--simple-lattice", type=int, default=0) return parser.parse_args() From 303d11ae0b1bb2a25e170e16643cd0e95b7c1fd3 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 19:28:50 -0400 Subject: [PATCH 074/183] Add test_tilt --- examples/Envelope/test_env.py | 8 ++++++-- py/orbit/envelope/matrix.py | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 97a5273b..85b99369 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -31,8 +31,11 @@ def make_lattice(nodes: list[AccNode]) -> AccLattice: lattice.addNode(node) lattice.initialize() for node in lattice.getNodes(): - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass return lattice @@ -193,4 +196,5 @@ def test_tilt( test_quad() test_dipole() test_kick() + test_tilt() diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 098da7dd..e133151b 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -139,8 +139,8 @@ def bend_matrix(self, length: float, theta: float, sync_part: SyncParticle) -> n def tilt_matrix(self, angle: float) -> np.ndarray: matrix = np.identity(7) matrix[0, 0] = matrix[1, 1] = +math.cos(angle) - matrix[0, 2] = matrix[1, 3] = +math.sin(angle) - matrix[2, 0] = matrix[3, 1] = -math.sin(angle) + matrix[0, 2] = matrix[1, 3] = -math.sin(angle) + matrix[2, 0] = matrix[3, 1] = +math.sin(angle) matrix[2, 2] = matrix[3, 3] = +math.cos(angle) return matrix @@ -189,12 +189,12 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) nparts = node.getnParts() scale = 1.0 - if node.waveform: + if node.waveform is not None: scale = node.waveform.getStrength() - kx = scale * node.getParam("kx") / (nparts - 1) - ky = scale * node.getParam("ky") / (nparts - 1) - kE = node.getParam("dE") / (nparts - 1) + kx = scale * node.getParam("kx") / nparts + ky = scale * node.getParam("ky") / nparts + kE = node.getParam("dE") / nparts return np.matmul( self.kick_matrix(kx=kx, ky=ky, kE=kE), From 989e0fd1af4ff6355c22b0cbc5313dbed64b3445 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 11 Jun 2026 19:35:18 -0400 Subject: [PATCH 075/183] Add nparts to tests --- examples/Envelope/test_env.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 85b99369..69500407 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -129,9 +129,10 @@ def make_default_cov_matrix( def test_drift( - kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None + kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, + nparts: int = 6, ) -> None: - nodes = [DriftTEAPOT(length=length)] + nodes = [DriftTEAPOT(length=length, nparts=nparts)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -143,8 +144,9 @@ def test_quad( length: float = 1.0, kq: float = 1.0, cov_matrix: np.ndarray = None, + nparts: int = 10, ) -> None: - nodes = [QuadTEAPOT(length=length, kq=kq)] + nodes = [QuadTEAPOT(length=length, kq=kq, nparts=nparts)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -156,8 +158,9 @@ def test_dipole( length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None, + nparts: int = 2, ) -> None: - nodes = [BendTEAPOT(length=length, theta=np.radians(theta))] + nodes = [BendTEAPOT(length=length, theta=np.radians(theta), nparts=nparts)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() @@ -171,8 +174,9 @@ def test_kick( ky: float = 0.001, dE: float = 0.00001, cov_matrix: np.ndarray = None, + nparts: int = 4, ) -> None: - nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length)] + nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length, nparts=nparts)] lattice = make_lattice(nodes) if cov_matrix is None: cov_matrix = make_default_cov_matrix() From 37f70c6ac603b6086e92a2cb89247b827d52f5b2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 13:42:31 -0400 Subject: [PATCH 076/183] Remove docstring --- py/orbit/envelope/envelope.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 8743dacb..d50c7909 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -214,8 +214,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: - """Tracks envelope through linear lattice with optional linear space charge kicks.""" - def __init__( self, lattice: AccLattice, From ff9e322d2a67a6b23c11a0829106a7a878176607 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 13:44:34 -0400 Subject: [PATCH 077/183] Add NodeTEAPOT to ignore list --- py/orbit/envelope/matrix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index e133151b..6cadd178 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -16,6 +16,7 @@ from ..teapot import MonitorTEAPOT from ..teapot import TurnCounterTEAPOT from ..teapot import MultipoleTEAPOT +from ..teapot import NodeTEAPOT from ..utils import speed_of_light @@ -63,6 +64,7 @@ def __init__(self, handle_unknown: str | None = None) -> None: FringeFieldTEAPOT, MonitorTEAPOT, TurnCounterTEAPOT, + NodeTEAPOT, ] self.handle_unknown = handle_unknown From cb323d6b06807fd5e3d7bafc213dcb84c7d1e915 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 13:51:13 -0400 Subject: [PATCH 078/183] Replace MultipoleTEAPOT with drift if all strengths are zero --- py/orbit/envelope/matrix.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 6cadd178..922ca430 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -211,7 +211,11 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) return np.identity(7) else: - if self.handle_unknown == "drift": + if type(node) is MultipoleTEAPOT: + if np.all(np.abs(node.getParam("kls")) == 0): + return self.drift_matrix(length=node.getLength(), sync_part=sync_part) + + elif self.handle_unknown == "drift": return self.drift_matrix(length=node.getLength(), sync_part=sync_part) elif self.handle_unknown == "fit": From 2962c54687b55d4361b19d131baea4d54b6effe0 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 14:31:53 -0400 Subject: [PATCH 079/183] Don't change sync particle time in matrixfactor That should happen by tracking syncparticle through node --- py/orbit/envelope/matrix.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 922ca430..372e1727 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -115,9 +115,6 @@ def bend_matrix(self, length: float, theta: float, sync_part: SyncParticle) -> n if length <= 0: return np.identity(7) - v = speed_of_light * sync_part.beta() - sync_part.time(sync_part.time() + length / v) - betasq = sync_part.beta() ** 2 rho = length / theta From c743dd4d7f2684886a1a4d194d7a9033aed69953 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 15:40:28 -0400 Subject: [PATCH 080/183] Add solenoid to envelope tracker --- examples/Envelope/test_env.py | 20 ++++- py/orbit/envelope/matrix.py | 165 +++++++++++++++++++++------------- 2 files changed, 120 insertions(+), 65 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 69500407..99f7289a 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -4,9 +4,12 @@ from orbit.core.bunch import BunchTwissAnalysis from orbit.lattice import AccNode from orbit.lattice import AccLattice -from orbit.teapot import QuadTEAPOT, KickTEAPOT, TiltTEAPOT from orbit.teapot import BendTEAPOT from orbit.teapot import DriftTEAPOT +from orbit.teapot import KickTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import SolenoidTEAPOT +from orbit.teapot import TiltTEAPOT from orbit.teapot import TEAPOT_Lattice from orbit.utils.consts import mass_proton from orbit.envelope import Envelope @@ -195,10 +198,25 @@ def test_tilt( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_solenoid( + kin_energy: float = 0.0025, + length: float = 2.0, + B: float = 1.0, + cov_matrix: np.ndarray = None, + nparts: int = 10, +) -> None: + nodes = [SolenoidTEAPOT(length=length, B=B, nparts=nparts)] + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + if __name__ == "__main__": test_drift() test_quad() test_dipole() test_kick() test_tilt() + test_solenoid() diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 372e1727..bd416ef6 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -17,6 +17,7 @@ from ..teapot import TurnCounterTEAPOT from ..teapot import MultipoleTEAPOT from ..teapot import NodeTEAPOT +from ..teapot import SolenoidTEAPOT from ..utils import speed_of_light @@ -37,11 +38,6 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np # v = A w # v -> M v # w -> A M A^-1 - dp_p_coeff = get_dp_p_coeff(sync_part) - matrix[:5, 5] *= dp_p_coeff - matrix[5, :5] /= dp_p_coeff - matrix[5, 6] /= dp_p_coeff - return matrix # scale = np.identity(7) # scale[5, 5] = dp_p_coeff @@ -51,6 +47,12 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np # # return np.linalg.multi_dot([scale, matrix, scale_inv]) + dp_p_coeff = get_dp_p_coeff(sync_part) + matrix[:5, 5] *= dp_p_coeff + matrix[5, :5] /= dp_p_coeff + matrix[5, 6] /= dp_p_coeff + return matrix + class MatrixFactory: """Factory for 7 x 7 transfer matrices. @@ -69,96 +71,124 @@ def __init__(self, handle_unknown: str | None = None) -> None: self.handle_unknown = handle_unknown def drift_matrix(self, length: float, sync_part: SyncParticle) -> np.ndarray: - matrix = np.identity(7) - matrix[0, 1] = length - matrix[2, 3] = length - matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) - return matrix + M = np.identity(7) + M[0, 1] = length + M[2, 3] = length + M[4, 5] = length / sync_part.gamma() ** 2 + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M def quad_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + if abs(kq) == 0: + return self.drift_matrix(length=length, sync_part=sync_part) + sqrt_abs_kq = math.sqrt(abs(kq)) - matrix = np.identity(7) + M = np.identity(7) if kq > 0: cx = np.cos(sqrt_abs_kq * length) sx = np.sin(sqrt_abs_kq * length) cy = np.cosh(sqrt_abs_kq * length) sy = np.sinh(sqrt_abs_kq * length) - matrix[0, 0] = cx - matrix[0, 1] = +sx / sqrt_abs_kq - matrix[1, 0] = -sx * sqrt_abs_kq - matrix[1, 1] = cx - matrix[2, 2] = cy - matrix[2, 3] = sy / sqrt_abs_kq - matrix[3, 2] = sy * sqrt_abs_kq - matrix[3, 3] = cy + M[0, 0] = cx + M[0, 1] = +sx / sqrt_abs_kq + M[1, 0] = -sx * sqrt_abs_kq + M[1, 1] = cx + M[2, 2] = cy + M[2, 3] = sy / sqrt_abs_kq + M[3, 2] = sy * sqrt_abs_kq + M[3, 3] = cy elif kq < 0: cx = np.cosh(sqrt_abs_kq * length) sx = np.sinh(sqrt_abs_kq * length) cy = np.cos(sqrt_abs_kq * length) sy = np.sin(sqrt_abs_kq * length) - matrix[0, 0] = cx - matrix[0, 1] = sx / sqrt_abs_kq - matrix[1, 0] = sx * sqrt_abs_kq - matrix[1, 1] = cx - matrix[2, 2] = cy - matrix[2, 3] = +sy / sqrt_abs_kq - matrix[3, 2] = -sy * sqrt_abs_kq - matrix[3, 3] = cy - - matrix[4, 5] = length / sync_part.gamma() ** 2 - matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) - return matrix + M[0, 0] = cx + M[0, 1] = sx / sqrt_abs_kq + M[1, 0] = sx * sqrt_abs_kq + M[1, 1] = cx + M[2, 2] = cy + M[2, 3] = +sy / sqrt_abs_kq + M[3, 2] = -sy * sqrt_abs_kq + M[3, 3] = cy + + M[4, 5] = length / sync_part.gamma() ** 2 + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M def bend_matrix(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: if length <= 0: return np.identity(7) - betasq = sync_part.beta() ** 2 - rho = length / theta cx = math.cos(theta) sx = math.sin(theta) - matrix = np.identity(7) - matrix[0, 0] = cx - matrix[0, 1] = rho * sx - matrix[0, 5] = rho * (1.0 - cx) - matrix[1, 0] = -sx / rho - matrix[1, 1] = cx - matrix[1, 5] = sx - matrix[2, 3] = length - matrix[4, 0] = -sx - matrix[4, 1] = -rho * (1.0 - cx) - matrix[4, 5] = -betasq * length + rho * sx - matrix = convert_matrix_dp_p_to_dE(matrix, sync_part) - return matrix + M = np.identity(7) + M[0, 0] = cx + M[0, 1] = rho * sx + M[0, 5] = rho * (1.0 - cx) + M[1, 0] = -sx / rho + M[1, 1] = cx + M[1, 5] = sx + M[2, 3] = length + M[4, 0] = -sx + M[4, 1] = -rho * (1.0 - cx) + M[4, 5] = -(sync_part.beta() ** 2) * length + rho * sx + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M def tilt_matrix(self, angle: float) -> np.ndarray: - matrix = np.identity(7) - matrix[0, 0] = matrix[1, 1] = +math.cos(angle) - matrix[0, 2] = matrix[1, 3] = -math.sin(angle) - matrix[2, 0] = matrix[3, 1] = +math.sin(angle) - matrix[2, 2] = matrix[3, 3] = +math.cos(angle) - return matrix + M = np.identity(7) + M[0, 0] = M[1, 1] = +math.cos(angle) + M[0, 2] = M[1, 3] = -math.sin(angle) + M[2, 0] = M[3, 1] = +math.sin(angle) + M[2, 2] = M[3, 3] = +math.cos(angle) + return M def translation_matrix(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: - matrix = np.identity(7) - matrix[0, -1] = x - matrix[2, -1] = y - matrix[4, -1] = z - return matrix + M = np.identity(7) + M[0, -1] = x + M[2, -1] = y + M[4, -1] = z + return M def kick_matrix(self, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: - matrix = np.identity(7) - matrix[1, -1] = kx - matrix[3, -1] = ky - matrix[5, -1] = kE - return matrix + M = np.identity(7) + M[1, -1] = kx + M[3, -1] = ky + M[5, -1] = kE + return M def solenoid_matrix(self, length: float, B: float, sync_part: SyncParticle) -> np.ndarray: - raise NotImplementedError() + if B == 0: + return self.drift_matrix(length=length, sync_part=sync_part) + + phase = B * length + + V = np.identity(7) + V[:4, :4] = 0.0 + V[0, 1] = -1.0 / B + V[0, 2] = 0.5 + V[1, 0] = 0.5 * B + V[1, 3] = 1.0 + V[2, 1] = 1.0 / B + V[2, 2] = 0.5 + V[3, 0] = -0.5 * B + V[3, 3] = 1.0 + + M = np.identity(7) + M[0, 0] = +1.0 + M[1, 1] = -1.0 + M[2, 2] = math.cos(phase) + M[2, 3] = math.sin(phase) / B + M[3, 2] = math.sin(phase) * (-B) + M[3, 3] = math.cos(phase) + M[4, 5] = length / sync_part.gamma() ** 2 + + M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M def cf_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() @@ -204,6 +234,13 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) angle = node.getTiltAngle() return self.tilt_matrix(angle) + elif type(node) is SolenoidTEAPOT: + B = node.getParam("B") + if node.waveform is not None: + B *= node.waveform.getStrength() + length = node.getLength(part_index) + return self.solenoid_matrix(length=length, B=B, sync_part=sync_part) + elif type(node) in self.ignore_node_types: return np.identity(7) From 7b1182191b1dc45c1105e4698e6805a95644eebe Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 12 Jun 2026 15:59:34 -0400 Subject: [PATCH 081/183] Fix drift replacement Drift length needs to be calculated for the part of the node, not the whole node --- examples/Envelope/test_env_sns_ring.py | 38 -------------------------- py/orbit/envelope/matrix.py | 4 +-- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 513c1040..0878dbf7 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -60,42 +60,6 @@ def main(args: argparse.Namespace) -> None: node = lattice.getNodeForName(name) node.setParam("B", 0.15) - if args.simple_lattice: - from orbit.teapot import QuadTEAPOT, DriftTEAPOT, BendTEAPOT - - new_nodes = [] - for node in lattice.getNodes(): - new_node = None - if type(node) is DriftTEAPOT: - new_node = DriftTEAPOT(length=node.getLength(), nparts=node.getnParts()) - elif type(node) is QuadTEAPOT: - new_node = QuadTEAPOT(length=node.getLength(), nparts=node.getnParts(), kq=node.getParam("kq")) - elif type(node) is BendTEAPOT: - new_node = BendTEAPOT(length=node.getLength(), nparts=node.getnParts(), theta=node.getParam("theta")) - else: - try: - new_node = DriftTEAPOT(length=node.getLength()) - except: - pass - - if new_node is not None: - new_nodes.append(new_node) - - lattice = TEAPOT_Ring() - for node in new_nodes: - lattice.addNode(node) - lattice.initialize() - - for node in lattice.getNodes(): - try: - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) - except: - pass - - for node in lattice.getNodes(): - print(node) - for node in lattice.getNodes(): max_length = 1.0 if node.getLength() > max_length: @@ -309,7 +273,6 @@ def main(args: argparse.Namespace) -> None: limits = list(zip(-xmax, xmax)) labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] - # Plot x-x' fig, ax = plt.subplots(figsize=(4, 4)) ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) @@ -366,7 +329,6 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--sc-grid", type=int, default=64) parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) - parser.add_argument("--simple-lattice", type=int, default=0) return parser.parse_args() diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index bd416ef6..92fb4345 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -247,10 +247,10 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) else: if type(node) is MultipoleTEAPOT: if np.all(np.abs(node.getParam("kls")) == 0): - return self.drift_matrix(length=node.getLength(), sync_part=sync_part) + return self.drift_matrix(length=node.getLength(part_index), sync_part=sync_part) elif self.handle_unknown == "drift": - return self.drift_matrix(length=node.getLength(), sync_part=sync_part) + return self.drift_matrix(length=node.getLength(part_index), sync_part=sync_part) elif self.handle_unknown == "fit": raise NotImplementedError() From 913fd6eb67a1c62aea44f686b5644fff9a3a0b5f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 19 Jun 2026 12:40:58 -0400 Subject: [PATCH 082/183] Start adding nodes from orbit.py_linac Quad element needs to calculate Brho, which requires the bunch charge. The bunch charge is stored in the Bunch object, not SyncParticle. So I changed the Envelope class to store reference to Bunch object rather than SyncParticle. The bunch is copied + emptied in the constructor. This might make sense anyway because we may want to track test particles. It could also be easier to create an envelope directly from the Bunch: the covariance matrix and centroid can be calculated from the bunch macroparticles. --- examples/Envelope/test_env.py | 102 ++++++++++++++++++++++--- examples/Envelope/test_env_2d_fodo.py | 2 +- examples/Envelope/test_env_3d_drift.py | 2 +- examples/Envelope/test_env_sns_ring.py | 2 +- py/orbit/envelope/envelope.py | 38 +++++---- py/orbit/envelope/matrix.py | 61 ++++++++++----- py/orbit/py_linac/lattice/__init__.py | 5 ++ 7 files changed, 167 insertions(+), 45 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 99f7289a..03dc232e 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -2,8 +2,13 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis + from orbit.lattice import AccNode from orbit.lattice import AccLattice +from orbit.py_linac.lattice import Drift +from orbit.py_linac.lattice import Quad +from orbit.py_linac.lattice import Bend +from orbit.py_linac.lattice import TiltElement from orbit.teapot import BendTEAPOT from orbit.teapot import DriftTEAPOT from orbit.teapot import KickTEAPOT @@ -16,6 +21,12 @@ from orbit.envelope import EnvelopeTracker +def get_lorentz_factors(kin_energy: float, mass: float) -> tuple[float, float]: + gamma = 1.0 + kin_energy / mass + beta = np.sqrt(1.0 - (1.0 / gamma)**2) + return (gamma, beta) + + def calc_bunch_cov(bunch: Bunch) -> np.ndarray: twiss_calc = BunchTwissAnalysis() twiss_calc.analyzeBunch(bunch) @@ -86,7 +97,7 @@ def track_and_compare_rms( data["bunch"]["cov"]["out"] = cov_scale * calc_bunch_cov(bunch) # Track envelope - envelope = Envelope(sync_part=bunch.getSyncParticle(), cov_matrix=cov_matrix) + envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix) envelope_tracker = EnvelopeTracker(lattice=lattice) data["env"]["cov"]["in"] = cov_scale * envelope.cov() @@ -131,9 +142,8 @@ def make_default_cov_matrix( return np.diag(np.square([rms_x, rms_xp, rms_y, rms_yp, rms_z, rms_dE])) -def test_drift( - kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, - nparts: int = 6, +def test_drift_teapot( + kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, nparts: int = 6, ) -> None: nodes = [DriftTEAPOT(length=length, nparts=nparts)] lattice = make_lattice(nodes) @@ -142,7 +152,21 @@ def test_drift( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_quad( +def test_drift_linac( + kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, nparts: int = 6, +) -> None: + node = Drift() + node.setLength(length) + node.setnParts(nparts) + nodes = [node] + + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + +def test_quad_teapot( kin_energy: float = 0.0025, length: float = 1.0, kq: float = 1.0, @@ -156,7 +180,26 @@ def test_quad( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_dipole( +def test_quad_linac( + kin_energy: float = 0.0025, + length: float = 1.0, + field_grad: float = 0.23, + cov_matrix: np.ndarray = None, + nparts: int = 10, +) -> None: + node = Quad() + node.setLength(length) + node.setnParts(nparts) + node.setParam("dB/dr", field_grad) + nodes = [node] + + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + +def test_dipole_teapot( kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, @@ -170,6 +213,25 @@ def test_dipole( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_dipole_linac( + kin_energy: float = 0.0025, + length: float = 1.0, + theta: float = 20.0, + cov_matrix: np.ndarray = None, + nparts: int = 2, +) -> None: + node = Bend() + node.setLength(length) + node.setnParts(nparts) + node.setParam("theta", np.radians(theta)) + nodes = [node] + + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + def test_kick( kin_energy: float = 0.0025, length: float = 0.1, @@ -186,7 +248,7 @@ def test_kick( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_tilt( +def test_tilt_teapot( kin_energy: float = 0.0025, angle: float = 0.25 * np.pi, cov_matrix: np.ndarray = None, @@ -198,6 +260,20 @@ def test_tilt( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_tilt_linac( + kin_energy: float = 0.0025, + angle: float = 0.25 * np.pi, + cov_matrix: np.ndarray = None, +) -> None: + node = TiltElement() + node.setTiltAngle(angle) + nodes = [node] + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + def test_solenoid( kin_energy: float = 0.0025, length: float = 2.0, @@ -213,10 +289,14 @@ def test_solenoid( if __name__ == "__main__": - test_drift() - test_quad() - test_dipole() + test_drift_teapot() + test_drift_linac() + test_quad_teapot() + test_quad_linac() + test_dipole_teapot() + test_dipole_linac() test_kick() - test_tilt() + test_tilt_teapot() + test_tilt_linac() test_solenoid() diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 8eb3de58..4b783131 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -139,7 +139,7 @@ def main(args: argparse.Namespace) -> None: # Create envelope envelope = Envelope( - sync_part=sync_part, + bunch=bunch, cov_matrix=cov_matrix_init, centroid=centroid_init, intensity=args.intensity, diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index fcac7d48..d6fd0088 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -63,7 +63,7 @@ def main(args: argparse.Namespace) -> None: centroid_init = np.zeros(6) envelope = Envelope( - sync_part=sync_part, + bunch=bunch, cov_matrix=cov_matrix_init, centroid=centroid_init, intensity=args.intensity, diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 0878dbf7..c9e04ca5 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -116,7 +116,7 @@ def main(args: argparse.Namespace) -> None: # Create envelope envelope = Envelope( - sync_part=sync_part, + bunch=bunch, cov_matrix=cov_matrix_init, centroid=centroid_init, intensity=args.intensity, diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index d50c7909..6c2a7aa5 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -3,11 +3,12 @@ import numpy as np import scipy.special -from ..core.bunch import SyncParticle -from ..lattice import AccNode -from ..lattice import AccLattice -from ..utils.consts import speed_of_light -from ..utils.consts import charge_electron +from orbit.core.bunch import Bunch +from orbit.core.bunch import SyncParticle +from orbit.lattice import AccNode +from orbit.lattice import AccLattice +from orbit.utils.consts import speed_of_light +from orbit.utils.consts import charge_electron from .matrix import MatrixFactory from .utils import gen_dist @@ -38,7 +39,6 @@ class Envelope: Attributes: matrix: 7 x 7 covariance matrix for augmented phase space vector. - Define the phase space vector X = [x, x', y, y', z, dE]^T and augmented vector Y = [x, x', y, y', z, dE, 1]. @@ -51,16 +51,25 @@ class Envelope: and C = is the mean/centroid vector. (To get the covariance matrix: <(X - C)(X - C)^T> = - ^T = R - CC^T.) S evolves according to S -> N S N^T. + bunch: Bunch containing synchronous particle and (optionally) test particles. """ def __init__( self, - sync_part: SyncParticle, + bunch: Bunch, cov_matrix: np.ndarray = None, centroid: np.ndarray = None, intensity: float = 0.0, ) -> None: - self.sync_part = sync_part + + # Eventually allow: + # - setting covariance matrix from bunch particles + # - tracking bunch particles as test particles + empty_bunch = Bunch() + bunch.copyEmptyBunchTo(empty_bunch) + self.bunch = empty_bunch + + self.sync_part = bunch.getSyncParticle() self.dim = 6 if centroid is None: @@ -96,6 +105,9 @@ def beta(self) -> float: def mass(self) -> float: return self.sync_part.mass() + def charge(self) -> float: + return self.bunch.charge() + def centroid(self) -> np.ndarray: return np.copy(self.moment_matrix[:self.dim, self.dim]) @@ -228,13 +240,13 @@ def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): for child_node in node.getChildNodes(ENTRANCE): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) + self.matrix_factory(child_node, envelope.bunch) ) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) + self.matrix_factory(child_node, envelope.bunch) ) if self.space_charge: @@ -250,15 +262,15 @@ def track(self, envelope: Envelope) -> None: envelope.apply_transfer_matrix(matrix) envelope.apply_transfer_matrix( - self.matrix_factory(node, envelope.sync_part, part_index) + self.matrix_factory(node, envelope.bunch, part_index) ) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) + self.matrix_factory(child_node, envelope.bunch) ) for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.sync_part) + self.matrix_factory(child_node, envelope.bunch) ) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 92fb4345..205d7290 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -2,23 +2,27 @@ import numpy as np -from ..core.bunch import Bunch -from ..core.bunch import SyncParticle -from ..lattice import AccNode -from ..teapot import DriftTEAPOT -from ..teapot import QuadTEAPOT -from ..teapot import BendTEAPOT -from ..teapot import TiltTEAPOT -from ..teapot import KickTEAPOT -from ..teapot import ApertureTEAPOT -from ..teapot import BunchWrapTEAPOT -from ..teapot import FringeFieldTEAPOT -from ..teapot import MonitorTEAPOT -from ..teapot import TurnCounterTEAPOT -from ..teapot import MultipoleTEAPOT -from ..teapot import NodeTEAPOT -from ..teapot import SolenoidTEAPOT -from ..utils import speed_of_light +from orbit.core.bunch import Bunch +from orbit.core.bunch import SyncParticle +from orbit.lattice import AccNode +from orbit.teapot import DriftTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import BendTEAPOT +from orbit.teapot import TiltTEAPOT +from orbit.teapot import KickTEAPOT +from orbit.teapot import ApertureTEAPOT +from orbit.teapot import BunchWrapTEAPOT +from orbit.teapot import FringeFieldTEAPOT +from orbit.teapot import MonitorTEAPOT +from orbit.teapot import TurnCounterTEAPOT +from orbit.teapot import MultipoleTEAPOT +from orbit.teapot import NodeTEAPOT +from orbit.teapot import SolenoidTEAPOT +from orbit.py_linac.lattice import Drift +from orbit.py_linac.lattice import Quad +from orbit.py_linac.lattice import Bend +from orbit.py_linac.lattice import TiltElement +from orbit.py_linac.lattice import FringeField def get_dp_p_coeff(sync_part: SyncParticle) -> float: @@ -67,6 +71,7 @@ def __init__(self, handle_unknown: str | None = None) -> None: MonitorTEAPOT, TurnCounterTEAPOT, NodeTEAPOT, + FringeField, ] self.handle_unknown = handle_unknown @@ -193,7 +198,9 @@ def solenoid_matrix(self, length: float, B: float, sync_part: SyncParticle) -> n def cf_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() - def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) -> np.ndarray: + def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarray: + sync_part = bunch.getSyncParticle() + if type(node) is DriftTEAPOT: length = node.getLength(part_index) return self.drift_matrix(length=length, sync_part=sync_part) @@ -241,6 +248,24 @@ def __call__(self, node: AccNode, sync_part: SyncParticle, part_index: int = 0) length = node.getLength(part_index) return self.solenoid_matrix(length=length, B=B, sync_part=sync_part) + elif type(node) is Drift: + length = node.getLength(part_index) + return self.drift_matrix(length=length, sync_part=sync_part) + + elif type(node) is Quad: + length = node.getLength(part_index) + kq = node.getParam("dB/dr") / bunch.B_Rho() + return self.quad_matrix(length=length, kq=kq, sync_part=sync_part) + + elif type(node) is Bend: + length = node.getLength(part_index) + theta = node.getParam("theta") / node.getnParts() + return self.bend_matrix(length=length, theta=theta, sync_part=sync_part) + + elif type(node) is TiltElement: + angle = node.getTiltAngle() + return self.tilt_matrix(angle) + elif type(node) in self.ignore_node_types: return np.identity(7) diff --git a/py/orbit/py_linac/lattice/__init__.py b/py/orbit/py_linac/lattice/__init__.py index 85a9df80..79da382e 100644 --- a/py/orbit/py_linac/lattice/__init__.py +++ b/py/orbit/py_linac/lattice/__init__.py @@ -38,6 +38,9 @@ from orbit.py_linac.lattice.LinacTransportMatrixGenNodes import LinacTrMatricesController from orbit.py_linac.lattice.LinacDiagnosticsNodes import LinacBPM +from orbit.py_linac.lattice.LinacAccNodes import TiltElement +from orbit.py_linac.lattice.LinacAccNodes import FringeField + __all__ = [] __all__.append("LinacAccLattice") @@ -91,3 +94,5 @@ __all__.append("LinacTrMatricesController") __all__.append("LinacBPM") +__all__.append("TiltElement") +__all__.append("FringeField") From 6ff8c16059960472b29fd91bdc19c4329352e226 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 19 Jun 2026 13:40:08 -0400 Subject: [PATCH 083/183] Fix x-axis label in 3d drift example --- examples/Envelope/test_env_3d_drift.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index d6fd0088..fddd9507 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -1,4 +1,8 @@ -"""Test 3D envelope tracker in drift.""" +"""Test 3D envelope tracker in drift. + +The initial beam is a uniform-density ball in the x-y-z plane (in the beam rest frame), +with zero initial velocity. +""" import argparse import copy @@ -162,14 +166,14 @@ def main(args: argparse.Namespace) -> None: # Plot rms bunch sizes for key in ["xrms", "yrms", "zrms"]: - fig, ax = plt.subplots(figsize=(5, 3)) + fig, ax = plt.subplots(figsize=(4, 3)) for i, model in enumerate(["envelope", "bunch"]): color = ["black", "red"][i] lw = [None, 0][i] ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) ax.set_ylim(0.0, ax.get_ylim()[1]) - ax.set_xlabel("Turn") - ax.set_ylabel("RMS [mm]") + ax.set_xlabel("s [mm]") + ax.set_ylabel("rms size [mm]") ax.legend(loc="upper left") plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() From 05fa0de90d0518c356154424385fc17b887ee453 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 19 Jun 2026 14:12:55 -0400 Subject: [PATCH 084/183] Start 3D drift with rotation example Seems to be working for x-y rotations but not y-z. Need to investigate --- examples/Envelope/test_env_3d_drift_rot.py | 279 +++++++++++++++++++++ examples/Envelope/utils.py | 27 +- 2 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 examples/Envelope/test_env_3d_drift_rot.py diff --git a/examples/Envelope/test_env_3d_drift_rot.py b/examples/Envelope/test_env_3d_drift_rot.py new file mode 100644 index 00000000..ed2b5496 --- /dev/null +++ b/examples/Envelope/test_env_3d_drift_rot.py @@ -0,0 +1,279 @@ +"""Test 3D envelope tracker in drift + +The initial beam is a uniform-density ellipsoid in the x-y-z plane, with zero initial velocity. +The ellipsoid can have arbitrary size and orientation. +""" + +import argparse +import copy +import math +import os +import pathlib + +import numpy as np +import matplotlib.pyplot as plt +import scipy.spatial + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.spacecharge import SpaceChargeCalc3D +from orbit.bunch_utils import collect_bunch +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.space_charge.sc3d import setSC3DAccNodes +from orbit.teapot import DriftTEAPOT +from orbit.teapot import TEAPOT_Lattice +from orbit.utils.consts import mass_proton + +from plot import plot_rms_ellipse +from plot import plot_corner +from utils import gen_dist +from utils import project_cov_matrix + +plt.style.use("style.mplstyle") + + +def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: + return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() + + +def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: + cov_matrix = np.diag(np.square(rms_sizes)) + if rotation_matrix is None: + return cov_matrix + return rotation_matrix @ cov_matrix @ rotation_matrix.T + + +def main(args: argparse.Namespace) -> None: + + # Setup + # ------------------------------------------------------------------------------ + path = pathlib.Path(__file__) + output_dir = os.path.join("outputs", path.stem) + os.makedirs(output_dir, exist_ok=True) + + + # Create lattice + # ------------------------------------------------------------------------------ + node = DriftTEAPOT(length=args.length) + node.setLength(args.length) + node.setnParts(args.nslice) + + lattice = TEAPOT_Lattice() + lattice.addNode(node) + lattice.initialize() + + + # Create envelope + # ------------------------------------------------------------------------------ + bunch = Bunch() + bunch.mass(mass_proton) + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(args.kin_energy) + + cov_matrix_init = np.zeros((6, 6)) + + rotation_matrix = rotation_matrix_3d( + math.radians(args.rot_x), + math.radians(args.rot_y), + math.radians(args.rot_z) + ) + print(rotation_matrix) + + cov_matrix_xyz = build_cov_matrix_xyz([args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix) + + lorentz_matrix = np.diag([1.0, 1.0, 1.0 / sync_part.gamma()]) + cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix + + spatial_idx = np.ix_([0, 2, 4], [0, 2, 4]) + cov_matrix_init[spatial_idx] = cov_matrix_xyz + + print(cov_matrix_xyz * 1e6) + print() + print(cov_matrix_init * 1e6) + + + centroid_init = np.zeros(6) + + envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, + ) + + # Track envelope + # ------------------------------------------------------------------------------ + print("TRACK ENVELOPE") + + tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) + + history = {"xrms": [], "yrms": [], "zrms": []} + for turn in range(args.turns): + if turn > 0: + tracker.track(envelope) + + cov_matrix = envelope.cov() + centroid = envelope.centroid() + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") + + histories = {} + histories["envelope"] = copy.deepcopy(history) + + + # Track bunch + # ------------------------------------------------------------------------------ + print("TRACK BUNCH") + + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, (0, 2, 4)] = gen_dist( + args.nparts, cov_matrix=cov_matrix_xyz, name="waterbag" + ) + + for x, xp, y, yp, z, dE in bunch_coords: + bunch.addParticle(x, xp, y, yp, z, dE) + + size_global = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / size_global) + + if args.sc: + sc_calc = SpaceChargeCalc3D(args.sc_grid, args.sc_grid, args.sc_grid) + sc_path_length_min = 0.01 + sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) + + history = {"xrms": [], "yrms": [], "zrms": []} + for turn in range(args.turns): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] + + xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * bunch.getSyncParticle().gamma() + + history["xrms"].append(xrms) + history["yrms"].append(yrms) + history["zrms"].append(zrms) + + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") + + histories["bunch"] = copy.deepcopy(history) + + + # Analysis + # ------------------------------------------------------------------------------ + for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + + # Print errors + for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + + # Plot rms bunch sizes + for key in ["xrms", "yrms", "zrms"]: + fig, ax = plt.subplots(figsize=(4, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1]) + ax.set_xlabel("s [mm]") + ax.set_ylabel("rms size [mm]") + ax.legend(loc="upper left") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() + + # Collect bunch/envelope data on final turn. + particles = collect_bunch(bunch)["coords"] + particles *= 1e3 + + env_cov_matrix = envelope.cov() + env_cov_matrix *= 1e6 + + env_centroid = envelope.centroid() + env_centroid *= 1e3 + + xmax = 4.0 * np.std(particles, axis=0) + limits = list(zip(-xmax, xmax)) + labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] + + # Plot x-x' + fig, ax = plt.subplots(figsize=(4, 4)) + ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) + plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, + ) + ax.set_xlabel(labels[0]) + ax.set_ylabel(labels[1]) + plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) + plt.close() + + # Plot corner + fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, + ) + for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) + plt.savefig(os.path.join(output_dir, "fig_dist_corner")) + plt.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--kin-energy", type=float, default=0.0025) + parser.add_argument("--intensity", type=float, default=5e10) + + parser.add_argument("--rms-x", type=float, default=0.010) + parser.add_argument("--rms-y", type=float, default=0.010) + parser.add_argument("--rms-z", type=float, default=0.010) + + parser.add_argument("--rot-x", type=float, default=0.0) + parser.add_argument("--rot-y", type=float, default=0.0) + parser.add_argument("--rot-z", type=float, default=0.0) + + parser.add_argument("--nslice", type=int, default=10) + parser.add_argument("--length", type=float, default=0.1) + parser.add_argument("--turns", type=int, default=20) + parser.add_argument("--sc-grid", type=int, default=64) + + parser.add_argument("--nparts", type=int, default=100_000) + parser.add_argument("--sc", type=int, default=0) + args = parser.parse_args() + + main(args) \ No newline at end of file diff --git a/examples/Envelope/utils.py b/examples/Envelope/utils.py index 432e4945..daf2a6c7 100644 --- a/examples/Envelope/utils.py +++ b/examples/Envelope/utils.py @@ -1,37 +1,38 @@ import numpy as np -def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: +def gen_dist_gauss(size: int, dim: np.ndarray) -> np.ndarray: return np.random.multivariate_normal( - mean=np.zeros(cov_matrix.shape[0]), - cov=cov_matrix, - size=n, + mean=np.zeros(dim), + cov=np.eye(dim), + size=size, ) -def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: - X = np.random.normal(size=(n, cov_matrix.shape[0])) +def gen_dist_kv(size: int, dim: int) -> np.ndarray: + X = np.random.normal(size=(size, dim)) X /= np.linalg.norm(X, axis=1)[:, None] X /= np.std(X, axis=0) return X -def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: - X = gen_dist_kv(n, cov_matrix) +def gen_dist_waterbag(size: int, dim: int) -> np.ndarray: + X = gen_dist_kv(size, dim) dim = X.shape[1] - r = np.random.uniform(size=n) ** (1.0 / dim) + r = np.random.uniform(size=size) ** (1.0 / dim) X *= r[:, None] X /= np.std(X, axis=0) return X -def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: +def gen_dist(size: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: + dim = cov_matrix.shape[0] if name == "kv": - X = gen_dist_kv(n, cov_matrix) + X = gen_dist_kv(size, dim) elif name == "waterbag": - X = gen_dist_waterbag(n, cov_matrix) + X = gen_dist_waterbag(size, dim) elif name == "gauss": - X = gen_dist_gauss(n, cov_matrix) + X = gen_dist_gauss(size, dim) else: raise ValueError(f"Invalid distribution name: {name}") From 4ab6ab2754cbfdf82ca8d3065c1ebb8c4314d5d9 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 19 Jun 2026 15:07:09 -0400 Subject: [PATCH 085/183] Fix longitudinal coordinate of 3D space charge matrix Transfer matrix was written for z' = dz/ds. Added conversion from z' to dE. --- examples/Envelope/test_env_3d_drift.py | 60 +++++++++++++++++++------- py/orbit/envelope/envelope.py | 14 ++++-- py/orbit/envelope/matrix.py | 25 +++++++++-- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index fddd9507..40cdcabc 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -1,7 +1,7 @@ -"""Test 3D envelope tracker in drift. +"""Test 3D envelope tracker in drift -The initial beam is a uniform-density ball in the x-y-z plane (in the beam rest frame), -with zero initial velocity. +The initial beam is a uniform-density ellipsoid in the x-y-z plane, with zero initial velocity. +The ellipsoid can have arbitrary size and orientation. """ import argparse @@ -12,6 +12,7 @@ import numpy as np import matplotlib.pyplot as plt +import scipy.spatial from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis @@ -32,6 +33,17 @@ plt.style.use("style.mplstyle") +def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: + return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() + + +def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: + cov_matrix = np.diag(np.square(rms_sizes)) + if rotation_matrix is None: + return cov_matrix + return rotation_matrix @ cov_matrix @ rotation_matrix.T + + def main(args: argparse.Namespace) -> None: # Setup @@ -60,9 +72,26 @@ def main(args: argparse.Namespace) -> None: sync_part.kinEnergy(args.kin_energy) cov_matrix_init = np.zeros((6, 6)) - cov_matrix_init[0, 0] = args.xrms**2 - cov_matrix_init[2, 2] = args.yrms**2 - cov_matrix_init[4, 4] = (args.zrms / sync_part.gamma()) ** 2 + + rotation_matrix = rotation_matrix_3d( + math.radians(args.rot_x), + math.radians(args.rot_y), + math.radians(args.rot_z) + ) + print(rotation_matrix) + + cov_matrix_xyz = build_cov_matrix_xyz([args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix) + + lorentz_matrix = np.diag([1.0, 1.0, 1.0 / sync_part.gamma()]) + cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix + + spatial_idx = np.ix_([0, 2, 4], [0, 2, 4]) + cov_matrix_init[spatial_idx] = cov_matrix_xyz + + print(cov_matrix_xyz * 1e6) + print() + print(cov_matrix_init * 1e6) + centroid_init = np.zeros(6) @@ -107,11 +136,8 @@ def main(args: argparse.Namespace) -> None: bunch_coords = np.zeros((args.nparts, 6)) bunch_coords[:, (0, 2, 4)] = gen_dist( - args.nparts, cov_matrix=np.eye(3), name="waterbag" + args.nparts, cov_matrix=cov_matrix_xyz, name="waterbag" ) - bunch_coords[:, 0] *= args.xrms - bunch_coords[:, 2] *= args.yrms - bunch_coords[:, 4] *= args.zrms / sync_part.gamma() for x, xp, y, yp, z, dE in bunch_coords: bunch.addParticle(x, xp, y, yp, z, dE) @@ -171,7 +197,7 @@ def main(args: argparse.Namespace) -> None: color = ["black", "red"][i] lw = [None, 0][i] ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1]) + ax.set_ylim(0.0, ax.get_ylim()[1] * 1.2) ax.set_xlabel("s [mm]") ax.set_ylabel("rms size [mm]") ax.legend(loc="upper left") @@ -231,11 +257,15 @@ def main(args: argparse.Namespace) -> None: if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--kin-energy", type=float, default=0.0025) - parser.add_argument("--intensity", type=float, default=5e10) + parser.add_argument("--intensity", type=float, default=3e10) + + parser.add_argument("--rms-x", type=float, default=0.010) + parser.add_argument("--rms-y", type=float, default=0.010) + parser.add_argument("--rms-z", type=float, default=0.010) - parser.add_argument("--xrms", type=float, default=0.010) - parser.add_argument("--yrms", type=float, default=0.010) - parser.add_argument("--zrms", type=float, default=0.010) + parser.add_argument("--rot-x", type=float, default=0.0) + parser.add_argument("--rot-y", type=float, default=0.0) + parser.add_argument("--rot-z", type=float, default=0.0) parser.add_argument("--nslice", type=int, default=10) parser.add_argument("--length", type=float, default=0.1) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 6c2a7aa5..9c0823a0 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -11,6 +11,7 @@ from orbit.utils.consts import charge_electron from .matrix import MatrixFactory +from .matrix import convert_matrix_zp_to_dE from .utils import gen_dist from .utils import proj_cov_matrix @@ -131,6 +132,8 @@ def sample(self, size: int, dist: str = "kv") -> np.ndarray: return particles def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: + # [TO DO] Move this to matrix.py? + # Extract beam centroid and covariance matrix. centroid = self.centroid() cov_matrix = self.cov() @@ -175,11 +178,15 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: + # [TO DO] Move this to matrix.py? + centroid = self.centroid() centroid[4] *= self.gamma() cov_matrix = self.cov() cov_matrix[4, 4] *= self.gamma() ** 2 + + # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) # Compute eigenvalues and eigenvectors of x-y covariance matrix. @@ -197,7 +204,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: kappa_x = factor * RDx # [1 / m] kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] - kappa_z *= self.gamma() ** 3 * self.beta() ** 2 * self.mass() # [GeV / m] M = np.identity(7) M[1, 0] = kappa_x * length @@ -221,8 +227,10 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # u -> M u # x -> V M V^-1 x V = np.matmul(T, A) - V_inv = np.linalg.inv(V) - return np.linalg.multi_dot([V, M, V_inv]) + M = V @ M @ np.linalg.inv(V) + + # Convert from z' to dE. + return convert_matrix_zp_to_dE(M, self.sync_part) class EnvelopeTracker: diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 205d7290..29b5dd64 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -26,16 +26,25 @@ def get_dp_p_coeff(sync_part: SyncParticle) -> float: - beta = sync_part.beta() - gamma = sync_part.gamma() - rest_energy = sync_part.mass() # GeV - # dE/E = (beta^2) * dp/p # dE = (beta^2 * E) * dp/p # dE = (beta^2 * gamma * m * c^2) * dp/p + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() # GeV return 1.0 / (beta**2 * gamma * rest_energy) +def get_zp_coeff(sync_part: SyncParticle) -> float: + # dE/E = (beta^2) * dp/p = (beta^2) * (gamma^2) z' + # dE = (beta^2 * gamma^2 * E) * z' + # dE = (beta^2 * gamma^3 * m * c^2) * z' + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() + return 1.0 / (beta**2 * gamma**3 * rest_energy) + + def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: # v = [x, x', y, y', z, dp/p] # w = [x, x', y, y', z, dE] @@ -58,6 +67,14 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np return matrix +def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + zp_coeff = get_zp_coeff(sync_part) + matrix[:5, 5] *= zp_coeff + matrix[5, :5] /= zp_coeff + matrix[5, 6] /= zp_coeff + return matrix + + class MatrixFactory: """Factory for 7 x 7 transfer matrices. From 40d18cf4c753f08a997686dae5387444623fbdd0 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 19 Jun 2026 15:10:22 -0400 Subject: [PATCH 086/183] Change argument in gen_dist examples --- examples/Envelope/test_env_2d_fodo.py | 2 +- examples/Envelope/test_env_3d_drift.py | 2 +- examples/Envelope/test_env_3d_drift_rot.py | 279 --------------------- examples/Envelope/test_env_sns_ring.py | 2 +- 4 files changed, 3 insertions(+), 282 deletions(-) delete mode 100644 examples/Envelope/test_env_3d_drift_rot.py diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 4b783131..15e4f69d 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -188,7 +188,7 @@ def main(args: argparse.Namespace) -> None: bunch_coords = np.zeros((args.nparts, 6)) bunch_coords[:, :4] = gen_dist( - n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist ) bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) bunch_coords += centroid_init[None, :6] diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 40cdcabc..8c7cd78a 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -136,7 +136,7 @@ def main(args: argparse.Namespace) -> None: bunch_coords = np.zeros((args.nparts, 6)) bunch_coords[:, (0, 2, 4)] = gen_dist( - args.nparts, cov_matrix=cov_matrix_xyz, name="waterbag" + size=args.nparts, cov_matrix=cov_matrix_xyz, name="waterbag" ) for x, xp, y, yp, z, dE in bunch_coords: diff --git a/examples/Envelope/test_env_3d_drift_rot.py b/examples/Envelope/test_env_3d_drift_rot.py deleted file mode 100644 index ed2b5496..00000000 --- a/examples/Envelope/test_env_3d_drift_rot.py +++ /dev/null @@ -1,279 +0,0 @@ -"""Test 3D envelope tracker in drift - -The initial beam is a uniform-density ellipsoid in the x-y-z plane, with zero initial velocity. -The ellipsoid can have arbitrary size and orientation. -""" - -import argparse -import copy -import math -import os -import pathlib - -import numpy as np -import matplotlib.pyplot as plt -import scipy.spatial - -from orbit.core.bunch import Bunch -from orbit.core.bunch import BunchTwissAnalysis -from orbit.core.spacecharge import SpaceChargeCalc3D -from orbit.bunch_utils import collect_bunch -from orbit.envelope import Envelope -from orbit.envelope import EnvelopeTracker -from orbit.space_charge.sc3d import setSC3DAccNodes -from orbit.teapot import DriftTEAPOT -from orbit.teapot import TEAPOT_Lattice -from orbit.utils.consts import mass_proton - -from plot import plot_rms_ellipse -from plot import plot_corner -from utils import gen_dist -from utils import project_cov_matrix - -plt.style.use("style.mplstyle") - - -def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: - return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() - - -def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: - cov_matrix = np.diag(np.square(rms_sizes)) - if rotation_matrix is None: - return cov_matrix - return rotation_matrix @ cov_matrix @ rotation_matrix.T - - -def main(args: argparse.Namespace) -> None: - - # Setup - # ------------------------------------------------------------------------------ - path = pathlib.Path(__file__) - output_dir = os.path.join("outputs", path.stem) - os.makedirs(output_dir, exist_ok=True) - - - # Create lattice - # ------------------------------------------------------------------------------ - node = DriftTEAPOT(length=args.length) - node.setLength(args.length) - node.setnParts(args.nslice) - - lattice = TEAPOT_Lattice() - lattice.addNode(node) - lattice.initialize() - - - # Create envelope - # ------------------------------------------------------------------------------ - bunch = Bunch() - bunch.mass(mass_proton) - sync_part = bunch.getSyncParticle() - sync_part.kinEnergy(args.kin_energy) - - cov_matrix_init = np.zeros((6, 6)) - - rotation_matrix = rotation_matrix_3d( - math.radians(args.rot_x), - math.radians(args.rot_y), - math.radians(args.rot_z) - ) - print(rotation_matrix) - - cov_matrix_xyz = build_cov_matrix_xyz([args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix) - - lorentz_matrix = np.diag([1.0, 1.0, 1.0 / sync_part.gamma()]) - cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix - - spatial_idx = np.ix_([0, 2, 4], [0, 2, 4]) - cov_matrix_init[spatial_idx] = cov_matrix_xyz - - print(cov_matrix_xyz * 1e6) - print() - print(cov_matrix_init * 1e6) - - - centroid_init = np.zeros(6) - - envelope = Envelope( - bunch=bunch, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, - ) - - # Track envelope - # ------------------------------------------------------------------------------ - print("TRACK ENVELOPE") - - tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) - - history = {"xrms": [], "yrms": [], "zrms": []} - for turn in range(args.turns): - if turn > 0: - tracker.track(envelope) - - cov_matrix = envelope.cov() - centroid = envelope.centroid() - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["zrms"].append(zrms) - - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") - - histories = {} - histories["envelope"] = copy.deepcopy(history) - - - # Track bunch - # ------------------------------------------------------------------------------ - print("TRACK BUNCH") - - bunch_coords = np.zeros((args.nparts, 6)) - bunch_coords[:, (0, 2, 4)] = gen_dist( - args.nparts, cov_matrix=cov_matrix_xyz, name="waterbag" - ) - - for x, xp, y, yp, z, dE in bunch_coords: - bunch.addParticle(x, xp, y, yp, z, dE) - - size_global = bunch.getSizeGlobal() - bunch.macroSize(args.intensity / size_global) - - if args.sc: - sc_calc = SpaceChargeCalc3D(args.sc_grid, args.sc_grid, args.sc_grid) - sc_path_length_min = 0.01 - sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) - - history = {"xrms": [], "yrms": [], "zrms": []} - for turn in range(args.turns): - if turn > 0: - lattice.trackBunch(bunch) - - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) - - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * bunch.getSyncParticle().gamma() - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["zrms"].append(zrms) - - print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} zrms={zrms:0.3f}") - - histories["bunch"] = copy.deepcopy(history) - - - # Analysis - # ------------------------------------------------------------------------------ - for history in histories.values(): - for key in history: - history[key] = np.array(history[key]) - - # Print errors - for key in histories["envelope"]: - deltas = histories["bunch"][key] - histories["envelope"][key] - print("key:", key) - print("max_abs_delta:", np.max(np.abs(deltas))) - print("avg_abs_delta:", np.mean(np.abs(deltas))) - - # Plot rms bunch sizes - for key in ["xrms", "yrms", "zrms"]: - fig, ax = plt.subplots(figsize=(4, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1]) - ax.set_xlabel("s [mm]") - ax.set_ylabel("rms size [mm]") - ax.legend(loc="upper left") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close() - - # Collect bunch/envelope data on final turn. - particles = collect_bunch(bunch)["coords"] - particles *= 1e3 - - env_cov_matrix = envelope.cov() - env_cov_matrix *= 1e6 - - env_centroid = envelope.centroid() - env_centroid *= 1e3 - - xmax = 4.0 * np.std(particles, axis=0) - limits = list(zip(-xmax, xmax)) - labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] - - # Plot x-x' - fig, ax = plt.subplots(figsize=(4, 4)) - ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) - plot_rms_ellipse( - env_cov_matrix[0:2, 0:2], - center=(env_centroid[0], env_centroid[1]), - level=2.0, - color="red", - ax=ax, - ) - ax.set_xlabel(labels[0]) - ax.set_ylabel(labels[1]) - plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) - plt.close() - - # Plot corner - fig, axs = plot_corner( - particles, - limits=limits, - bins=100, - labels=labels, - ) - for i in range(6): - for j in range(i): - env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) - plot_rms_ellipse( - env_cov_matrix_proj, - center=(env_centroid[j], env_centroid[i]), - level=2.0, - color="red", - ax=axs[i, j], - ) - plt.savefig(os.path.join(output_dir, "fig_dist_corner")) - plt.close() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--kin-energy", type=float, default=0.0025) - parser.add_argument("--intensity", type=float, default=5e10) - - parser.add_argument("--rms-x", type=float, default=0.010) - parser.add_argument("--rms-y", type=float, default=0.010) - parser.add_argument("--rms-z", type=float, default=0.010) - - parser.add_argument("--rot-x", type=float, default=0.0) - parser.add_argument("--rot-y", type=float, default=0.0) - parser.add_argument("--rot-z", type=float, default=0.0) - - parser.add_argument("--nslice", type=int, default=10) - parser.add_argument("--length", type=float, default=0.1) - parser.add_argument("--turns", type=int, default=20) - parser.add_argument("--sc-grid", type=int, default=64) - - parser.add_argument("--nparts", type=int, default=100_000) - parser.add_argument("--sc", type=int, default=0) - args = parser.parse_args() - - main(args) \ No newline at end of file diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index c9e04ca5..a43e8444 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -169,7 +169,7 @@ def main(args: argparse.Namespace) -> None: bunch_coords = np.zeros((args.nparts, 6)) bunch_coords[:, :4] = gen_dist( - n=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist ) bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) bunch_coords += centroid_init[None, :6] From 511b5855d398fb01afcc727abaebc80a49c22ff6 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 11:48:51 -0400 Subject: [PATCH 087/183] Move functions out of MatrixFactory --- py/orbit/envelope/matrix.py | 207 +++++++++++++++++++++++++++++++++--- 1 file changed, 194 insertions(+), 13 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 29b5dd64..aaa0e3b3 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -75,6 +75,187 @@ def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.n return matrix +def get_dp_p_coeff(sync_part: SyncParticle) -> float: + # dE/E = (beta^2) * dp/p + # dE = (beta^2 * E) * dp/p + # dE = (beta^2 * gamma * m * c^2) * dp/p + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() # GeV + return 1.0 / (beta ** 2 * gamma * rest_energy) + + +def get_zp_coeff(sync_part: SyncParticle) -> float: + # dE/E = (beta^2) * dp/p = (beta^2) * (gamma^2) z' + # dE = (beta^2 * gamma^2 * E) * z' + # dE = (beta^2 * gamma^3 * m * c^2) * z' + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() + return 1.0 / (beta ** 2 * gamma ** 3 * rest_energy) + + +def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + # v = [x, x', y, y', z, dp/p] + # w = [x, x', y, y', z, dE] + # v = A w + # v -> M v + # w -> A M A^-1 + + # scale = np.identity(7) + # scale[5, 5] = dp_p_coeff + # + # scale_inv = np.identity(7) + # scale_inv[5, 5] = 1.0 / dp_p_coeff + # + # return np.linalg.multi_dot([scale, matrix, scale_inv]) + + dp_p_coeff = get_dp_p_coeff(sync_part) + matrix[:5, 5] *= dp_p_coeff + matrix[5, :5] /= dp_p_coeff + matrix[5, 6] /= dp_p_coeff + return matrix + + +def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + zp_coeff = get_zp_coeff(sync_part) + matrix[:5, 5] *= zp_coeff + matrix[5, :5] /= zp_coeff + matrix[5, 6] /= zp_coeff + return matrix + + +def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: + M = np.identity(7) + M[0, 1] = length + M[2, 3] = length + M[4, 5] = length / sync_part.gamma() ** 2 + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M + + +def quad_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + if abs(kq) == 0: + return drift_matrix(length=length, sync_part=sync_part) + + sqrt_abs_kq = math.sqrt(abs(kq)) + + M = np.identity(7) + if kq > 0: + cx = np.cos(sqrt_abs_kq * length) + sx = np.sin(sqrt_abs_kq * length) + cy = np.cosh(sqrt_abs_kq * length) + sy = np.sinh(sqrt_abs_kq * length) + M[0, 0] = cx + M[0, 1] = +sx / sqrt_abs_kq + M[1, 0] = -sx * sqrt_abs_kq + M[1, 1] = cx + M[2, 2] = cy + M[2, 3] = sy / sqrt_abs_kq + M[3, 2] = sy * sqrt_abs_kq + M[3, 3] = cy + elif kq < 0: + cx = np.cosh(sqrt_abs_kq * length) + sx = np.sinh(sqrt_abs_kq * length) + cy = np.cos(sqrt_abs_kq * length) + sy = np.sin(sqrt_abs_kq * length) + M[0, 0] = cx + M[0, 1] = sx / sqrt_abs_kq + M[1, 0] = sx * sqrt_abs_kq + M[1, 1] = cx + M[2, 2] = cy + M[2, 3] = +sy / sqrt_abs_kq + M[3, 2] = -sy * sqrt_abs_kq + M[3, 3] = cy + + M[4, 5] = length / sync_part.gamma() ** 2 + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M + + +def bend_matrix(length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: + if length <= 0: + return np.identity(7) + + rho = length / theta + cx = math.cos(theta) + sx = math.sin(theta) + + M = np.identity(7) + M[0, 0] = cx + M[0, 1] = rho * sx + M[0, 5] = rho * (1.0 - cx) + M[1, 0] = -sx / rho + M[1, 1] = cx + M[1, 5] = sx + M[2, 3] = length + M[4, 0] = -sx + M[4, 1] = -rho * (1.0 - cx) + M[4, 5] = -(sync_part.beta() ** 2) * length + rho * sx + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M + + +def tilt_matrix(angle: float) -> np.ndarray: + M = np.identity(7) + M[0, 0] = M[1, 1] = +math.cos(angle) + M[0, 2] = M[1, 3] = -math.sin(angle) + M[2, 0] = M[3, 1] = +math.sin(angle) + M[2, 2] = M[3, 3] = +math.cos(angle) + return M + + +def translation_matrix(x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: + M = np.identity(7) + M[0, -1] = x + M[2, -1] = y + M[4, -1] = z + return M + + +def kick_matrix(kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: + M = np.identity(7) + M[1, -1] = kx + M[3, -1] = ky + M[5, -1] = kE + return M + + +def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndarray: + if B == 0: + return drift_matrix(length=length, sync_part=sync_part) + + phase = B * length + + V = np.identity(7) + V[:4, :4] = 0.0 + V[0, 1] = -1.0 / B + V[0, 2] = 0.5 + V[1, 0] = 0.5 * B + V[1, 3] = 1.0 + V[2, 1] = 1.0 / B + V[2, 2] = 0.5 + V[3, 0] = -0.5 * B + V[3, 3] = 1.0 + + M = np.identity(7) + M[0, 0] = +1.0 + M[1, 1] = -1.0 + M[2, 2] = math.cos(phase) + M[2, 3] = math.sin(phase) / B + M[3, 2] = math.sin(phase) * (-B) + M[3, 3] = math.cos(phase) + M[4, 5] = length / sync_part.gamma() ** 2 + + M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) + M = convert_matrix_dp_p_to_dE(M, sync_part) + return M + + +def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: + raise NotImplementedError() + + class MatrixFactory: """Factory for 7 x 7 transfer matrices. @@ -220,7 +401,7 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarr if type(node) is DriftTEAPOT: length = node.getLength(part_index) - return self.drift_matrix(length=length, sync_part=sync_part) + return drift_matrix(length=length, sync_part=sync_part) elif type(node) is QuadTEAPOT: length = node.getLength(part_index) @@ -230,12 +411,12 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarr scale = node.waveform.getStrength() kq = scale * node.getParam("kq") - return self.quad_matrix(length=length, kq=kq, sync_part=sync_part) + return quad_matrix(length=length, kq=kq, sync_part=sync_part) elif type(node) is BendTEAPOT: length = node.getLength(part_index) theta = node.getParam("theta") / node.getnParts() - return self.bend_matrix(length=length, theta=theta, sync_part=sync_part) + return bend_matrix(length=length, theta=theta, sync_part=sync_part) elif type(node) is KickTEAPOT: length = node.getLength(part_index) @@ -250,38 +431,38 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarr kE = node.getParam("dE") / nparts return np.matmul( - self.kick_matrix(kx=kx, ky=ky, kE=kE), - self.drift_matrix(length=length, sync_part=sync_part) + kick_matrix(kx=kx, ky=ky, kE=kE), + drift_matrix(length=length, sync_part=sync_part) ) elif type(node) is TiltTEAPOT: angle = node.getTiltAngle() - return self.tilt_matrix(angle) + return tilt_matrix(angle) elif type(node) is SolenoidTEAPOT: B = node.getParam("B") if node.waveform is not None: B *= node.waveform.getStrength() length = node.getLength(part_index) - return self.solenoid_matrix(length=length, B=B, sync_part=sync_part) + return solenoid_matrix(length=length, B=B, sync_part=sync_part) elif type(node) is Drift: length = node.getLength(part_index) - return self.drift_matrix(length=length, sync_part=sync_part) + return drift_matrix(length=length, sync_part=sync_part) elif type(node) is Quad: length = node.getLength(part_index) kq = node.getParam("dB/dr") / bunch.B_Rho() - return self.quad_matrix(length=length, kq=kq, sync_part=sync_part) + return quad_matrix(length=length, kq=kq, sync_part=sync_part) elif type(node) is Bend: length = node.getLength(part_index) theta = node.getParam("theta") / node.getnParts() - return self.bend_matrix(length=length, theta=theta, sync_part=sync_part) + return bend_matrix(length=length, theta=theta, sync_part=sync_part) elif type(node) is TiltElement: angle = node.getTiltAngle() - return self.tilt_matrix(angle) + return tilt_matrix(angle) elif type(node) in self.ignore_node_types: return np.identity(7) @@ -289,10 +470,10 @@ def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarr else: if type(node) is MultipoleTEAPOT: if np.all(np.abs(node.getParam("kls")) == 0): - return self.drift_matrix(length=node.getLength(part_index), sync_part=sync_part) + return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) elif self.handle_unknown == "drift": - return self.drift_matrix(length=node.getLength(part_index), sync_part=sync_part) + return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) elif self.handle_unknown == "fit": raise NotImplementedError() From 81eac6d13f1982c6c7f578b9eae00d3c3461b384 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 13:27:52 -0400 Subject: [PATCH 088/183] Remove funcs --- py/orbit/envelope/matrix.py | 123 ------------------------------------ 1 file changed, 123 deletions(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index aaa0e3b3..74996dfd 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -273,129 +273,6 @@ def __init__(self, handle_unknown: str | None = None) -> None: ] self.handle_unknown = handle_unknown - def drift_matrix(self, length: float, sync_part: SyncParticle) -> np.ndarray: - M = np.identity(7) - M[0, 1] = length - M[2, 3] = length - M[4, 5] = length / sync_part.gamma() ** 2 - M = convert_matrix_dp_p_to_dE(M, sync_part) - return M - - def quad_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: - if abs(kq) == 0: - return self.drift_matrix(length=length, sync_part=sync_part) - - sqrt_abs_kq = math.sqrt(abs(kq)) - - M = np.identity(7) - if kq > 0: - cx = np.cos(sqrt_abs_kq * length) - sx = np.sin(sqrt_abs_kq * length) - cy = np.cosh(sqrt_abs_kq * length) - sy = np.sinh(sqrt_abs_kq * length) - M[0, 0] = cx - M[0, 1] = +sx / sqrt_abs_kq - M[1, 0] = -sx * sqrt_abs_kq - M[1, 1] = cx - M[2, 2] = cy - M[2, 3] = sy / sqrt_abs_kq - M[3, 2] = sy * sqrt_abs_kq - M[3, 3] = cy - elif kq < 0: - cx = np.cosh(sqrt_abs_kq * length) - sx = np.sinh(sqrt_abs_kq * length) - cy = np.cos(sqrt_abs_kq * length) - sy = np.sin(sqrt_abs_kq * length) - M[0, 0] = cx - M[0, 1] = sx / sqrt_abs_kq - M[1, 0] = sx * sqrt_abs_kq - M[1, 1] = cx - M[2, 2] = cy - M[2, 3] = +sy / sqrt_abs_kq - M[3, 2] = -sy * sqrt_abs_kq - M[3, 3] = cy - - M[4, 5] = length / sync_part.gamma() ** 2 - M = convert_matrix_dp_p_to_dE(M, sync_part) - return M - - def bend_matrix(self, length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: - if length <= 0: - return np.identity(7) - - rho = length / theta - cx = math.cos(theta) - sx = math.sin(theta) - - M = np.identity(7) - M[0, 0] = cx - M[0, 1] = rho * sx - M[0, 5] = rho * (1.0 - cx) - M[1, 0] = -sx / rho - M[1, 1] = cx - M[1, 5] = sx - M[2, 3] = length - M[4, 0] = -sx - M[4, 1] = -rho * (1.0 - cx) - M[4, 5] = -(sync_part.beta() ** 2) * length + rho * sx - M = convert_matrix_dp_p_to_dE(M, sync_part) - return M - - def tilt_matrix(self, angle: float) -> np.ndarray: - M = np.identity(7) - M[0, 0] = M[1, 1] = +math.cos(angle) - M[0, 2] = M[1, 3] = -math.sin(angle) - M[2, 0] = M[3, 1] = +math.sin(angle) - M[2, 2] = M[3, 3] = +math.cos(angle) - return M - - def translation_matrix(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: - M = np.identity(7) - M[0, -1] = x - M[2, -1] = y - M[4, -1] = z - return M - - def kick_matrix(self, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: - M = np.identity(7) - M[1, -1] = kx - M[3, -1] = ky - M[5, -1] = kE - return M - - def solenoid_matrix(self, length: float, B: float, sync_part: SyncParticle) -> np.ndarray: - if B == 0: - return self.drift_matrix(length=length, sync_part=sync_part) - - phase = B * length - - V = np.identity(7) - V[:4, :4] = 0.0 - V[0, 1] = -1.0 / B - V[0, 2] = 0.5 - V[1, 0] = 0.5 * B - V[1, 3] = 1.0 - V[2, 1] = 1.0 / B - V[2, 2] = 0.5 - V[3, 0] = -0.5 * B - V[3, 3] = 1.0 - - M = np.identity(7) - M[0, 0] = +1.0 - M[1, 1] = -1.0 - M[2, 2] = math.cos(phase) - M[2, 3] = math.sin(phase) / B - M[3, 2] = math.sin(phase) * (-B) - M[3, 3] = math.cos(phase) - M[4, 5] = length / sync_part.gamma() ** 2 - - M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) - M = convert_matrix_dp_p_to_dE(M, sync_part) - return M - - def cf_matrix(self, length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: - raise NotImplementedError() - def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarray: sync_part = bunch.getSyncParticle() From 7c2d22cc6379136e51f69cc6601212fa7e4fb90d Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 20:41:05 -0400 Subject: [PATCH 089/183] Write `matrix` function for each node This function returns the transfer matrix from the synchronous particle and the node part index. This way we don't have to check the type of the node during tracking, and we could also store the transfer matrix as a node attribute if we want. --- examples/Envelope/test_env.py | 43 +++- examples/Envelope/test_env_2d_fodo.py | 26 -- py/orbit/envelope/envelope.py | 32 ++- py/orbit/envelope/matrix.py | 270 ++++++++------------- py/orbit/py_linac/lattice/LinacAccNodes.py | 62 +++++ py/orbit/teapot/teapot.py | 74 ++++++ 6 files changed, 289 insertions(+), 218 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 03dc232e..a0a8c8ef 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -9,6 +9,7 @@ from orbit.py_linac.lattice import Quad from orbit.py_linac.lattice import Bend from orbit.py_linac.lattice import TiltElement +from orbit.py_linac.lattice import Solenoid from orbit.teapot import BendTEAPOT from orbit.teapot import DriftTEAPOT from orbit.teapot import KickTEAPOT @@ -199,7 +200,7 @@ def test_quad_linac( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_dipole_teapot( +def test_bend_teapot( kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, @@ -213,7 +214,7 @@ def test_dipole_teapot( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_dipole_linac( +def test_bend_linac( kin_energy: float = 0.0025, length: float = 1.0, theta: float = 20.0, @@ -232,7 +233,7 @@ def test_dipole_linac( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_kick( +def test_kick_teapot( kin_energy: float = 0.0025, length: float = 0.1, kx: float = 0.001, @@ -274,7 +275,7 @@ def test_tilt_linac( track_and_compare_rms(lattice, kin_energy, cov_matrix) -def test_solenoid( +def test_solenoid_teapot( kin_energy: float = 0.0025, length: float = 2.0, B: float = 1.0, @@ -288,15 +289,37 @@ def test_solenoid( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_solenoid_linac( + kin_energy: float = 0.0025, + length: float = 2.0, + B: float = 1.0, + cov_matrix: np.ndarray = None, + nparts: int = 10, +) -> None: + node = Solenoid() + node.setLength(length) + node.setnParts(nparts) + node.setParam("B", B) + nodes = [node] + + lattice = make_lattice(nodes) + if cov_matrix is None: + cov_matrix = make_default_cov_matrix() + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + if __name__ == "__main__": test_drift_teapot() - test_drift_linac() test_quad_teapot() - test_quad_linac() - test_dipole_teapot() - test_dipole_linac() - test_kick() + test_bend_teapot() test_tilt_teapot() + test_solenoid_teapot() + test_kick_teapot() + + test_drift_linac() + test_quad_linac() + test_bend_linac() test_tilt_linac() - test_solenoid() + test_solenoid_linac() + diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 15e4f69d..8e60b297 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -34,32 +34,6 @@ plt.style.use("style.mplstyle") -# Parse arguments -# ------------------------------------------------------------------------------ - -parser = argparse.ArgumentParser() -parser.add_argument("--zrms", type=float, default=5.0) -parser.add_argument("--kin-energy", type=float, default=0.0025) -parser.add_argument("--intensity", type=float, default=5e9) - -parser.add_argument( - "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] -) -parser.add_argument("--mismatch-x", type=float, default=0.0) -parser.add_argument("--mismatch-y", type=float, default=0.0) -parser.add_argument("--offset-x", type=float, default=0.0) -parser.add_argument("--offset-y", type=float, default=0.0) -parser.add_argument("--tilt", type=float, default=0) - -parser.add_argument("--nslice", type=int, default=10) -parser.add_argument("--kq", type=float, default=0.25) - -parser.add_argument("--nparts", type=int, default=100_000) -parser.add_argument("--turns", type=int, default=25) -parser.add_argument("--sc", type=int, default=0) -args = parser.parse_args() - - def main(args: argparse.Namespace) -> None: # Setup diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 9c0823a0..49cfe1c1 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -10,11 +10,11 @@ from orbit.utils.consts import speed_of_light from orbit.utils.consts import charge_electron -from .matrix import MatrixFactory from .matrix import convert_matrix_zp_to_dE from .utils import gen_dist from .utils import proj_cov_matrix + ENTRANCE = AccNode.ENTRANCE BODY = AccNode.BODY EXIT = AccNode.EXIT @@ -121,8 +121,9 @@ def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov())) return rms_arr[axis] - def apply_transfer_matrix(self, m: np.ndarray) -> None: - self.moment_matrix = np.linalg.multi_dot([m, self.moment_matrix, m.T]) + def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: + if transfer_matrix is not None: + self.moment_matrix = transfer_matrix @ self.moment_matrix @ transfer_matrix.T def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, @@ -241,24 +242,26 @@ def __init__( handle_unknown: str | None = None, ) -> None: self.lattice = lattice - self.matrix_factory = MatrixFactory(handle_unknown=handle_unknown) + # self.matrix_factory = MatrixFactory(handle_unknown=handle_unknown) self.space_charge = space_charge def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): for child_node in node.getChildNodes(ENTRANCE): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.bunch) + child_node.matrix(envelope.sync_part) + # self.matrix_factory(child_node, envelope.bunch) ) - for part_index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + for index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, index, place_in_part=BEFORE): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.bunch) + child_node.matrix(envelope.sync_part) + # self.matrix_factory(child_node, envelope.bunch) ) if self.space_charge: - length = node.getLength(part_index) + length = node.getLength(index) if self.space_charge == "2d": matrix = envelope.sc_transfer_matrix_2d(length) elif self.space_charge == "3d": @@ -270,15 +273,18 @@ def track(self, envelope: Envelope) -> None: envelope.apply_transfer_matrix(matrix) envelope.apply_transfer_matrix( - self.matrix_factory(node, envelope.bunch, part_index) + node.matrix(envelope.sync_part, index) + # self.matrix_factory(node, envelope.bunch, index) ) - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + for child_node in node.getChildNodes(BODY, index, place_in_part=AFTER): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.bunch) + child_node.matrix(envelope.sync_part) + # self.matrix_factory(child_node, envelope.bunch) ) for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix( - self.matrix_factory(child_node, envelope.bunch) + child_node.matrix(envelope.sync_part) + # self.matrix_factory(child_node, envelope.bunch) ) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 74996dfd..8cafbd3a 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -5,24 +5,6 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle from orbit.lattice import AccNode -from orbit.teapot import DriftTEAPOT -from orbit.teapot import QuadTEAPOT -from orbit.teapot import BendTEAPOT -from orbit.teapot import TiltTEAPOT -from orbit.teapot import KickTEAPOT -from orbit.teapot import ApertureTEAPOT -from orbit.teapot import BunchWrapTEAPOT -from orbit.teapot import FringeFieldTEAPOT -from orbit.teapot import MonitorTEAPOT -from orbit.teapot import TurnCounterTEAPOT -from orbit.teapot import MultipoleTEAPOT -from orbit.teapot import NodeTEAPOT -from orbit.teapot import SolenoidTEAPOT -from orbit.py_linac.lattice import Drift -from orbit.py_linac.lattice import Quad -from orbit.py_linac.lattice import Bend -from orbit.py_linac.lattice import TiltElement -from orbit.py_linac.lattice import FringeField def get_dp_p_coeff(sync_part: SyncParticle) -> float: @@ -75,56 +57,6 @@ def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.n return matrix -def get_dp_p_coeff(sync_part: SyncParticle) -> float: - # dE/E = (beta^2) * dp/p - # dE = (beta^2 * E) * dp/p - # dE = (beta^2 * gamma * m * c^2) * dp/p - beta = sync_part.beta() - gamma = sync_part.gamma() - rest_energy = sync_part.mass() # GeV - return 1.0 / (beta ** 2 * gamma * rest_energy) - - -def get_zp_coeff(sync_part: SyncParticle) -> float: - # dE/E = (beta^2) * dp/p = (beta^2) * (gamma^2) z' - # dE = (beta^2 * gamma^2 * E) * z' - # dE = (beta^2 * gamma^3 * m * c^2) * z' - beta = sync_part.beta() - gamma = sync_part.gamma() - rest_energy = sync_part.mass() - return 1.0 / (beta ** 2 * gamma ** 3 * rest_energy) - - -def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: - # v = [x, x', y, y', z, dp/p] - # w = [x, x', y, y', z, dE] - # v = A w - # v -> M v - # w -> A M A^-1 - - # scale = np.identity(7) - # scale[5, 5] = dp_p_coeff - # - # scale_inv = np.identity(7) - # scale_inv[5, 5] = 1.0 / dp_p_coeff - # - # return np.linalg.multi_dot([scale, matrix, scale_inv]) - - dp_p_coeff = get_dp_p_coeff(sync_part) - matrix[:5, 5] *= dp_p_coeff - matrix[5, :5] /= dp_p_coeff - matrix[5, 6] /= dp_p_coeff - return matrix - - -def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: - zp_coeff = get_zp_coeff(sync_part) - matrix[:5, 5] *= zp_coeff - matrix[5, :5] /= zp_coeff - matrix[5, 6] /= zp_coeff - return matrix - - def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: M = np.identity(7) M[0, 1] = length @@ -255,104 +187,104 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() - -class MatrixFactory: - """Factory for 7 x 7 transfer matrices. - - Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] - """ - def __init__(self, handle_unknown: str | None = None) -> None: - self.ignore_node_types = [ - ApertureTEAPOT, - BunchWrapTEAPOT, - FringeFieldTEAPOT, - MonitorTEAPOT, - TurnCounterTEAPOT, - NodeTEAPOT, - FringeField, - ] - self.handle_unknown = handle_unknown - - def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarray: - sync_part = bunch.getSyncParticle() - - if type(node) is DriftTEAPOT: - length = node.getLength(part_index) - return drift_matrix(length=length, sync_part=sync_part) - - elif type(node) is QuadTEAPOT: - length = node.getLength(part_index) - - scale = 1.0 - if node.waveform: - scale = node.waveform.getStrength() - - kq = scale * node.getParam("kq") - return quad_matrix(length=length, kq=kq, sync_part=sync_part) - - elif type(node) is BendTEAPOT: - length = node.getLength(part_index) - theta = node.getParam("theta") / node.getnParts() - return bend_matrix(length=length, theta=theta, sync_part=sync_part) - - elif type(node) is KickTEAPOT: - length = node.getLength(part_index) - nparts = node.getnParts() - - scale = 1.0 - if node.waveform is not None: - scale = node.waveform.getStrength() - - kx = scale * node.getParam("kx") / nparts - ky = scale * node.getParam("ky") / nparts - kE = node.getParam("dE") / nparts - - return np.matmul( - kick_matrix(kx=kx, ky=ky, kE=kE), - drift_matrix(length=length, sync_part=sync_part) - ) - - elif type(node) is TiltTEAPOT: - angle = node.getTiltAngle() - return tilt_matrix(angle) - - elif type(node) is SolenoidTEAPOT: - B = node.getParam("B") - if node.waveform is not None: - B *= node.waveform.getStrength() - length = node.getLength(part_index) - return solenoid_matrix(length=length, B=B, sync_part=sync_part) - - elif type(node) is Drift: - length = node.getLength(part_index) - return drift_matrix(length=length, sync_part=sync_part) - - elif type(node) is Quad: - length = node.getLength(part_index) - kq = node.getParam("dB/dr") / bunch.B_Rho() - return quad_matrix(length=length, kq=kq, sync_part=sync_part) - - elif type(node) is Bend: - length = node.getLength(part_index) - theta = node.getParam("theta") / node.getnParts() - return bend_matrix(length=length, theta=theta, sync_part=sync_part) - - elif type(node) is TiltElement: - angle = node.getTiltAngle() - return tilt_matrix(angle) - - elif type(node) in self.ignore_node_types: - return np.identity(7) - - else: - if type(node) is MultipoleTEAPOT: - if np.all(np.abs(node.getParam("kls")) == 0): - return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) - - elif self.handle_unknown == "drift": - return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) - - elif self.handle_unknown == "fit": - raise NotImplementedError() - - raise NotImplementedError("Unsupported node: {}.".format(node)) +# +# class MatrixFactory: +# """Factory for 7 x 7 transfer matrices. +# +# Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] +# """ +# def __init__(self, handle_unknown: str | None = None) -> None: +# self.ignore_node_types = [ +# ApertureTEAPOT, +# BunchWrapTEAPOT, +# FringeFieldTEAPOT, +# MonitorTEAPOT, +# TurnCounterTEAPOT, +# NodeTEAPOT, +# FringeField, +# ] +# self.handle_unknown = handle_unknown +# +# def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarray: +# sync_part = bunch.getSyncParticle() +# +# if type(node) is DriftTEAPOT: +# length = node.getLength(part_index) +# return drift_matrix(length=length, sync_part=sync_part) +# +# elif type(node) is QuadTEAPOT: +# length = node.getLength(part_index) +# +# scale = 1.0 +# if node.waveform: +# scale = node.waveform.getStrength() +# +# kq = scale * node.getParam("kq") +# return quad_matrix(length=length, kq=kq, sync_part=sync_part) +# +# elif type(node) is BendTEAPOT: +# length = node.getLength(part_index) +# theta = node.getParam("theta") / node.getnParts() +# return bend_matrix(length=length, theta=theta, sync_part=sync_part) +# +# elif type(node) is KickTEAPOT: +# length = node.getLength(part_index) +# nparts = node.getnParts() +# +# scale = 1.0 +# if node.waveform is not None: +# scale = node.waveform.getStrength() +# +# kx = scale * node.getParam("kx") / nparts +# ky = scale * node.getParam("ky") / nparts +# kE = node.getParam("dE") / nparts +# +# return np.matmul( +# kick_matrix(kx=kx, ky=ky, kE=kE), +# drift_matrix(length=length, sync_part=sync_part) +# ) +# +# elif type(node) is TiltTEAPOT: +# angle = node.getTiltAngle() +# return tilt_matrix(angle) +# +# elif type(node) is SolenoidTEAPOT: +# B = node.getParam("B") +# if node.waveform is not None: +# B *= node.waveform.getStrength() +# length = node.getLength(part_index) +# return solenoid_matrix(length=length, B=B, sync_part=sync_part) +# +# elif type(node) is Drift: +# length = node.getLength(part_index) +# return drift_matrix(length=length, sync_part=sync_part) +# +# elif type(node) is Quad: +# length = node.getLength(part_index) +# kq = node.getParam("dB/dr") / bunch.B_Rho() +# return quad_matrix(length=length, kq=kq, sync_part=sync_part) +# +# elif type(node) is Bend: +# length = node.getLength(part_index) +# theta = node.getParam("theta") / node.getnParts() +# return bend_matrix(length=length, theta=theta, sync_part=sync_part) +# +# elif type(node) is TiltElement: +# angle = node.getTiltAngle() +# return tilt_matrix(angle) +# +# elif type(node) in self.ignore_node_types: +# return np.identity(7) +# +# else: +# if type(node) is MultipoleTEAPOT: +# if np.all(np.abs(node.getParam("kls")) == 0): +# return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) +# +# elif self.handle_unknown == "drift": +# return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) +# +# elif self.handle_unknown == "fit": +# raise NotImplementedError() +# +# raise NotImplementedError("Unsupported node: {}.".format(node)) diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index 5bc5ec16..d18f2c30 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -9,6 +9,8 @@ import os import math +import numpy as np + # import the finalization function from orbit.utils import orbitFinalize @@ -25,6 +27,16 @@ # quad2 - linac quad non-linear part of tracking from orbit.core.linac import linac_tracking +from orbit.core.bunch import SyncParticle + +from orbit.envelope.matrix import drift_matrix +from orbit.envelope.matrix import bend_matrix +from orbit.envelope.matrix import quad_matrix +from orbit.envelope.matrix import solenoid_matrix +from orbit.envelope.matrix import kick_matrix +from orbit.envelope.matrix import tilt_matrix +from orbit.envelope.matrix import translation_matrix + class BaseLinacNode(AccNodeBunchTracker): """ @@ -129,6 +141,9 @@ def trackDesign(paramsDict): self.trackActions(actionContainer, paramsDict) actionContainer.removeAction(trackDesign, AccActionsContainer.BODY) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + raise NotImplementedError() + class MarkerLinacNode(BaseLinacNode): """ @@ -140,6 +155,9 @@ def __init__(self, name="none"): BaseLinacNode.__init__(self, name) self.setType("markerLinacNode") + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class LinacNode(BaseLinacNode): """ @@ -307,6 +325,10 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] self.tracking_module.drift(bunch, length) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + return drift_matrix(length=length, sync_part=sync_part) + class Quad(LinacMagnetNode): """ @@ -505,6 +527,13 @@ def track(self, paramsDict): """ return + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + charge = 1.0 # sync_part has no charge parameter... + brho = 3.335640952 * sync_part.momentum() / charge + kq = self.getParam("dB/dr") / brho + return quad_matrix(length=length, kq=kq, sync_part=sync_part) + def getTotalField(self, z): """ Returns the field of the quad. @@ -708,6 +737,11 @@ def track(self, paramsDict): TPB.bend1(bunch, length, theta / 2.0) return + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + theta = self.getParam("theta") / self.getnParts() + return bend_matrix(length=length, theta=theta, sync_part=sync_part) + class DCorrectorH(LinacMagnetNode): """ @@ -753,6 +787,14 @@ def track(self, paramsDict): kick = -field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, kick, 0.0, 0.0) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getParam("effLength") / self.getnParts() + field = self.getParam("B") + charge = 1.0 + # dp/p = Q*c*B*L/p p in GeV/c c = 2.99792*10^8/10^9 + delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() + return kick_matrix(delta_xp, 0.0, 0.0) + class DCorrectorV(LinacMagnetNode): """ @@ -798,6 +840,14 @@ def track(self, paramsDict): kick = field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, 0, kick, 0.0) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getParam("effLength") / self.getnParts() + field = self.getParam("B") + charge = 1.0 + # dp/p = Q*c*B*L/p p in GeV/c c = 2.99792*10^8/10^9 + delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() + return kick_matrix(0.0, delta_yp, 0.0) + class ThickKick(LinacMagnetNode): """ @@ -895,6 +945,12 @@ def track(self, paramsDict): useCharge = paramsDict["useCharge"] TPB.soln(bunch, length, B, useCharge) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + B = self.getParam("B") + length = self.getLength(index) + return solenoid_matrix(length=length, B=B, sync_part=sync_part) + + class AbstractRF_Gap(BaseLinacNode): """ This is an abstarct class for all RF Gap classes. @@ -1013,6 +1069,9 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return tilt_matrix(self.__angle) + class FringeField(BaseLinacNode): """ @@ -1062,3 +1121,6 @@ def getUsage(self): field will be used in calculation. """ return self.__usage + + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None \ No newline at end of file diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 63a09b27..ffc023cd 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -23,6 +23,8 @@ from typing import Callable from typing import Union +import numpy as np + from ..lattice import AccLattice from ..lattice import AccNode from ..lattice import AccActionsContainer @@ -34,9 +36,18 @@ from ..parsers.madx_parser import MADX_Parser from ..parsers.madx_parser import MADX_LattElement +from ..envelope.matrix import drift_matrix +from ..envelope.matrix import bend_matrix +from ..envelope.matrix import quad_matrix +from ..envelope.matrix import solenoid_matrix +from ..envelope.matrix import kick_matrix +from ..envelope.matrix import tilt_matrix +from ..envelope.matrix import translation_matrix + from orbit.core.aperture import Aperture from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.bunch import SyncParticle class TEAPOT_Lattice(AccLattice): @@ -558,6 +569,9 @@ def getUsageFringeFieldOUT(self) -> bool: """ return self.__fringeFieldOUT.getUsage() + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + raise NotImplementedError() + class DriftTEAPOT(NodeTEAPOT): """ @@ -582,6 +596,10 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.drift(bunch, length) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + return drift_matrix(length=length, sync_part=sync_part) + class ApertureTEAPOT(NodeTEAPOT): """ @@ -622,6 +640,9 @@ def track(self, paramsDict: dict) -> None: lostbunch = paramsDict["lostbunch"] self.aperture.checkBunch(bunch, lostbunch) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class MonitorTEAPOT(NodeTEAPOT): """ @@ -649,6 +670,9 @@ def track(self, paramsDict: dict) -> None: self.addParam("yAvg", self.twiss.getAverage(2)) self.addParam("ypAvg", self.twiss.getAverage(3)) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class BunchWrapTEAPOT(NodeTEAPOT): """ @@ -673,6 +697,9 @@ def track(self, paramsDict: dict) -> None: length = self.getParam("ring_length") TPB.wrapbunch(bunch, length) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class SolenoidTEAPOT(NodeTEAPOT): """ @@ -721,6 +748,13 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + B = self.getParam("B") + if self.waveform is not None: + B *= self.waveform.getStrength() + length = self.getLength(index) + return solenoid_matrix(length=length, B=B, sync_part=sync_part) + class MultipoleTEAPOT(NodeTEAPOT): """ @@ -871,6 +905,12 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + if np.all(np.abs(self.getParam("kls")) == 0): + length = self.getLength(index) + return drift_matrix(length=length, sync_part=sync_part) + raise NotImplementedError() + class QuadTEAPOT(NodeTEAPOT): """ @@ -1034,6 +1074,13 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + kq = self.getParam("kq") + if self.waveform: + kq *= self.waveform.getStrength() + return quad_matrix(length=length, kq=kq, sync_part=sync_part) + class BendTEAPOT(NodeTEAPOT): """ @@ -1248,6 +1295,11 @@ def track(self, paramsDict: dict) -> None: TPB.bend1(bunch, length, theta / 2.0) return + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + theta = self.getParam("theta") / self.getnParts() + return bend_matrix(length=length, theta=theta, sync_part=sync_part) + class RingRFTEAPOT(NodeTEAPOT): """ @@ -1414,6 +1466,23 @@ def setWaveform(self, waveform): """ self.waveform = waveform + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + length = self.getLength(index) + nparts = self.getnParts() + + scale = 1.0 + if self.waveform is not None: + scale = self.waveform.getStrength() + + kx = scale * self.getParam("kx") / nparts + ky = scale * self.getParam("ky") / nparts + kE = self.getParam("dE") / nparts + + return np.matmul( + kick_matrix(kx=kx, ky=ky, kE=kE), + drift_matrix(length=length, sync_part=sync_part) + ) + class TiltTEAPOT(BaseTEAPOT): """ @@ -1449,6 +1518,8 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return tilt_matrix(self.getTiltAngle()) class FringeFieldTEAPOT(BaseTEAPOT): """ @@ -1504,6 +1575,9 @@ def getUsage(self) -> bool: """ return self.__usage + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class ContinuousLinearFocusingTEAPOT(NodeTEAPOT): def __init__( From 83c2d79fb8f1fe77bc9e533fbc398cb02721837c Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 20:44:39 -0400 Subject: [PATCH 090/183] Print node when matrix function not implemented --- py/orbit/py_linac/lattice/LinacAccNodes.py | 2 +- py/orbit/teapot/teapot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index d18f2c30..f1c3f120 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -142,7 +142,7 @@ def trackDesign(paramsDict): actionContainer.removeAction(trackDesign, AccActionsContainer.BODY) def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - raise NotImplementedError() + raise NotImplementedError(str(self)) class MarkerLinacNode(BaseLinacNode): diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index ffc023cd..95b9d864 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -570,7 +570,7 @@ def getUsageFringeFieldOUT(self) -> bool: return self.__fringeFieldOUT.getUsage() def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - raise NotImplementedError() + raise NotImplementedError(str(self)) class DriftTEAPOT(NodeTEAPOT): From 0547877515b78a99b73b86fb6353ff2fdb74394d Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 20:50:10 -0400 Subject: [PATCH 091/183] Clean up --- py/orbit/envelope/envelope.py | 26 +++++--------------------- py/orbit/teapot/teapot.py | 8 +++++++- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 49cfe1c1..e1531714 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -242,23 +242,16 @@ def __init__( handle_unknown: str | None = None, ) -> None: self.lattice = lattice - # self.matrix_factory = MatrixFactory(handle_unknown=handle_unknown) self.space_charge = space_charge def track(self, envelope: Envelope) -> None: for node in self.lattice.getNodes(): for child_node in node.getChildNodes(ENTRANCE): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - # self.matrix_factory(child_node, envelope.bunch) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) for index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, index, place_in_part=BEFORE): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - # self.matrix_factory(child_node, envelope.bunch) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) if self.space_charge: length = node.getLength(index) @@ -272,19 +265,10 @@ def track(self, envelope: Envelope) -> None: ) envelope.apply_transfer_matrix(matrix) - envelope.apply_transfer_matrix( - node.matrix(envelope.sync_part, index) - # self.matrix_factory(node, envelope.bunch, index) - ) + envelope.apply_transfer_matrix(node.matrix(envelope.sync_part, index)) for child_node in node.getChildNodes(BODY, index, place_in_part=AFTER): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - # self.matrix_factory(child_node, envelope.bunch) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) for child_node in node.getChildNodes(EXIT): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - # self.matrix_factory(child_node, envelope.bunch) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 95b9d864..79c64ef7 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -433,6 +433,9 @@ def __init__(self, name: str = "no name") -> None: AccNodeBunchTracker.__init__(self, name) self.setType("base teapot") + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + raise NotImplementedError(str(self)) + class TurnCounterTEAPOT(BaseTEAPOT): def __init__(self, name: str = "TurnCounter") -> None: @@ -451,6 +454,9 @@ def track(self, paramsDict: dict) -> None: turn = bunch.bunchAttrInt("TurnNumber") bunch.bunchAttrInt("TurnNumber", turn + 1) + def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + return None + class NodeTEAPOT(BaseTEAPOT): def __init__(self, name: str = "no name") -> None: @@ -570,7 +576,7 @@ def getUsageFringeFieldOUT(self) -> bool: return self.__fringeFieldOUT.getUsage() def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - raise NotImplementedError(str(self)) + return None class DriftTEAPOT(NodeTEAPOT): From 4414c897ea6c247b9886fc4de420f93e7fc9d4f2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:10:31 -0400 Subject: [PATCH 092/183] Move matrix calculation to new module The new module is under orbit.matrix_lattice.analytic. It is a set of functions that return transfer matrices dipoles, quads, etc. It is used for envelope tracking but could also be used elsewhere. --- py/orbit/envelope/envelope.py | 4 +- py/orbit/envelope/meson.build | 1 - py/orbit/matrix_lattice/__init__.py | 29 +++++ .../matrix.py => matrix_lattice/analytic.py} | 103 ------------------ py/orbit/matrix_lattice/meson.build | 3 +- py/orbit/py_linac/lattice/LinacAccNodes.py | 20 ++-- py/orbit/teapot/teapot.py | 24 ++-- 7 files changed, 62 insertions(+), 122 deletions(-) rename py/orbit/{envelope/matrix.py => matrix_lattice/analytic.py} (56%) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e1531714..a2194d3e 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -7,10 +7,10 @@ from orbit.core.bunch import SyncParticle from orbit.lattice import AccNode from orbit.lattice import AccLattice +from orbit.matrix_lattice.analytic import convert_matrix_zp_to_dE from orbit.utils.consts import speed_of_light from orbit.utils.consts import charge_electron -from .matrix import convert_matrix_zp_to_dE from .utils import gen_dist from .utils import proj_cov_matrix @@ -39,7 +39,7 @@ class Envelope: """Represents beam envelope and centroid. Attributes: - matrix: 7 x 7 covariance matrix for augmented phase space vector. + moment_matrix: 7 x 7 covariance matrix for augmented phase space vector. Define the phase space vector X = [x, x', y, y', z, dE]^T and augmented vector Y = [x, x', y, y', z, dE, 1]. diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 31c9033b..1e9fe35c 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -1,7 +1,6 @@ py_sources = files([ '__init__.py', 'envelope.py', - 'matrix.py', 'utils.py' ]) diff --git a/py/orbit/matrix_lattice/__init__.py b/py/orbit/matrix_lattice/__init__.py index 84598241..ed7ada08 100644 --- a/py/orbit/matrix_lattice/__init__.py +++ b/py/orbit/matrix_lattice/__init__.py @@ -5,6 +5,35 @@ from .MATRIX_Lattice import MATRIX_Lattice from .BaseMATRIX import BaseMATRIX +# from .analytic import get_dp_p_coeff +# from .analytic import get_zp_coeff +# from .analytic import convert_matrix_dp_p_to_dE +# from .analytic import convert_matrix_zp_to_dE +# from .analytic import drift_matrix +# from .analytic import quad_matrix +# from .analytic import bend_matrix +# from .analytic import tilt_matrix +# from .analytic import translation_matrix +# from .analytic import kick_matrix +# from .analytic import solenoid_matrix +# from .analytic import cf_matrix +from . import analytic + + __all__ = [] __all__.append("MATRIX_Lattice") __all__.append("BaseMATRIX") +__all__.append("analytic") +# __all__.append("get_dp_p_coeff") +# __all__.append("get_zp_coeff") +# __all__.append("convert_matrix_dp_p_to_dE") +# __all__.append("convert_matrix_zp_to_dE") +# __all__.append("drift_matrix") +# __all__.append("quad_matrix") +# __all__.append("bend_matrix") +# __all__.append("tilt_matrix") +# __all__.append("translation_matrix") +# __all__.append("kick_matrix") +# __all__.append("solenoid_matrix") +# __all__.append("cf_matrix") +# diff --git a/py/orbit/envelope/matrix.py b/py/orbit/matrix_lattice/analytic.py similarity index 56% rename from py/orbit/envelope/matrix.py rename to py/orbit/matrix_lattice/analytic.py index 8cafbd3a..28f353e2 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -4,7 +4,6 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle -from orbit.lattice import AccNode def get_dp_p_coeff(sync_part: SyncParticle) -> float: @@ -186,105 +185,3 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() - -# -# class MatrixFactory: -# """Factory for 7 x 7 transfer matrices. -# -# Units: x [m], x' [rad], y [m], y' [rad], z [m], dE [GeV] -# """ -# def __init__(self, handle_unknown: str | None = None) -> None: -# self.ignore_node_types = [ -# ApertureTEAPOT, -# BunchWrapTEAPOT, -# FringeFieldTEAPOT, -# MonitorTEAPOT, -# TurnCounterTEAPOT, -# NodeTEAPOT, -# FringeField, -# ] -# self.handle_unknown = handle_unknown -# -# def __call__(self, node: AccNode, bunch: Bunch, part_index: int = 0) -> np.ndarray: -# sync_part = bunch.getSyncParticle() -# -# if type(node) is DriftTEAPOT: -# length = node.getLength(part_index) -# return drift_matrix(length=length, sync_part=sync_part) -# -# elif type(node) is QuadTEAPOT: -# length = node.getLength(part_index) -# -# scale = 1.0 -# if node.waveform: -# scale = node.waveform.getStrength() -# -# kq = scale * node.getParam("kq") -# return quad_matrix(length=length, kq=kq, sync_part=sync_part) -# -# elif type(node) is BendTEAPOT: -# length = node.getLength(part_index) -# theta = node.getParam("theta") / node.getnParts() -# return bend_matrix(length=length, theta=theta, sync_part=sync_part) -# -# elif type(node) is KickTEAPOT: -# length = node.getLength(part_index) -# nparts = node.getnParts() -# -# scale = 1.0 -# if node.waveform is not None: -# scale = node.waveform.getStrength() -# -# kx = scale * node.getParam("kx") / nparts -# ky = scale * node.getParam("ky") / nparts -# kE = node.getParam("dE") / nparts -# -# return np.matmul( -# kick_matrix(kx=kx, ky=ky, kE=kE), -# drift_matrix(length=length, sync_part=sync_part) -# ) -# -# elif type(node) is TiltTEAPOT: -# angle = node.getTiltAngle() -# return tilt_matrix(angle) -# -# elif type(node) is SolenoidTEAPOT: -# B = node.getParam("B") -# if node.waveform is not None: -# B *= node.waveform.getStrength() -# length = node.getLength(part_index) -# return solenoid_matrix(length=length, B=B, sync_part=sync_part) -# -# elif type(node) is Drift: -# length = node.getLength(part_index) -# return drift_matrix(length=length, sync_part=sync_part) -# -# elif type(node) is Quad: -# length = node.getLength(part_index) -# kq = node.getParam("dB/dr") / bunch.B_Rho() -# return quad_matrix(length=length, kq=kq, sync_part=sync_part) -# -# elif type(node) is Bend: -# length = node.getLength(part_index) -# theta = node.getParam("theta") / node.getnParts() -# return bend_matrix(length=length, theta=theta, sync_part=sync_part) -# -# elif type(node) is TiltElement: -# angle = node.getTiltAngle() -# return tilt_matrix(angle) -# -# elif type(node) in self.ignore_node_types: -# return np.identity(7) -# -# else: -# if type(node) is MultipoleTEAPOT: -# if np.all(np.abs(node.getParam("kls")) == 0): -# return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) -# -# elif self.handle_unknown == "drift": -# return drift_matrix(length=node.getLength(part_index), sync_part=sync_part) -# -# elif self.handle_unknown == "fit": -# raise NotImplementedError() -# -# raise NotImplementedError("Unsupported node: {}.".format(node)) diff --git a/py/orbit/matrix_lattice/meson.build b/py/orbit/matrix_lattice/meson.build index ed29e83e..38ab625a 100644 --- a/py/orbit/matrix_lattice/meson.build +++ b/py/orbit/matrix_lattice/meson.build @@ -4,7 +4,8 @@ py_sources = files([ 'MATRIX_Lattice.py', '__init__.py', - 'BaseMATRIX.py' + 'BaseMATRIX.py', + 'analytic.py' ]) python.install_sources( diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index f1c3f120..abee5e13 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -29,13 +29,19 @@ from orbit.core.bunch import SyncParticle -from orbit.envelope.matrix import drift_matrix -from orbit.envelope.matrix import bend_matrix -from orbit.envelope.matrix import quad_matrix -from orbit.envelope.matrix import solenoid_matrix -from orbit.envelope.matrix import kick_matrix -from orbit.envelope.matrix import tilt_matrix -from orbit.envelope.matrix import translation_matrix +# from orbit.matrix_lattice import drift_matrix +# from orbit.matrix_lattice import bend_matrix +# from orbit.matrix_lattice import quad_matrix +# from orbit.matrix_lattice import solenoid_matrix +# from orbit.matrix_lattice import kick_matrix +# from orbit.matrix_lattice import tilt_matrix + +from orbit.matrix_lattice.analytic import drift_matrix +from orbit.matrix_lattice.analytic import bend_matrix +from orbit.matrix_lattice.analytic import quad_matrix +from orbit.matrix_lattice.analytic import solenoid_matrix +from orbit.matrix_lattice.analytic import kick_matrix +from orbit.matrix_lattice.analytic import tilt_matrix class BaseLinacNode(AccNodeBunchTracker): diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 79c64ef7..4690935b 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -36,13 +36,19 @@ from ..parsers.madx_parser import MADX_Parser from ..parsers.madx_parser import MADX_LattElement -from ..envelope.matrix import drift_matrix -from ..envelope.matrix import bend_matrix -from ..envelope.matrix import quad_matrix -from ..envelope.matrix import solenoid_matrix -from ..envelope.matrix import kick_matrix -from ..envelope.matrix import tilt_matrix -from ..envelope.matrix import translation_matrix +# from orbit.matrix_lattice import drift_matrix +# from orbit.matrix_lattice import bend_matrix +# from orbit.matrix_lattice import quad_matrix +# from orbit.matrix_lattice import solenoid_matrix +# from orbit.matrix_lattice import kick_matrix +# from orbit.matrix_lattice import tilt_matrix + +from orbit.matrix_lattice.analytic import drift_matrix +from orbit.matrix_lattice.analytic import bend_matrix +from orbit.matrix_lattice.analytic import quad_matrix +from orbit.matrix_lattice.analytic import solenoid_matrix +from orbit.matrix_lattice.analytic import kick_matrix +from orbit.matrix_lattice.analytic import tilt_matrix from orbit.core.aperture import Aperture from orbit.core.bunch import Bunch @@ -915,7 +921,9 @@ def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: if np.all(np.abs(self.getParam("kls")) == 0): length = self.getLength(index) return drift_matrix(length=length, sync_part=sync_part) - raise NotImplementedError() + + # [TO DO] Return matrix for dipole + quadrupole components? + raise NotImplementedError(str(self)) class QuadTEAPOT(NodeTEAPOT): From f06d073063a926ee7d3bd3bc13b0ac67f127ff2b Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:18:44 -0400 Subject: [PATCH 093/183] Fix lorentz transform of xyz cov matrix in example --- examples/Envelope/test_env_3d_drift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 8c7cd78a..1de7df4e 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -83,7 +83,7 @@ def main(args: argparse.Namespace) -> None: cov_matrix_xyz = build_cov_matrix_xyz([args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix) lorentz_matrix = np.diag([1.0, 1.0, 1.0 / sync_part.gamma()]) - cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix + cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix.T spatial_idx = np.ix_([0, 2, 4], [0, 2, 4]) cov_matrix_init[spatial_idx] = cov_matrix_xyz From 5dbf992c66b315fb41a119d2e9d06ac37ee97921 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:19:36 -0400 Subject: [PATCH 094/183] Fix arg in gen_dist --- py/orbit/envelope/envelope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index a2194d3e..e10e237e 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -128,7 +128,7 @@ def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, # giving error in cholesky decomposition. - particles = gen_dist(n=size, cov_matrix=self.cov(), name=dist) + particles = gen_dist(size=size, cov_matrix=self.cov(), name=dist) particles = particles + self.centroid() return particles From 9e018b7da41e42ca62d05586eb690c4512933d5a Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:20:06 -0400 Subject: [PATCH 095/183] Remove comment --- py/orbit/envelope/envelope.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e10e237e..e69e74aa 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -133,8 +133,6 @@ def sample(self, size: int, dist: str = "kv") -> np.ndarray: return particles def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: - # [TO DO] Move this to matrix.py? - # Extract beam centroid and covariance matrix. centroid = self.centroid() cov_matrix = self.cov() @@ -179,8 +177,6 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - # [TO DO] Move this to matrix.py? - centroid = self.centroid() centroid[4] *= self.gamma() From 38b80cf6f99e1e6da2f50d1b90f6e86f31e4d918 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:20:39 -0400 Subject: [PATCH 096/183] Remove skip line --- py/orbit/envelope/envelope.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e69e74aa..3171da76 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -256,9 +256,7 @@ def track(self, envelope: Envelope) -> None: elif self.space_charge == "3d": matrix = envelope.sc_transfer_matrix_3d(length) else: - raise ValueError( - f"Invalid space charge model: {self.space_charge}" - ) + raise ValueError(f"Invalid space charge model: {self.space_charge}") envelope.apply_transfer_matrix(matrix) envelope.apply_transfer_matrix(node.matrix(envelope.sync_part, index)) From 1167aaf1f571a95c5464335ecbea01db9cfe1d84 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:22:14 -0400 Subject: [PATCH 097/183] Rename self.perveance to self.sc_factor The perveance involves the line density: perveance = sc_factor / bunch_length. --- py/orbit/envelope/envelope.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 3171da76..86b2d86b 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -90,7 +90,7 @@ def __init__( def set_intensity(self, intensity: float) -> None: self.intensity = intensity - self.perveance = ( + self.sc_factor = ( 2.0 * intensity * CLASSICAL_PROTON_RADIUS @@ -151,7 +151,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: # Build transfer matrix in upright frame. bunch_length = 4.0 * self.rms(axis=4) - perveance = self.perveance / bunch_length + perveance = self.sc_factor / bunch_length factor = 2.0 * perveance / (rx + ry) kappa_x = factor / rx kappa_y = factor / ry @@ -197,7 +197,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: RDy = scipy.special.elliprd(cov_xx, cov_zz, cov_yy) RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) - factor = 0.5 * self.perveance * ((1.0 / 5.0) ** 1.5) + factor = 0.5 * self.sc_factor * ((1.0 / 5.0) ** 1.5) kappa_x = factor * RDx # [1 / m] kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] From 4e14a4469a42c4a9916021375dbfe38741f2ebe3 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:28:01 -0400 Subject: [PATCH 098/183] Add empty test function test_sc_3d_cold_expansion --- examples/Envelope/test_env.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index a0a8c8ef..683725e3 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -308,6 +308,14 @@ def test_solenoid_linac( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_sc_3d_cold_expansion(): + # This should test expansion of cold uniform-density sphere + # (in rest frame). We can calculate the time to expand to + # twice the initial size. (See examples from A. Shishlo or + # from the ImpactX repo.) + raise NotImplementedError + + if __name__ == "__main__": test_drift_teapot() test_quad_teapot() From d215c68690796bf122cbd9527406a46e90116e31 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:32:16 -0400 Subject: [PATCH 099/183] Lorentz transform was not used in 3D space charge Results are still incorrect at high energy... --- py/orbit/envelope/envelope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 86b2d86b..7c2c792e 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -223,8 +223,8 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # x = L V u = L T A u. # u -> M u # x -> V M V^-1 x - V = np.matmul(T, A) - M = V @ M @ np.linalg.inv(V) + V = np.linalg.multi_dot([L, T, A]) + M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) # Convert from z' to dE. return convert_matrix_zp_to_dE(M, self.sync_part) From 7be874557d885d2b9d16fdb127180f5d3f742525 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:46:55 -0400 Subject: [PATCH 100/183] Add comments --- py/orbit/envelope/envelope.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 7c2c792e..ac313dec 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -177,16 +177,18 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: + # Get centroid in rest frame. centroid = self.centroid() centroid[4] *= self.gamma() + # Get covariance matrix in rest frame. cov_matrix = self.cov() cov_matrix[4, 4] *= self.gamma() ** 2 # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) - # Compute eigenvalues and eigenvectors of x-y covariance matrix. + # Compute eigenvalues and eigenvectors of x-y-z covariance matrix. cov_eig_res = np.linalg.eig(cov_matrix_proj) cov_eig_vals = cov_eig_res.eigenvalues cov_eig_vecs = cov_eig_res.eigenvectors @@ -226,7 +228,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: V = np.linalg.multi_dot([L, T, A]) M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) - # Convert from z' to dE. + # Convert from z' to dE return convert_matrix_zp_to_dE(M, self.sync_part) From 5cd2ba0d6492b05629b14ca3350dfeb253722876 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:56:16 -0400 Subject: [PATCH 101/183] Use matrix for lorentz transform of cov matrix --- py/orbit/envelope/envelope.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index ac313dec..cff00f42 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -177,13 +177,22 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: + # Build Lorentz matrix + lorentz_matrix = np.identity(7) + lorentz_matrix[4, 4] = 1.0 / self.gamma() + lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) + # Get centroid in rest frame. centroid = self.centroid() centroid[4] *= self.gamma() # Get covariance matrix in rest frame. cov_matrix = self.cov() - cov_matrix[4, 4] *= self.gamma() ** 2 + cov_matrix = np.linalg.multi_dot([ + lorentz_matrix_inv[:-1, :-1], + cov_matrix, + lorentz_matrix_inv[:-1, :-1].T] + ) # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) @@ -218,8 +227,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: T[i, -1] = centroid[i] # Build matrix for Lorentz boost (length contraction). - L = np.identity(7) - L[4, 4] = 1.0 / self.gamma() + L = lorentz_matrix # Compute matrix in lab frame. # x = L V u = L T A u. From d1ce6fcbbf4f61525d415d12d20080170a980527 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 21:57:27 -0400 Subject: [PATCH 102/183] Remove comments --- py/orbit/envelope/envelope.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index cff00f42..f5b4270b 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -168,10 +168,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: T[0, -1] = centroid[0] T[2, -1] = centroid[2] - # Compute matrix in lab frame. - # x = V u = T A u. - # u -> M u - # x -> V M V^-1 x + # Compute transfer matrix in lab frame. V = np.matmul(T, A) V_inv = np.linalg.inv(V) return np.linalg.multi_dot([V, M, V_inv]) @@ -229,10 +226,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build matrix for Lorentz boost (length contraction). L = lorentz_matrix - # Compute matrix in lab frame. - # x = L V u = L T A u. - # u -> M u - # x -> V M V^-1 x + # Compute transfer matrix in lab frame. V = np.linalg.multi_dot([L, T, A]) M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) From 013656583fa0c1f1991380f0c06e2141d627758a Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:17:58 -0400 Subject: [PATCH 103/183] Fix gamma factor in 3D space charge Factor was double counted in transverse kick. Things are now working when gamma >> 1, but not when there are x-z or y-z correlations --- py/orbit/envelope/envelope.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index f5b4270b..bbb26e14 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -176,7 +176,10 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix lorentz_matrix = np.identity(7) + # lorentz_matrix[1, 1] = 1.0 / self.gamma() + # lorentz_matrix[3, 3] = 1.0 / self.gamma() lorentz_matrix[4, 4] = 1.0 / self.gamma() + # lorentz_matrix[5, 5] = 1.0 / self.gamma() lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) # Get centroid in rest frame. @@ -206,10 +209,13 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) factor = 0.5 * self.sc_factor * ((1.0 / 5.0) ** 1.5) - kappa_x = factor * RDx # [1 / m] - kappa_y = factor * RDy # [1 / m] + kappa_x = factor * RDx # [1 / m] + kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] + kappa_x *= self.gamma() + kappa_y *= self.gamma() + M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length @@ -231,7 +237,9 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) # Convert from z' to dE - return convert_matrix_zp_to_dE(M, self.sync_part) + M = convert_matrix_zp_to_dE(M, self.sync_part) + + return M class EnvelopeTracker: From 90084573412c729c41b5506baa430b125872a16f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:27:59 -0400 Subject: [PATCH 104/183] Looking good for upright beams relativistic + nonrelativistic Not working with x-z tilt when gamma >> 1 --- py/orbit/envelope/envelope.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index bbb26e14..bd3f8863 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -176,10 +176,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix lorentz_matrix = np.identity(7) - # lorentz_matrix[1, 1] = 1.0 / self.gamma() - # lorentz_matrix[3, 3] = 1.0 / self.gamma() lorentz_matrix[4, 4] = 1.0 / self.gamma() - # lorentz_matrix[5, 5] = 1.0 / self.gamma() lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) # Get centroid in rest frame. From 98980154932f78f75cb9e45e01a162137fd3a645 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:38:13 -0400 Subject: [PATCH 105/183] Fix lorentz boost for x' and y' This fixes space charge kick for x and y when there are x-z or y-z correlations in the bunch ellipsoid. --- py/orbit/envelope/envelope.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index bd3f8863..0bf28c20 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -176,6 +176,8 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix lorentz_matrix = np.identity(7) + lorentz_matrix[1, 1] = self.gamma() + lorentz_matrix[3, 3] = self.gamma() lorentz_matrix[4, 4] = 1.0 / self.gamma() lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) @@ -210,9 +212,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] - kappa_x *= self.gamma() - kappa_y *= self.gamma() - M = np.identity(7) M[1, 0] = kappa_x * length M[3, 2] = kappa_y * length From ce89caca3f36c68436c15787a710c1259ef0410b Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:41:06 -0400 Subject: [PATCH 106/183] Use eigh instead of eig for cov matrix eigvectors --- py/orbit/envelope/envelope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 0bf28c20..e44ae1c9 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -141,7 +141,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) # Compute eigenvalues and eigenvectors of x-y covariance matrix. - cov_eig_res = np.linalg.eig(cov_matrix_proj) + cov_eig_res = np.linalg.eigh(cov_matrix_proj) cov_eig_vals = cov_eig_res.eigenvalues cov_eig_vecs = cov_eig_res.eigenvectors @@ -197,7 +197,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) # Compute eigenvalues and eigenvectors of x-y-z covariance matrix. - cov_eig_res = np.linalg.eig(cov_matrix_proj) + cov_eig_res = np.linalg.eigh(cov_matrix_proj) cov_eig_vals = cov_eig_res.eigenvalues cov_eig_vecs = cov_eig_res.eigenvectors From 90a6dc5e025ff8f177074df90a3838dbb3460dfd Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:54:37 -0400 Subject: [PATCH 107/183] Formatting and comments --- examples/Envelope/test_env.py | 18 +++++---- examples/Envelope/test_env_2d_fodo.py | 13 ++---- examples/Envelope/test_env_3d_drift.py | 23 +++++------ examples/Envelope/test_env_sns_ring.py | 16 ++++---- py/orbit/envelope/envelope.py | 56 +++++++++++++++----------- 5 files changed, 66 insertions(+), 60 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 683725e3..3c60e5c1 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -24,7 +24,7 @@ def get_lorentz_factors(kin_energy: float, mass: float) -> tuple[float, float]: gamma = 1.0 + kin_energy / mass - beta = np.sqrt(1.0 - (1.0 / gamma)**2) + beta = np.sqrt(1.0 - (1.0 / gamma) ** 2) return (gamma, beta) @@ -144,7 +144,10 @@ def make_default_cov_matrix( def test_drift_teapot( - kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, nparts: int = 6, + kin_energy: float = 0.0025, + length: float = 1.0, + cov_matrix: np.ndarray = None, + nparts: int = 6, ) -> None: nodes = [DriftTEAPOT(length=length, nparts=nparts)] lattice = make_lattice(nodes) @@ -154,7 +157,10 @@ def test_drift_teapot( def test_drift_linac( - kin_energy: float = 0.0025, length: float = 1.0, cov_matrix: np.ndarray = None, nparts: int = 6, + kin_energy: float = 0.0025, + length: float = 1.0, + cov_matrix: np.ndarray = None, + nparts: int = 6, ) -> None: node = Drift() node.setLength(length) @@ -247,8 +253,8 @@ def test_kick_teapot( if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) - - + + def test_tilt_teapot( kin_energy: float = 0.0025, angle: float = 0.25 * np.pi, @@ -329,5 +335,3 @@ def test_sc_3d_cold_expansion(): test_bend_linac() test_tilt_linac() test_solenoid_linac() - - diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 8e60b297..a3890e8a 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -43,7 +43,6 @@ def main(args: argparse.Namespace) -> None: output_dir = os.path.join("outputs", path.stem) os.makedirs(output_dir, exist_ok=True) - # Create lattice # ------------------------------------------------------------------------------ @@ -64,7 +63,6 @@ def main(args: argparse.Namespace) -> None: lattice.initialize() - # Create envelope # ------------------------------------------------------------------------------ @@ -119,7 +117,6 @@ def main(args: argparse.Namespace) -> None: intensity=args.intensity, ) - # Track envelope # ------------------------------------------------------------------------------ @@ -152,7 +149,6 @@ def main(args: argparse.Namespace) -> None: histories = {} histories["envelope"] = copy.deepcopy(history) - # Track bunch # ------------------------------------------------------------------------------ @@ -208,7 +204,6 @@ def main(args: argparse.Namespace) -> None: histories["bunch"] = copy.deepcopy(history) - # Analysis # ------------------------------------------------------------------------------ @@ -251,7 +246,6 @@ def main(args: argparse.Namespace) -> None: plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() - # Collect bunch/envelope data on final turn. particles = collect_bunch(bunch)["coords"] particles[:, :4] *= 1000.0 @@ -266,7 +260,6 @@ def main(args: argparse.Namespace) -> None: limits = list(zip(-xmax, xmax)) labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] - # Plot x-x' fig, ax = plt.subplots(figsize=(4, 4)) ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) @@ -309,7 +302,9 @@ def main(args: argparse.Namespace) -> None: parser.add_argument("--kin-energy", type=float, default=0.0025) parser.add_argument("--intensity", type=float, default=5e9) - parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) + parser.add_argument( + "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] + ) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) @@ -324,4 +319,4 @@ def main(args: argparse.Namespace) -> None: parser.add_argument("--sc", type=int, default=0) args = parser.parse_args() - main(args) \ No newline at end of file + main(args) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 1de7df4e..1987a873 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -34,10 +34,14 @@ def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: - return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() + return scipy.spatial.transform.Rotation.from_euler( + "xyz", [angle_x, angle_y, angle_z] + ).as_matrix() -def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: +def build_cov_matrix_xyz( + rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None +) -> np.ndarray: cov_matrix = np.diag(np.square(rms_sizes)) if rotation_matrix is None: return cov_matrix @@ -52,7 +56,6 @@ def main(args: argparse.Namespace) -> None: output_dir = os.path.join("outputs", path.stem) os.makedirs(output_dir, exist_ok=True) - # Create lattice # ------------------------------------------------------------------------------ node = DriftTEAPOT(length=args.length) @@ -63,7 +66,6 @@ def main(args: argparse.Namespace) -> None: lattice.addNode(node) lattice.initialize() - # Create envelope # ------------------------------------------------------------------------------ bunch = Bunch() @@ -74,13 +76,13 @@ def main(args: argparse.Namespace) -> None: cov_matrix_init = np.zeros((6, 6)) rotation_matrix = rotation_matrix_3d( - math.radians(args.rot_x), - math.radians(args.rot_y), - math.radians(args.rot_z) + math.radians(args.rot_x), math.radians(args.rot_y), math.radians(args.rot_z) ) print(rotation_matrix) - cov_matrix_xyz = build_cov_matrix_xyz([args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix) + cov_matrix_xyz = build_cov_matrix_xyz( + [args.rms_x, args.rms_y, args.rms_z], rotation_matrix=rotation_matrix + ) lorentz_matrix = np.diag([1.0, 1.0, 1.0 / sync_part.gamma()]) cov_matrix_xyz = lorentz_matrix @ cov_matrix_xyz @ lorentz_matrix.T @@ -92,7 +94,6 @@ def main(args: argparse.Namespace) -> None: print() print(cov_matrix_init * 1e6) - centroid_init = np.zeros(6) envelope = Envelope( @@ -129,7 +130,6 @@ def main(args: argparse.Namespace) -> None: histories = {} histories["envelope"] = copy.deepcopy(history) - # Track bunch # ------------------------------------------------------------------------------ print("TRACK BUNCH") @@ -176,7 +176,6 @@ def main(args: argparse.Namespace) -> None: histories["bunch"] = copy.deepcopy(history) - # Analysis # ------------------------------------------------------------------------------ for history in histories.values(): @@ -276,4 +275,4 @@ def main(args: argparse.Namespace) -> None: parser.add_argument("--sc", type=int, default=0) args = parser.parse_args() - main(args) \ No newline at end of file + main(args) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index a43e8444..b2f4e837 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -40,7 +40,6 @@ def main(args: argparse.Namespace) -> None: output_dir = os.path.join("outputs", path.stem) os.makedirs(output_dir, exist_ok=True) - # Create lattice # ------------------------------------------------------------------------------ @@ -65,7 +64,6 @@ def main(args: argparse.Namespace) -> None: if node.getLength() > max_length: node.setnParts(1 + int(node.getLength() / max_length)) - # Create envelope # ------------------------------------------------------------------------------ @@ -122,7 +120,6 @@ def main(args: argparse.Namespace) -> None: intensity=args.intensity, ) - # Track envelope # ------------------------------------------------------------------------------ @@ -159,7 +156,6 @@ def main(args: argparse.Namespace) -> None: histories = {} histories["envelope"] = copy.deepcopy(history) - # Track bunch # ------------------------------------------------------------------------------ @@ -215,7 +211,6 @@ def main(args: argparse.Namespace) -> None: histories["bunch"] = copy.deepcopy(history) - # Analysis # ------------------------------------------------------------------------------ @@ -258,7 +253,6 @@ def main(args: argparse.Namespace) -> None: plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() - # Collect bunch/envelope data on final turn. particles = collect_bunch(bunch)["coords"] particles[:, :4] *= 1000.0 @@ -315,7 +309,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--kin-energy", type=float, default=1.300) parser.add_argument("--intensity", type=float, default=2e14) - parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) + parser.add_argument( + "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] + ) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) @@ -328,10 +324,12 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) - parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) + parser.add_argument( + "--handle-unknown", type=str, default=None, choices=["drift", "fit"] + ) return parser.parse_args() if __name__ == "__main__": args = parse_args() - main(args) \ No newline at end of file + main(args) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e44ae1c9..bca9a63d 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -8,8 +8,6 @@ from orbit.lattice import AccNode from orbit.lattice import AccLattice from orbit.matrix_lattice.analytic import convert_matrix_zp_to_dE -from orbit.utils.consts import speed_of_light -from orbit.utils.consts import charge_electron from .utils import gen_dist from .utils import proj_cov_matrix @@ -78,11 +76,13 @@ def __init__( if cov_matrix is None: cov_matrix = np.eye(6) - + self.moment_matrix = np.zeros((7, 7)) - self.moment_matrix[:self.dim, :self.dim] = cov_matrix + np.outer(centroid, centroid) - self.moment_matrix[:self.dim, self.dim] = centroid - self.moment_matrix[self.dim, :self.dim] = centroid + self.moment_matrix[: self.dim, : self.dim] = cov_matrix + np.outer( + centroid, centroid + ) + self.moment_matrix[: self.dim, self.dim] = centroid + self.moment_matrix[self.dim, : self.dim] = centroid self.moment_matrix[self.dim, self.dim] = 1.0 self.intensity = 0.0 @@ -110,11 +110,11 @@ def charge(self) -> float: return self.bunch.charge() def centroid(self) -> np.ndarray: - return np.copy(self.moment_matrix[:self.dim, self.dim]) + return np.copy(self.moment_matrix[: self.dim, self.dim]) def cov(self) -> np.ndarray: - autocorrelation_matrix = self.moment_matrix[:self.dim, :self.dim] - centroid = self.moment_matrix[:self.dim, self.dim] + autocorrelation_matrix = self.moment_matrix[: self.dim, : self.dim] + centroid = self.moment_matrix[: self.dim, self.dim] return autocorrelation_matrix - np.outer(centroid, centroid) def rms(self, axis: int = None) -> float | np.ndarray: @@ -123,7 +123,9 @@ def rms(self, axis: int = None) -> float | np.ndarray: def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: if transfer_matrix is not None: - self.moment_matrix = transfer_matrix @ self.moment_matrix @ transfer_matrix.T + self.moment_matrix = ( + transfer_matrix @ self.moment_matrix @ transfer_matrix.T + ) def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, @@ -174,7 +176,13 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: return np.linalg.multi_dot([V, M, V_inv]) def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: - # Build Lorentz matrix + # Build Lorentz matrix: rest frame to lab frame. + # x -> x + # y -> y + # z -> gamma * z + # x' = dx/ds -> x' / gamma + # y' = dy/ds -> y' / gamma + # z' = dz/ds -> z' lorentz_matrix = np.identity(7) lorentz_matrix[1, 1] = self.gamma() lorentz_matrix[3, 3] = self.gamma() @@ -187,10 +195,8 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Get covariance matrix in rest frame. cov_matrix = self.cov() - cov_matrix = np.linalg.multi_dot([ - lorentz_matrix_inv[:-1, :-1], - cov_matrix, - lorentz_matrix_inv[:-1, :-1].T] + cov_matrix = np.linalg.multi_dot( + [lorentz_matrix_inv[:-1, :-1], cov_matrix, lorentz_matrix_inv[:-1, :-1].T] ) # Project covariance matrix onto x-y-z plane. @@ -208,8 +214,8 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) factor = 0.5 * self.sc_factor * ((1.0 / 5.0) ** 1.5) - kappa_x = factor * RDx # [1 / m] - kappa_y = factor * RDy # [1 / m] + kappa_x = factor * RDx # [1 / m] + kappa_y = factor * RDy # [1 / m] kappa_z = factor * RDz # [1 / m] M = np.identity(7) @@ -233,9 +239,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) # Convert from z' to dE - M = convert_matrix_zp_to_dE(M, self.sync_part) - - return M + return convert_matrix_zp_to_dE(M, self.sync_part) class EnvelopeTracker: @@ -255,7 +259,9 @@ def track(self, envelope: Envelope) -> None: for index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, index, place_in_part=BEFORE): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + envelope.apply_transfer_matrix( + child_node.matrix(envelope.sync_part) + ) if self.space_charge: length = node.getLength(index) @@ -264,13 +270,17 @@ def track(self, envelope: Envelope) -> None: elif self.space_charge == "3d": matrix = envelope.sc_transfer_matrix_3d(length) else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") + raise ValueError( + f"Invalid space charge model: {self.space_charge}" + ) envelope.apply_transfer_matrix(matrix) envelope.apply_transfer_matrix(node.matrix(envelope.sync_part, index)) for child_node in node.getChildNodes(BODY, index, place_in_part=AFTER): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + envelope.apply_transfer_matrix( + child_node.matrix(envelope.sync_part) + ) for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) From a079b3318d79db78fea4e8c3321576e0c05141da Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Tue, 23 Jun 2026 22:57:50 -0400 Subject: [PATCH 108/183] Remove commented lines --- py/orbit/matrix_lattice/__init__.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/py/orbit/matrix_lattice/__init__.py b/py/orbit/matrix_lattice/__init__.py index ed7ada08..2000a874 100644 --- a/py/orbit/matrix_lattice/__init__.py +++ b/py/orbit/matrix_lattice/__init__.py @@ -4,19 +4,6 @@ ## These classes use orbit::utils::matrix::Matrix C++ wrappers from .MATRIX_Lattice import MATRIX_Lattice from .BaseMATRIX import BaseMATRIX - -# from .analytic import get_dp_p_coeff -# from .analytic import get_zp_coeff -# from .analytic import convert_matrix_dp_p_to_dE -# from .analytic import convert_matrix_zp_to_dE -# from .analytic import drift_matrix -# from .analytic import quad_matrix -# from .analytic import bend_matrix -# from .analytic import tilt_matrix -# from .analytic import translation_matrix -# from .analytic import kick_matrix -# from .analytic import solenoid_matrix -# from .analytic import cf_matrix from . import analytic @@ -24,16 +11,3 @@ __all__.append("MATRIX_Lattice") __all__.append("BaseMATRIX") __all__.append("analytic") -# __all__.append("get_dp_p_coeff") -# __all__.append("get_zp_coeff") -# __all__.append("convert_matrix_dp_p_to_dE") -# __all__.append("convert_matrix_zp_to_dE") -# __all__.append("drift_matrix") -# __all__.append("quad_matrix") -# __all__.append("bend_matrix") -# __all__.append("tilt_matrix") -# __all__.append("translation_matrix") -# __all__.append("kick_matrix") -# __all__.append("solenoid_matrix") -# __all__.append("cf_matrix") -# From e7b0958298a8cf447fb6f1e4ce47531efed96631 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 12:18:45 -0400 Subject: [PATCH 109/183] Add rf_gap_matrix function --- examples/Envelope/test_env.py | 48 +++++++++++++++++++++++++- py/orbit/matrix_lattice/analytic.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 3c60e5c1..f9b72398 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -2,7 +2,8 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis - +from orbit.core.linac import MatrixRfGap +from orbit.bunch_utils import collect_bunch from orbit.lattice import AccNode from orbit.lattice import AccLattice from orbit.py_linac.lattice import Drift @@ -314,6 +315,49 @@ def test_solenoid_linac( track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_rf_gap_matrix( + kin_energy: float = 0.0025, + frequency: float = 402.5e+06, + E0TL: float = 0.001, + phase: float = 0.0, +): + # Just tests matrix against MatrixRFGap. Node not implemented yet. + + cov_matrix = make_default_cov_matrix() + + bunch_in = Bunch() + bunch_in.mass(mass_proton) + bunch_in.getSyncParticle().kinEnergy(kin_energy) + + coords_in = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=10) + for x in coords_in: + bunch_in.addParticle(*x) + + bunch_out_1 = Bunch() + bunch_in.copyBunchTo(bunch_out_1) + + matrix_rf_gap = MatrixRfGap() + matrix_rf_gap.trackBunch(bunch_out_1, frequency, E0TL, phase) + + coords_out_1 = collect_bunch(bunch_out_1)["coords"] + + from orbit.matrix_lattice.analytic import rf_gap_matrix + + bunch_out_2 = Bunch() + bunch_in.copyBunchTo(bunch_out_2) + + matrix = rf_gap_matrix( + frequency=frequency, + E0TL=E0TL, + phase=phase, + sync_part=bunch_out_2.getSyncParticle(), + ) + coords_in = np.column_stack([coords_in, np.ones(coords_in.shape[0])]) + coords_out_2 = np.matmul(coords_in, matrix.T) + coords_out_2 = coords_out_2[:, :-1] + assert np.allclose(coords_out_1, coords_out_2) + + def test_sc_3d_cold_expansion(): # This should test expansion of cold uniform-density sphere # (in rest frame). We can calculate the time to expand to @@ -335,3 +379,5 @@ def test_sc_3d_cold_expansion(): test_bend_linac() test_tilt_linac() test_solenoid_linac() + + test_rf_gap_matrix() diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/matrix_lattice/analytic.py index 28f353e2..3409508d 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -4,6 +4,7 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle +from orbit.utils.consts import speed_of_light def get_dp_p_coeff(sync_part: SyncParticle) -> float: @@ -185,3 +186,55 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() + + +def rf_gap_matrix( + frequency: float, E0TL: float, phase: float, sync_part: SyncParticle +) -> np.ndarray: + """Matrix for thin RF gap. + + E0TL: maximal energy gain in the gap [GeV]. + frequency: RF frequency [Hz] + phase: RF phase [rad] + """ + gamma = sync_part.gamma() + beta = sync_part.beta() + mass = sync_part.mass() + charge = 1.0 # TEMP! + + kin_energy_in = sync_part.kinEnergy() + chargeE0TLsin = charge * E0TL * math.sin(phase) + kin_energy_delta = charge * E0TL * math.cos(phase) + + # Calculate parameters in the center of the gap. + sync_part.momentum(sync_part.energyToMomentum(kin_energy_in + kin_energy_delta / 2.0)) + gamma_gap = sync_part.gamma() + beta_gap = sync_part.beta() + + # Move to the end of the gap. + kin_energy_out = kin_energy_in + kin_energy_delta + sync_part.momentum(sync_part.energyToMomentum(kin_energy_out)) + + # The base RF gap is simple - no phase correction. + delta_time = 0.0 + sync_part.time(sync_part.time() + delta_time) + gamma_out = sync_part.gamma() + beta_out = sync_part.beta() + prime_coeff = (beta * gamma) / (beta_out * gamma_out) + + # Wave momentum + k = 2.0 * math.pi * frequency / speed_of_light + phase_time_coeff = k / beta + + # Transverse focusing coefficient + kappa = -charge * E0TL * k / (2.0 * mass * beta_gap**2 * beta_out * gamma_gap**2 * gamma_out) + d_rp = kappa * math.sin(phase) + + M = np.eye(7) + M[5, 4] = chargeE0TLsin * phase_time_coeff + M[4, 4] = beta_out / beta + M[1, 1] = prime_coeff + M[3, 3] = prime_coeff + M[1, 0] = d_rp + M[3, 2] = d_rp + return M From 1c5250d3f9bcd257929e15b54b360f7f9c046e01 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 12:30:20 -0400 Subject: [PATCH 110/183] Remove setting sync part time (does nothing in MatrixRFGap) --- py/orbit/matrix_lattice/analytic.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/matrix_lattice/analytic.py index 3409508d..71f995d5 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -188,9 +188,7 @@ def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() -def rf_gap_matrix( - frequency: float, E0TL: float, phase: float, sync_part: SyncParticle -) -> np.ndarray: +def rf_gap_matrix(frequency: float, E0TL: float, phase: float, sync_part: SyncParticle) -> np.ndarray: """Matrix for thin RF gap. E0TL: maximal energy gain in the gap [GeV]. @@ -216,8 +214,6 @@ def rf_gap_matrix( sync_part.momentum(sync_part.energyToMomentum(kin_energy_out)) # The base RF gap is simple - no phase correction. - delta_time = 0.0 - sync_part.time(sync_part.time() + delta_time) gamma_out = sync_part.gamma() beta_out = sync_part.beta() prime_coeff = (beta * gamma) / (beta_out * gamma_out) From a6c1879a33684af2bdcb1de2387f417e7345a1d9 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 12:38:46 -0400 Subject: [PATCH 111/183] Print time when tracking through sns ring --- examples/Envelope/test_env_sns_ring.py | 38 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index b2f4e837..dd01ed7c 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -5,6 +5,7 @@ import math import os import pathlib +import time import numpy as np import matplotlib.pyplot as plt @@ -132,6 +133,8 @@ def main(args: argparse.Namespace) -> None: ) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + start_time = time.time() + for turn in range(args.turns): if turn > 0: tracker.track(envelope) @@ -144,9 +147,14 @@ def main(args: argparse.Namespace) -> None: xavg = 1000.0 * centroid[0] yavg = 1000.0 * centroid[2] - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + message = "" + message += " turn={}".format(turn) + message += " time={:0.2f}".format(time.time() - start_time) + message += " xrms={:0.2f}".format(xrms) + message += " yrms={:0.2f}".format(yrms) + message += " xavg={:0.2f}".format(xavg) + message += " yavg={:0.2f}".format(yavg) + print(message) history["xrms"].append(xrms) history["yrms"].append(yrms) @@ -182,6 +190,8 @@ def main(args: argparse.Namespace) -> None: bunch.macroSize(args.intensity / bunch_size) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} + start_time = time.time() + for turn in range(args.turns): if turn > 0: lattice.trackBunch(bunch) @@ -200,9 +210,15 @@ def main(args: argparse.Namespace) -> None: xavg = 1000.0 * twiss_calc.getAverage(0) yavg = 1000.0 * twiss_calc.getAverage(2) - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + message = "" + message += " turn={}".format(turn) + message += " time={:0.2f}".format(time.time() - start_time) + message += " xrms={:0.2f}".format(xrms) + message += " yrms={:0.2f}".format(yrms) + message += " xavg={:0.2f}".format(xavg) + message += " yavg={:0.2f}".format(yavg) + print(message) + history["xrms"].append(xrms) history["yrms"].append(yrms) @@ -309,9 +325,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--kin-energy", type=float, default=1.300) parser.add_argument("--intensity", type=float, default=2e14) - parser.add_argument( - "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] - ) + parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) @@ -319,14 +333,12 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--tilt", type=float, default=0) parser.add_argument("--nparts", type=int, default=100_000) - parser.add_argument("--turns", type=int, default=25) + parser.add_argument("--turns", type=int, default=100) parser.add_argument("--sol", type=int, default=0) parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) - parser.add_argument( - "--handle-unknown", type=str, default=None, choices=["drift", "fit"] - ) + parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) return parser.parse_args() From 7358600d2056be83732bd0222da96982a10f944a Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 12:52:57 -0400 Subject: [PATCH 112/183] Return drift or none if params are zero in TEAPOT nodes --- py/orbit/teapot/teapot.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 4690935b..3d3b990d 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -610,7 +610,8 @@ def track(self, paramsDict: dict) -> None: def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: length = self.getLength(index) - return drift_matrix(length=length, sync_part=sync_part) + if length > 0: + return drift_matrix(length=length, sync_part=sync_part) class ApertureTEAPOT(NodeTEAPOT): @@ -765,7 +766,10 @@ def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: if self.waveform is not None: B *= self.waveform.getStrength() length = self.getLength(index) - return solenoid_matrix(length=length, B=B, sync_part=sync_part) + if abs(B) > 0: + return solenoid_matrix(length=length, B=B, sync_part=sync_part) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) class MultipoleTEAPOT(NodeTEAPOT): @@ -1093,7 +1097,10 @@ def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: kq = self.getParam("kq") if self.waveform: kq *= self.waveform.getStrength() - return quad_matrix(length=length, kq=kq, sync_part=sync_part) + if abs(kq) > 0: + return quad_matrix(length=length, kq=kq, sync_part=sync_part) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) class BendTEAPOT(NodeTEAPOT): @@ -1312,7 +1319,10 @@ def track(self, paramsDict: dict) -> None: def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: length = self.getLength(index) theta = self.getParam("theta") / self.getnParts() - return bend_matrix(length=length, theta=theta, sync_part=sync_part) + if abs(theta) > 0: + return bend_matrix(length=length, theta=theta, sync_part=sync_part) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) class RingRFTEAPOT(NodeTEAPOT): @@ -1492,10 +1502,13 @@ def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: ky = scale * self.getParam("ky") / nparts kE = self.getParam("dE") / nparts - return np.matmul( - kick_matrix(kx=kx, ky=ky, kE=kE), - drift_matrix(length=length, sync_part=sync_part) - ) + if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: + return np.matmul( + kick_matrix(kx=kx, ky=ky, kE=kE), + drift_matrix(length=length, sync_part=sync_part) + ) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) class TiltTEAPOT(BaseTEAPOT): @@ -1533,7 +1546,10 @@ def track(self, paramsDict: dict) -> None: TPB.rotatexy(bunch, self.__angle) def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - return tilt_matrix(self.getTiltAngle()) + tilt_angle = self.getTiltAngle() + if abs(tilt_angle) > 0: + return tilt_matrix(self.getTiltAngle()) + class FringeFieldTEAPOT(BaseTEAPOT): """ From 41d245d47860ef6bcf90b54ebac92859c48d23d0 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 13:36:17 -0400 Subject: [PATCH 113/183] Remove comment --- py/orbit/matrix_lattice/analytic.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/matrix_lattice/analytic.py index 71f995d5..8ced9df6 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -33,15 +33,6 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np # v = A w # v -> M v # w -> A M A^-1 - - # scale = np.identity(7) - # scale[5, 5] = dp_p_coeff - # - # scale_inv = np.identity(7) - # scale_inv[5, 5] = 1.0 / dp_p_coeff - # - # return np.linalg.multi_dot([scale, matrix, scale_inv]) - dp_p_coeff = get_dp_p_coeff(sync_part) matrix[:5, 5] *= dp_p_coeff matrix[5, :5] /= dp_p_coeff @@ -175,7 +166,7 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar M[1, 1] = -1.0 M[2, 2] = math.cos(phase) M[2, 3] = math.sin(phase) / B - M[3, 2] = math.sin(phase) * (-B) + M[3, 2] = math.sin(phase) * B * -1.0 M[3, 3] = math.cos(phase) M[4, 5] = length / sync_part.gamma() ** 2 From 5b9349a60f31684b9d396b027249f627dc73ee81 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 13:36:39 -0400 Subject: [PATCH 114/183] Add decimal to time per turm when printing --- examples/Envelope/test_env_sns_ring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index dd01ed7c..09411a2b 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -149,7 +149,7 @@ def main(args: argparse.Namespace) -> None: message = "" message += " turn={}".format(turn) - message += " time={:0.2f}".format(time.time() - start_time) + message += " time={:0.3f}".format(time.time() - start_time) message += " xrms={:0.2f}".format(xrms) message += " yrms={:0.2f}".format(yrms) message += " xavg={:0.2f}".format(xavg) @@ -212,7 +212,7 @@ def main(args: argparse.Namespace) -> None: message = "" message += " turn={}".format(turn) - message += " time={:0.2f}".format(time.time() - start_time) + message += " time={:0.3f}".format(time.time() - start_time) message += " xrms={:0.2f}".format(xrms) message += " yrms={:0.2f}".format(yrms) message += " xavg={:0.2f}".format(xavg) From b149eb5fcff9328808709ee92d4925d47b2f78f4 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 13:46:35 -0400 Subject: [PATCH 115/183] Add profiler --- examples/Envelope/test_env_sns_ring.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 09411a2b..d8924713 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -7,6 +7,9 @@ import pathlib import time +import cProfile +import pstats + import numpy as np import matplotlib.pyplot as plt @@ -124,6 +127,9 @@ def main(args: argparse.Namespace) -> None: # Track envelope # ------------------------------------------------------------------------------ + profiler = cProfile.Profile() + profiler.enable() + print("TRACK ENVELOPE") tracker = EnvelopeTracker( @@ -135,7 +141,7 @@ def main(args: argparse.Namespace) -> None: history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} start_time = time.time() - for turn in range(args.turns): + for turn in range(args.turns + 1): if turn > 0: tracker.track(envelope) @@ -161,9 +167,16 @@ def main(args: argparse.Namespace) -> None: history["xavg"].append(xavg) history["yavg"].append(yavg) + profiler.disable() + + stats = pstats.Stats(profiler) + stats.sort_stats(pstats.SortKey.TIME) + stats.print_stats(20) + histories = {} histories["envelope"] = copy.deepcopy(history) + # Track bunch # ------------------------------------------------------------------------------ @@ -192,7 +205,7 @@ def main(args: argparse.Namespace) -> None: history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} start_time = time.time() - for turn in range(args.turns): + for turn in range(args.turns + 1): if turn > 0: lattice.trackBunch(bunch) @@ -333,7 +346,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--tilt", type=float, default=0) parser.add_argument("--nparts", type=int, default=100_000) - parser.add_argument("--turns", type=int, default=100) + parser.add_argument("--turns", type=int, default=25) parser.add_argument("--sol", type=int, default=0) parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) From 8b288956b36f56759c74befa173b9b7425e71fac Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:12:33 -0400 Subject: [PATCH 116/183] Small speedup: conversion from dp_p to dE --- examples/Envelope/test_env_sns_ring.py | 7 +++---- py/orbit/matrix_lattice/analytic.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index d8924713..f3a30a78 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -127,9 +127,6 @@ def main(args: argparse.Namespace) -> None: # Track envelope # ------------------------------------------------------------------------------ - profiler = cProfile.Profile() - profiler.enable() - print("TRACK ENVELOPE") tracker = EnvelopeTracker( @@ -141,6 +138,9 @@ def main(args: argparse.Namespace) -> None: history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} start_time = time.time() + profiler = cProfile.Profile() + profiler.enable() + for turn in range(args.turns + 1): if turn > 0: tracker.track(envelope) @@ -168,7 +168,6 @@ def main(args: argparse.Namespace) -> None: history["yavg"].append(yavg) profiler.disable() - stats = pstats.Stats(profiler) stats.sort_stats(pstats.SortKey.TIME) stats.print_stats(20) diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/matrix_lattice/analytic.py index 8ced9df6..9549c9e2 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -36,7 +36,7 @@ def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np dp_p_coeff = get_dp_p_coeff(sync_part) matrix[:5, 5] *= dp_p_coeff matrix[5, :5] /= dp_p_coeff - matrix[5, 6] /= dp_p_coeff + matrix[5, 6] /= dp_p_coeff # driving term return matrix @@ -44,7 +44,7 @@ def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.n zp_coeff = get_zp_coeff(sync_part) matrix[:5, 5] *= zp_coeff matrix[5, :5] /= zp_coeff - matrix[5, 6] /= zp_coeff + matrix[5, 6] /= zp_coeff # driving term return matrix @@ -52,8 +52,8 @@ def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: M = np.identity(7) M[0, 1] = length M[2, 3] = length - M[4, 5] = length / sync_part.gamma() ** 2 - M = convert_matrix_dp_p_to_dE(M, sync_part) + M[4, 5] = length / (sync_part.gamma() ** 2) + M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) return M @@ -91,8 +91,8 @@ def quad_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray M[3, 2] = -sy * sqrt_abs_kq M[3, 3] = cy - M[4, 5] = length / sync_part.gamma() ** 2 - M = convert_matrix_dp_p_to_dE(M, sync_part) + M[4, 5] = length / (sync_part.gamma()**2) + M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) return M @@ -115,7 +115,7 @@ def bend_matrix(length: float, theta: float, sync_part: SyncParticle) -> np.ndar M[4, 0] = -sx M[4, 1] = -rho * (1.0 - cx) M[4, 5] = -(sync_part.beta() ** 2) * length + rho * sx - M = convert_matrix_dp_p_to_dE(M, sync_part) + M[:5, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) return M @@ -168,10 +168,10 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar M[2, 3] = math.sin(phase) / B M[3, 2] = math.sin(phase) * B * -1.0 M[3, 3] = math.cos(phase) - M[4, 5] = length / sync_part.gamma() ** 2 + M[4, 5] = length / (sync_part.gamma()**2) M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) - M = convert_matrix_dp_p_to_dE(M, sync_part) + M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) return M From 674c7435ab8d7fd5831c96583084efb2bc29bd7f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:16:50 -0400 Subject: [PATCH 117/183] Remove handle_unknown --- py/orbit/envelope/envelope.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index bca9a63d..90af33e2 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -243,12 +243,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: - def __init__( - self, - lattice: AccLattice, - space_charge: str | None = None, - handle_unknown: str | None = None, - ) -> None: + def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: self.lattice = lattice self.space_charge = space_charge From 3b2ac80be698024a1136179a8b2fba10c45738cb Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:17:31 -0400 Subject: [PATCH 118/183] Longer lines --- py/orbit/envelope/envelope.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 90af33e2..060c21e9 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -254,9 +254,7 @@ def track(self, envelope: Envelope) -> None: for index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, index, place_in_part=BEFORE): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) if self.space_charge: length = node.getLength(index) @@ -265,17 +263,13 @@ def track(self, envelope: Envelope) -> None: elif self.space_charge == "3d": matrix = envelope.sc_transfer_matrix_3d(length) else: - raise ValueError( - f"Invalid space charge model: {self.space_charge}" - ) + raise ValueError(f"Invalid space charge model: {self.space_charge}") envelope.apply_transfer_matrix(matrix) envelope.apply_transfer_matrix(node.matrix(envelope.sync_part, index)) for child_node in node.getChildNodes(BODY, index, place_in_part=AFTER): - envelope.apply_transfer_matrix( - child_node.matrix(envelope.sync_part) - ) + envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) for child_node in node.getChildNodes(EXIT): envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) From 389a1b179761f09c85aa00da78208686155d3232 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:25:45 -0400 Subject: [PATCH 119/183] Add test_env_sns_ring_speed.py --- examples/Envelope/test_env_sns_ring_speed.py | 151 +++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 examples/Envelope/test_env_sns_ring_speed.py diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/test_env_sns_ring_speed.py new file mode 100644 index 00000000..eb3ca754 --- /dev/null +++ b/examples/Envelope/test_env_sns_ring_speed.py @@ -0,0 +1,151 @@ +"""Test envelope tracker in SNS ring.""" + +import argparse +import copy +import math +import os +import pathlib +import time + +import cProfile +import pstats + +import numpy as np +import matplotlib.pyplot as plt +from tqdm import trange +from tqdm import tqdm + +from orbit.core.bunch import Bunch +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.space_charge.sc2p5d import setSC2p5DAccNodes +from orbit.teapot import TEAPOT_Ring +from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.utils.consts import mass_proton + +from utils import gen_dist + + +def main(args: argparse.Namespace) -> None: + + lattice = TEAPOT_Ring() + lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") + lattice.initialize() + + for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass + + bunch = Bunch() + bunch.mass(mass_proton) + sync_part = bunch.getSyncParticle() + sync_part.kinEnergy(args.kin_energy) + + matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) + matrix_lattice_params = matrix_lattice.getRingParametersDict() + alpha_x = matrix_lattice_params["alpha x"] + alpha_y = matrix_lattice_params["alpha y"] + beta_x = matrix_lattice_params["beta x [m]"] + beta_y = matrix_lattice_params["beta y [m]"] + eps_x = 25.0e-06 + eps_y = eps_x + + cov_matrix = np.zeros((6, 6)) + cov_matrix[0, 0] = eps_x * beta_x + cov_matrix[2, 2] = eps_y * beta_y + cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x + cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y + cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x + cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y + cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 + cov_matrix[5, 5] = 0.0 + + cov_matrix_init = np.copy(cov_matrix) + + # Track envelope + print("ENVELOPE") + + envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + intensity=args.intensity, + ) + envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) + + start_time = time.time() + + profiler = cProfile.Profile() + profiler.enable() + + for turn in trange(args.turns): + envelope_tracker.track(envelope) + + time_per_turn = (time.time() - start_time) / args.turns + + profiler.disable() + + print("Time per turn:", time_per_turn) + + profiler_stats = pstats.Stats(profiler) + profiler_stats.sort_stats(pstats.SortKey.TIME) + profiler_stats.print_stats(10) + + # Track bunch + print("BUNCH") + + rng = np.random.default_rng() + + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") + bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) + + for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + + if args.sc: + sc_calc = SpaceChargeCalc2p5D(64, 64, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + + start_time = time.time() + + profiler = cProfile.Profile() + profiler.enable() + + for turn in trange(args.turns): + lattice.trackBunch(bunch) + + time_per_turn = (time.time() - start_time) / args.turns + + profiler.disable() + + print("Time per turn:", time_per_turn) + + profiler_stats = pstats.Stats(profiler) + profiler_stats.sort_stats(pstats.SortKey.TIME) + profiler_stats.print_stats(10) + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--bunch-length", type=float, default=120.0) + parser.add_argument("--kin-energy", type=float, default=1.300) + parser.add_argument("--intensity", type=float, default=2e14) + + parser.add_argument("--nparts", type=int, default=100_000) + parser.add_argument("--turns", type=int, default=25) + parser.add_argument("--sc", type=int, default=0) + parser.add_argument("--sc-grid", type=int, default=64) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + main(args) From 85b4dd9fabe6e99cd9cfe987091bcf807188ee94 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:26:53 -0400 Subject: [PATCH 120/183] Remove old argument handle-unknown Right now every unknown element will throw an error except MultipoleTEAPOT if all k are zero --- examples/Envelope/test_env_sns_ring.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index f3a30a78..06bcd6f7 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -7,9 +7,6 @@ import pathlib import time -import cProfile -import pstats - import numpy as np import matplotlib.pyplot as plt @@ -129,18 +126,11 @@ def main(args: argparse.Namespace) -> None: print("TRACK ENVELOPE") - tracker = EnvelopeTracker( - lattice, - handle_unknown=args.handle_unknown, - space_charge=("2d" if args.sc else None), - ) + tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} start_time = time.time() - profiler = cProfile.Profile() - profiler.enable() - for turn in range(args.turns + 1): if turn > 0: tracker.track(envelope) @@ -167,11 +157,6 @@ def main(args: argparse.Namespace) -> None: history["xavg"].append(xavg) history["yavg"].append(yavg) - profiler.disable() - stats = pstats.Stats(profiler) - stats.sort_stats(pstats.SortKey.TIME) - stats.print_stats(20) - histories = {} histories["envelope"] = copy.deepcopy(history) From f23211531fe64bdf95bdc608aa8ae9a36bd90c9c Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:28:10 -0400 Subject: [PATCH 121/183] Formatting --- examples/Envelope/plot.py | 8 ++------ examples/Envelope/test_env.py | 2 +- examples/Envelope/test_env_2d_fodo.py | 12 +++--------- examples/Envelope/test_env_3d_drift.py | 4 +--- examples/Envelope/test_env_sns_ring.py | 2 -- examples/Envelope/test_env_sns_ring_speed.py | 5 ++++- 6 files changed, 11 insertions(+), 22 deletions(-) diff --git a/examples/Envelope/plot.py b/examples/Envelope/plot.py index 99a77f80..f493b108 100644 --- a/examples/Envelope/plot.py +++ b/examples/Envelope/plot.py @@ -77,9 +77,7 @@ def plot_corner( if labels is None: labels = ndim * [""] - fig, axs = plt.subplots( - ncols=ndim, nrows=ndim, sharex=None, sharey=None, figsize=(8, 8) - ) + fig, axs = plt.subplots(ncols=ndim, nrows=ndim, sharex=None, sharey=None, figsize=(8, 8)) for i in range(ndim): for j in range(ndim): axis = (j, i) @@ -99,9 +97,7 @@ def plot_corner( shading="auto", ) elif i == j: - values, edges = np.histogram( - particles[:, i], bins=bins, range=limits[i] - ) + values, edges = np.histogram(particles[:, i], bins=bins, range=limits[i]) if blur: values = scipy.ndimage.gaussian_filter(values, sigma=blur) ax.stairs(values, edges, lw=1.5, color="black") diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index f9b72398..829a7147 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -317,7 +317,7 @@ def test_solenoid_linac( def test_rf_gap_matrix( kin_energy: float = 0.0025, - frequency: float = 402.5e+06, + frequency: float = 402.5e06, E0TL: float = 0.001, phase: float = 0.0, ): diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index a3890e8a..dbde263a 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -137,9 +137,7 @@ def main(args: argparse.Namespace) -> None: xavg = 1000.0 * centroid[0] yavg = 1000.0 * centroid[2] - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") history["xrms"].append(xrms) history["yrms"].append(yrms) @@ -193,9 +191,7 @@ def main(args: argparse.Namespace) -> None: xavg = 1000.0 * twiss_calc.getAverage(0) yavg = 1000.0 * twiss_calc.getAverage(2) - print( - f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}" - ) + print(f"turn={turn} xrms={xrms:0.3f} yrms={yrms:0.3f} xavg={xavg:0.3f} yavg={yavg:0.3f}") history["xrms"].append(xrms) history["yrms"].append(yrms) @@ -302,9 +298,7 @@ def main(args: argparse.Namespace) -> None: parser.add_argument("--kin-energy", type=float, default=0.0025) parser.add_argument("--intensity", type=float, default=5e9) - parser.add_argument( - "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] - ) + parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 1987a873..1c15bb65 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -39,9 +39,7 @@ def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.nda ).as_matrix() -def build_cov_matrix_xyz( - rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None -) -> np.ndarray: +def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: cov_matrix = np.diag(np.square(rms_sizes)) if rotation_matrix is None: return cov_matrix diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 06bcd6f7..093c953b 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -160,7 +160,6 @@ def main(args: argparse.Namespace) -> None: histories = {} histories["envelope"] = copy.deepcopy(history) - # Track bunch # ------------------------------------------------------------------------------ @@ -216,7 +215,6 @@ def main(args: argparse.Namespace) -> None: message += " yavg={:0.2f}".format(yavg) print(message) - history["xrms"].append(xrms) history["yrms"].append(yrms) history["xavg"].append(xavg) diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/test_env_sns_ring_speed.py index eb3ca754..cfb331b6 100644 --- a/examples/Envelope/test_env_sns_ring_speed.py +++ b/examples/Envelope/test_env_sns_ring_speed.py @@ -101,7 +101,9 @@ def main(args: argparse.Namespace) -> None: rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) - bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") + bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" + ) bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) for i in range(bunch_coords.shape[0]): @@ -133,6 +135,7 @@ def main(args: argparse.Namespace) -> None: profiler_stats.sort_stats(pstats.SortKey.TIME) profiler_stats.print_stats(10) + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--bunch-length", type=float, default=120.0) From 43d850f9f8ce6202de97334e51a7a4a574b44ef9 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 14:32:47 -0400 Subject: [PATCH 122/183] Change defaults in test_env_sns_ring_speed.py --- examples/Envelope/test_env_sns_ring_speed.py | 189 +++++++++---------- 1 file changed, 87 insertions(+), 102 deletions(-) diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/test_env_sns_ring_speed.py index cfb331b6..21668734 100644 --- a/examples/Envelope/test_env_sns_ring_speed.py +++ b/examples/Envelope/test_env_sns_ring_speed.py @@ -1,19 +1,11 @@ -"""Test envelope tracker in SNS ring.""" - +"""Test envelope tracker speed in SNS ring.""" import argparse -import copy -import math -import os -import pathlib import time - import cProfile import pstats import numpy as np -import matplotlib.pyplot as plt from tqdm import trange -from tqdm import tqdm from orbit.core.bunch import Bunch from orbit.core.spacecharge import SpaceChargeCalc2p5D @@ -28,127 +20,120 @@ from utils import gen_dist -def main(args: argparse.Namespace) -> None: - - lattice = TEAPOT_Ring() - lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") - lattice.initialize() - - for node in lattice.getNodes(): - try: - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) - except: - pass +parser = argparse.ArgumentParser() +parser.add_argument("--bunch-length", type=float, default=120.0) +parser.add_argument("--kin-energy", type=float, default=1.300) +parser.add_argument("--intensity", type=float, default=2e14) - bunch = Bunch() - bunch.mass(mass_proton) - sync_part = bunch.getSyncParticle() - sync_part.kinEnergy(args.kin_energy) +parser.add_argument("--nparts", type=int, default=10_000) +parser.add_argument("--turns", type=int, default=100) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--sc-grid", type=int, default=64) +args = parser.parse_args() - matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) - matrix_lattice_params = matrix_lattice.getRingParametersDict() - alpha_x = matrix_lattice_params["alpha x"] - alpha_y = matrix_lattice_params["alpha y"] - beta_x = matrix_lattice_params["beta x [m]"] - beta_y = matrix_lattice_params["beta y [m]"] - eps_x = 25.0e-06 - eps_y = eps_x - cov_matrix = np.zeros((6, 6)) - cov_matrix[0, 0] = eps_x * beta_x - cov_matrix[2, 2] = eps_y * beta_y - cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x - cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y - cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x - cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y - cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 - cov_matrix[5, 5] = 0.0 +lattice = TEAPOT_Ring() +lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") +lattice.initialize() - cov_matrix_init = np.copy(cov_matrix) +for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass - # Track envelope - print("ENVELOPE") +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(args.kin_energy) - envelope = Envelope( - bunch=bunch, - cov_matrix=cov_matrix_init, - intensity=args.intensity, - ) - envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) +matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) +matrix_lattice_params = matrix_lattice.getRingParametersDict() +alpha_x = matrix_lattice_params["alpha x"] +alpha_y = matrix_lattice_params["alpha y"] +beta_x = matrix_lattice_params["beta x [m]"] +beta_y = matrix_lattice_params["beta y [m]"] +eps_x = 25.0e-06 +eps_y = eps_x - start_time = time.time() +cov_matrix = np.zeros((6, 6)) +cov_matrix[0, 0] = eps_x * beta_x +cov_matrix[2, 2] = eps_y * beta_y +cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x +cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y +cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x +cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y +cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 +cov_matrix[5, 5] = 0.0 - profiler = cProfile.Profile() - profiler.enable() +cov_matrix_init = np.copy(cov_matrix) - for turn in trange(args.turns): - envelope_tracker.track(envelope) +# Track envelope +print("ENVELOPE") - time_per_turn = (time.time() - start_time) / args.turns +envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + intensity=args.intensity, +) +envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) - profiler.disable() +start_time = time.time() - print("Time per turn:", time_per_turn) +profiler = cProfile.Profile() +profiler.enable() - profiler_stats = pstats.Stats(profiler) - profiler_stats.sort_stats(pstats.SortKey.TIME) - profiler_stats.print_stats(10) +for turn in trange(args.turns): + envelope_tracker.track(envelope) - # Track bunch - print("BUNCH") +time_per_turn = (time.time() - start_time) / args.turns - rng = np.random.default_rng() +profiler.disable() - bunch_coords = np.zeros((args.nparts, 6)) - bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" - ) - bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) +print("Time per turn:", time_per_turn) - for i in range(bunch_coords.shape[0]): - bunch.addParticle(*bunch_coords[i]) +profiler_stats = pstats.Stats(profiler) +profiler_stats.sort_stats(pstats.SortKey.TIME) +profiler_stats.print_stats(10) - if args.sc: - sc_calc = SpaceChargeCalc2p5D(64, 64, 1) - sc_path_length_min = 1.00e-06 - sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) +# Track bunch +print("BUNCH") - bunch_size = bunch.getSizeGlobal() - bunch.macroSize(args.intensity / bunch_size) +rng = np.random.default_rng() - start_time = time.time() +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" +) +bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) - profiler = cProfile.Profile() - profiler.enable() +for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) - for turn in trange(args.turns): - lattice.trackBunch(bunch) +if args.sc: + sc_calc = SpaceChargeCalc2p5D(64, 64, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) - time_per_turn = (time.time() - start_time) / args.turns + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) - profiler.disable() +start_time = time.time() - print("Time per turn:", time_per_turn) +profiler = cProfile.Profile() +profiler.enable() - profiler_stats = pstats.Stats(profiler) - profiler_stats.sort_stats(pstats.SortKey.TIME) - profiler_stats.print_stats(10) +for turn in trange(args.turns): + lattice.trackBunch(bunch) +time_per_turn = (time.time() - start_time) / args.turns -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--bunch-length", type=float, default=120.0) - parser.add_argument("--kin-energy", type=float, default=1.300) - parser.add_argument("--intensity", type=float, default=2e14) +profiler.disable() - parser.add_argument("--nparts", type=int, default=100_000) - parser.add_argument("--turns", type=int, default=25) - parser.add_argument("--sc", type=int, default=0) - parser.add_argument("--sc-grid", type=int, default=64) - return parser.parse_args() +print("Time per turn:", time_per_turn) +profiler_stats = pstats.Stats(profiler) +profiler_stats.sort_stats(pstats.SortKey.TIME) +profiler_stats.print_stats(10) -if __name__ == "__main__": - args = parse_args() - main(args) From bdca2f31be649ca17fd58a15e23f846d23680e10 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 15:42:09 -0400 Subject: [PATCH 123/183] Add checks for linac node transfer matrices --- py/orbit/py_linac/lattice/LinacAccNodes.py | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index abee5e13..bd0e2626 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -7,7 +7,6 @@ """ import os -import math import numpy as np @@ -29,13 +28,6 @@ from orbit.core.bunch import SyncParticle -# from orbit.matrix_lattice import drift_matrix -# from orbit.matrix_lattice import bend_matrix -# from orbit.matrix_lattice import quad_matrix -# from orbit.matrix_lattice import solenoid_matrix -# from orbit.matrix_lattice import kick_matrix -# from orbit.matrix_lattice import tilt_matrix - from orbit.matrix_lattice.analytic import drift_matrix from orbit.matrix_lattice.analytic import bend_matrix from orbit.matrix_lattice.analytic import quad_matrix @@ -333,7 +325,8 @@ def track(self, paramsDict): def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: length = self.getLength(index) - return drift_matrix(length=length, sync_part=sync_part) + if length > 0: + return drift_matrix(length=length, sync_part=sync_part) class Quad(LinacMagnetNode): @@ -538,7 +531,10 @@ def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: charge = 1.0 # sync_part has no charge parameter... brho = 3.335640952 * sync_part.momentum() / charge kq = self.getParam("dB/dr") / brho - return quad_matrix(length=length, kq=kq, sync_part=sync_part) + if abs(kq) > 0: + return quad_matrix(length=length, kq=kq, sync_part=sync_part) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) def getTotalField(self, z): """ @@ -796,10 +792,10 @@ def track(self, paramsDict): def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: length = self.getParam("effLength") / self.getnParts() field = self.getParam("B") - charge = 1.0 - # dp/p = Q*c*B*L/p p in GeV/c c = 2.99792*10^8/10^9 + charge = 1.0 # TEMP delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() - return kick_matrix(delta_xp, 0.0, 0.0) + if abs(delta_xp) > 0: + return kick_matrix(delta_xp, 0.0, 0.0) class DCorrectorV(LinacMagnetNode): @@ -849,10 +845,10 @@ def track(self, paramsDict): def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: length = self.getParam("effLength") / self.getnParts() field = self.getParam("B") - charge = 1.0 - # dp/p = Q*c*B*L/p p in GeV/c c = 2.99792*10^8/10^9 + charge = 1.0 # TEMP delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() - return kick_matrix(0.0, delta_yp, 0.0) + if abs(delta_yp) > 0: + return kick_matrix(0.0, delta_yp, 0.0) class ThickKick(LinacMagnetNode): @@ -924,6 +920,7 @@ def track(self, paramsDict): self.tracking_module.kick(bunch, kickX, kickY, 0.0) self.tracking_module.drift(bunch, length / 2.0) + class Solenoid(BaseLinacNode): """ Solenoid TEAPOT based element. @@ -954,7 +951,10 @@ def track(self, paramsDict): def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: B = self.getParam("B") length = self.getLength(index) - return solenoid_matrix(length=length, B=B, sync_part=sync_part) + if abs(B) > 0: + return solenoid_matrix(length=length, B=B, sync_part=sync_part) + elif length > 0: + return drift_matrix(length=length, sync_part=sync_part) class AbstractRF_Gap(BaseLinacNode): @@ -1076,7 +1076,9 @@ def track(self, paramsDict): TPB.rotatexy(bunch, self.__angle) def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - return tilt_matrix(self.__angle) + angle = self.__angle + if abs(angle) > 0: + return tilt_matrix(angle) class FringeField(BaseLinacNode): From 14f09474e86dd3fccf4f4dcbf51acc3626e726d7 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 16:18:33 -0400 Subject: [PATCH 124/183] Run tests at different energies --- examples/Envelope/test_env.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 829a7147..0e53ac6c 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -367,17 +367,18 @@ def test_sc_3d_cold_expansion(): if __name__ == "__main__": - test_drift_teapot() - test_quad_teapot() - test_bend_teapot() - test_tilt_teapot() - test_solenoid_teapot() - test_kick_teapot() - - test_drift_linac() - test_quad_linac() - test_bend_linac() - test_tilt_linac() - test_solenoid_linac() - - test_rf_gap_matrix() + for kin_energy in [0.0025, 1.0, 10.0]: + test_drift_teapot(kin_energy=kin_energy) + test_quad_teapot(kin_energy=kin_energy) + test_bend_teapot(kin_energy=kin_energy) + test_tilt_teapot(kin_energy=kin_energy) + test_solenoid_teapot(kin_energy=kin_energy) + test_kick_teapot(kin_energy=kin_energy) + + test_drift_linac(kin_energy=kin_energy) + test_quad_linac(kin_energy=kin_energy) + test_bend_linac(kin_energy=kin_energy) + test_tilt_linac(kin_energy=kin_energy) + test_solenoid_linac(kin_energy=kin_energy) + + test_rf_gap_matrix(kin_energy=kin_energy) From d2bd7227bded11b84b9c42d7a1cfec51c74a996e Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 16:31:34 -0400 Subject: [PATCH 125/183] Split lattice in test_env_sns_ring_speed --- examples/Envelope/test_env_sns_ring_speed.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/test_env_sns_ring_speed.py index 21668734..81a97978 100644 --- a/examples/Envelope/test_env_sns_ring_speed.py +++ b/examples/Envelope/test_env_sns_ring_speed.py @@ -43,6 +43,11 @@ except: pass +for node in lattice.getNodes(): + max_length = 1.0 + if node.getLength() > max_length: + node.setnParts(1 + int(node.getLength() / max_length)) + bunch = Bunch() bunch.mass(mass_proton) sync_part = bunch.getSyncParticle() From 850666a89b9636d8197e8d7e9b0e29afeba43ce5 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 17:00:55 -0400 Subject: [PATCH 126/183] Use @property for accessing cov_matrix and mean attributes --- examples/Envelope/test_env.py | 5 +- examples/Envelope/test_env_2d_fodo.py | 8 +- examples/Envelope/test_env_2d_fodo_speed.py | 149 ++++++++++++++++++++ examples/Envelope/test_env_3d_drift.py | 8 +- examples/Envelope/test_env_sns_ring.py | 8 +- py/orbit/envelope/envelope.py | 41 +++--- py/orbit/envelope/utils.py | 22 +-- 7 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 examples/Envelope/test_env_2d_fodo_speed.py diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 0e53ac6c..b456cd67 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -102,9 +102,9 @@ def track_and_compare_rms( envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix) envelope_tracker = EnvelopeTracker(lattice=lattice) - data["env"]["cov"]["in"] = cov_scale * envelope.cov() + data["env"]["cov"]["in"] = cov_scale * envelope.cov_matrix envelope_tracker.track(envelope) - data["env"]["cov"]["out"] = cov_scale * envelope.cov() + data["env"]["cov"]["out"] = cov_scale * envelope.cov_matrix # Compare for mode in ["env", "bunch"]: @@ -382,3 +382,4 @@ def test_sc_3d_cold_expansion(): test_solenoid_linac(kin_energy=kin_energy) test_rf_gap_matrix(kin_energy=kin_energy) + diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index dbde263a..f3a876ed 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -129,8 +129,8 @@ def main(args: argparse.Namespace) -> None: if turn > 0: tracker.track(envelope) - cov_matrix = envelope.cov() - centroid = envelope.centroid() + cov_matrix = envelope.cov_matrix + centroid = envelope.centroid xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) @@ -246,10 +246,10 @@ def main(args: argparse.Namespace) -> None: particles = collect_bunch(bunch)["coords"] particles[:, :4] *= 1000.0 - env_cov_matrix = envelope.cov() + env_cov_matrix = envelope.cov_matrix env_cov_matrix[:4, :4] *= 1000.0**2 - env_centroid = envelope.centroid() + env_centroid = envelope.centroid env_centroid[:4] *= 1000.0 xmax = 4.0 * np.std(particles, axis=0) diff --git a/examples/Envelope/test_env_2d_fodo_speed.py b/examples/Envelope/test_env_2d_fodo_speed.py new file mode 100644 index 00000000..a2d88d87 --- /dev/null +++ b/examples/Envelope/test_env_2d_fodo_speed.py @@ -0,0 +1,149 @@ +"""Test envelope tracker speed in SNS ring.""" +import argparse +import time +import cProfile +import pstats + +import numpy as np +from tqdm import trange + +from orbit.core.bunch import Bunch +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.core.spacecharge import SpaceChargeCalc2p5D +from orbit.space_charge.sc2p5d import setSC2p5DAccNodes +from orbit.teapot import QuadTEAPOT +from orbit.teapot import DriftTEAPOT +from orbit.teapot import TEAPOT_Lattice +from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.utils.consts import mass_proton + +from utils import gen_dist + + +parser = argparse.ArgumentParser() +parser.add_argument("--bunch-length", type=float, default=120.0) +parser.add_argument("--kin-energy", type=float, default=1.300) +parser.add_argument("--intensity", type=float, default=2e14) + +parser.add_argument("--nslice", type=int, default=10) +parser.add_argument("--kq", type=float, default=0.25) + +parser.add_argument("--nparts", type=int, default=10_000) +parser.add_argument("--turns", type=int, default=5000) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--sc-grid", type=int, default=64) +args = parser.parse_args() + +nodes = [ + QuadTEAPOT(length=0.5, kq=+args.kq), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=1.0, kq=-args.kq), + DriftTEAPOT(length=1.0), + QuadTEAPOT(length=0.5, kq=+args.kq), +] + +lattice = TEAPOT_Lattice() +for node in nodes: + node.setnParts(args.nslice) + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + lattice.addNode(node) +lattice.initialize() + +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(args.kin_energy) + +matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) +matrix_lattice_params = matrix_lattice.getRingParametersDict() +alpha_x = matrix_lattice_params["alpha x"] +alpha_y = matrix_lattice_params["alpha y"] +beta_x = matrix_lattice_params["beta x [m]"] +beta_y = matrix_lattice_params["beta y [m]"] +eps_x = 25.0e-06 +eps_y = eps_x + +cov_matrix = np.zeros((6, 6)) +cov_matrix[0, 0] = eps_x * beta_x +cov_matrix[2, 2] = eps_y * beta_y +cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x +cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y +cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x +cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y +cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 +cov_matrix[5, 5] = 0.0 + +cov_matrix_init = np.copy(cov_matrix) + +# Track envelope +print("ENVELOPE") + +envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + intensity=args.intensity, +) +envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) + +start_time = time.time() + +profiler = cProfile.Profile() +profiler.enable() + +for turn in trange(args.turns): + envelope_tracker.track(envelope) + +time_per_turn = (time.time() - start_time) / args.turns + +profiler.disable() + +print("Time per turn:", time_per_turn) + +profiler_stats = pstats.Stats(profiler) +profiler_stats.sort_stats(pstats.SortKey.TIME) +profiler_stats.print_stats(10) + +# Track bunch +print("BUNCH") + +rng = np.random.default_rng() + +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" +) +bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) + +for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + +if args.sc: + sc_calc = SpaceChargeCalc2p5D(64, 64, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + +start_time = time.time() + +profiler = cProfile.Profile() +profiler.enable() + +for turn in trange(args.turns): + lattice.trackBunch(bunch) + +time_per_turn = (time.time() - start_time) / args.turns + +profiler.disable() + +print("Time per turn:", time_per_turn) + +profiler_stats = pstats.Stats(profiler) +profiler_stats.sort_stats(pstats.SortKey.TIME) +profiler_stats.print_stats(10) + + diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index 1c15bb65..cb5f7fe6 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -112,8 +112,8 @@ def main(args: argparse.Namespace) -> None: if turn > 0: tracker.track(envelope) - cov_matrix = envelope.cov() - centroid = envelope.centroid() + cov_matrix = envelope.cov_matrix + centroid = envelope.centroid xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) @@ -205,10 +205,10 @@ def main(args: argparse.Namespace) -> None: particles = collect_bunch(bunch)["coords"] particles *= 1e3 - env_cov_matrix = envelope.cov() + env_cov_matrix = envelope.cov_matrix env_cov_matrix *= 1e6 - env_centroid = envelope.centroid() + env_centroid = envelope.centroid env_centroid *= 1e3 xmax = 4.0 * np.std(particles, axis=0) diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/test_env_sns_ring.py index 093c953b..5797bd5f 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/test_env_sns_ring.py @@ -135,8 +135,8 @@ def main(args: argparse.Namespace) -> None: if turn > 0: tracker.track(envelope) - cov_matrix = envelope.cov() - centroid = envelope.centroid() + cov_matrix = envelope.cov_matrix + centroid = envelope.centroid xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) @@ -268,10 +268,10 @@ def main(args: argparse.Namespace) -> None: particles = collect_bunch(bunch)["coords"] particles[:, :4] *= 1000.0 - env_cov_matrix = envelope.cov() + env_cov_matrix = envelope.cov_matrix env_cov_matrix[:4, :4] *= 1000.0**2 - env_centroid = envelope.centroid() + env_centroid = envelope.centroid env_centroid[:4] *= 1000.0 xmax = 4.0 * np.std(particles, axis=0) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 060c21e9..b9db2ec0 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -37,7 +37,7 @@ class Envelope: """Represents beam envelope and centroid. Attributes: - moment_matrix: 7 x 7 covariance matrix for augmented phase space vector. + moment_matrix: 7 x 7 matrix containing first and second moments. Define the phase space vector X = [x, x', y, y', z, dE]^T and augmented vector Y = [x, x', y, y', z, dE, 1]. @@ -78,11 +78,9 @@ def __init__( cov_matrix = np.eye(6) self.moment_matrix = np.zeros((7, 7)) - self.moment_matrix[: self.dim, : self.dim] = cov_matrix + np.outer( - centroid, centroid - ) - self.moment_matrix[: self.dim, self.dim] = centroid - self.moment_matrix[self.dim, : self.dim] = centroid + self.moment_matrix[:self.dim, :self.dim] = cov_matrix + np.outer(centroid, centroid) + self.moment_matrix[:self.dim, self.dim] = centroid + self.moment_matrix[self.dim, :self.dim] = centroid self.moment_matrix[self.dim, self.dim] = 1.0 self.intensity = 0.0 @@ -109,16 +107,21 @@ def mass(self) -> float: def charge(self) -> float: return self.bunch.charge() - def centroid(self) -> np.ndarray: - return np.copy(self.moment_matrix[: self.dim, self.dim]) + @property + def centroid(self): + return self.moment_matrix[:self.dim, self.dim] + + @property + def autocorr_matrix(self): + return self.moment_matrix[:self.dim, :self.dim] - def cov(self) -> np.ndarray: - autocorrelation_matrix = self.moment_matrix[: self.dim, : self.dim] - centroid = self.moment_matrix[: self.dim, self.dim] - return autocorrelation_matrix - np.outer(centroid, centroid) + @property + def cov_matrix(self): + mu = self.centroid + return self.autocorr_matrix - np.outer(mu, mu) def rms(self, axis: int = None) -> float | np.ndarray: - rms_arr = np.sqrt(np.diag(self.cov())) + rms_arr = np.sqrt(np.diag(self.cov_matrix)) return rms_arr[axis] def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: @@ -130,14 +133,14 @@ def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, # giving error in cholesky decomposition. - particles = gen_dist(size=size, cov_matrix=self.cov(), name=dist) - particles = particles + self.centroid() + particles = gen_dist(size=size, cov_matrix=self.cov_matrix, name=dist) + particles = particles + self.centroid return particles def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: # Extract beam centroid and covariance matrix. - centroid = self.centroid() - cov_matrix = self.cov() + centroid = self.centroid + cov_matrix = self.cov_matrix # Project covariance matrix onto x-y plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) @@ -190,11 +193,11 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) # Get centroid in rest frame. - centroid = self.centroid() + centroid = self.centroid centroid[4] *= self.gamma() # Get covariance matrix in rest frame. - cov_matrix = self.cov() + cov_matrix = self.cov_matrix cov_matrix = np.linalg.multi_dot( [lorentz_matrix_inv[:-1, :-1], cov_matrix, lorentz_matrix_inv[:-1, :-1].T] ) diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index 83d8e847..5eb76799 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -1,37 +1,37 @@ import numpy as np -def gen_dist_gauss(n: int, cov_matrix: np.ndarray) -> np.ndarray: +def gen_dist_gauss(size: int, cov_matrix: np.ndarray) -> np.ndarray: return np.random.multivariate_normal( mean=np.zeros(cov_matrix.shape[0]), cov=cov_matrix, - size=n, + size=size, ) -def gen_dist_kv(n: int, cov_matrix: np.ndarray) -> np.ndarray: - X = np.random.normal(size=(n, cov_matrix.shape[0])) +def gen_dist_kv(size: int, cov_matrix: np.ndarray) -> np.ndarray: + X = np.random.normal(size=(size, cov_matrix.shape[0])) X /= np.linalg.norm(X, axis=1)[:, None] X /= np.std(X, axis=0) return X -def gen_dist_waterbag(n: int, cov_matrix: np.ndarray) -> np.ndarray: - X = gen_dist_kv(n, cov_matrix) +def gen_dist_waterbag(size: int, cov_matrix: np.ndarray) -> np.ndarray: + X = gen_dist_kv(size, cov_matrix) dim = X.shape[1] - r = np.random.uniform(size=n) ** (1.0 / dim) + r = np.random.uniform(size=size) ** (1.0 / dim) X *= r[:, None] X /= np.std(X, axis=0) return X -def gen_dist(n: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: +def gen_dist(size: int, cov_matrix: np.ndarray, name: str) -> np.ndarray: if name == "kv": - X = gen_dist_kv(n, cov_matrix) + X = gen_dist_kv(size, cov_matrix) elif name == "waterbag": - X = gen_dist_waterbag(n, cov_matrix) + X = gen_dist_waterbag(size, cov_matrix) elif name == "gauss": - X = gen_dist_gauss(n, cov_matrix) + X = gen_dist_gauss(size, cov_matrix) else: raise ValueError(f"Invalid distribution name: {name}") From d705ffc49b61d43b0ec461dc72f5eb39d321c58d Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 17:08:49 -0400 Subject: [PATCH 127/183] Reduce default turns --- examples/Envelope/test_env_sns_ring_speed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/test_env_sns_ring_speed.py index 81a97978..3d89838c 100644 --- a/examples/Envelope/test_env_sns_ring_speed.py +++ b/examples/Envelope/test_env_sns_ring_speed.py @@ -26,7 +26,7 @@ parser.add_argument("--intensity", type=float, default=2e14) parser.add_argument("--nparts", type=int, default=10_000) -parser.add_argument("--turns", type=int, default=100) +parser.add_argument("--turns", type=int, default=25) parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) args = parser.parse_args() From b04d0e4722faacf146b15c0b333f817b2d1caff3 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 17:26:10 -0400 Subject: [PATCH 128/183] Avoid using np.linalg.inv --- py/orbit/envelope/envelope.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b9db2ec0..a6a44ca9 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -167,16 +167,17 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: # Build matrix to undo x-y diagonalization. A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) + A_inv = A.T # Build matrix to translate to centroid. T = np.identity(7) T[0, -1] = centroid[0] T[2, -1] = centroid[2] + T_inv = np.copy(T) + T_inv[:-1, -1] = -T[:-1, -1] # Compute transfer matrix in lab frame. - V = np.matmul(T, A) - V_inv = np.linalg.inv(V) - return np.linalg.multi_dot([V, M, V_inv]) + return T @ A @ M @ A_inv @ T_inv def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix: rest frame to lab frame. @@ -186,11 +187,11 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # x' = dx/ds -> x' / gamma # y' = dy/ds -> y' / gamma # z' = dz/ds -> z' - lorentz_matrix = np.identity(7) - lorentz_matrix[1, 1] = self.gamma() - lorentz_matrix[3, 3] = self.gamma() - lorentz_matrix[4, 4] = 1.0 / self.gamma() - lorentz_matrix_inv = np.linalg.inv(lorentz_matrix) + L = np.identity(7) + L[1, 1] = self.gamma() + L[3, 3] = self.gamma() + L[4, 4] = 1.0 / self.gamma() + L_inv = np.diag(1.0 / np.diag(L)) # Get centroid in rest frame. centroid = self.centroid @@ -198,9 +199,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Get covariance matrix in rest frame. cov_matrix = self.cov_matrix - cov_matrix = np.linalg.multi_dot( - [lorentz_matrix_inv[:-1, :-1], cov_matrix, lorentz_matrix_inv[:-1, :-1].T] - ) + cov_matrix = L_inv[:-1, :-1] @ cov_matrix @ L_inv[:-1, :-1].T # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) @@ -228,18 +227,17 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # Build matrix to undo x-y-z diagonalization. A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) + A_inv = A.T # Build matrix for translation to centroid. T = np.identity(7) for i in (0, 2, 4): T[i, -1] = centroid[i] - - # Build matrix for Lorentz boost (length contraction). - L = lorentz_matrix + T_inv = np.copy(T) + T_inv[:-1, -1] = -T[:-1, -1] # Compute transfer matrix in lab frame. - V = np.linalg.multi_dot([L, T, A]) - M = np.linalg.multi_dot([V, M, np.linalg.inv(V)]) + M = L @ T @ A @ M @ A_inv @ T_inv @ L_inv # Convert from z' to dE return convert_matrix_zp_to_dE(M, self.sync_part) From 3ec7131c00ba507bed64b1fa848ddcd4d585ea00 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 17:36:30 -0400 Subject: [PATCH 129/183] Use -1 instead of dim --- py/orbit/envelope/envelope.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index a6a44ca9..f311ddbd 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -69,7 +69,6 @@ def __init__( self.bunch = empty_bunch self.sync_part = bunch.getSyncParticle() - self.dim = 6 if centroid is None: centroid = np.zeros(6) @@ -78,10 +77,10 @@ def __init__( cov_matrix = np.eye(6) self.moment_matrix = np.zeros((7, 7)) - self.moment_matrix[:self.dim, :self.dim] = cov_matrix + np.outer(centroid, centroid) - self.moment_matrix[:self.dim, self.dim] = centroid - self.moment_matrix[self.dim, :self.dim] = centroid - self.moment_matrix[self.dim, self.dim] = 1.0 + self.moment_matrix[:-1, :-1] = cov_matrix + np.outer(centroid, centroid) + self.moment_matrix[:-1, -1] = centroid + self.moment_matrix[-1, :-1] = centroid + self.moment_matrix[-1, -1] = 1.0 self.intensity = 0.0 self.set_intensity(intensity) @@ -109,11 +108,11 @@ def charge(self) -> float: @property def centroid(self): - return self.moment_matrix[:self.dim, self.dim] + return self.moment_matrix[:-1, -1] @property def autocorr_matrix(self): - return self.moment_matrix[:self.dim, :self.dim] + return self.moment_matrix[:-1, :-1] @property def cov_matrix(self): @@ -173,6 +172,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: T = np.identity(7) T[0, -1] = centroid[0] T[2, -1] = centroid[2] + T_inv = np.copy(T) T_inv[:-1, -1] = -T[:-1, -1] @@ -187,15 +187,18 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: # x' = dx/ds -> x' / gamma # y' = dy/ds -> y' / gamma # z' = dz/ds -> z' + gamma = self.gamma() + gamma_inv = 1.0 / gamma + L = np.identity(7) - L[1, 1] = self.gamma() - L[3, 3] = self.gamma() - L[4, 4] = 1.0 / self.gamma() + L[1, 1] = gamma + L[3, 3] = gamma + L[4, 4] = gamma_inv + L_inv = np.diag(1.0 / np.diag(L)) # Get centroid in rest frame. - centroid = self.centroid - centroid[4] *= self.gamma() + centroid = np.matmul(L_inv[:-1, :-1], self.centroid) # Get covariance matrix in rest frame. cov_matrix = self.cov_matrix @@ -233,6 +236,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: T = np.identity(7) for i in (0, 2, 4): T[i, -1] = centroid[i] + T_inv = np.copy(T) T_inv[:-1, -1] = -T[:-1, -1] From 7295de473419906410fcf6bc027852b1c5b5709d Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 17:38:47 -0400 Subject: [PATCH 130/183] Use cov matrix for bunch length calculation --- py/orbit/envelope/envelope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index f311ddbd..3fb3a3c3 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -154,7 +154,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: ry = 2.0 * math.sqrt(cov_eig_vals[1]) # Build transfer matrix in upright frame. - bunch_length = 4.0 * self.rms(axis=4) + bunch_length = 4.0 * np.sqrt(cov_matrix[4, 4]) perveance = self.sc_factor / bunch_length factor = 2.0 * perveance / (rx + ry) kappa_x = factor / rx From 14fb64972b5e82b512e44cb5a0bd5e9aba109c41 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 18:09:58 -0400 Subject: [PATCH 131/183] Use rotation instead of eigenvector calc for 2D space charge --- py/orbit/envelope/envelope.py | 41 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 3fb3a3c3..324a66d7 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -137,38 +137,38 @@ def sample(self, size: int, dist: str = "kv") -> np.ndarray: return particles def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: - # Extract beam centroid and covariance matrix. centroid = self.centroid cov_matrix = self.cov_matrix - # Project covariance matrix onto x-y plane. - cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2)) + # Calculate transfer matrix in normalized (upright) frame. + cov_xx = cov_matrix[0, 0] + cov_yy = cov_matrix[2, 2] + cov_xy = cov_matrix[0, 2] - # Compute eigenvalues and eigenvectors of x-y covariance matrix. - cov_eig_res = np.linalg.eigh(cov_matrix_proj) - cov_eig_vals = cov_eig_res.eigenvalues - cov_eig_vecs = cov_eig_res.eigenvectors + phi = -0.5 * np.arctan2(2 * cov_xy, cov_xx - cov_yy) + sin_phi = np.sin(phi) + cos_phi = np.cos(phi) + rx = 2.0 * np.sqrt(abs(cov_xx * cos_phi**2 + cov_yy * sin_phi**2 - 2.0 * cov_xy * sin_phi * cos_phi)) + ry = 2.0 * np.sqrt(abs(cov_xx * sin_phi**2 + cov_yy * cos_phi**2 + 2.0 * cov_xy * sin_phi * cos_phi)) - # Compute rms beam sizes in upright frame. - rx = 2.0 * math.sqrt(cov_eig_vals[0]) - ry = 2.0 * math.sqrt(cov_eig_vals[1]) - - # Build transfer matrix in upright frame. bunch_length = 4.0 * np.sqrt(cov_matrix[4, 4]) perveance = self.sc_factor / bunch_length - factor = 2.0 * perveance / (rx + ry) - kappa_x = factor / rx - kappa_y = factor / ry + kappa_factor = 2.0 * perveance / (rx + ry) M = np.identity(7) - M[1, 0] = kappa_x * length - M[3, 2] = kappa_y * length + M[1, 0] = kappa_factor * length / rx + M[3, 2] = kappa_factor * length / ry + + # Build matrix A to transform out of normalized frame. + A = np.eye(7) + A[0, 0] = A[1, 1] = +cos_phi + A[0, 2] = A[1, 3] = +sin_phi + A[2, 0] = A[3, 1] = -sin_phi + A[2, 2] = A[3, 3] = +cos_phi - # Build matrix to undo x-y diagonalization. - A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) A_inv = A.T - # Build matrix to translate to centroid. + # Build matrix T to shift to beam centroid. T = np.identity(7) T[0, -1] = centroid[0] T[2, -1] = centroid[2] @@ -194,7 +194,6 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: L[1, 1] = gamma L[3, 3] = gamma L[4, 4] = gamma_inv - L_inv = np.diag(1.0 / np.diag(L)) # Get centroid in rest frame. From 3c6b8c9d6acd155bb1e2e5e18920be93fa1a2dff Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Wed, 24 Jun 2026 18:23:47 -0400 Subject: [PATCH 132/183] Track cov and mean instead of 7 x 7 moment matrix No time saved because to get cov you need to call outer(mu, mu) and subtract from moment matrix. This is more straightforward --- py/orbit/envelope/envelope.py | 40 ++++++++++------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 324a66d7..97177d88 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -67,20 +67,15 @@ def __init__( empty_bunch = Bunch() bunch.copyEmptyBunchTo(empty_bunch) self.bunch = empty_bunch - self.sync_part = bunch.getSyncParticle() - if centroid is None: - centroid = np.zeros(6) - - if cov_matrix is None: - cov_matrix = np.eye(6) + self.centroid = centroid + if self.centroid is None: + self.centroid = np.zeros(6) - self.moment_matrix = np.zeros((7, 7)) - self.moment_matrix[:-1, :-1] = cov_matrix + np.outer(centroid, centroid) - self.moment_matrix[:-1, -1] = centroid - self.moment_matrix[-1, :-1] = centroid - self.moment_matrix[-1, -1] = 1.0 + self.cov_matrix = cov_matrix + if self.cov_matrix is None: + self.cov_matrix = np.eye(6) self.intensity = 0.0 self.set_intensity(intensity) @@ -106,28 +101,16 @@ def mass(self) -> float: def charge(self) -> float: return self.bunch.charge() - @property - def centroid(self): - return self.moment_matrix[:-1, -1] - - @property - def autocorr_matrix(self): - return self.moment_matrix[:-1, :-1] - - @property - def cov_matrix(self): - mu = self.centroid - return self.autocorr_matrix - np.outer(mu, mu) - def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov_matrix)) return rms_arr[axis] def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: if transfer_matrix is not None: - self.moment_matrix = ( - transfer_matrix @ self.moment_matrix @ transfer_matrix.T - ) + M = transfer_matrix[:-1, :-1] + u = transfer_matrix[:-1, -1] + self.cov_matrix = M @ self.cov_matrix @ M.T + self.centroid = np.matmul(M, self.centroid) + u def sample(self, size: int, dist: str = "kv") -> np.ndarray: # Issue: covariance matrix is becoming non semi-positive definite, @@ -200,8 +183,7 @@ def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: centroid = np.matmul(L_inv[:-1, :-1], self.centroid) # Get covariance matrix in rest frame. - cov_matrix = self.cov_matrix - cov_matrix = L_inv[:-1, :-1] @ cov_matrix @ L_inv[:-1, :-1].T + cov_matrix = L_inv[:-1, :-1] @ self.cov_matrix @ L_inv[:-1, :-1].T # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) From 1e1da896a892c6ccd4519f15094c9cc0d808a571 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 01:41:01 -0400 Subject: [PATCH 133/183] Add charge as argument everywhere SyncPart does not have charge --- py/orbit/envelope/envelope.py | 26 ++++++----- py/orbit/matrix_lattice/analytic.py | 34 ++++++++++----- py/orbit/py_linac/lattice/LinacAccNodes.py | 46 ++++++++++---------- py/orbit/py_linac/lattice/LinacRfGapNodes.py | 38 ++++++++++++++++ py/orbit/teapot/teapot.py | 34 +++++++-------- 5 files changed, 116 insertions(+), 62 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 97177d88..d2f4c865 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -234,16 +234,19 @@ def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None self.space_charge = space_charge def track(self, envelope: Envelope) -> None: - for node in self.lattice.getNodes(): + charge = envelope.charge() + for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.apply_transfer_matrix(matrix) - for index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, index, place_in_part=BEFORE): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.apply_transfer_matrix(matrix) if self.space_charge: - length = node.getLength(index) + length = node.getLength(part_index) if self.space_charge == "2d": matrix = envelope.sc_transfer_matrix_2d(length) elif self.space_charge == "3d": @@ -252,10 +255,13 @@ def track(self, envelope: Envelope) -> None: raise ValueError(f"Invalid space charge model: {self.space_charge}") envelope.apply_transfer_matrix(matrix) - envelope.apply_transfer_matrix(node.matrix(envelope.sync_part, index)) + matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) + envelope.apply_transfer_matrix(matrix) - for child_node in node.getChildNodes(BODY, index, place_in_part=AFTER): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.apply_transfer_matrix(matrix) for child_node in node.getChildNodes(EXIT): - envelope.apply_transfer_matrix(child_node.matrix(envelope.sync_part)) + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.apply_transfer_matrix(matrix) \ No newline at end of file diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/matrix_lattice/analytic.py index 9549c9e2..673e201b 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/matrix_lattice/analytic.py @@ -1,3 +1,7 @@ +"""Functions to compute transfer matrices. + +These functions track the synchronous particle! +""" import math import numpy as np @@ -54,11 +58,13 @@ def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: M[2, 3] = length M[4, 5] = length / (sync_part.gamma() ** 2) M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) + + sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) return M -def quad_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: - if abs(kq) == 0: +def quad_matrix(length: float, kq: float, sync_part: SyncParticle, charge: float) -> np.ndarray: + if abs(kq) == 0 or charge == 0: return drift_matrix(length=length, sync_part=sync_part) sqrt_abs_kq = math.sqrt(abs(kq)) @@ -93,10 +99,12 @@ def quad_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray M[4, 5] = length / (sync_part.gamma()**2) M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) + + sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) return M -def bend_matrix(length: float, theta: float, sync_part: SyncParticle) -> np.ndarray: +def bend_matrix(length: float, theta: float, sync_part: SyncParticle, charge: float) -> np.ndarray: if length <= 0: return np.identity(7) @@ -116,6 +124,8 @@ def bend_matrix(length: float, theta: float, sync_part: SyncParticle) -> np.ndar M[4, 1] = -rho * (1.0 - cx) M[4, 5] = -(sync_part.beta() ** 2) * length + rho * sx M[:5, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) + + sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) return M @@ -144,7 +154,7 @@ def kick_matrix(kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray return M -def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndarray: +def solenoid_matrix(length: float, B: float, sync_part: SyncParticle, charge: float) -> np.ndarray: if B == 0: return drift_matrix(length=length, sync_part=sync_part) @@ -172,6 +182,8 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle) -> np.ndar M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) + + sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) return M @@ -179,20 +191,20 @@ def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: raise NotImplementedError() -def rf_gap_matrix(frequency: float, E0TL: float, phase: float, sync_part: SyncParticle) -> np.ndarray: +def rf_gap_matrix(frequency: float, E0TL: float, phase: float, sync_part: SyncParticle, charge: float) -> np.ndarray: """Matrix for thin RF gap. - E0TL: maximal energy gain in the gap [GeV]. - frequency: RF frequency [Hz] - phase: RF phase [rad] + Args: + frequency: RF frequency [Hz] + E0TL: maximum energy gain in the gap [GeV]. + phase: RF phase [rad] """ gamma = sync_part.gamma() beta = sync_part.beta() mass = sync_part.mass() - charge = 1.0 # TEMP! kin_energy_in = sync_part.kinEnergy() - chargeE0TLsin = charge * E0TL * math.sin(phase) + charge_E0TL_sin = charge * E0TL * math.sin(phase) kin_energy_delta = charge * E0TL * math.cos(phase) # Calculate parameters in the center of the gap. @@ -218,7 +230,7 @@ def rf_gap_matrix(frequency: float, E0TL: float, phase: float, sync_part: SyncPa d_rp = kappa * math.sin(phase) M = np.eye(7) - M[5, 4] = chargeE0TLsin * phase_time_coeff + M[5, 4] = charge_E0TL_sin * phase_time_coeff M[4, 4] = beta_out / beta M[1, 1] = prime_coeff M[3, 3] = prime_coeff diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index bd0e2626..08bf074f 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -9,6 +9,7 @@ import os import numpy as np +from mesonbuild.backend import nonebackend # import the finalization function from orbit.utils import orbitFinalize @@ -139,7 +140,7 @@ def trackDesign(paramsDict): self.trackActions(actionContainer, paramsDict) actionContainer.removeAction(trackDesign, AccActionsContainer.BODY) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: raise NotImplementedError(str(self)) @@ -153,7 +154,7 @@ def __init__(self, name="none"): BaseLinacNode.__init__(self, name) self.setType("markerLinacNode") - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -323,7 +324,7 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] self.tracking_module.drift(bunch, length) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) if length > 0: return drift_matrix(length=length, sync_part=sync_part) @@ -526,15 +527,14 @@ def track(self, paramsDict): """ return - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) - charge = 1.0 # sync_part has no charge parameter... + if length <= 0: + return None + brho = 3.335640952 * sync_part.momentum() / charge - kq = self.getParam("dB/dr") / brho - if abs(kq) > 0: - return quad_matrix(length=length, kq=kq, sync_part=sync_part) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) + kq = self.getParam("dB/dr") / brho + return quad_matrix(length=length, kq=kq, sync_part=sync_part, charge=charge) def getTotalField(self, z): """ @@ -739,10 +739,10 @@ def track(self, paramsDict): TPB.bend1(bunch, length, theta / 2.0) return - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) theta = self.getParam("theta") / self.getnParts() - return bend_matrix(length=length, theta=theta, sync_part=sync_part) + return bend_matrix(length=length, theta=theta, sync_part=sync_part, charge=charge) class DCorrectorH(LinacMagnetNode): @@ -789,10 +789,9 @@ def track(self, paramsDict): kick = -field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, kick, 0.0, 0.0) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getParam("effLength") / self.getnParts() field = self.getParam("B") - charge = 1.0 # TEMP delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() if abs(delta_xp) > 0: return kick_matrix(delta_xp, 0.0, 0.0) @@ -842,10 +841,9 @@ def track(self, paramsDict): kick = field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, 0, kick, 0.0) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getParam("effLength") / self.getnParts() field = self.getParam("B") - charge = 1.0 # TEMP delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() if abs(delta_yp) > 0: return kick_matrix(0.0, delta_yp, 0.0) @@ -948,13 +946,13 @@ def track(self, paramsDict): useCharge = paramsDict["useCharge"] TPB.soln(bunch, length, B, useCharge) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: - B = self.getParam("B") + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) - if abs(B) > 0: - return solenoid_matrix(length=length, B=B, sync_part=sync_part) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) + if length <= 0: + return None + + B = self.getParam("B") + return solenoid_matrix(length=length, B=B, sync_part=sync_part, charge=charge) class AbstractRF_Gap(BaseLinacNode): @@ -1075,7 +1073,7 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: angle = self.__angle if abs(angle) > 0: return tilt_matrix(angle) @@ -1130,5 +1128,5 @@ def getUsage(self): """ return self.__usage - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None \ No newline at end of file diff --git a/py/orbit/py_linac/lattice/LinacRfGapNodes.py b/py/orbit/py_linac/lattice/LinacRfGapNodes.py index 30b1fbe8..f5c4cdb8 100644 --- a/py/orbit/py_linac/lattice/LinacRfGapNodes.py +++ b/py/orbit/py_linac/lattice/LinacRfGapNodes.py @@ -6,6 +6,8 @@ import os import math +import numpy as np + # ---- MPI module function and classes from orbit.core.orbit_mpi import mpi_comm, mpi_datatype, MPI_Comm_rank, MPI_Bcast @@ -35,6 +37,9 @@ # quad2 - linac quad non-linear part of tracking from orbit.core.bunch import Bunch +from orbit.core.bunch import SyncParticle + +from orbit.matrix_lattice.analytic import rf_gap_matrix class BaseRF_Gap(AbstractRF_Gap): @@ -330,6 +335,39 @@ def ttf_track_bunch__(self, bunch, frequency, E0L, phase): orbitFinalize(msg) self.cppGapModel.trackBunch(bunch, frequency, E0L, phase, self.polyT, self.polyS, self.polyTp, self.polySp) + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: + E0TL = self.getParam("E0TL") + mode_phase = self.getParam("mode") * math.pi + + cavity = self.getRF_Cavity() + frequency = cavity.getFrequency() + phase = cavity.getPhase() + mode_phase + amplitude = cavity.getAmp() + + arrival_time = sync_part.time() + arrival_time_design = cavity.getDesignArrivalTime() + + if self.__isFirstGap: + if cavity.isDesignSetUp(): + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) + else: + orbitFinalize("Run `trackDesign` first to initialize cavity phases.") + else: + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) + + self.setGapPhase(phase) + + if amplitude == 0.0: + return None + + return rf_gap_matrix( + frequency=frequency, + E0TL=(E0TL * amplitude), + phase=phase, + sync_part=sync_part, + charge=charge, + ) + # ----------------------------------------------------------------------- # diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 3d3b990d..658410b1 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -439,7 +439,7 @@ def __init__(self, name: str = "no name") -> None: AccNodeBunchTracker.__init__(self, name) self.setType("base teapot") - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: raise NotImplementedError(str(self)) @@ -460,7 +460,7 @@ def track(self, paramsDict: dict) -> None: turn = bunch.bunchAttrInt("TurnNumber") bunch.bunchAttrInt("TurnNumber", turn + 1) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -581,7 +581,7 @@ def getUsageFringeFieldOUT(self) -> bool: """ return self.__fringeFieldOUT.getUsage() - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -608,7 +608,7 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.drift(bunch, length) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) if length > 0: return drift_matrix(length=length, sync_part=sync_part) @@ -653,7 +653,7 @@ def track(self, paramsDict: dict) -> None: lostbunch = paramsDict["lostbunch"] self.aperture.checkBunch(bunch, lostbunch) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -683,7 +683,7 @@ def track(self, paramsDict: dict) -> None: self.addParam("yAvg", self.twiss.getAverage(2)) self.addParam("ypAvg", self.twiss.getAverage(3)) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -710,7 +710,7 @@ def track(self, paramsDict: dict) -> None: length = self.getParam("ring_length") TPB.wrapbunch(bunch, length) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None @@ -761,13 +761,13 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: B = self.getParam("B") if self.waveform is not None: B *= self.waveform.getStrength() length = self.getLength(index) if abs(B) > 0: - return solenoid_matrix(length=length, B=B, sync_part=sync_part) + return solenoid_matrix(length=length, B=B, sync_part=sync_part, charge=charge) elif length > 0: return drift_matrix(length=length, sync_part=sync_part) @@ -921,7 +921,7 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: if np.all(np.abs(self.getParam("kls")) == 0): length = self.getLength(index) return drift_matrix(length=length, sync_part=sync_part) @@ -1092,13 +1092,13 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) kq = self.getParam("kq") if self.waveform: kq *= self.waveform.getStrength() if abs(kq) > 0: - return quad_matrix(length=length, kq=kq, sync_part=sync_part) + return quad_matrix(length=length, kq=kq, sync_part=sync_part, charge=charge) elif length > 0: return drift_matrix(length=length, sync_part=sync_part) @@ -1316,11 +1316,11 @@ def track(self, paramsDict: dict) -> None: TPB.bend1(bunch, length, theta / 2.0) return - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) theta = self.getParam("theta") / self.getnParts() if abs(theta) > 0: - return bend_matrix(length=length, theta=theta, sync_part=sync_part) + return bend_matrix(length=length, theta=theta, sync_part=sync_part, charge=charge) elif length > 0: return drift_matrix(length=length, sync_part=sync_part) @@ -1490,7 +1490,7 @@ def setWaveform(self, waveform): """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: length = self.getLength(index) nparts = self.getnParts() @@ -1545,7 +1545,7 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: tilt_angle = self.getTiltAngle() if abs(tilt_angle) > 0: return tilt_matrix(self.getTiltAngle()) @@ -1605,7 +1605,7 @@ def getUsage(self) -> bool: """ return self.__usage - def matrix(self, sync_part: SyncParticle, index: int = -1) -> np.ndarray: + def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: return None From ed1fb0a55b66b45ad14dd337d96ec31e0294f62e Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 01:41:52 -0400 Subject: [PATCH 134/183] Start SNS linac envelope tracking example --- examples/Envelope/sns_linac/sns_linac.xml | 13475 ++++++++++++++++++++ examples/Envelope/sns_linac/track.py | 189 + 2 files changed, 13664 insertions(+) create mode 100644 examples/Envelope/sns_linac/sns_linac.xml create mode 100755 examples/Envelope/sns_linac/track.py diff --git a/examples/Envelope/sns_linac/sns_linac.xml b/examples/Envelope/sns_linac/sns_linac.xml new file mode 100644 index 00000000..62cdbf41 --- /dev/null +++ b/examples/Envelope/sns_linac/sns_linac.xml @@ -0,0 +1,13475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py new file mode 100755 index 00000000..3a868444 --- /dev/null +++ b/examples/Envelope/sns_linac/track.py @@ -0,0 +1,189 @@ +import argparse +import math +import random +import time + +import numpy as np +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis +from orbit.core.bunch import SyncParticle +from orbit.core.linac import BaseRfGap +from orbit.core.linac import MatrixRfGap +from orbit.core.spacecharge import SpaceChargeCalcUnifEllipse +from orbit.core.spacecharge import SpaceChargeCalc3D +from orbit.bunch_generators import TwissContainer +from orbit.bunch_generators import WaterBagDist3D +from orbit.bunch_utils import collect_bunch +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker +from orbit.lattice import AccLattice +from orbit.lattice import AccNode +from orbit.lattice import AccActionsContainer +from orbit.py_linac.linac_parsers import SNS_LinacLatticeFactory +from orbit.py_linac.lattice import LinacAccLattice +from orbit.space_charge.sc3d import setSC3DAccNodes +from orbit.space_charge.sc3d import setUniformEllipsesSCAccNodes +from orbit.utils.consts import mass_proton +from orbit.utils.consts import charge_electron +from orbit.utils.consts import speed_of_light + + +# Parse arguments +# -------------------------------------------------------------------------------- + +parser = argparse.ArgumentParser() +parser.add_argument("--seq", type=str, default=None) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--sc-model", type=str, default="ellipsoid") +parser.add_argument("--nparts", type=int, default=10_000) +parser.add_argument("--current", type=float, default=0.038) +args = parser.parse_args() + + +# Setup +# -------------------------------------------------------------------------------- + +random.seed(100) + + +# Bunch +# -------------------------------------------------------------------------------- + +kin_energy = 0.0025 # [GeV] +mass = mass_proton +frequency = 402.5e+06 +charge = -1.0 +intensity = args.current / frequency / (math.fabs(charge) * charge_electron) + +bunch = Bunch() +bunch.mass(mass) +bunch.getSyncParticle().kinEnergy(kin_energy) +bunch.macroSize(intensity / args.nparts) +bunch.charge(charge) + +alpha_x, beta_x, eps_x = (-1.962, 0.183, 2.874e-06) +alpha_y, beta_y, eps_y = (+1.768, 0.162, 2.874e-06) +alpha_z, beta_z, eps_z = (-0.0196, 116.414, 1.651e-08) + +twiss_x = TwissContainer(alpha_x, beta_x, eps_x) +twiss_y = TwissContainer(alpha_y, beta_y, eps_y) +twiss_z = TwissContainer(alpha_z, beta_z, eps_z) + +dist = WaterBagDist3D(twiss_x, twiss_y, twiss_z) +for _ in range(args.nparts): + bunch.addParticle(*dist.getCoordinates()) + + +# Lattice +# -------------------------------------------------------------------------------- + +sequence_names = [ + "MEBT", + "DTL1", + "DTL2", + "DTL3", + "DTL4", + "DTL5", + "DTL6", + "CCL1", + "CCL2", + "CCL3", + "CCL4", + "SCLMed", + "SCLHigh", + "HEBT1", + "HEBT2", +] +if args.seq: + stop_index = sequence_names.index(args.seq) + 1 + sequence_names = sequence_names[:stop_index] + +sns_linac_factory = SNS_LinacLatticeFactory() +sns_linac_factory.setMaxDriftLength(0.01) +lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "sns_linac.xml") + +rf_gaps = lattice.getRF_Gaps() +for rf_gap in rf_gaps: + rf_gap.setCppGapModel(MatrixRfGap()) + +lattice.trackDesignBunch(bunch) + + +# Track envelope +# -------------------------------------------------------------------------------- + +twiss_calc = BunchTwissAnalysis() +twiss_calc.analyzeBunch(bunch) + +cov_matrix = np.zeros((6, 6)) +for i in range(6): + for j in range(6): + cov_matrix[i, j] = cov_matrix[j, i] = twiss_calc.getCorrelation(i, j) + +envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix) + +envelope_tracker = EnvelopeTracker(lattice, space_charge=None) +envelope_tracker.track(envelope) + + +# Track bunch +# -------------------------------------------------------------------------------- + +lattice.trackDesignBunch(bunch) + +if args.sc: + sc_path_length_min = 0.01 + if args.sc_model == "ellipsoid": + n_ellipsoids = 1 + sc_calc = SpaceChargeCalcUnifEllipse(n_ellipsoids) + sc_nodes = setUniformEllipsesSCAccNodes(lattice, sc_path_length_min, sc_calc) + if args.sc_model == "3d": + sc_calc = SpaceChargeCalc3D(64, 64, 64) + sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) + +lattice.trackDesignBunch(bunch) + +params_dict = {"old_pos": -1.0, "count": 0, "pos_step": 0.1} +action_container = AccActionsContainer() + +position_start = 0.0 +twiss_analysis = BunchTwissAnalysis() + +def action_entrance(params_dict: dict) -> None: + bunch = params_dict["bunch"] + node = params_dict["node"] + position = params_dict["path_length"] + + if params_dict["old_pos"] == position: + return + if params_dict["old_pos"] + params_dict["pos_step"] > position: + return + params_dict["old_pos"] = position + params_dict["count"] += 1 + + twiss_analysis.analyzeBunch(bunch) + x_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(0, 0)) + y_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(2, 2)) + z_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(4, 4)) + + message = "" + message += " s={:0.3f}".format(position + position_start) + message += " xrms={:0.3f}".format(x_rms) + message += " yrms={:0.3f}".format(y_rms) + message += " zrms={:0.3f}".format(z_rms) + message += " node={}".format(node.getName()) + print(message) + +action_container.addAction(action_entrance, AccActionsContainer.ENTRANCE) +lattice.trackBunch(bunch, paramsDict=params_dict, actionContainer=action_container) + + +# Analysis +# -------------------------------------------------------------------------------- + +bunch_coords = collect_bunch(bunch)["coords"] +bunch_cov_matrix = np.cov(bunch_coords.T) + +print(np.round(1000.0 * np.sqrt(np.diag(bunch_cov_matrix)), 2)) +print(np.round(1000.0 * np.sqrt(np.diag(envelope.cov_matrix)), 2)) + From 8bd62e681d0f61f995dcbf2fafda2c36ec2d8453 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 01:42:21 -0400 Subject: [PATCH 135/183] Add charge param to tests --- examples/Envelope/test_env.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index b456cd67..bb3b680b 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -320,13 +320,13 @@ def test_rf_gap_matrix( frequency: float = 402.5e06, E0TL: float = 0.001, phase: float = 0.0, -): - # Just tests matrix against MatrixRFGap. Node not implemented yet. - + charge: float = -1.0, +) -> None: cov_matrix = make_default_cov_matrix() bunch_in = Bunch() bunch_in.mass(mass_proton) + bunch_in.charge(charge) bunch_in.getSyncParticle().kinEnergy(kin_energy) coords_in = np.random.multivariate_normal(np.zeros(6), cov_matrix, size=10) @@ -351,6 +351,7 @@ def test_rf_gap_matrix( E0TL=E0TL, phase=phase, sync_part=bunch_out_2.getSyncParticle(), + charge=bunch_in.charge(), ) coords_in = np.column_stack([coords_in, np.ones(coords_in.shape[0])]) coords_out_2 = np.matmul(coords_in, matrix.T) From 0edcfa4b4a60df43a12706f7294f9167a5b9cfc4 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 01:46:12 -0400 Subject: [PATCH 136/183] Turn off fringe fields in sns linac example --- examples/Envelope/sns_linac/track.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 3a868444..539f5726 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -13,6 +13,8 @@ from orbit.core.spacecharge import SpaceChargeCalc3D from orbit.bunch_generators import TwissContainer from orbit.bunch_generators import WaterBagDist3D +from orbit.bunch_generators import GaussDist3D +from orbit.bunch_generators import KVDist3D from orbit.bunch_utils import collect_bunch from orbit.envelope import Envelope from orbit.envelope import EnvelopeTracker @@ -102,6 +104,13 @@ sns_linac_factory.setMaxDriftLength(0.01) lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "sns_linac.xml") +for node in lattice.getNodes(): + try: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + except: + pass + rf_gaps = lattice.getRF_Gaps() for rf_gap in rf_gaps: rf_gap.setCppGapModel(MatrixRfGap()) From f7da0aa249cc720a21ceeafeca9a34a651902a44 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 01:48:52 -0400 Subject: [PATCH 137/183] Change default seq stop --- examples/Envelope/sns_linac/track.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 539f5726..48876b9d 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -34,7 +34,7 @@ # -------------------------------------------------------------------------------- parser = argparse.ArgumentParser() -parser.add_argument("--seq", type=str, default=None) +parser.add_argument("--seq", type=str, default="MEBT") parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-model", type=str, default="ellipsoid") parser.add_argument("--nparts", type=int, default=10_000) @@ -114,6 +114,9 @@ rf_gaps = lattice.getRF_Gaps() for rf_gap in rf_gaps: rf_gap.setCppGapModel(MatrixRfGap()) + +for index, node in enumerate(lattice.getNodes()): + print(index, type(node), node.getName()) lattice.trackDesignBunch(bunch) From c2cb35500526318afdce74a264ff5e3152a52685 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 02:32:29 -0400 Subject: [PATCH 138/183] Start linac benchmark --- examples/Envelope/sns_linac/style.mplstyle | 8 ++ examples/Envelope/sns_linac/track.py | 122 +++++++++++++++------ py/orbit/envelope/envelope.py | 86 ++++++++++++--- 3 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 examples/Envelope/sns_linac/style.mplstyle diff --git a/examples/Envelope/sns_linac/style.mplstyle b/examples/Envelope/sns_linac/style.mplstyle new file mode 100644 index 00000000..71f36605 --- /dev/null +++ b/examples/Envelope/sns_linac/style.mplstyle @@ -0,0 +1,8 @@ +axes.linewidth: 1.25 +axes.titlesize: "medium" +image.cmap: "Greys" +figure.constrained_layout.use: True +savefig.dpi: 300 +savefig.format: "png" +xtick.minor.visible: True +ytick.minor.visible: True diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 48876b9d..662de0e2 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -4,6 +4,8 @@ import time import numpy as np +import matplotlib.pyplot as plt + from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis from orbit.core.bunch import SyncParticle @@ -29,6 +31,8 @@ from orbit.utils.consts import charge_electron from orbit.utils.consts import speed_of_light +plt.style.use("style.mplstyle") + # Parse arguments # -------------------------------------------------------------------------------- @@ -117,7 +121,7 @@ for index, node in enumerate(lattice.getNodes()): print(index, type(node), node.getName()) - + lattice.trackDesignBunch(bunch) @@ -135,7 +139,9 @@ envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix) envelope_tracker = EnvelopeTracker(lattice, space_charge=None) -envelope_tracker.track(envelope) + +histories = {} +histories["envelope"] = envelope_tracker.track_history(envelope) # Track bunch @@ -153,49 +159,97 @@ sc_calc = SpaceChargeCalc3D(64, 64, 64) sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) -lattice.trackDesignBunch(bunch) -params_dict = {"old_pos": -1.0, "count": 0, "pos_step": 0.1} +class BunchMonitor: + def __init__(self) -> None: + self.twiss_calc = BunchTwissAnalysis() + self.position_start = 0.0 + + self.history = {} + self.history["position"] = [] + self.history["rms_x"] = [] + self.history["rms_y"] = [] + self.history["rms_z"] = [] + + def __call__(self, params_dict: dict) -> None: + bunch = params_dict["bunch"] + node = params_dict["node"] + position = params_dict["path_length"] + + if params_dict["old_pos"] == position: + return + if params_dict["old_pos"] + params_dict["pos_step"] > position: + return + params_dict["old_pos"] = position + params_dict["count"] += 1 + + self.twiss_calc.analyzeBunch(bunch) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(6): + cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation(i, j) + + xrms = 1000.0 * np.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * np.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * np.sqrt(cov_matrix[4, 4]) + + message = "" + message += " s={:0.3f}".format(position + self.position_start) + message += " xrms={:0.3f}".format(xrms) + message += " yrms={:0.3f}".format(yrms) + message += " zrms={:0.3f}".format(zrms) + message += " node={}".format(node.getName()) + print(message) + + self.history["position"].append(position + self.position_start) + self.history["rms_x"].append(xrms) + self.history["rms_y"].append(yrms) + self.history["rms_z"].append(zrms) + + +monitor = BunchMonitor() + action_container = AccActionsContainer() +action_container.addAction(monitor, AccActionsContainer.ENTRANCE) +action_container.addAction(monitor, AccActionsContainer.EXIT) + +params_dict = {"old_pos": -1.0, "count": 0, "pos_step": 0.1} -position_start = 0.0 -twiss_analysis = BunchTwissAnalysis() - -def action_entrance(params_dict: dict) -> None: - bunch = params_dict["bunch"] - node = params_dict["node"] - position = params_dict["path_length"] - - if params_dict["old_pos"] == position: - return - if params_dict["old_pos"] + params_dict["pos_step"] > position: - return - params_dict["old_pos"] = position - params_dict["count"] += 1 - - twiss_analysis.analyzeBunch(bunch) - x_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(0, 0)) - y_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(2, 2)) - z_rms = 1000.0 * math.sqrt(twiss_analysis.getCorrelation(4, 4)) - - message = "" - message += " s={:0.3f}".format(position + position_start) - message += " xrms={:0.3f}".format(x_rms) - message += " yrms={:0.3f}".format(y_rms) - message += " zrms={:0.3f}".format(z_rms) - message += " node={}".format(node.getName()) - print(message) - -action_container.addAction(action_entrance, AccActionsContainer.ENTRANCE) lattice.trackBunch(bunch, paramsDict=params_dict, actionContainer=action_container) +histories["bunch"] = monitor.history + # Analysis # -------------------------------------------------------------------------------- + +# History +for mode in histories: + for key in histories[mode]: + histories[mode][key] = np.array(histories[mode][key]) + +fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) +for i, mode in enumerate(["bunch", "envelope"]): + history = histories[mode] + color = ["black", "red"][i] + ls = ["-", "--"][i] + for ax, key in zip(axs, ["rms_x", "rms_y", "rms_z"]): + ax.plot(history["position"], history[key], color=color, ls=ls, label=mode) + +for ax in axs: + ax.legend(loc="lower right") +axs[0].set_ylabel("x rms [mm]") +axs[1].set_ylabel("y rms [mm]") +axs[2].set_ylabel("z rms [mm]") +axs[2].set_xlabel("s [m]") +plt.show() + + +# Final coordinates bunch_coords = collect_bunch(bunch)["coords"] bunch_cov_matrix = np.cov(bunch_coords.T) print(np.round(1000.0 * np.sqrt(np.diag(bunch_cov_matrix)), 2)) print(np.round(1000.0 * np.sqrt(np.diag(envelope.cov_matrix)), 2)) - diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index d2f4c865..f4a18712 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -105,21 +105,19 @@ def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov_matrix)) return rms_arr[axis] - def apply_transfer_matrix(self, transfer_matrix: np.ndarray | None) -> None: - if transfer_matrix is not None: - M = transfer_matrix[:-1, :-1] - u = transfer_matrix[:-1, -1] - self.cov_matrix = M @ self.cov_matrix @ M.T - self.centroid = np.matmul(M, self.centroid) + u + def transform(self, matrix: np.ndarray | None) -> None: + if matrix is not None: + m = matrix[:-1, :-1] + u = matrix[:-1, -1] + self.cov_matrix = m @ self.cov_matrix @ m.T + self.centroid = np.matmul(m, self.centroid) + u def sample(self, size: int, dist: str = "kv") -> np.ndarray: - # Issue: covariance matrix is becoming non semi-positive definite, - # giving error in cholesky decomposition. particles = gen_dist(size=size, cov_matrix=self.cov_matrix, name=dist) particles = particles + self.centroid return particles - def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: + def sc_matrix_2d(self, length: float) -> np.ndarray: centroid = self.centroid cov_matrix = self.cov_matrix @@ -162,7 +160,7 @@ def sc_transfer_matrix_2d(self, length: float) -> np.ndarray: # Compute transfer matrix in lab frame. return T @ A @ M @ A_inv @ T_inv - def sc_transfer_matrix_3d(self, length: float) -> np.ndarray: + def sc_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix: rest frame to lab frame. # x -> x # y -> y @@ -238,30 +236,82 @@ def track(self, envelope: Envelope) -> None: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) - envelope.apply_transfer_matrix(matrix) + envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) - envelope.apply_transfer_matrix(matrix) + envelope.transform(matrix) if self.space_charge: length = node.getLength(part_index) if self.space_charge == "2d": - matrix = envelope.sc_transfer_matrix_2d(length) + matrix = envelope.sc_matrix_2d(length) elif self.space_charge == "3d": - matrix = envelope.sc_transfer_matrix_3d(length) + matrix = envelope.sc_matrix_3d(length) else: raise ValueError(f"Invalid space charge model: {self.space_charge}") - envelope.apply_transfer_matrix(matrix) + envelope.transform(matrix) matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) - envelope.apply_transfer_matrix(matrix) + envelope.transform(matrix) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) - envelope.apply_transfer_matrix(matrix) + envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) - envelope.apply_transfer_matrix(matrix) \ No newline at end of file + envelope.transform(matrix) + + def track_history(self, envelope: Envelope) -> dict[str, list]: + history = {} + history["position"] = [] + history["rms_x"] = [] + history["rms_y"] = [] + history["rms_z"] = [] + + charge = envelope.charge() + + history["position"].append(0.0) + history["rms_x"].append(envelope.rms(0)) + history["rms_y"].append(envelope.rms(2)) + history["rms_z"].append(envelope.rms(4)) + + for node_index, node in enumerate(self.lattice.getNodes()): + for child_node in node.getChildNodes(ENTRANCE): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.transform(matrix) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.transform(matrix) + + if self.space_charge: + length = node.getLength(part_index) + if self.space_charge == "2d": + matrix = envelope.sc_matrix_2d(length) + elif self.space_charge == "3d": + matrix = envelope.sc_matrix_3d(length) + else: + raise ValueError(f"Invalid space charge model: {self.space_charge}") + envelope.transform(matrix) + + matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) + envelope.transform(matrix) + + history["position"].append(node.getPosition() + node.getLength(part_index)) + history["rms_x"].append(1000.0 * envelope.rms(0)) + history["rms_y"].append(1000.0 * envelope.rms(2)) + history["rms_z"].append(1000.0 * envelope.rms(4)) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.transform(matrix) + + for child_node in node.getChildNodes(EXIT): + matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + envelope.transform(matrix) + + return history \ No newline at end of file From cf4d7c71c64e1a1ff0f62c11848fb5ad671f687c Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:04:25 -0400 Subject: [PATCH 139/183] Copy bunch in envelope constructor Also correct node position calculation in track_history --- py/orbit/envelope/envelope.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index f4a18712..ad93be7b 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -67,7 +67,7 @@ def __init__( empty_bunch = Bunch() bunch.copyEmptyBunchTo(empty_bunch) self.bunch = empty_bunch - self.sync_part = bunch.getSyncParticle() + self.sync_part = empty_bunch.getSyncParticle() self.centroid = centroid if self.centroid is None: @@ -272,11 +272,12 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_z"] = [] charge = envelope.charge() + node_positions = self.lattice.getNodePositionsDict() history["position"].append(0.0) - history["rms_x"].append(envelope.rms(0)) - history["rms_y"].append(envelope.rms(2)) - history["rms_z"].append(envelope.rms(4)) + history["rms_x"].append(1000.0 * envelope.rms(0)) + history["rms_y"].append(1000.0 * envelope.rms(2)) + history["rms_z"].append(1000.0 * envelope.rms(4)) for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): @@ -301,7 +302,10 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) envelope.transform(matrix) - history["position"].append(node.getPosition() + node.getLength(part_index)) + position_start, position_stop = node_positions[node] + position = position_start + node.getLength(part_index) + + history["position"].append(position) history["rms_x"].append(1000.0 * envelope.rms(0)) history["rms_y"].append(1000.0 * envelope.rms(2)) history["rms_z"].append(1000.0 * envelope.rms(4)) From 9e5c25c7e8b85e880d6febbbacfebb0add2053af Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:20:51 -0400 Subject: [PATCH 140/183] track kin energy --- py/orbit/envelope/envelope.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index ad93be7b..6539bf70 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -270,6 +270,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_x"] = [] history["rms_y"] = [] history["rms_z"] = [] + history["kin_energy"] = [] charge = envelope.charge() node_positions = self.lattice.getNodePositionsDict() @@ -278,7 +279,8 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_x"].append(1000.0 * envelope.rms(0)) history["rms_y"].append(1000.0 * envelope.rms(2)) history["rms_z"].append(1000.0 * envelope.rms(4)) - + history["kin_energy"].append(envelope.sync_part.kinEnergy()) + for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) @@ -309,6 +311,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_x"].append(1000.0 * envelope.rms(0)) history["rms_y"].append(1000.0 * envelope.rms(2)) history["rms_z"].append(1000.0 * envelope.rms(4)) + history["kin_energy"].append(envelope.sync_part.kinEnergy()) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) From 0885fba331e00abaef93c1d4babe657eb0cf0b0e Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:21:01 -0400 Subject: [PATCH 141/183] Improve sns linac benchmark --- examples/Envelope/sns_linac/diagnostics.py | 58 +++++++++ examples/Envelope/sns_linac/track.py | 131 ++++++++++----------- 2 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 examples/Envelope/sns_linac/diagnostics.py diff --git a/examples/Envelope/sns_linac/diagnostics.py b/examples/Envelope/sns_linac/diagnostics.py new file mode 100644 index 00000000..d546b422 --- /dev/null +++ b/examples/Envelope/sns_linac/diagnostics.py @@ -0,0 +1,58 @@ +import numpy as np + +from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis + + +class BunchMonitor: + def __init__(self) -> None: + self.twiss_calc = BunchTwissAnalysis() + self.position_start = 0.0 + + self.history = {} + self.history["position"] = [] + self.history["rms_x"] = [] + self.history["rms_y"] = [] + self.history["rms_z"] = [] + self.history["kin_energy"] = [] + + def __call__(self, params_dict: dict) -> None: + bunch = params_dict["bunch"] + node = params_dict["node"] + position = params_dict["path_length"] + + if params_dict["old_pos"] == position: + return + if params_dict["old_pos"] + params_dict["pos_step"] > position: + return + params_dict["old_pos"] = position + params_dict["count"] += 1 + + sync_part = bunch.getSyncParticle() + + self.twiss_calc.analyzeBunch(bunch) + + cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(6): + cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation(i, j) + + xrms = 1000.0 * np.sqrt(cov_matrix[0, 0]) + yrms = 1000.0 * np.sqrt(cov_matrix[2, 2]) + zrms = 1000.0 * np.sqrt(cov_matrix[4, 4]) + + message = "" + message += " s={:0.3f}".format(position + self.position_start) + message += " xrms={:0.3f}".format(xrms) + message += " yrms={:0.3f}".format(yrms) + message += " zrms={:0.3f}".format(zrms) + message += " node={}".format(node.getName()) + print(message) + + self.history["position"].append(position + self.position_start) + self.history["rms_x"].append(xrms) + self.history["rms_y"].append(yrms) + self.history["rms_z"].append(zrms) + self.history["kin_energy"].append(sync_part.kinEnergy()) + + diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 662de0e2..5119a615 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -1,5 +1,6 @@ import argparse import math +import os import random import time @@ -28,8 +29,11 @@ from orbit.space_charge.sc3d import setSC3DAccNodes from orbit.space_charge.sc3d import setUniformEllipsesSCAccNodes from orbit.utils.consts import mass_proton +from orbit.utils.consts import mass_electron from orbit.utils.consts import charge_electron -from orbit.utils.consts import speed_of_light + +# local +from diagnostics import BunchMonitor plt.style.use("style.mplstyle") @@ -38,17 +42,42 @@ # -------------------------------------------------------------------------------- parser = argparse.ArgumentParser() -parser.add_argument("--seq", type=str, default="MEBT") +parser.add_argument( + "--seq", + type=str, + default=None, + choices=[ + "MEBT", + "DTL1", + "DTL2", + "DTL3", + "DTL4", + "DTL5", + "DTL6", + "CCL1", + "CCL2", + "CCL3", + "CCL4", + "SCLMed", + "SCLHigh", + "HEBT1", + "HEBT2", + ], +) parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-model", type=str, default="ellipsoid") parser.add_argument("--nparts", type=int, default=10_000) parser.add_argument("--current", type=float, default=0.038) +parser.add_argument("--sc-path-length-min", type=float, default=0.01) args = parser.parse_args() # Setup # -------------------------------------------------------------------------------- +output_dir = "outputs" +os.makedirs(output_dir, exist_ok=True) + random.seed(100) @@ -56,17 +85,20 @@ # -------------------------------------------------------------------------------- kin_energy = 0.0025 # [GeV] -mass = mass_proton -frequency = 402.5e+06 +mass = mass_proton + 2.0 * mass_electron +frequency = 402.5e06 charge = -1.0 intensity = args.current / frequency / (math.fabs(charge) * charge_electron) bunch = Bunch() bunch.mass(mass) -bunch.getSyncParticle().kinEnergy(kin_energy) bunch.macroSize(intensity / args.nparts) bunch.charge(charge) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(kin_energy) +sync_part.time(0.0) + alpha_x, beta_x, eps_x = (-1.962, 0.183, 2.874e-06) alpha_y, beta_y, eps_y = (+1.768, 0.162, 2.874e-06) alpha_z, beta_z, eps_z = (-0.0196, 116.414, 1.651e-08) @@ -136,9 +168,9 @@ for j in range(6): cov_matrix[i, j] = cov_matrix[j, i] = twiss_calc.getCorrelation(i, j) -envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix) +envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix, intensity=intensity) -envelope_tracker = EnvelopeTracker(lattice, space_charge=None) +envelope_tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) histories = {} histories["envelope"] = envelope_tracker.track_history(envelope) @@ -147,10 +179,8 @@ # Track bunch # -------------------------------------------------------------------------------- -lattice.trackDesignBunch(bunch) - if args.sc: - sc_path_length_min = 0.01 + sc_path_length_min = args.sc_path_length_min if args.sc_model == "ellipsoid": n_ellipsoids = 1 sc_calc = SpaceChargeCalcUnifEllipse(n_ellipsoids) @@ -160,54 +190,6 @@ sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) -class BunchMonitor: - def __init__(self) -> None: - self.twiss_calc = BunchTwissAnalysis() - self.position_start = 0.0 - - self.history = {} - self.history["position"] = [] - self.history["rms_x"] = [] - self.history["rms_y"] = [] - self.history["rms_z"] = [] - - def __call__(self, params_dict: dict) -> None: - bunch = params_dict["bunch"] - node = params_dict["node"] - position = params_dict["path_length"] - - if params_dict["old_pos"] == position: - return - if params_dict["old_pos"] + params_dict["pos_step"] > position: - return - params_dict["old_pos"] = position - params_dict["count"] += 1 - - self.twiss_calc.analyzeBunch(bunch) - - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(6): - cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation(i, j) - - xrms = 1000.0 * np.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * np.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * np.sqrt(cov_matrix[4, 4]) - - message = "" - message += " s={:0.3f}".format(position + self.position_start) - message += " xrms={:0.3f}".format(xrms) - message += " yrms={:0.3f}".format(yrms) - message += " zrms={:0.3f}".format(zrms) - message += " node={}".format(node.getName()) - print(message) - - self.history["position"].append(position + self.position_start) - self.history["rms_x"].append(xrms) - self.history["rms_y"].append(yrms) - self.history["rms_z"].append(zrms) - - monitor = BunchMonitor() action_container = AccActionsContainer() @@ -225,31 +207,36 @@ def __call__(self, params_dict: dict) -> None: # -------------------------------------------------------------------------------- -# History +# History: rms for mode in histories: for key in histories[mode]: histories[mode][key] = np.array(histories[mode][key]) +plot_kws = {} +plot_kws["bunch"] = {"color": "black", "ls": "-"} +plot_kws["envelope"] = {"color": "red", "ls": "--"} + fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) -for i, mode in enumerate(["bunch", "envelope"]): +for mode in ["bunch", "envelope"]: history = histories[mode] - color = ["black", "red"][i] - ls = ["-", "--"][i] for ax, key in zip(axs, ["rms_x", "rms_y", "rms_z"]): - ax.plot(history["position"], history[key], color=color, ls=ls, label=mode) - + ax.plot(history["position"], history[key], **plot_kws[mode], label=mode) for ax in axs: ax.legend(loc="lower right") axs[0].set_ylabel("x rms [mm]") axs[1].set_ylabel("y rms [mm]") axs[2].set_ylabel("z rms [mm]") axs[2].set_xlabel("s [m]") -plt.show() - +plt.savefig(os.path.join(output_dir, "fig_history_rms.png")) +plt.close() -# Final coordinates -bunch_coords = collect_bunch(bunch)["coords"] -bunch_cov_matrix = np.cov(bunch_coords.T) - -print(np.round(1000.0 * np.sqrt(np.diag(bunch_cov_matrix)), 2)) -print(np.round(1000.0 * np.sqrt(np.diag(envelope.cov_matrix)), 2)) +# History: energy +fig, ax = plt.subplots(figsize=(5, 3)) +for mode in ["bunch", "envelope"]: + history = histories[mode] + ax.plot(history["position"], history["kin_energy"], **plot_kws[mode], label=mode) +ax.legend(loc="lower right") +ax.set_ylabel("energy [GeV]") +ax.set_xlabel("s [m]") +plt.savefig(os.path.join(output_dir, "fig_history_energy.png")) +plt.close() \ No newline at end of file From 6049381ac8d00b9e45303d19ec78a22b43e93a29 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:27:37 -0400 Subject: [PATCH 142/183] Fix envelope tracker position --- py/orbit/envelope/envelope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 6539bf70..3c34ff59 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -305,7 +305,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: envelope.transform(matrix) position_start, position_stop = node_positions[node] - position = position_start + node.getLength(part_index) + position = position_start + node.getLength(part_index) * (part_index + 1) history["position"].append(position) history["rms_x"].append(1000.0 * envelope.rms(0)) From 763dd487ecf3baea7b525dbabf22c141223ea801 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:27:59 -0400 Subject: [PATCH 143/183] Add show arg --- examples/Envelope/sns_linac/track.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 5119a615..ea0a9aa7 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -69,6 +69,7 @@ parser.add_argument("--nparts", type=int, default=10_000) parser.add_argument("--current", type=float, default=0.038) parser.add_argument("--sc-path-length-min", type=float, default=0.01) +parser.add_argument("--show", type=int, default=0) args = parser.parse_args() @@ -213,8 +214,17 @@ histories[mode][key] = np.array(histories[mode][key]) plot_kws = {} -plot_kws["bunch"] = {"color": "black", "ls": "-"} -plot_kws["envelope"] = {"color": "red", "ls": "--"} +plot_kws["bunch"] = dict( + color="black", + ls="-", +) +plot_kws["envelope"] = dict( + color="red", + # ls="--", + lw=0, + marker=".", + ms=1, +) fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) for mode in ["bunch", "envelope"]: @@ -228,6 +238,8 @@ axs[2].set_ylabel("z rms [mm]") axs[2].set_xlabel("s [m]") plt.savefig(os.path.join(output_dir, "fig_history_rms.png")) +if args.show: + plt.show() plt.close() # History: energy From dd367b99f17c6e7b2c655662d50599b734e99474 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:37:38 -0400 Subject: [PATCH 144/183] SNS linac benchmark working Results are pretty close when space charge is off. Beam size and energy is correct through the linac. Results diverge when space charge is turned on. Agreement is pretty good in the MEBT but starts to get worse in the DTL. --- examples/Envelope/sns_linac/track.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index ea0a9aa7..71758939 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -79,7 +79,7 @@ output_dir = "outputs" os.makedirs(output_dir, exist_ok=True) -random.seed(100) +random.seed(23) # Bunch From 1709bca8666f932b611959574c5642cb24f11aa9 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:54:17 -0400 Subject: [PATCH 145/183] Don't connect dots when plotting rms history --- examples/Envelope/sns_linac/track.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 71758939..87403a04 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -216,11 +216,12 @@ plot_kws = {} plot_kws["bunch"] = dict( color="black", - ls="-", + lw=0, + marker=".", + ms=4, ) plot_kws["envelope"] = dict( color="red", - # ls="--", lw=0, marker=".", ms=1, From 4d34da7b15ef808e335d5ed385ca12325e79dc15 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 03:54:47 -0400 Subject: [PATCH 146/183] Fix space charge factor: make dynamic --- py/orbit/envelope/envelope.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 3c34ff59..e1d9795d 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,6 +1,7 @@ import math import numpy as np +import scipy.constants import scipy.special from orbit.core.bunch import Bunch @@ -20,8 +21,6 @@ BEFORE = AccNode.BEFORE AFTER = AccNode.AFTER -CLASSICAL_PROTON_RADIUS = 1.534697049469832e-18 # [m] - def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: A = np.eye(7) @@ -66,9 +65,12 @@ def __init__( # - tracking bunch particles as test particles empty_bunch = Bunch() bunch.copyEmptyBunchTo(empty_bunch) + self.bunch = empty_bunch self.sync_part = empty_bunch.getSyncParticle() + self.classical_radius = self.bunch.classicalRadius() + self.centroid = centroid if self.centroid is None: self.centroid = np.zeros(6) @@ -80,12 +82,16 @@ def __init__( self.intensity = 0.0 self.set_intensity(intensity) + def set_intensity(self, intensity: float) -> None: self.intensity = intensity - self.sc_factor = ( + + @property + def sc_factor(self) -> float: + return ( 2.0 - * intensity - * CLASSICAL_PROTON_RADIUS + * self.intensity + * self.classical_radius / (self.beta() ** 2 * self.gamma() ** 3) ) From 0b0467acbdda75487ae84f3a18a6c5f3d57980de Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Thu, 25 Jun 2026 04:00:30 -0400 Subject: [PATCH 147/183] Decrease step size for diagnostics --- examples/Envelope/sns_linac/track.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/track.py index 87403a04..37ccbf8c 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/track.py @@ -138,7 +138,7 @@ sequence_names = sequence_names[:stop_index] sns_linac_factory = SNS_LinacLatticeFactory() -sns_linac_factory.setMaxDriftLength(0.01) +sns_linac_factory.setMaxDriftLength(args.sc_path_length_min) lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "sns_linac.xml") for node in lattice.getNodes(): @@ -181,14 +181,13 @@ # -------------------------------------------------------------------------------- if args.sc: - sc_path_length_min = args.sc_path_length_min if args.sc_model == "ellipsoid": n_ellipsoids = 1 sc_calc = SpaceChargeCalcUnifEllipse(n_ellipsoids) - sc_nodes = setUniformEllipsesSCAccNodes(lattice, sc_path_length_min, sc_calc) + sc_nodes = setUniformEllipsesSCAccNodes(lattice, args.sc_path_length_min, sc_calc) if args.sc_model == "3d": sc_calc = SpaceChargeCalc3D(64, 64, 64) - sc_nodes = setSC3DAccNodes(lattice, sc_path_length_min, sc_calc) + sc_nodes = setSC3DAccNodes(lattice, args.sc_path_length_min, sc_calc) monitor = BunchMonitor() @@ -197,7 +196,7 @@ action_container.addAction(monitor, AccActionsContainer.ENTRANCE) action_container.addAction(monitor, AccActionsContainer.EXIT) -params_dict = {"old_pos": -1.0, "count": 0, "pos_step": 0.1} +params_dict = {"old_pos": -1.0, "count": 0, "pos_step": args.sc_path_length_min} lattice.trackBunch(bunch, paramsDict=params_dict, actionContainer=action_container) @@ -218,13 +217,13 @@ color="black", lw=0, marker=".", - ms=4, + ms=1 ) plot_kws["envelope"] = dict( color="red", lw=0, marker=".", - ms=1, + ms=1 ) fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) From 64f4276f3cb8f8517ad118a6c7f8846f33723e83 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 13:05:08 -0400 Subject: [PATCH 148/183] Move all envelope tracking code to orbit.envelope module The functions in orbit.envelope.matrix will calculate the 7 x 7 transfer matrix from common elements and, at the same time, update the synchronous particle time + kinetic energy. There is now a big series of if else statements to determine which of these functions to various AccNodes, including in orbit.teapot and orbit.py_linac modules. This check doesn't have much impact on the runtime. I don't know if this is the best way to do it but it is working. --- examples/Envelope/test_env.py | 9 +- py/orbit/envelope/__init__.py | 1 + py/orbit/envelope/envelope.py | 229 +++++++++++++++++- .../analytic.py => envelope/matrix.py} | 74 +++--- py/orbit/envelope/meson.build | 1 + py/orbit/matrix_lattice/__init__.py | 2 - py/orbit/matrix_lattice/meson.build | 1 - py/orbit/py_linac/lattice/LinacAccNodes.py | 80 +----- py/orbit/py_linac/lattice/LinacRfGapNodes.py | 44 +--- py/orbit/teapot/teapot.py | 106 +------- 10 files changed, 265 insertions(+), 282 deletions(-) rename py/orbit/{matrix_lattice/analytic.py => envelope/matrix.py} (79%) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index bb3b680b..5bc701ba 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -45,13 +45,16 @@ def make_lattice(nodes: list[AccNode]) -> AccLattice: lattice = TEAPOT_Lattice() for node in nodes: lattice.addNode(node) + lattice.initialize() + for node in lattice.getNodes(): try: node.setUsageFringeFieldIN(False) node.setUsageFringeFieldOUT(False) except: pass + return lattice @@ -341,16 +344,16 @@ def test_rf_gap_matrix( coords_out_1 = collect_bunch(bunch_out_1)["coords"] - from orbit.matrix_lattice.analytic import rf_gap_matrix + from orbit.envelope.matrix import track_sync_part_rf_gap bunch_out_2 = Bunch() bunch_in.copyBunchTo(bunch_out_2) - matrix = rf_gap_matrix( + matrix = track_sync_part_rf_gap( + sync_part=bunch_in.getSyncParticle(), frequency=frequency, E0TL=E0TL, phase=phase, - sync_part=bunch_out_2.getSyncParticle(), charge=bunch_in.charge(), ) coords_in = np.column_stack([coords_in, np.ones(coords_in.shape[0])]) diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index 70a5fcbc..428e361b 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1,2 +1,3 @@ from .envelope import Envelope from .envelope import EnvelopeTracker +from . import matrix diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index e1d9795d..5b70a6aa 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -8,8 +8,46 @@ from orbit.core.bunch import SyncParticle from orbit.lattice import AccNode from orbit.lattice import AccLattice -from orbit.matrix_lattice.analytic import convert_matrix_zp_to_dE +from orbit.teapot import ApertureTEAPOT +from orbit.teapot import DriftTEAPOT +from orbit.teapot import BendTEAPOT +from orbit.teapot import KickTEAPOT +from orbit.teapot import MonitorTEAPOT +from orbit.teapot import MultipoleTEAPOT +from orbit.teapot import NodeTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import SolenoidTEAPOT +from orbit.teapot import FringeFieldTEAPOT +from orbit.teapot import BunchWrapTEAPOT +from orbit.teapot import TiltTEAPOT +from orbit.teapot import ContinuousLinearFocusingTEAPOT +from orbit.teapot import TurnCounterTEAPOT + +from orbit.py_linac.lattice import MarkerLinacNode as MarkerLINAC +from orbit.py_linac.lattice import Drift as DriftLINAC +from orbit.py_linac.lattice import Quad as QuadLINAC +from orbit.py_linac.lattice import Bend as BendLINAC +from orbit.py_linac.lattice import DCorrectorH as DCorrectorHLINAC +from orbit.py_linac.lattice import DCorrectorV as DCorrectorVLINAC +from orbit.py_linac.lattice import Solenoid as SolenoidLINAC +from orbit.py_linac.lattice import TiltElement as TiltLINAC +from orbit.py_linac.lattice import FringeField as FringeFieldLINAC +from orbit.py_linac.lattice import BaseRF_Gap as BaseRF_Gap +from orbit.py_linac.lattice import LinacApertureNode as ApertureLINAC + +from .matrix import get_dp_p_coeff +from .matrix import get_zp_coeff +from .matrix import convert_matrix_dp_p_to_dE +from .matrix import convert_matrix_zp_to_dE +from .matrix import track_sync_part_tilt +from .matrix import track_sync_part_kick +from .matrix import track_sync_part_drift +from .matrix import track_sync_part_quad +from .matrix import track_sync_part_bend +from .matrix import track_sync_part_solenoid +from .matrix import track_sync_part_rf_gap +from .matrix import track_sync_part_cf from .utils import gen_dist from .utils import proj_cov_matrix @@ -21,6 +59,17 @@ BEFORE = AccNode.BEFORE AFTER = AccNode.AFTER +IGNORE_NODE_TYPES = [ + NodeTEAPOT, + MonitorTEAPOT, + ApertureTEAPOT, + BunchWrapTEAPOT, + FringeFieldTEAPOT, + MarkerLINAC, + FringeFieldLINAC, + TurnCounterTEAPOT, +] + def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: A = np.eye(7) @@ -32,6 +81,160 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A +def track_sync_part(node: AccNode, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: + node_type = type(node) + if node_type in IGNORE_NODE_TYPES: + return None + + length = node.getLength(index) + nparts = node.getnParts() + + if node_type is DriftTEAPOT: + if length <= 0: + return None + return track_sync_part_drift(sync_part=sync_part, length=length) + + elif node_type is SolenoidTEAPOT: + if length <= 0: + return None + B = node.getParam("B") + if node.waveform: + B *= self.waveform.getStrength() + return track_sync_part_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + + elif node_type is MultipoleTEAPOT: + if length <= 0: + return None + if np.all(np.abs(node.getParam("kls")) == 0): + return track_sync_part_drift(sync_part=sync_part, length=length) + + elif node_type is QuadTEAPOT: + if length <= 0: + return None + kq = node.getParam("kq") + if node.waveform: + kq *= node.waveform.getStrength() + return track_sync_part_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + + elif node_type is BendTEAPOT: + if length <= 0: + return None + theta = node.getParam("theta") / nparts + return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + + elif node_type is KickTEAPOT: + scale = 1.0 + if node.waveform is not None: + scale = node.waveform.getStrength() + + kx = scale * node.getParam("kx") / nparts + ky = scale * node.getParam("ky") / nparts + kE = node.getParam("dE") / nparts + + if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: + return np.matmul( + track_sync_part_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), + track_sync_part_drift(sync_part=sync_part, length=length) + ) + else: + return track_sync_part_drift(sync_part=sync_part, length=length) + + elif node_type is TiltTEAPOT: + angle = node.getTiltAngle() + if angle == 0: + return None + return track_sync_part_tilt(sync_part=sync_part, angle=angle) + + elif node_type is ContinuousLinearFocusingTEAPOT: + if length <= 0: + return None + kq = node.getParam("kq") + if node.waveform: + kq *= node.waveform.getStrength() + return track_sync_part_cf(sync_part=sync_part, length=length, kq=kq) + + elif node_type is DriftLINAC: + if length <= 0: + return None + return track_sync_part_drift(sync_part=sync_part, length=length) + + elif node_type is QuadLINAC: + if length <= 0: + return None + brho = 3.335640952 * sync_part.momentum() / charge + kq = node.getParam("dB/dr") / brho + return track_sync_part_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + + elif node_type is BendLINAC: + if length <= 0: + return None + theta = node.getParam("theta") / nparts + return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + + elif node_type is DCorrectorHLINAC: + length = node.getParam("effLength") / nparts + field = node.getParam("B") + delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() + if delta_xp == 0: + return None + return track_sync_part_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) + + elif node_type is DCorrectorVLINAC: + length = node.getParam("effLength") / nparts + field = node.getParam("B") + delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() + if delta_yp == 0: + return None + return track_sync_part_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) + + elif node_type is SolenoidLINAC: + if length <= 0: + return None + B = node.getParam("B") + return track_sync_part_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + + elif node_type is TiltLINAC: + angle = node.getTiltAngle() + if angle == 0: + return None + return track_sync_part_tilt(sync_part=sync_part, angle=angle) + + elif node_type is BaseRF_Gap: + E0TL = node.getParam("E0TL") + mode_phase = node.getParam("mode") * math.pi + + cavity = node.getRF_Cavity() + frequency = cavity.getFrequency() + phase = cavity.getPhase() + mode_phase + amplitude = cavity.getAmp() + + arrival_time = sync_part.time() + arrival_time_design = cavity.getDesignArrivalTime() + + if node.isFirstRFGap(): + if cavity.isDesignSetUp(): + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) + else: + orbitFinalize("Run `trackDesign` first to initialize cavity phases.") + else: + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase,2.0 * math.pi) + + node.setGapPhase(phase) + + if amplitude == 0.0: + return None + + return track_sync_part_rf_gap( + sync_part=sync_part, + frequency=frequency, + E0TL=(E0TL * amplitude), + phase=phase, + charge=charge, + ) + + raise NotImplementedError(str(node)) + + class Envelope: """Represents beam envelope and centroid. @@ -238,15 +441,17 @@ def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None self.space_charge = space_charge def track(self, envelope: Envelope) -> None: + sync_part = envelope.sync_part charge = envelope.charge() + for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) if self.space_charge: @@ -259,17 +464,18 @@ def track(self, envelope: Envelope) -> None: raise ValueError(f"Invalid space charge model: {self.space_charge}") envelope.transform(matrix) - matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) + matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) envelope.transform(matrix) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) + def track_history(self, envelope: Envelope) -> dict[str, list]: history = {} history["position"] = [] @@ -278,6 +484,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_z"] = [] history["kin_energy"] = [] + sync_part = envelope.sync_part charge = envelope.charge() node_positions = self.lattice.getNodePositionsDict() @@ -289,12 +496,12 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) if self.space_charge: @@ -307,7 +514,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: raise ValueError(f"Invalid space charge model: {self.space_charge}") envelope.transform(matrix) - matrix = node.matrix(sync_part=envelope.sync_part, charge=charge, index=part_index) + matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) envelope.transform(matrix) position_start, position_stop = node_positions[node] @@ -320,11 +527,11 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["kin_energy"].append(envelope.sync_part.kinEnergy()) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = child_node.matrix(sync_part=envelope.sync_part, charge=charge) + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) envelope.transform(matrix) return history \ No newline at end of file diff --git a/py/orbit/matrix_lattice/analytic.py b/py/orbit/envelope/matrix.py similarity index 79% rename from py/orbit/matrix_lattice/analytic.py rename to py/orbit/envelope/matrix.py index 673e201b..30e192b4 100644 --- a/py/orbit/matrix_lattice/analytic.py +++ b/py/orbit/envelope/matrix.py @@ -1,7 +1,3 @@ -"""Functions to compute transfer matrices. - -These functions track the synchronous particle! -""" import math import numpy as np @@ -52,7 +48,27 @@ def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.n return matrix -def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: +def track_sync_part_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: + cos_phi = math.cos(angle) + sin_phi = math.sin(angle) + + M = np.identity(7) + M[0, 0] = M[1, 1] = +cos_phi + M[0, 2] = M[1, 3] = -sin_phi + M[2, 0] = M[3, 1] = +sin_phi + M[2, 2] = M[3, 3] = +cos_phi + return M + + +def track_sync_part_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: + M = np.identity(7) + M[1, -1] = kx + M[3, -1] = ky + M[5, -1] = kE + return M + + +def track_sync_part_drift(sync_part: SyncParticle, length: float) -> np.ndarray: M = np.identity(7) M[0, 1] = length M[2, 3] = length @@ -63,9 +79,9 @@ def drift_matrix(length: float, sync_part: SyncParticle) -> np.ndarray: return M -def quad_matrix(length: float, kq: float, sync_part: SyncParticle, charge: float) -> np.ndarray: +def track_sync_part_quad(sync_part: SyncParticle, length: float, kq: float, charge: float) -> np.ndarray: if abs(kq) == 0 or charge == 0: - return drift_matrix(length=length, sync_part=sync_part) + return track_sync_part_drift(sync_part=sync_part, length=length) sqrt_abs_kq = math.sqrt(abs(kq)) @@ -104,7 +120,7 @@ def quad_matrix(length: float, kq: float, sync_part: SyncParticle, charge: float return M -def bend_matrix(length: float, theta: float, sync_part: SyncParticle, charge: float) -> np.ndarray: +def track_sync_part_bend(sync_part: SyncParticle, length: float, theta: float, charge: float) -> np.ndarray: if length <= 0: return np.identity(7) @@ -129,34 +145,9 @@ def bend_matrix(length: float, theta: float, sync_part: SyncParticle, charge: fl return M -def tilt_matrix(angle: float) -> np.ndarray: - M = np.identity(7) - M[0, 0] = M[1, 1] = +math.cos(angle) - M[0, 2] = M[1, 3] = -math.sin(angle) - M[2, 0] = M[3, 1] = +math.sin(angle) - M[2, 2] = M[3, 3] = +math.cos(angle) - return M - - -def translation_matrix(x: float = 0.0, y: float = 0.0, z: float = 0.0) -> np.ndarray: - M = np.identity(7) - M[0, -1] = x - M[2, -1] = y - M[4, -1] = z - return M - - -def kick_matrix(kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: - M = np.identity(7) - M[1, -1] = kx - M[3, -1] = ky - M[5, -1] = kE - return M - - -def solenoid_matrix(length: float, B: float, sync_part: SyncParticle, charge: float) -> np.ndarray: +def track_sync_part_solenoid(sync_part: SyncParticle, length: float, B: float, charge: float) -> np.ndarray: if B == 0: - return drift_matrix(length=length, sync_part=sync_part) + return track_sync_part_drift(sync_part=sync_part, length=length) phase = B * length @@ -180,25 +171,18 @@ def solenoid_matrix(length: float, B: float, sync_part: SyncParticle, charge: fl M[3, 3] = math.cos(phase) M[4, 5] = length / (sync_part.gamma()**2) - M = np.linalg.multi_dot([np.linalg.inv(V), M, V]) + M = np.linalg.inv(V) @ M @ V M[4, 5] *= get_dp_p_coeff(sync_part) # convert_matrix_dp_p_to_dE(M, sync_part) sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) return M -def cf_matrix(length: float, kq: float, sync_part: SyncParticle) -> np.ndarray: +def track_sync_part_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarray: raise NotImplementedError() -def rf_gap_matrix(frequency: float, E0TL: float, phase: float, sync_part: SyncParticle, charge: float) -> np.ndarray: - """Matrix for thin RF gap. - - Args: - frequency: RF frequency [Hz] - E0TL: maximum energy gain in the gap [GeV]. - phase: RF phase [rad] - """ +def track_sync_part_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, phase: float, charge: float) -> np.ndarray: gamma = sync_part.gamma() beta = sync_part.beta() mass = sync_part.mass() diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 1e9fe35c..31c9033b 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -1,6 +1,7 @@ py_sources = files([ '__init__.py', 'envelope.py', + 'matrix.py', 'utils.py' ]) diff --git a/py/orbit/matrix_lattice/__init__.py b/py/orbit/matrix_lattice/__init__.py index 2000a874..fc7da0fe 100644 --- a/py/orbit/matrix_lattice/__init__.py +++ b/py/orbit/matrix_lattice/__init__.py @@ -4,10 +4,8 @@ ## These classes use orbit::utils::matrix::Matrix C++ wrappers from .MATRIX_Lattice import MATRIX_Lattice from .BaseMATRIX import BaseMATRIX -from . import analytic __all__ = [] __all__.append("MATRIX_Lattice") __all__.append("BaseMATRIX") -__all__.append("analytic") diff --git a/py/orbit/matrix_lattice/meson.build b/py/orbit/matrix_lattice/meson.build index 38ab625a..aca8e60d 100644 --- a/py/orbit/matrix_lattice/meson.build +++ b/py/orbit/matrix_lattice/meson.build @@ -5,7 +5,6 @@ py_sources = files([ 'MATRIX_Lattice.py', '__init__.py', 'BaseMATRIX.py', - 'analytic.py' ]) python.install_sources( diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index 08bf074f..48c37a58 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -7,9 +7,7 @@ """ import os - -import numpy as np -from mesonbuild.backend import nonebackend +import math # import the finalization function from orbit.utils import orbitFinalize @@ -27,15 +25,6 @@ # quad2 - linac quad non-linear part of tracking from orbit.core.linac import linac_tracking -from orbit.core.bunch import SyncParticle - -from orbit.matrix_lattice.analytic import drift_matrix -from orbit.matrix_lattice.analytic import bend_matrix -from orbit.matrix_lattice.analytic import quad_matrix -from orbit.matrix_lattice.analytic import solenoid_matrix -from orbit.matrix_lattice.analytic import kick_matrix -from orbit.matrix_lattice.analytic import tilt_matrix - class BaseLinacNode(AccNodeBunchTracker): """ @@ -53,24 +42,24 @@ def __init__(self, name="none"): self.setParam("pos", 0.0) self.__linacSeqence = None #------------------------------------------------- - # XML data adaptor of this node. + # XML data adaptor of this node. #------------------------------------------------- self.data_adaptor = None # by default we use the TEAPOT tracker module self.tracking_module = TPB - + def setDataAdaptor(self,data_adaptor): """ Sets the XML data adaptor of this node. """ self.data_adaptor = data_adaptor - + def getDataAdaptor(self): """ Returns the XML data adaptor of this node. """ return self.data_adaptor - + def setLinacTracker(self, switch=True): """ This method will switch tracker module to the linac specific traker by default @@ -140,9 +129,6 @@ def trackDesign(paramsDict): self.trackActions(actionContainer, paramsDict) actionContainer.removeAction(trackDesign, AccActionsContainer.BODY) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - raise NotImplementedError(str(self)) - class MarkerLinacNode(BaseLinacNode): """ @@ -154,9 +140,6 @@ def __init__(self, name="none"): BaseLinacNode.__init__(self, name) self.setType("markerLinacNode") - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class LinacNode(BaseLinacNode): """ @@ -324,11 +307,6 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] self.tracking_module.drift(bunch, length) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - if length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class Quad(LinacMagnetNode): """ @@ -527,15 +505,6 @@ def track(self, paramsDict): """ return - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - if length <= 0: - return None - - brho = 3.335640952 * sync_part.momentum() / charge - kq = self.getParam("dB/dr") / brho - return quad_matrix(length=length, kq=kq, sync_part=sync_part, charge=charge) - def getTotalField(self, z): """ Returns the field of the quad. @@ -739,11 +708,6 @@ def track(self, paramsDict): TPB.bend1(bunch, length, theta / 2.0) return - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - theta = self.getParam("theta") / self.getnParts() - return bend_matrix(length=length, theta=theta, sync_part=sync_part, charge=charge) - class DCorrectorH(LinacMagnetNode): """ @@ -789,13 +753,6 @@ def track(self, paramsDict): kick = -field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, kick, 0.0, 0.0) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getParam("effLength") / self.getnParts() - field = self.getParam("B") - delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() - if abs(delta_xp) > 0: - return kick_matrix(delta_xp, 0.0, 0.0) - class DCorrectorV(LinacMagnetNode): """ @@ -841,13 +798,6 @@ def track(self, paramsDict): kick = field * charge * length * 0.299792 / momentum self.tracking_module.kick(bunch, 0, kick, 0.0) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getParam("effLength") / self.getnParts() - field = self.getParam("B") - delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() - if abs(delta_yp) > 0: - return kick_matrix(0.0, delta_yp, 0.0) - class ThickKick(LinacMagnetNode): """ @@ -918,7 +868,6 @@ def track(self, paramsDict): self.tracking_module.kick(bunch, kickX, kickY, 0.0) self.tracking_module.drift(bunch, length / 2.0) - class Solenoid(BaseLinacNode): """ Solenoid TEAPOT based element. @@ -946,15 +895,6 @@ def track(self, paramsDict): useCharge = paramsDict["useCharge"] TPB.soln(bunch, length, B, useCharge) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - if length <= 0: - return None - - B = self.getParam("B") - return solenoid_matrix(length=length, B=B, sync_part=sync_part, charge=charge) - - class AbstractRF_Gap(BaseLinacNode): """ This is an abstarct class for all RF Gap classes. @@ -1073,11 +1013,6 @@ def track(self, paramsDict): bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - angle = self.__angle - if abs(angle) > 0: - return tilt_matrix(angle) - class FringeField(BaseLinacNode): """ @@ -1126,7 +1061,4 @@ def getUsage(self): Returns the boolean flag describing if the fringe field will be used in calculation. """ - return self.__usage - - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None \ No newline at end of file + return self.__usage \ No newline at end of file diff --git a/py/orbit/py_linac/lattice/LinacRfGapNodes.py b/py/orbit/py_linac/lattice/LinacRfGapNodes.py index f5c4cdb8..9a25b054 100644 --- a/py/orbit/py_linac/lattice/LinacRfGapNodes.py +++ b/py/orbit/py_linac/lattice/LinacRfGapNodes.py @@ -6,8 +6,6 @@ import os import math -import numpy as np - # ---- MPI module function and classes from orbit.core.orbit_mpi import mpi_comm, mpi_datatype, MPI_Comm_rank, MPI_Bcast @@ -37,9 +35,6 @@ # quad2 - linac quad non-linear part of tracking from orbit.core.bunch import Bunch -from orbit.core.bunch import SyncParticle - -from orbit.matrix_lattice.analytic import rf_gap_matrix class BaseRF_Gap(AbstractRF_Gap): @@ -335,39 +330,6 @@ def ttf_track_bunch__(self, bunch, frequency, E0L, phase): orbitFinalize(msg) self.cppGapModel.trackBunch(bunch, frequency, E0L, phase, self.polyT, self.polyS, self.polyTp, self.polySp) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - E0TL = self.getParam("E0TL") - mode_phase = self.getParam("mode") * math.pi - - cavity = self.getRF_Cavity() - frequency = cavity.getFrequency() - phase = cavity.getPhase() + mode_phase - amplitude = cavity.getAmp() - - arrival_time = sync_part.time() - arrival_time_design = cavity.getDesignArrivalTime() - - if self.__isFirstGap: - if cavity.isDesignSetUp(): - phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) - else: - orbitFinalize("Run `trackDesign` first to initialize cavity phases.") - else: - phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) - - self.setGapPhase(phase) - - if amplitude == 0.0: - return None - - return rf_gap_matrix( - frequency=frequency, - E0TL=(E0TL * amplitude), - phase=phase, - sync_part=sync_part, - charge=charge, - ) - # ----------------------------------------------------------------------- # @@ -703,7 +665,7 @@ def track(self, paramsDict): # ---- Calculate the phase at the center if index == (nParts - 1): self._phase_gap_func_analysis() - + def _phase_gap_func_analysis(self): """ Performs analysis of the RF gap function phase vs. postion @@ -725,7 +687,7 @@ def _phase_gap_func_analysis(self): phase_gap = self.gap_pos_phase_func.y(pos_ind) self.gap_pos_phase_func.updatePoint(pos_ind,phase_gap - phase_gap_shift) self.setGapPhase(phase_gap_center) - + def getPhaseVsPositionFuncion(self): """ Retuns the RF gap function phase vs. postion. @@ -896,4 +858,4 @@ def __calculate_first_part_phase(self, bunch_in): # ---- undo the last change in the while loop phase_start += 0.8 * (phase_cavity_new - phase_cavity) #print ("debug phase_start=",phase_start*180./math.pi) - return phase_start + return phase_start \ No newline at end of file diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index 658410b1..f3ac45f0 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -23,8 +23,6 @@ from typing import Callable from typing import Union -import numpy as np - from ..lattice import AccLattice from ..lattice import AccNode from ..lattice import AccActionsContainer @@ -36,24 +34,9 @@ from ..parsers.madx_parser import MADX_Parser from ..parsers.madx_parser import MADX_LattElement -# from orbit.matrix_lattice import drift_matrix -# from orbit.matrix_lattice import bend_matrix -# from orbit.matrix_lattice import quad_matrix -# from orbit.matrix_lattice import solenoid_matrix -# from orbit.matrix_lattice import kick_matrix -# from orbit.matrix_lattice import tilt_matrix - -from orbit.matrix_lattice.analytic import drift_matrix -from orbit.matrix_lattice.analytic import bend_matrix -from orbit.matrix_lattice.analytic import quad_matrix -from orbit.matrix_lattice.analytic import solenoid_matrix -from orbit.matrix_lattice.analytic import kick_matrix -from orbit.matrix_lattice.analytic import tilt_matrix - from orbit.core.aperture import Aperture from orbit.core.bunch import Bunch from orbit.core.bunch import BunchTwissAnalysis -from orbit.core.bunch import SyncParticle class TEAPOT_Lattice(AccLattice): @@ -439,9 +422,6 @@ def __init__(self, name: str = "no name") -> None: AccNodeBunchTracker.__init__(self, name) self.setType("base teapot") - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - raise NotImplementedError(str(self)) - class TurnCounterTEAPOT(BaseTEAPOT): def __init__(self, name: str = "TurnCounter") -> None: @@ -460,9 +440,6 @@ def track(self, paramsDict: dict) -> None: turn = bunch.bunchAttrInt("TurnNumber") bunch.bunchAttrInt("TurnNumber", turn + 1) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class NodeTEAPOT(BaseTEAPOT): def __init__(self, name: str = "no name") -> None: @@ -581,9 +558,6 @@ def getUsageFringeFieldOUT(self) -> bool: """ return self.__fringeFieldOUT.getUsage() - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class DriftTEAPOT(NodeTEAPOT): """ @@ -608,11 +582,6 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.drift(bunch, length) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - if length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class ApertureTEAPOT(NodeTEAPOT): """ @@ -653,9 +622,6 @@ def track(self, paramsDict: dict) -> None: lostbunch = paramsDict["lostbunch"] self.aperture.checkBunch(bunch, lostbunch) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class MonitorTEAPOT(NodeTEAPOT): """ @@ -683,9 +649,6 @@ def track(self, paramsDict: dict) -> None: self.addParam("yAvg", self.twiss.getAverage(2)) self.addParam("ypAvg", self.twiss.getAverage(3)) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class BunchWrapTEAPOT(NodeTEAPOT): """ @@ -710,9 +673,6 @@ def track(self, paramsDict: dict) -> None: length = self.getParam("ring_length") TPB.wrapbunch(bunch, length) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class SolenoidTEAPOT(NodeTEAPOT): """ @@ -761,16 +721,6 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - B = self.getParam("B") - if self.waveform is not None: - B *= self.waveform.getStrength() - length = self.getLength(index) - if abs(B) > 0: - return solenoid_matrix(length=length, B=B, sync_part=sync_part, charge=charge) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class MultipoleTEAPOT(NodeTEAPOT): """ @@ -921,14 +871,6 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - if np.all(np.abs(self.getParam("kls")) == 0): - length = self.getLength(index) - return drift_matrix(length=length, sync_part=sync_part) - - # [TO DO] Return matrix for dipole + quadrupole components? - raise NotImplementedError(str(self)) - class QuadTEAPOT(NodeTEAPOT): """ @@ -1092,16 +1034,6 @@ def setWaveform(self, waveform: Any) -> None: """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - kq = self.getParam("kq") - if self.waveform: - kq *= self.waveform.getStrength() - if abs(kq) > 0: - return quad_matrix(length=length, kq=kq, sync_part=sync_part, charge=charge) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class BendTEAPOT(NodeTEAPOT): """ @@ -1316,14 +1248,6 @@ def track(self, paramsDict: dict) -> None: TPB.bend1(bunch, length, theta / 2.0) return - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - theta = self.getParam("theta") / self.getnParts() - if abs(theta) > 0: - return bend_matrix(length=length, theta=theta, sync_part=sync_part, charge=charge) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class RingRFTEAPOT(NodeTEAPOT): """ @@ -1490,26 +1414,6 @@ def setWaveform(self, waveform): """ self.waveform = waveform - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - length = self.getLength(index) - nparts = self.getnParts() - - scale = 1.0 - if self.waveform is not None: - scale = self.waveform.getStrength() - - kx = scale * self.getParam("kx") / nparts - ky = scale * self.getParam("ky") / nparts - kE = self.getParam("dE") / nparts - - if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: - return np.matmul( - kick_matrix(kx=kx, ky=ky, kE=kE), - drift_matrix(length=length, sync_part=sync_part) - ) - elif length > 0: - return drift_matrix(length=length, sync_part=sync_part) - class TiltTEAPOT(BaseTEAPOT): """ @@ -1545,11 +1449,6 @@ def track(self, paramsDict: dict) -> None: bunch = paramsDict["bunch"] TPB.rotatexy(bunch, self.__angle) - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - tilt_angle = self.getTiltAngle() - if abs(tilt_angle) > 0: - return tilt_matrix(self.getTiltAngle()) - class FringeFieldTEAPOT(BaseTEAPOT): """ @@ -1605,9 +1504,6 @@ def getUsage(self) -> bool: """ return self.__usage - def matrix(self, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: - return None - class ContinuousLinearFocusingTEAPOT(NodeTEAPOT): def __init__( @@ -1698,4 +1594,4 @@ def track(self, paramsDict): return def setWaveform(self, waveform): - self.waveform = waveform + self.waveform = waveform \ No newline at end of file From add4f9ffd4edb6f2c725d232d5946754db4c0b38 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 14:07:48 -0400 Subject: [PATCH 149/183] Rename examples --- examples/Envelope/sns_linac/{ => inputs}/sns_linac.xml | 0 .../Envelope/sns_linac/{track.py => test_sns_linac.py} | 4 ++-- examples/Envelope/{ => sns_ring}/inputs/sns_ring.lat | 0 examples/Envelope/sns_ring/style.mplstyle | 8 ++++++++ .../{test_env_sns_ring.py => sns_ring/test_sns_ring.py} | 2 ++ .../test_sns_ring_speed.py} | 2 ++ 6 files changed, 14 insertions(+), 2 deletions(-) rename examples/Envelope/sns_linac/{ => inputs}/sns_linac.xml (100%) rename examples/Envelope/sns_linac/{track.py => test_sns_linac.py} (99%) rename examples/Envelope/{ => sns_ring}/inputs/sns_ring.lat (100%) create mode 100644 examples/Envelope/sns_ring/style.mplstyle rename examples/Envelope/{test_env_sns_ring.py => sns_ring/test_sns_ring.py} (99%) rename examples/Envelope/{test_env_sns_ring_speed.py => sns_ring/test_sns_ring_speed.py} (99%) diff --git a/examples/Envelope/sns_linac/sns_linac.xml b/examples/Envelope/sns_linac/inputs/sns_linac.xml similarity index 100% rename from examples/Envelope/sns_linac/sns_linac.xml rename to examples/Envelope/sns_linac/inputs/sns_linac.xml diff --git a/examples/Envelope/sns_linac/track.py b/examples/Envelope/sns_linac/test_sns_linac.py similarity index 99% rename from examples/Envelope/sns_linac/track.py rename to examples/Envelope/sns_linac/test_sns_linac.py index 37ccbf8c..240ec861 100755 --- a/examples/Envelope/sns_linac/track.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -139,7 +139,7 @@ sns_linac_factory = SNS_LinacLatticeFactory() sns_linac_factory.setMaxDriftLength(args.sc_path_length_min) -lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "sns_linac.xml") +lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "inputs/sns_linac.xml") for node in lattice.getNodes(): try: @@ -251,4 +251,4 @@ ax.set_ylabel("energy [GeV]") ax.set_xlabel("s [m]") plt.savefig(os.path.join(output_dir, "fig_history_energy.png")) -plt.close() \ No newline at end of file +plt.close() diff --git a/examples/Envelope/inputs/sns_ring.lat b/examples/Envelope/sns_ring/inputs/sns_ring.lat similarity index 100% rename from examples/Envelope/inputs/sns_ring.lat rename to examples/Envelope/sns_ring/inputs/sns_ring.lat diff --git a/examples/Envelope/sns_ring/style.mplstyle b/examples/Envelope/sns_ring/style.mplstyle new file mode 100644 index 00000000..71f36605 --- /dev/null +++ b/examples/Envelope/sns_ring/style.mplstyle @@ -0,0 +1,8 @@ +axes.linewidth: 1.25 +axes.titlesize: "medium" +image.cmap: "Greys" +figure.constrained_layout.use: True +savefig.dpi: 300 +savefig.format: "png" +xtick.minor.visible: True +ytick.minor.visible: True diff --git a/examples/Envelope/test_env_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py similarity index 99% rename from examples/Envelope/test_env_sns_ring.py rename to examples/Envelope/sns_ring/test_sns_ring.py index 5797bd5f..45a93540 100644 --- a/examples/Envelope/test_env_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -6,6 +6,7 @@ import os import pathlib import time +import sys import numpy as np import matplotlib.pyplot as plt @@ -23,6 +24,7 @@ from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.utils.consts import mass_proton +sys.path.append("..") from plot import plot_rms_ellipse from plot import plot_corner from utils import gen_dist diff --git a/examples/Envelope/test_env_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py similarity index 99% rename from examples/Envelope/test_env_sns_ring_speed.py rename to examples/Envelope/sns_ring/test_sns_ring_speed.py index 3d89838c..75caa274 100644 --- a/examples/Envelope/test_env_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -3,6 +3,7 @@ import time import cProfile import pstats +import sys import numpy as np from tqdm import trange @@ -17,6 +18,7 @@ from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.utils.consts import mass_proton +sys.path.append("..") from utils import gen_dist From 75adc285a2e2d823f4b1c1ddcabb8bc13f93a61b Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 14:08:11 -0400 Subject: [PATCH 150/183] Format tests --- examples/Envelope/sns_linac/diagnostics.py | 2 -- examples/Envelope/sns_linac/test_sns_linac.py | 14 ++------------ examples/Envelope/sns_ring/test_sns_ring_speed.py | 6 ++---- examples/Envelope/test_env.py | 1 - examples/Envelope/test_env_2d_fodo_speed.py | 7 ++----- 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/examples/Envelope/sns_linac/diagnostics.py b/examples/Envelope/sns_linac/diagnostics.py index d546b422..7edfebdd 100644 --- a/examples/Envelope/sns_linac/diagnostics.py +++ b/examples/Envelope/sns_linac/diagnostics.py @@ -54,5 +54,3 @@ def __call__(self, params_dict: dict) -> None: self.history["rms_y"].append(yrms) self.history["rms_z"].append(zrms) self.history["kin_energy"].append(sync_part.kinEnergy()) - - diff --git a/examples/Envelope/sns_linac/test_sns_linac.py b/examples/Envelope/sns_linac/test_sns_linac.py index 240ec861..ccbbf696 100755 --- a/examples/Envelope/sns_linac/test_sns_linac.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -213,18 +213,8 @@ histories[mode][key] = np.array(histories[mode][key]) plot_kws = {} -plot_kws["bunch"] = dict( - color="black", - lw=0, - marker=".", - ms=1 -) -plot_kws["envelope"] = dict( - color="red", - lw=0, - marker=".", - ms=1 -) +plot_kws["bunch"] = dict(color="black", lw=0, marker=".", ms=1) +plot_kws["envelope"] = dict(color="red", lw=0, marker=".", ms=1) fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) for mode in ["bunch", "envelope"]: diff --git a/examples/Envelope/sns_ring/test_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py index 75caa274..f5c50598 100644 --- a/examples/Envelope/sns_ring/test_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -1,4 +1,5 @@ """Test envelope tracker speed in SNS ring.""" + import argparse import time import cProfile @@ -110,9 +111,7 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" -) +bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) for i in range(bunch_coords.shape[0]): @@ -143,4 +142,3 @@ profiler_stats = pstats.Stats(profiler) profiler_stats.sort_stats(pstats.SortKey.TIME) profiler_stats.print_stats(10) - diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 5bc701ba..86422e97 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -386,4 +386,3 @@ def test_sc_3d_cold_expansion(): test_solenoid_linac(kin_energy=kin_energy) test_rf_gap_matrix(kin_energy=kin_energy) - diff --git a/examples/Envelope/test_env_2d_fodo_speed.py b/examples/Envelope/test_env_2d_fodo_speed.py index a2d88d87..541c2ee7 100644 --- a/examples/Envelope/test_env_2d_fodo_speed.py +++ b/examples/Envelope/test_env_2d_fodo_speed.py @@ -1,4 +1,5 @@ """Test envelope tracker speed in SNS ring.""" + import argparse import time import cProfile @@ -112,9 +113,7 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" -) +bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) for i in range(bunch_coords.shape[0]): @@ -145,5 +144,3 @@ profiler_stats = pstats.Stats(profiler) profiler_stats.sort_stats(pstats.SortKey.TIME) profiler_stats.print_stats(10) - - From 9930326dcd8598be1986471f93847a4da78801b5 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 16:35:03 -0400 Subject: [PATCH 151/183] Undo old changes to modules outside orbit.envelope The only changes are to make some nodes importable --- py/orbit/matrix_lattice/__init__.py | 1 - py/orbit/matrix_lattice/meson.build | 2 +- py/orbit/py_linac/lattice/LinacAccNodes.py | 10 +- py/orbit/py_linac/lattice/LinacRfGapNodes.py | 6 +- py/orbit/py_linac/lattice/__init__.py | 10 +- .../.ipynb_checkpoints/sc1DNode-checkpoint.py | 304 ------------------ py/orbit/teapot/__init__.py | 9 +- py/orbit/teapot/teapot.py | 2 +- 8 files changed, 17 insertions(+), 327 deletions(-) delete mode 100644 py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py diff --git a/py/orbit/matrix_lattice/__init__.py b/py/orbit/matrix_lattice/__init__.py index fc7da0fe..84598241 100644 --- a/py/orbit/matrix_lattice/__init__.py +++ b/py/orbit/matrix_lattice/__init__.py @@ -5,7 +5,6 @@ from .MATRIX_Lattice import MATRIX_Lattice from .BaseMATRIX import BaseMATRIX - __all__ = [] __all__.append("MATRIX_Lattice") __all__.append("BaseMATRIX") diff --git a/py/orbit/matrix_lattice/meson.build b/py/orbit/matrix_lattice/meson.build index aca8e60d..ed29e83e 100644 --- a/py/orbit/matrix_lattice/meson.build +++ b/py/orbit/matrix_lattice/meson.build @@ -4,7 +4,7 @@ py_sources = files([ 'MATRIX_Lattice.py', '__init__.py', - 'BaseMATRIX.py', + 'BaseMATRIX.py' ]) python.install_sources( diff --git a/py/orbit/py_linac/lattice/LinacAccNodes.py b/py/orbit/py_linac/lattice/LinacAccNodes.py index 48c37a58..5bc5ec16 100755 --- a/py/orbit/py_linac/lattice/LinacAccNodes.py +++ b/py/orbit/py_linac/lattice/LinacAccNodes.py @@ -42,24 +42,24 @@ def __init__(self, name="none"): self.setParam("pos", 0.0) self.__linacSeqence = None #------------------------------------------------- - # XML data adaptor of this node. + # XML data adaptor of this node. #------------------------------------------------- self.data_adaptor = None # by default we use the TEAPOT tracker module self.tracking_module = TPB - + def setDataAdaptor(self,data_adaptor): """ Sets the XML data adaptor of this node. """ self.data_adaptor = data_adaptor - + def getDataAdaptor(self): """ Returns the XML data adaptor of this node. """ return self.data_adaptor - + def setLinacTracker(self, switch=True): """ This method will switch tracker module to the linac specific traker by default @@ -1061,4 +1061,4 @@ def getUsage(self): Returns the boolean flag describing if the fringe field will be used in calculation. """ - return self.__usage \ No newline at end of file + return self.__usage diff --git a/py/orbit/py_linac/lattice/LinacRfGapNodes.py b/py/orbit/py_linac/lattice/LinacRfGapNodes.py index 9a25b054..30b1fbe8 100644 --- a/py/orbit/py_linac/lattice/LinacRfGapNodes.py +++ b/py/orbit/py_linac/lattice/LinacRfGapNodes.py @@ -665,7 +665,7 @@ def track(self, paramsDict): # ---- Calculate the phase at the center if index == (nParts - 1): self._phase_gap_func_analysis() - + def _phase_gap_func_analysis(self): """ Performs analysis of the RF gap function phase vs. postion @@ -687,7 +687,7 @@ def _phase_gap_func_analysis(self): phase_gap = self.gap_pos_phase_func.y(pos_ind) self.gap_pos_phase_func.updatePoint(pos_ind,phase_gap - phase_gap_shift) self.setGapPhase(phase_gap_center) - + def getPhaseVsPositionFuncion(self): """ Retuns the RF gap function phase vs. postion. @@ -858,4 +858,4 @@ def __calculate_first_part_phase(self, bunch_in): # ---- undo the last change in the while loop phase_start += 0.8 * (phase_cavity_new - phase_cavity) #print ("debug phase_start=",phase_start*180./math.pi) - return phase_start \ No newline at end of file + return phase_start diff --git a/py/orbit/py_linac/lattice/__init__.py b/py/orbit/py_linac/lattice/__init__.py index 79da382e..eb9ed985 100644 --- a/py/orbit/py_linac/lattice/__init__.py +++ b/py/orbit/py_linac/lattice/__init__.py @@ -13,6 +13,8 @@ from orbit.py_linac.lattice.LinacAccNodes import Solenoid from orbit.py_linac.lattice.LinacAccNodes import DCorrectorH, DCorrectorV from orbit.py_linac.lattice.LinacAccNodes import ThickKick +from orbit.py_linac.lattice.LinacAccNodes import TiltElement +from orbit.py_linac.lattice.LinacAccNodes import FringeField from orbit.py_linac.lattice.LinacRfGapNodes import BaseRF_Gap, AxisFieldRF_Gap, RF_AxisFieldsStore @@ -38,9 +40,6 @@ from orbit.py_linac.lattice.LinacTransportMatrixGenNodes import LinacTrMatricesController from orbit.py_linac.lattice.LinacDiagnosticsNodes import LinacBPM -from orbit.py_linac.lattice.LinacAccNodes import TiltElement -from orbit.py_linac.lattice.LinacAccNodes import FringeField - __all__ = [] __all__.append("LinacAccLattice") @@ -59,7 +58,6 @@ __all__.append("Bend") __all__.append("Solenoid") - __all__.append("LinacApertureNode") __all__.append("CircleLinacApertureNode") __all__.append("EllipseLinacApertureNode") @@ -67,7 +65,6 @@ __all__.append("LinacPhaseApertureNode") __all__.append("LinacEnergyApertureNode") - __all__.append("RF_Cavity") __all__.append("Sequence") @@ -94,5 +91,6 @@ __all__.append("LinacTrMatricesController") __all__.append("LinacBPM") -__all__.append("TiltElement") + __all__.append("FringeField") +__all__.append("TiltElement") diff --git a/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py b/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py deleted file mode 100644 index 0bf968e1..00000000 --- a/py/orbit/space_charge/sc1d/.ipynb_checkpoints/sc1DNode-checkpoint.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -Module. Includes classes for 1D longidutinal space charge accelerator nodes. -""" - -import sys -import os -import math - -from orbit.core.bunch import Bunch -from orbit.core.spacecharge import LSpaceChargeCalc -from orbit.lattice import AccLattice -from orbit.lattice import AccNode -from orbit.lattice import AccActionsContainer -from orbit.lattice import AccNodeBunchTracker -from orbit.utils import consts -from orbit.utils import orbitFinalize -from orbit.teapot import DriftTEAPOT - - -class SC1D_AccNode(DriftTEAPOT): - """Longitudinal space charge node.""" - - def __init__( - self, - b_a: float, - phase_length: float, - nmacros_min: float, - use_sc: float, - nbins: float, - nmodes: int = None, - use_grad: bool = False, - name="long sc node", - ) -> None: - """ - Constructor. Creates the SC1D-teapot element. - """ - DriftTEAPOT.__init__(self, name) - self.lspacecharge = LSpaceChargeCalc(b_a, phase_length, nmacros_min, use_sc, nbins) - self.setNumModes(nmodes) - # self.setUseGrad(use_grad) - self.setType("long sc node") - self.setLength(0.0) - - def setUseGrad(self, use_grad: bool) -> None: - """Sets whether to use gradient-based solver instead of impedance solver.""" - self.lspacecharge.setUseGrad(int(use_grad)) - - def setNumModes(self, n: int) -> None: - """Sets number of FFT modes used to calculate energy kick.""" - self.lspacecharge.setNumModes(n) - - def trackBunch(self, bunch: Bunch) -> None: - """ - The SC1D-teapot class implementation of the - AccNodeBunchTracker class trackBunch(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - self.lspacecharge.trackBunch(bunch) # track method goes here - - def track(self, params_dict: dict) -> None: - """ - The SC1D-teapot class implementation of the - AccNodeBunchTracker class track(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - bunch = params_dict["bunch"] - self.lspacecharge.trackBunch(bunch) - - def assignImpedance(self, py_cmplx_arr: list[float]) -> None: - self.lspacecharge.assignImpedance(py_cmplx_arr) - - -class FreqDep_SC1D_AccNode(DriftTEAPOT): - """Longitudinal space charge node (frequency-dependent).""" - - def __init__( - self, - b_a: float, - phase_length: float, - nmacros_min: int, - use_sc: int, - nbins: int, - bunch: Bunch, - imp_dict: dict, - name: str = "freq. dep. long sc node", - ) -> None: - """ - Constructor. Creates the FreqDep_SC1D-teapot element. - """ - DriftTEAPOT.__init__(self, name) - self.lspacecharge = LSpaceChargeCalc( - b_a, phase_length, nmacros_min, use_sc, nbins - ) - self.setType("freq. dep. long sc node") - self.setLength(0.0) - self.phase_length = phase_length - self.nbins = nbins - self.localDict = imp_dict - self.freq_tuple = self.localDict["freqs"] - self.freq_range = len(self.freq_tuple) - 1 - self.z_tuple = self.localDict["z_imp"] - self.c = consts.speed_of_light - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins // 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - - def trackBunch(self, bunch: Bunch) -> None: - """ - The FreqDep_SC1D-teapot class implementation of - the AccNodeBunchTracker class track(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins // 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - self.lspacecharge.trackBunch(bunch) - - def track(self, params_dict: dict) -> None: - """ - The FreqDep_SC1D-teapot class implementation of - the AccNodeBunchTracker class track(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - bunch = params_dict["bunch"] - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins // 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = interp(freq_mode, self.freq_range, self.freq_tuple, self.z_tuple) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - self.lspacecharge.trackBunch(bunch) - - -class BetFreqDep_SC1D_AccNode(DriftTEAPOT): - """Longitudinal space charge node (frequency- and velocity-dependent).""" - - def __init__( - self, - b_a: float, - phase_length: float, - nmacros_min: float, - use_sc: int, - nbins: int, - bunch: Bunch, - imp_dict: dict, - name: str = "freq. dep. long sc node", - ) -> None: - """ - Constructor. Creates the BetFreqDep_SC1D-teapot element. - """ - DriftTEAPOT.__init__(self, name) - self.lspacecharge = LSpaceChargeCalc( - b_a, phase_length, nmacros_min, use_sc, nbins - ) - self.setType("beta-freq. dep. long sc node") - self.setLength(0.0) - self.phase_length = phase_length - self.nbins = nbins - self.localDict = imp_dict - self.bet_tuple = self.localDict["betas"] - self.bet_range = len(self.bet_tuple) - 1 - self.freq_tuple = self.localDict["freqs"] - self.freq_range = len(self.freq_tuple) - 1 - self.z_bf = self.localDict["z_imp"] - self.c = consts.speed_of_light - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins / 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = bilinterp( - BetaRel, - freq_mode, - self.bet_range, - self.freq_range, - self.bet_tuple, - self.freq_tuple, - self.z_bf, - ) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - - def trackBunch(self, bunch: Bunch) -> None: - """ - The BetFreqDep_SC1D-teapot class implementation of - the AccNodeBunchTracker class track(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins / 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = bilinterp( - BetaRel, - freq_mode, - self.bet_range, - self.freq_range, - self.bet_tuple, - self.freq_tuple, - self.z_bf, - ) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - self.lspacecharge.trackBunch(bunch) - - def track(self, params_dict: dict) -> None: - """ - The BetFreqDep_SC1D-teapot class implementation of - the AccNodeBunchTracker class track(probe) method. - """ - length = self.getLength(self.getActivePartIndex()) - bunch = params_dict["bunch"] - BetaRel = bunch.getSyncParticle().beta() - Freq0 = (BetaRel * self.c) / self.phase_length - Z = [] - for n in range(self.nbins / 2 - 1): - freq_mode = Freq0 * (n + 1) - z_mode = bilinterp( - BetaRel, - freq_mode, - self.bet_range, - self.freq_range, - self.bet_tuple, - self.freq_tuple, - self.z_bf, - ) - Z.append(z_mode) - self.lspacecharge.assignImpedance(Z) - self.lspacecharge.trackBunch(bunch) - - -def interp(x: float, n_tuple: int, x_tuple: list[float], y_tuple: list[float]) -> float: - """ - Linear interpolation: Given n-tuple + 1 points, - x_tuple and y_tuple, routine finds y = y_tuple - at x in x_tuple. Assumes x_tuple is increasing array. - """ - if x < x_tuple[0]: - y = y_tuple[0] - return y - if x > x_tuple[n_tuple]: - y = y_tuple[n_tuple] - return y - dxp = x - x_tuple[0] - for n in range(n_tuple): - dxm = dxp - dxp = x - x_tuple[n + 1] - dxmp = dxm * dxp - if dxmp <= 0: - break - y = (-dxp * y_tuple[n] + dxm * y_tuple[n + 1]) / (dxm - dxp) - return y - - -def bilinterp( - x: float, - y: float, - nx_tuple: int, - ny_tuple: int, - x_tuple: list[float], - y_tuple: list[float], - fxy: list[list[float]], -) -> float: - """ - Bilinear interpolation: Given nx-tuple + 1 x-points, - ny-tuple + 1 y-points, x_tuple and y_tuple, - routine finds f(x, y) = fxy at (x, y) in (x_tuple, y_tuple). - Assumes x_tuple and y_tuple are increasing arrays. - """ - f_tuple = [] - if x < x_tuple[0]: - for ny in range(ny_tuple + 1): - vf = fxy[0][ny] - f_tuple.append(vf) - elif x > x_tuple[nx_tuple]: - for ny in range(ny_tuple + 1): - vf = fxy[x_tuple][ny] - f_tuple.append(vf) - else: - dxp = x - x_tuple[0] - for nx in range(nx_tuple): - dxm = dxp - dxp = x - x_tuple[nx + 1] - dxmp = dxm * dxp - if dxmp <= 0: - break - for ny in range(ny_tuple + 1): - vf = (-dxp * fxy[nx][ny] + dxm * fxy[nx + 1][ny]) / (dxm - dxp) - f_tuple.append(vf) - f = interp(y, ny_tuple, y_tuple, f_tuple) - return f diff --git a/py/orbit/teapot/__init__.py b/py/orbit/teapot/__init__.py index ffc7bae9..9f62adfc 100644 --- a/py/orbit/teapot/__init__.py +++ b/py/orbit/teapot/__init__.py @@ -17,13 +17,10 @@ from .teapot import SolenoidTEAPOT from .teapot import TiltTEAPOT from .teapot import NodeTEAPOT - -from .teapot import TurnCounterTEAPOT +from .teapot import ContinuousLinearFocusingTEAPOT from .teapot import ApertureTEAPOT from .teapot import MonitorTEAPOT -from .teapot import BunchWrapTEAPOT - -from .teapot import ContinuousLinearFocusingTEAPOT +from .teapot import TurnCounterTEAPOT from .teapot import TPB @@ -47,6 +44,6 @@ __all__.append("ContinuousLinearFocusingTEAPOT") __all__.append("TPB") __all__.append("TEAPOT_MATRIX_Lattice") -__all__.append("TurnCounterTEAPOT") __all__.append("ApertureTEAPOT") __all__.append("MonitorTEAPOT") +__all__.append("TurnCounterTEAPOT") \ No newline at end of file diff --git a/py/orbit/teapot/teapot.py b/py/orbit/teapot/teapot.py index f3ac45f0..63a09b27 100644 --- a/py/orbit/teapot/teapot.py +++ b/py/orbit/teapot/teapot.py @@ -1594,4 +1594,4 @@ def track(self, paramsDict): return def setWaveform(self, waveform): - self.waveform = waveform \ No newline at end of file + self.waveform = waveform From 6c405e2375723434aeb0944ed13a792ed4a62d20 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 16:52:06 -0400 Subject: [PATCH 152/183] Use covariance matrix from initial bunch in sns ring example This ensures envelope and bunch have exact same initial covariance matrix. --- examples/Envelope/sns_ring/test_sns_ring.py | 68 ++++++++++----------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index 45a93540..f2d84d94 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -43,7 +43,7 @@ def main(args: argparse.Namespace) -> None: output_dir = os.path.join("outputs", path.stem) os.makedirs(output_dir, exist_ok=True) - # Create lattice + # Lattice # ------------------------------------------------------------------------------ lattice = TEAPOT_Lattice() @@ -67,16 +67,14 @@ def main(args: argparse.Namespace) -> None: if node.getLength() > max_length: node.setnParts(1 + int(node.getLength() / max_length)) - # Create envelope + # Bunch # ------------------------------------------------------------------------------ - # Create bunch bunch = Bunch() bunch.mass(mass_proton) sync_part = bunch.getSyncParticle() sync_part.kinEnergy(args.kin_energy) - # Find periodic lattice parameters matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) matrix_lattice_params = matrix_lattice.getRingParametersDict() alpha_x = matrix_lattice_params["alpha x"] @@ -86,9 +84,6 @@ def main(args: argparse.Namespace) -> None: eps_x = 25.0e-06 eps_y = eps_x - print(matrix_lattice_params) - - # Generate covariance matrix cov_matrix = np.zeros((6, 6)) cov_matrix[0, 0] = eps_x * beta_x cov_matrix[2, 2] = eps_y * beta_y @@ -99,43 +94,56 @@ def main(args: argparse.Namespace) -> None: cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 cov_matrix[5, 5] = 0.0 - # Tilt if args.tilt: rot_matrix = np.identity(6) rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) - # Mismatch - cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 - cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 - cov_matrix_init = np.copy(cov_matrix) + if args.mismatch_x or args.mismatch_y: + cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 + cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 - # Offset - centroid_init = np.zeros(6) - centroid_init[0] += args.offset_x - centroid_init[2] += args.offset_y + centroid = np.zeros(6) + centroid[0] += args.offset_x + centroid[2] += args.offset_y - # Create envelope - envelope = Envelope( - bunch=bunch, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, + rng = np.random.default_rng() + bunch_coords = np.zeros((args.nparts, 6)) + bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist ) + bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) + bunch_coords += centroid[None, :6] + + for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + + # Use covariance matrix from initial bunch, which is slightly + # different from the one used to generate the bunch due to + # finite statistics. + cov_matrix_init = np.cov(bunch_coords, rowvar=False) + centroid_init = np.mean(bunch_coords, axis=0) # Track envelope # ------------------------------------------------------------------------------ print("TRACK ENVELOPE") - tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) + envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, + ) + + envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} start_time = time.time() for turn in range(args.turns + 1): if turn > 0: - tracker.track(envelope) + envelope_tracker.track(envelope) cov_matrix = envelope.cov_matrix centroid = envelope.centroid @@ -167,18 +175,6 @@ def main(args: argparse.Namespace) -> None: print("TRACK BUNCH") - rng = np.random.default_rng() - - bunch_coords = np.zeros((args.nparts, 6)) - bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist - ) - bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) - bunch_coords += centroid_init[None, :6] - - for i in range(bunch_coords.shape[0]): - bunch.addParticle(*bunch_coords[i]) - if args.sc: sc_calc = SpaceChargeCalc2p5D(64, 64, 1) sc_path_length_min = 1.00e-06 From 41c0de57e7e5782e86238ec9e2b47c90fc849cf7 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 17:11:46 -0400 Subject: [PATCH 153/183] Documentation --- py/orbit/envelope/envelope.py | 44 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 5b70a6aa..633ae174 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -81,7 +81,30 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A -def track_sync_part(node: AccNode, sync_part: SyncParticle, charge: float, index: int = -1) -> np.ndarray: +def track_sync_part( + node: AccNode, + sync_part: SyncParticle, + charge: float, + index: int = -1, +) -> np.ndarray | None: + """Calculate transfer matrix and update synchronous particle. + + This function maps various accelerator nodes to 7 x 7 transfer matrices + for envelope tracking. For non-accelerating, finite-length nodes, the + synchronous particle time is updated as in a drift. Accelerating nodes + such as RF gaps will update the synchronous particle energy. + + Args: + node: The accelerator node. + sync_part: Synchronous particle. + charge: Particle charge. (The charge is currently an attribute of the + bunch, not the synchronous particle.) + index: Node part index. An index of -1 will return the transfer matrix + for the entire node. + Returns: + 7 x 7 transfer matrix or None. If None, the node can be ignored during + envelope tracking. + """ node_type = type(node) if node_type in IGNORE_NODE_TYPES: return None @@ -236,23 +259,13 @@ def track_sync_part(node: AccNode, sync_part: SyncParticle, charge: float, index class Envelope: - """Represents beam envelope and centroid. + """Represents beam envelope/centroid. Attributes: - moment_matrix: 7 x 7 matrix containing first and second moments. - Define the phase space vector X = [x, x', y, y', z, dE]^T and - augmented vector Y = [x, x', y, y', z, dE, 1]. - - Let X evolve according to X -> MX + U, where M is a 6 x 6 transfer matrix - and U is 6 x 1 "driving" vector. The augmented vector Y evolves according - to Y -> NY, where N = [[M, U], [0, 1]] is a 7 x 7 matrix. - - Let S = = [[, ], [, 1]] = [[R, C], [C^T, 1]]. Here - R = is the matrix of second moments, or "autocorrelation" matrix, - and C = is the mean/centroid vector. (To get the covariance matrix: - <(X - C)(X - C)^T> = - ^T = R - CC^T.) S evolves according - to S -> N S N^T. bunch: Bunch containing synchronous particle and (optionally) test particles. + cov_matrix: 6 x 6 covariance matrix + centroid: 6 x 1 centroid vector. + intensity: Total number of particles. """ def __init__( @@ -477,6 +490,7 @@ def track(self, envelope: Envelope) -> None: def track_history(self, envelope: Envelope) -> dict[str, list]: + """Same as track but returns parameters vs. position in lattice.""" history = {} history["position"] = [] history["rms_x"] = [] From 10d329a3e8cbde65ab69f9d5b5f28ed2f6356073 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 17:35:53 -0400 Subject: [PATCH 154/183] Change bend nparts to match teapot --- py/orbit/envelope/envelope.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 633ae174..3761e941 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -142,7 +142,9 @@ def track_sync_part( elif node_type is BendTEAPOT: if length <= 0: return None - theta = node.getParam("theta") / nparts + theta = node.getParam("theta") / (nparts - 1) + if index == 0 or index == nparts - 1: + theta *= 0.5 return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) elif node_type is KickTEAPOT: @@ -150,14 +152,15 @@ def track_sync_part( if node.waveform is not None: scale = node.waveform.getStrength() - kx = scale * node.getParam("kx") / nparts - ky = scale * node.getParam("ky") / nparts - kE = node.getParam("dE") / nparts + scale /= (nparts - 1) + kx = scale * node.getParam("kx") + ky = scale * node.getParam("ky") + kE = node.getParam("dE") if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: return np.matmul( track_sync_part_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), - track_sync_part_drift(sync_part=sync_part, length=length) + track_sync_part_drift(sync_part=sync_part, length=length), ) else: return track_sync_part_drift(sync_part=sync_part, length=length) @@ -191,7 +194,9 @@ def track_sync_part( elif node_type is BendLINAC: if length <= 0: return None - theta = node.getParam("theta") / nparts + theta = node.getParam("theta") / (nparts - 1) + if index == 0 or index == nparts - 1: + theta *= 0.5 return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) elif node_type is DCorrectorHLINAC: From 46b78bd85a8aaefbc4d78ba29a8a2e3ac7df3135 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 17:43:25 -0400 Subject: [PATCH 155/183] Increase nparts in bend test --- examples/Envelope/test_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 86422e97..18b45e64 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -215,7 +215,7 @@ def test_bend_teapot( length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None, - nparts: int = 2, + nparts: int = 5, ) -> None: nodes = [BendTEAPOT(length=length, theta=np.radians(theta), nparts=nparts)] lattice = make_lattice(nodes) @@ -229,7 +229,7 @@ def test_bend_linac( length: float = 1.0, theta: float = 20.0, cov_matrix: np.ndarray = None, - nparts: int = 2, + nparts: int = 5, ) -> None: node = Bend() node.setLength(length) From af17446da28cc5cc11c6d218a927ad978e4e2a33 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 17:43:34 -0400 Subject: [PATCH 156/183] Change axis label in yrms and yavg plots --- examples/Envelope/sns_ring/test_sns_ring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index f2d84d94..078533f8 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -243,7 +243,7 @@ def main(args: argparse.Namespace) -> None: ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) ax.set_xlabel("Turn") - ax.set_ylabel("RMS [mm]") + ax.set_ylabel(key + " [mm]") ax.legend(loc="upper right") plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() @@ -257,7 +257,7 @@ def main(args: argparse.Namespace) -> None: ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) ax.set_ylim(-5.0, 5.0) ax.set_xlabel("Turn") - ax.set_ylabel("AVG [mm]") + ax.set_ylabel(key + " [mm]") ax.legend(loc="upper right") plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() From 99987a15bf433f3010f0922dd680aa57eacfe065 Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 18:28:41 -0400 Subject: [PATCH 157/183] Set edge angles to zero with warning This took a while to track down. The fringe field function for BendTEAPOT (and all TEAPOT nodes) is called even if fringe field calculations are disabled. The actual fringe field kick is not applied, but wedgerotate is applied. Setting the ea1 and ea2 parameters to zero makes things agree without space charge. Added a warning and set these to zero automatically. This is for the dh_a11, dh_a12, etc. dipoles in the sns ring. --- py/orbit/envelope/envelope.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 3761e941..18ded2af 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,4 +1,5 @@ import math +import warnings import numpy as np import scipy.constants @@ -62,12 +63,12 @@ IGNORE_NODE_TYPES = [ NodeTEAPOT, MonitorTEAPOT, + FringeFieldTEAPOT, ApertureTEAPOT, BunchWrapTEAPOT, - FringeFieldTEAPOT, + TurnCounterTEAPOT, MarkerLINAC, FringeFieldLINAC, - TurnCounterTEAPOT, ] @@ -458,6 +459,17 @@ def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None self.lattice = lattice self.space_charge = space_charge + for node in self.lattice.getNodes(): + if type(node) in (BendTEAPOT, BendLINAC): + if node.getParam("ea1") != 0.0 or node.getParam("ea2") != 0.0: + message = f"Found bend ea1 or ea2 != 0.0 ({node.getName()}.)" + message += " Nonzero edge angles are not yet supported in envelope tracking." + message += " Setting ea1 and ea2 to 0.0." + warnings.warn(message) + + node.setParam("ea1", 0.0) + node.setParam("ea2", 0.0) + def track(self, envelope: Envelope) -> None: sync_part = envelope.sync_part charge = envelope.charge() From 17af04055778805e6abd27478c94422ac00f9c9e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 18:49:39 -0400 Subject: [PATCH 158/183] Combine space charge and body transfer matrices by transform Small speedup --- py/orbit/envelope/envelope.py | 79 +++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 18ded2af..b5adbee5 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -333,12 +333,11 @@ def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov_matrix)) return rms_arr[axis] - def transform(self, matrix: np.ndarray | None) -> None: - if matrix is not None: - m = matrix[:-1, :-1] - u = matrix[:-1, -1] - self.cov_matrix = m @ self.cov_matrix @ m.T - self.centroid = np.matmul(m, self.centroid) + u + def transform(self, matrix: np.ndarray) -> None: + m = matrix[:-1, :-1] + u = matrix[:-1, -1] + self.cov_matrix = m @ self.cov_matrix @ m.T + self.centroid = np.matmul(m, self.centroid) + u def sample(self, size: int, dist: str = "kv") -> np.ndarray: particles = gen_dist(size=size, cov_matrix=self.cov_matrix, name=dist) @@ -477,33 +476,37 @@ def track(self, envelope: Envelope) -> None: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) - - if self.space_charge: - length = node.getLength(part_index) - if self.space_charge == "2d": - matrix = envelope.sc_matrix_2d(length) - elif self.space_charge == "3d": - matrix = envelope.sc_matrix_3d(length) - else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) - envelope.transform(matrix) + if matrix is not None: + if self.space_charge: + length = node.getLength(part_index) + if self.space_charge == "2d": + matrix_sc = envelope.sc_matrix_2d(length) + elif self.space_charge == "3d": + matrix_sc = envelope.sc_matrix_3d(length) + else: + raise ValueError(f"Invalid space charge model: {self.space_charge}") + matrix = matrix @ matrix_sc + envelope.transform(matrix) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) def track_history(self, envelope: Envelope) -> dict[str, list]: @@ -528,25 +531,27 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) - - if self.space_charge: - length = node.getLength(part_index) - if self.space_charge == "2d": - matrix = envelope.sc_matrix_2d(length) - elif self.space_charge == "3d": - matrix = envelope.sc_matrix_3d(length) - else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) - envelope.transform(matrix) + if matrix is not None: + if self.space_charge: + length = node.getLength(part_index) + if self.space_charge == "2d": + matrix_sc = envelope.sc_matrix_2d(length) + elif self.space_charge == "3d": + matrix_sc = envelope.sc_matrix_3d(length) + else: + raise ValueError(f"Invalid space charge model: {self.space_charge}") + matrix = matrix @ matrix_sc + envelope.transform(matrix) position_start, position_stop = node_positions[node] position = position_start + node.getLength(part_index) * (part_index + 1) @@ -559,10 +564,12 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) - envelope.transform(matrix) + if matrix is not None: + envelope.transform(matrix) return history \ No newline at end of file From bd954db8004b7f3e17d109bb268d95df085288dd Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 19:17:46 -0400 Subject: [PATCH 159/183] Zero ea1 ea2 in sns_ring script --- examples/Envelope/sns_ring/test_sns_ring.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index 078533f8..9a5c46c7 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -22,6 +22,7 @@ from orbit.teapot import TEAPOT_Lattice from orbit.teapot import TEAPOT_Ring from orbit.teapot import TEAPOT_MATRIX_Lattice +from orbit.teapot import teapot from orbit.utils.consts import mass_proton sys.path.append("..") @@ -46,16 +47,17 @@ def main(args: argparse.Namespace) -> None: # Lattice # ------------------------------------------------------------------------------ - lattice = TEAPOT_Lattice() + lattice = TEAPOT_Ring() lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") lattice.initialize() for node in lattice.getNodes(): - try: + if type(node) != teapot.TurnCounterTEAPOT: node.setUsageFringeFieldIN(False) node.setUsageFringeFieldOUT(False) - except: - pass + if type(node) is teapot.BendTEAPOT: + node.setParam("ea1", 0.0) + node.setParam("ea2", 0.0) if args.sol: for name in ["scbdsol_c13a", "scbdsol_c13b"]: @@ -81,8 +83,8 @@ def main(args: argparse.Namespace) -> None: alpha_y = matrix_lattice_params["alpha y"] beta_x = matrix_lattice_params["beta x [m]"] beta_y = matrix_lattice_params["beta y [m]"] - eps_x = 25.0e-06 - eps_y = eps_x + eps_x = args.eps_x * 1e-6 + eps_y = args.eps_y * 1e-6 cov_matrix = np.zeros((6, 6)) cov_matrix[0, 0] = eps_x * beta_x @@ -113,6 +115,7 @@ def main(args: argparse.Namespace) -> None: size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist ) bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) + bunch_coords[:, 5] *= 0.0 bunch_coords += centroid[None, :6] for i in range(bunch_coords.shape[0]): @@ -319,6 +322,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--intensity", type=float, default=2e14) parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) + parser.add_argument("--eps-x", type=float, default=25.0) + parser.add_argument("--eps-y", type=float, default=25.0) parser.add_argument("--mismatch-x", type=float, default=0.0) parser.add_argument("--mismatch-y", type=float, default=0.0) parser.add_argument("--offset-x", type=float, default=0.0) From febd45f75d4693e29e016cf640c8d49dbb97b97e Mon Sep 17 00:00:00 2001 From: Austin Hoover Date: Thu, 25 Jun 2026 19:38:16 -0400 Subject: [PATCH 160/183] Fix bunch length calculation in 2D space charge Was assuming L = 4 * z_rms but really L = sqrt(12) * z_rms for 1D uniform distribution! Finally fixed sns ring space charge agreement. --- py/orbit/envelope/envelope.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b5adbee5..b5a3b92c 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -304,6 +304,7 @@ def __init__( self.intensity = 0.0 self.set_intensity(intensity) + self.rms_bunch_length_factor = np.sqrt(12.0) def set_intensity(self, intensity: float) -> None: self.intensity = intensity @@ -359,7 +360,7 @@ def sc_matrix_2d(self, length: float) -> np.ndarray: rx = 2.0 * np.sqrt(abs(cov_xx * cos_phi**2 + cov_yy * sin_phi**2 - 2.0 * cov_xy * sin_phi * cos_phi)) ry = 2.0 * np.sqrt(abs(cov_xx * sin_phi**2 + cov_yy * cos_phi**2 + 2.0 * cov_xy * sin_phi * cos_phi)) - bunch_length = 4.0 * np.sqrt(cov_matrix[4, 4]) + bunch_length = self.rms_bunch_length_factor * np.sqrt(cov_matrix[4, 4]) perveance = self.sc_factor / bunch_length kappa_factor = 2.0 * perveance / (rx + ry) From c8dc9623e5da501f276a2ddfce76c9b9bb4b8460 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 00:25:02 -0400 Subject: [PATCH 161/183] SNS linac default --- examples/Envelope/sns_linac/test_sns_linac.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/Envelope/sns_linac/test_sns_linac.py b/examples/Envelope/sns_linac/test_sns_linac.py index ccbbf696..b099036b 100755 --- a/examples/Envelope/sns_linac/test_sns_linac.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -45,7 +45,7 @@ parser.add_argument( "--seq", type=str, - default=None, + default="DTL6", choices=[ "MEBT", "DTL1", @@ -180,6 +180,8 @@ # Track bunch # -------------------------------------------------------------------------------- +lattice.trackDesignBunch(bunch) + if args.sc: if args.sc_model == "ellipsoid": n_ellipsoids = 1 @@ -213,7 +215,7 @@ histories[mode][key] = np.array(histories[mode][key]) plot_kws = {} -plot_kws["bunch"] = dict(color="black", lw=0, marker=".", ms=1) +plot_kws["bunch"] = dict(color="black", lw=0, marker=".", ms=2) plot_kws["envelope"] = dict(color="red", lw=0, marker=".", ms=1) fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) From 691100134fc5309ea9eb9aefa017d3cbba484776 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 00:25:30 -0400 Subject: [PATCH 162/183] Calculate space charge matrix before calculating transfer matrix Want to calculate space charge matrix before possible energy change. --- py/orbit/envelope/envelope.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index b5a3b92c..862e33c9 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -486,16 +486,20 @@ def track(self, envelope: Envelope) -> None: if matrix is not None: envelope.transform(matrix) - matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) - if matrix is not None: - if self.space_charge: - length = node.getLength(part_index) + matrix_sc = None + if self.space_charge: + length = node.getLength(part_index) + if length > 0: if self.space_charge == "2d": matrix_sc = envelope.sc_matrix_2d(length) elif self.space_charge == "3d": matrix_sc = envelope.sc_matrix_3d(length) else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") + raise ValueError + + matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + if matrix_sc is not None: matrix = matrix @ matrix_sc envelope.transform(matrix) @@ -541,16 +545,20 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: if matrix is not None: envelope.transform(matrix) - matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) - if matrix is not None: - if self.space_charge: - length = node.getLength(part_index) + matrix_sc = None + if self.space_charge: + length = node.getLength(part_index) + if length > 0: if self.space_charge == "2d": matrix_sc = envelope.sc_matrix_2d(length) elif self.space_charge == "3d": matrix_sc = envelope.sc_matrix_3d(length) else: - raise ValueError(f"Invalid space charge model: {self.space_charge}") + raise ValueError + + matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + if matrix_sc is not None: matrix = matrix @ matrix_sc envelope.transform(matrix) From 0bf05a0122c421c0fdee4687d999fb201161fd61 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 01:17:25 -0400 Subject: [PATCH 163/183] Fix comment --- py/orbit/envelope/envelope.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 862e33c9..765092f0 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -392,9 +392,9 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: # Build Lorentz matrix: rest frame to lab frame. # x -> x # y -> y - # z -> gamma * z - # x' = dx/ds -> x' / gamma - # y' = dy/ds -> y' / gamma + # z -> z / gamma + # x' = dx/ds -> x' * gamma + # y' = dy/ds -> y' * gamma # z' = dz/ds -> z' gamma = self.gamma() gamma_inv = 1.0 / gamma @@ -426,9 +426,9 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: RDz = scipy.special.elliprd(cov_xx, cov_yy, cov_zz) factor = 0.5 * self.sc_factor * ((1.0 / 5.0) ** 1.5) - kappa_x = factor * RDx # [1 / m] - kappa_y = factor * RDy # [1 / m] - kappa_z = factor * RDz # [1 / m] + kappa_x = factor * RDx + kappa_y = factor * RDy + kappa_z = factor * RDz M = np.identity(7) M[1, 0] = kappa_x * length From 3064cc3b32ea86974f1fb9c521f5d00fbd10a608 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 13:15:41 -0400 Subject: [PATCH 164/183] Use rest length in space charge kick --- py/orbit/envelope/envelope.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 765092f0..adfbd14e 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -411,6 +411,9 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: # Get covariance matrix in rest frame. cov_matrix = L_inv[:-1, :-1] @ self.cov_matrix @ L_inv[:-1, :-1].T + # Get kick length in rest frame + length_rest = length * gamma + # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) @@ -431,9 +434,9 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: kappa_z = factor * RDz M = np.identity(7) - M[1, 0] = kappa_x * length - M[3, 2] = kappa_y * length - M[5, 4] = kappa_z * length + M[1, 0] = kappa_x * length_rest + M[3, 2] = kappa_y * length_rest + M[5, 4] = kappa_z * length_rest # Build matrix to undo x-y-z diagonalization. A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) From 91752603d6f6d22dd10b4e10119b330045758c1b Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 13:56:30 -0400 Subject: [PATCH 165/183] Typo: remove self --- py/orbit/envelope/envelope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index adfbd14e..6f9346b3 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -123,7 +123,7 @@ def track_sync_part( return None B = node.getParam("B") if node.waveform: - B *= self.waveform.getStrength() + B *= node.waveform.getStrength() return track_sync_part_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) elif node_type is MultipoleTEAPOT: From 573e38fcc92a822e7c5f766475ee3cfbc9da09b0 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 14:00:36 -0400 Subject: [PATCH 166/183] Formatting --- examples/Envelope/sns_linac/diagnostics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/Envelope/sns_linac/diagnostics.py b/examples/Envelope/sns_linac/diagnostics.py index 7edfebdd..d8f557fa 100644 --- a/examples/Envelope/sns_linac/diagnostics.py +++ b/examples/Envelope/sns_linac/diagnostics.py @@ -35,7 +35,9 @@ def __call__(self, params_dict: dict) -> None: cov_matrix = np.zeros((6, 6)) for i in range(6): for j in range(6): - cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation(i, j) + cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation( + i, j + ) xrms = 1000.0 * np.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * np.sqrt(cov_matrix[2, 2]) From c23a80f3e9ade79152d7635d7d5d54625a9a8da2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 14:00:43 -0400 Subject: [PATCH 167/183] Add choice of phase space distribution for sns linac benchmark --- examples/Envelope/sns_linac/test_sns_linac.py | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/examples/Envelope/sns_linac/test_sns_linac.py b/examples/Envelope/sns_linac/test_sns_linac.py index b099036b..8183128e 100755 --- a/examples/Envelope/sns_linac/test_sns_linac.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -3,6 +3,7 @@ import os import random import time +import sys import numpy as np import matplotlib.pyplot as plt @@ -35,6 +36,11 @@ # local from diagnostics import BunchMonitor +sys.path.append("..") +from plot import plot_corner +from plot import plot_rms_ellipse +from utils import project_cov_matrix + plt.style.use("style.mplstyle") @@ -42,34 +48,14 @@ # -------------------------------------------------------------------------------- parser = argparse.ArgumentParser() -parser.add_argument( - "--seq", - type=str, - default="DTL6", - choices=[ - "MEBT", - "DTL1", - "DTL2", - "DTL3", - "DTL4", - "DTL5", - "DTL6", - "CCL1", - "CCL2", - "CCL3", - "CCL4", - "SCLMed", - "SCLHigh", - "HEBT1", - "HEBT2", - ], -) +parser.add_argument("--dist", type=str, default="kv") +parser.add_argument("--nparts", type=int, default=20_000) parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-model", type=str, default="ellipsoid") -parser.add_argument("--nparts", type=int, default=10_000) -parser.add_argument("--current", type=float, default=0.038) parser.add_argument("--sc-path-length-min", type=float, default=0.01) +parser.add_argument("--current", type=float, default=0.038) parser.add_argument("--show", type=int, default=0) +parser.add_argument("--seq-stop", type=str, default="CCL2") args = parser.parse_args() @@ -108,15 +94,22 @@ twiss_y = TwissContainer(alpha_y, beta_y, eps_y) twiss_z = TwissContainer(alpha_z, beta_z, eps_z) -dist = WaterBagDist3D(twiss_x, twiss_y, twiss_z) +if args.dist == "waterbag": + dist = WaterBagDist3D(twiss_x, twiss_y, twiss_z) +elif args.dist == "kv": + dist = KVDist3D(twiss_x, twiss_y, twiss_z) +elif args.dist == "gauss": + dist = GaussDist3D(twiss_x, twiss_y, twiss_z) +else: + raise ValueError("Unknown distribution '{}'".format(args.dist)) + for _ in range(args.nparts): bunch.addParticle(*dist.getCoordinates()) - # Lattice # -------------------------------------------------------------------------------- -sequence_names = [ +seq_names = [ "MEBT", "DTL1", "DTL2", @@ -133,13 +126,13 @@ "HEBT1", "HEBT2", ] -if args.seq: - stop_index = sequence_names.index(args.seq) + 1 - sequence_names = sequence_names[:stop_index] +if args.seq_stop: + index = seq_names.index(args.seq_stop) + 1 + seq_names = seq_names[:index] sns_linac_factory = SNS_LinacLatticeFactory() sns_linac_factory.setMaxDriftLength(args.sc_path_length_min) -lattice = sns_linac_factory.getLinacAccLattice(sequence_names, "inputs/sns_linac.xml") +lattice = sns_linac_factory.getLinacAccLattice(seq_names, "inputs/sns_linac.xml") for node in lattice.getNodes(): try: @@ -186,7 +179,9 @@ if args.sc_model == "ellipsoid": n_ellipsoids = 1 sc_calc = SpaceChargeCalcUnifEllipse(n_ellipsoids) - sc_nodes = setUniformEllipsesSCAccNodes(lattice, args.sc_path_length_min, sc_calc) + sc_nodes = setUniformEllipsesSCAccNodes( + lattice, args.sc_path_length_min, sc_calc + ) if args.sc_model == "3d": sc_calc = SpaceChargeCalc3D(64, 64, 64) sc_nodes = setSC3DAccNodes(lattice, args.sc_path_length_min, sc_calc) @@ -218,7 +213,7 @@ plot_kws["bunch"] = dict(color="black", lw=0, marker=".", ms=2) plot_kws["envelope"] = dict(color="red", lw=0, marker=".", ms=1) -fig, axs = plt.subplots(nrows=3, figsize=(5, 7), sharex=True, constrained_layout=True) +fig, axs = plt.subplots(nrows=3, figsize=(10, 5), sharex=True, constrained_layout=True) for mode in ["bunch", "envelope"]: history = histories[mode] for ax, key in zip(axs, ["rms_x", "rms_y", "rms_z"]): @@ -244,3 +239,52 @@ ax.set_xlabel("s [m]") plt.savefig(os.path.join(output_dir, "fig_history_energy.png")) plt.close() + +# Collect bunch/envelope data +particles = collect_bunch(bunch)["coords"] +particles *= 1e3 + +env_cov_matrix = envelope.cov_matrix +env_cov_matrix *= 1e6 + +env_centroid = envelope.centroid +env_centroid *= 1e3 + +xmax = 4.0 * np.std(particles, axis=0) +limits = list(zip(-xmax, xmax)) +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [mm]", "dE [MeV]"] + +# Plot x-x' +fig, ax = plt.subplots(figsize=(4, 4)) +ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) +plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, +) +ax.set_xlabel(labels[0]) +ax.set_ylabel(labels[1]) +plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) +plt.close() + +# Plot corner +fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, +) +for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) +plt.savefig(os.path.join(output_dir, "fig_dist_corner")) +plt.close() From 0dc141a6fed324fade47fb69f9b4f66ddc723801 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 14:47:23 -0400 Subject: [PATCH 168/183] Format --- examples/Envelope/sns_ring/test_sns_ring.py | 652 ++++++++++-------- .../Envelope/sns_ring/test_sns_ring_speed.py | 5 +- 2 files changed, 354 insertions(+), 303 deletions(-) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index 9a5c46c7..d6fd62b8 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -19,7 +19,6 @@ from orbit.envelope import EnvelopeTracker from orbit.core.spacecharge import SpaceChargeCalc2p5D from orbit.space_charge.sc2p5d import setSC2p5DAccNodes -from orbit.teapot import TEAPOT_Lattice from orbit.teapot import TEAPOT_Ring from orbit.teapot import TEAPOT_MATRIX_Lattice from orbit.teapot import teapot @@ -35,311 +34,362 @@ plt.style.use("style.mplstyle") -def main(args: argparse.Namespace) -> None: - - # Setup - # ------------------------------------------------------------------------------ - - path = pathlib.Path(__file__) - output_dir = os.path.join("outputs", path.stem) - os.makedirs(output_dir, exist_ok=True) - - # Lattice - # ------------------------------------------------------------------------------ - - lattice = TEAPOT_Ring() - lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") - lattice.initialize() - - for node in lattice.getNodes(): - if type(node) != teapot.TurnCounterTEAPOT: - node.setUsageFringeFieldIN(False) - node.setUsageFringeFieldOUT(False) - if type(node) is teapot.BendTEAPOT: - node.setParam("ea1", 0.0) - node.setParam("ea2", 0.0) - - if args.sol: - for name in ["scbdsol_c13a", "scbdsol_c13b"]: - node = lattice.getNodeForName(name) - node.setParam("B", 0.15) - - for node in lattice.getNodes(): - max_length = 1.0 - if node.getLength() > max_length: - node.setnParts(1 + int(node.getLength() / max_length)) - - # Bunch - # ------------------------------------------------------------------------------ - - bunch = Bunch() - bunch.mass(mass_proton) - sync_part = bunch.getSyncParticle() - sync_part.kinEnergy(args.kin_energy) - - matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) - matrix_lattice_params = matrix_lattice.getRingParametersDict() - alpha_x = matrix_lattice_params["alpha x"] - alpha_y = matrix_lattice_params["alpha y"] - beta_x = matrix_lattice_params["beta x [m]"] - beta_y = matrix_lattice_params["beta y [m]"] - eps_x = args.eps_x * 1e-6 - eps_y = args.eps_y * 1e-6 +# Arguments +# ------------------------------------------------------------------------------ + +parser = argparse.ArgumentParser() +parser.add_argument("--bunch-length", type=float, default=120.0) +parser.add_argument("--kin-energy", type=float, default=1.300) +parser.add_argument("--intensity", type=float, default=2e14) + +parser.add_argument( + "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] +) +parser.add_argument("--eps-x", type=float, default=25.0) +parser.add_argument("--eps-y", type=float, default=25.0) +parser.add_argument("--mismatch-x", type=float, default=0.0) +parser.add_argument("--mismatch-y", type=float, default=0.0) +parser.add_argument("--offset-x", type=float, default=0.0) +parser.add_argument("--offset-y", type=float, default=0.0) +parser.add_argument("--tilt", type=float, default=0) + +parser.add_argument("--nparts", type=int, default=100_000) +parser.add_argument("--turns", type=int, default=25) +parser.add_argument("--sol", type=int, default=0) +parser.add_argument("--sc", type=int, default=0) +parser.add_argument("--sc-grid", type=int, default=64) + +parser.add_argument( + "--handle-unknown", type=str, default=None, choices=["drift", "fit"] +) +args = parser.parse_args() + +# Setup +# ------------------------------------------------------------------------------ + +path = pathlib.Path(__file__) +output_dir = os.path.join("outputs", path.stem) +os.makedirs(output_dir, exist_ok=True) + +# Lattice +# ------------------------------------------------------------------------------ + +lattice = TEAPOT_Ring() +lattice.readMADX("inputs/sns_ring.lat", "rnginjsol") +lattice.initialize() + +for node in lattice.getNodes(): + if type(node) != teapot.TurnCounterTEAPOT: + node.setUsageFringeFieldIN(False) + node.setUsageFringeFieldOUT(False) + if type(node) is teapot.BendTEAPOT: + node.setParam("ea1", 0.0) + node.setParam("ea2", 0.0) + +if args.sol: + for name in ["scbdsol_c13a", "scbdsol_c13b"]: + node = lattice.getNodeForName(name) + node.setParam("B", 0.15) + +for node in lattice.getNodes(): + max_length = 1.0 + if node.getLength() > max_length: + node.setnParts(1 + int(node.getLength() / max_length)) + +# Bunch +# ------------------------------------------------------------------------------ + +bunch = Bunch() +bunch.mass(mass_proton) +sync_part = bunch.getSyncParticle() +sync_part.kinEnergy(args.kin_energy) + +matrix_lattice = TEAPOT_MATRIX_Lattice(lattice, bunch) +matrix_lattice_params = matrix_lattice.getRingParametersDict() +alpha_x = matrix_lattice_params["alpha x"] +alpha_y = matrix_lattice_params["alpha y"] +beta_x = matrix_lattice_params["beta x [m]"] +beta_y = matrix_lattice_params["beta y [m]"] +eps_x = args.eps_x * 1e-6 +eps_y = args.eps_y * 1e-6 + +cov_matrix = np.zeros((6, 6)) +cov_matrix[0, 0] = eps_x * beta_x +cov_matrix[2, 2] = eps_y * beta_y +cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x +cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y +cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x +cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y +cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 +cov_matrix[5, 5] = 0.0 + +if args.tilt: + rot_matrix = np.identity(6) + rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) + cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) + +if args.mismatch_x or args.mismatch_y: + cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 + cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 + +centroid = np.zeros(6) +centroid[0] += args.offset_x +centroid[2] += args.offset_y + +rng = np.random.default_rng() +bunch_coords = np.zeros((args.nparts, 6)) +bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist +) +bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) +bunch_coords[:, 5] *= 0.0 +bunch_coords += centroid[None, :6] + +for i in range(bunch_coords.shape[0]): + bunch.addParticle(*bunch_coords[i]) + +# Use covariance matrix from initial bunch, which is slightly +# different from the one used to generate the bunch due to +# finite statistics. +cov_matrix_init = np.cov(bunch_coords, rowvar=False) +centroid_init = np.mean(bunch_coords, axis=0) + +# Track envelope +# ------------------------------------------------------------------------------ + +print("TRACK ENVELOPE") + +envelope = Envelope( + bunch=bunch, + cov_matrix=cov_matrix_init, + centroid=centroid_init, + intensity=args.intensity, +) +envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) + +history_keys = [ + "rms_x", + "rms_y", + "avg_x", + "avg_y", + "eps_x", + "eps_y", +] +history = {key: [] for key in history_keys} + +start_time = time.time() + +for turn in range(args.turns + 1): + if turn > 0: + envelope_tracker.track(envelope) + + cov_matrix = envelope.cov_matrix + centroid = envelope.centroid + + rms_x = 1000.0 * math.sqrt(cov_matrix[0, 0]) + rms_y = 1000.0 * math.sqrt(cov_matrix[2, 2]) + avg_x = 1000.0 * centroid[0] + avg_y = 1000.0 * centroid[2] + eps_x = 1e6 * np.sqrt(np.linalg.det(cov_matrix[0:2, 0:2])) + eps_y = 1e6 * np.sqrt(np.linalg.det(cov_matrix[2:4, 2:4])) + + message = "" + message += " turn={}".format(turn) + message += " time={:0.3f}".format(time.time() - start_time) + message += " eps_x={:0.2f}".format(eps_x) + message += " eps_y={:0.2f}".format(eps_y) + message += " xrms={:0.2f}".format(rms_x) + message += " yrms={:0.2f}".format(rms_y) + message += " xavg={:0.2f}".format(avg_x) + message += " yavg={:0.2f}".format(avg_y) + print(message) + + history["rms_x"].append(rms_x) + history["rms_y"].append(rms_y) + history["avg_x"].append(avg_x) + history["avg_y"].append(avg_y) + history["eps_x"].append(eps_x) + history["eps_y"].append(eps_y) + +histories = {} +histories["envelope"] = copy.deepcopy(history) + +# Track bunch +# ------------------------------------------------------------------------------ + +print("TRACK BUNCH") + +if args.sc: + sc_calc = SpaceChargeCalc2p5D(64, 64, 1) + sc_path_length_min = 1.00e-06 + sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) + + bunch_size = bunch.getSizeGlobal() + bunch.macroSize(args.intensity / bunch_size) + +history_keys = [ + "rms_x", + "rms_y", + "avg_x", + "avg_y", + "eps_x", + "eps_y", +] +history = {key: [] for key in history_keys} + +start_time = time.time() + +for turn in range(args.turns + 1): + if turn > 0: + lattice.trackBunch(bunch) + + twiss_calc = BunchTwissAnalysis() + twiss_calc.computeBunchMoments(bunch, 2, 0, 0) cov_matrix = np.zeros((6, 6)) - cov_matrix[0, 0] = eps_x * beta_x - cov_matrix[2, 2] = eps_y * beta_y - cov_matrix[0, 1] = cov_matrix[1, 0] = -eps_x * alpha_x - cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y - cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x - cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y - cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 - cov_matrix[5, 5] = 0.0 - - if args.tilt: - rot_matrix = np.identity(6) - rot_matrix[:4, :4] = build_rotation_matrix_xy(angle=(args.tilt * math.pi)) - cov_matrix = np.linalg.multi_dot([rot_matrix, cov_matrix, rot_matrix.T]) - - if args.mismatch_x or args.mismatch_y: - cov_matrix[0, 0] *= (1.0 + args.mismatch_x) ** 2 - cov_matrix[2, 2] *= (1.0 + args.mismatch_y) ** 2 + for i in range(6): + for j in range(i + 1): + cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) + cov_matrix[j, i] = cov_matrix[i, j] centroid = np.zeros(6) - centroid[0] += args.offset_x - centroid[2] += args.offset_y - - rng = np.random.default_rng() - bunch_coords = np.zeros((args.nparts, 6)) - bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist - ) - bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) - bunch_coords[:, 5] *= 0.0 - bunch_coords += centroid[None, :6] - - for i in range(bunch_coords.shape[0]): - bunch.addParticle(*bunch_coords[i]) - - # Use covariance matrix from initial bunch, which is slightly - # different from the one used to generate the bunch due to - # finite statistics. - cov_matrix_init = np.cov(bunch_coords, rowvar=False) - centroid_init = np.mean(bunch_coords, axis=0) - - # Track envelope - # ------------------------------------------------------------------------------ - - print("TRACK ENVELOPE") - - envelope = Envelope( - bunch=bunch, - cov_matrix=cov_matrix_init, - centroid=centroid_init, - intensity=args.intensity, - ) - - envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) - - history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} - start_time = time.time() - - for turn in range(args.turns + 1): - if turn > 0: - envelope_tracker.track(envelope) - - cov_matrix = envelope.cov_matrix - centroid = envelope.centroid - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * centroid[0] - yavg = 1000.0 * centroid[2] - - message = "" - message += " turn={}".format(turn) - message += " time={:0.3f}".format(time.time() - start_time) - message += " xrms={:0.2f}".format(xrms) - message += " yrms={:0.2f}".format(yrms) - message += " xavg={:0.2f}".format(xavg) - message += " yavg={:0.2f}".format(yavg) - print(message) - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) - - histories = {} - histories["envelope"] = copy.deepcopy(history) - - # Track bunch - # ------------------------------------------------------------------------------ - - print("TRACK BUNCH") - - if args.sc: - sc_calc = SpaceChargeCalc2p5D(64, 64, 1) - sc_path_length_min = 1.00e-06 - sc_nodes = setSC2p5DAccNodes(lattice, sc_path_length_min, sc_calc) - - bunch_size = bunch.getSizeGlobal() - bunch.macroSize(args.intensity / bunch_size) - - history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} - start_time = time.time() - - for turn in range(args.turns + 1): - if turn > 0: - lattice.trackBunch(bunch) - - twiss_calc = BunchTwissAnalysis() - twiss_calc.computeBunchMoments(bunch, 2, 0, 0) - - cov_matrix = np.zeros((6, 6)) - for i in range(6): - for j in range(i + 1): - cov_matrix[i, j] = twiss_calc.getCorrelation(j, i) - cov_matrix[j, i] = cov_matrix[i, j] - - xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) - yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - xavg = 1000.0 * twiss_calc.getAverage(0) - yavg = 1000.0 * twiss_calc.getAverage(2) - - message = "" - message += " turn={}".format(turn) - message += " time={:0.3f}".format(time.time() - start_time) - message += " xrms={:0.2f}".format(xrms) - message += " yrms={:0.2f}".format(yrms) - message += " xavg={:0.2f}".format(xavg) - message += " yavg={:0.2f}".format(yavg) - print(message) - - history["xrms"].append(xrms) - history["yrms"].append(yrms) - history["xavg"].append(xavg) - history["yavg"].append(yavg) - - histories["bunch"] = copy.deepcopy(history) - - # Analysis - # ------------------------------------------------------------------------------ - - for history in histories.values(): - for key in history: - history[key] = np.array(history[key]) - - # Print errors - for key in histories["envelope"]: - deltas = histories["bunch"][key] - histories["envelope"][key] - print("key:", key) - print("max_abs_delta:", np.max(np.abs(deltas))) - print("avg_abs_delta:", np.mean(np.abs(deltas))) - - # Plot rms bunch sizes - for key in ["xrms", "yrms"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) - ax.set_xlabel("Turn") - ax.set_ylabel(key + " [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close() - - # Plot centroids - for key in ["xavg", "yavg"]: - fig, ax = plt.subplots(figsize=(5, 3)) - for i, model in enumerate(["envelope", "bunch"]): - color = ["black", "red"][i] - lw = [None, 0][i] - ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) - ax.set_ylim(-5.0, 5.0) - ax.set_xlabel("Turn") - ax.set_ylabel(key + " [mm]") - ax.legend(loc="upper right") - plt.savefig(os.path.join(output_dir, f"fig_{key}")) - plt.close() - - # Collect bunch/envelope data on final turn. - particles = collect_bunch(bunch)["coords"] - particles[:, :4] *= 1000.0 - - env_cov_matrix = envelope.cov_matrix - env_cov_matrix[:4, :4] *= 1000.0**2 - - env_centroid = envelope.centroid - env_centroid[:4] *= 1000.0 - - xmax = 4.0 * np.std(particles, axis=0) - limits = list(zip(-xmax, xmax)) - labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] - - # Plot x-x' - fig, ax = plt.subplots(figsize=(4, 4)) - ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) - plot_rms_ellipse( - env_cov_matrix[0:2, 0:2], - center=(env_centroid[0], env_centroid[1]), - level=2.0, - color="red", - ax=ax, - ) - ax.set_xlabel(labels[0]) - ax.set_ylabel(labels[1]) - plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) - plt.close() - - # Plot corner - fig, axs = plot_corner( - particles, - limits=limits, - bins=100, - labels=labels, - ) for i in range(6): - for j in range(i): - env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) - plot_rms_ellipse( - env_cov_matrix_proj, - center=(env_centroid[j], env_centroid[i]), - level=2.0, - color="red", - ax=axs[i, j], - ) - plt.savefig(os.path.join(output_dir, "fig_dist_corner")) + centroid[i] = twiss_calc.getAverage(i) + + rms_x = 1000.0 * math.sqrt(cov_matrix[0, 0]) + rms_y = 1000.0 * math.sqrt(cov_matrix[2, 2]) + avg_x = 1000.0 * centroid[0] + avg_y = 1000.0 * centroid[2] + eps_x = 1e6 * np.sqrt(np.linalg.det(cov_matrix[0:2, 0:2])) + eps_y = 1e6 * np.sqrt(np.linalg.det(cov_matrix[2:4, 2:4])) + + message = "" + message += " turn={}".format(turn) + message += " time={:0.3f}".format(time.time() - start_time) + message += " eps_x={:0.2f}".format(eps_x) + message += " eps_y={:0.2f}".format(eps_y) + message += " xrms={:0.2f}".format(rms_x) + message += " yrms={:0.2f}".format(rms_y) + message += " xavg={:0.2f}".format(avg_x) + message += " yavg={:0.2f}".format(avg_y) + print(message) + + history["rms_x"].append(rms_x) + history["rms_y"].append(rms_y) + history["avg_x"].append(avg_x) + history["avg_y"].append(avg_y) + history["eps_x"].append(eps_x) + history["eps_y"].append(eps_y) + +histories["bunch"] = copy.deepcopy(history) + +# Analysis +# ------------------------------------------------------------------------------ + +for history in histories.values(): + for key in history: + history[key] = np.array(history[key]) + +# Print errors +for key in histories["envelope"]: + deltas = histories["bunch"][key] - histories["envelope"][key] + print("key:", key) + print("max_abs_delta:", np.max(np.abs(deltas))) + print("avg_abs_delta:", np.mean(np.abs(deltas))) + +# Plot rms bunch sizes +for key in ["rms_x", "rms_y"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(0.0, ax.get_ylim()[1] * 2.0) + ax.set_xlabel("Turn") + ax.set_ylabel(key + " [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) plt.close() +# Plot rms emittances +color_cycle = plt.rcParams["axes.prop_cycle"].by_key()["color"] + +fig, ax = plt.subplots(figsize=(5, 3)) +for key, color in zip(["eps_x", "eps_y"], color_cycle): + for i, model in enumerate(["envelope", "bunch"]): + if model == "envelope": + plot_kws = dict() + else: + plot_kws = dict(marker=".", lw=0) + label = key + "_" + model + ax.plot(histories[model][key], color=color, label=label, **plot_kws) + +ymax = 2.0 * np.mean(histories["envelope"]["eps_x"]) +ax.set_ylim(0.0, ymax) +ax.set_xlabel("Turn") +ax.set_ylabel("[mm mrad]") +plt.savefig(os.path.join(output_dir, f"fig_emittances")) +plt.close() + +# Plot centroids +for key in ["avg_x", "avg_y"]: + fig, ax = plt.subplots(figsize=(5, 3)) + for i, model in enumerate(["envelope", "bunch"]): + color = ["black", "red"][i] + lw = [None, 0][i] + ax.plot(histories[model][key], marker=".", lw=lw, color=color, label=model) + ax.set_ylim(-5.0, 5.0) + ax.set_xlabel("Turn") + ax.set_ylabel(key + " [mm]") + ax.legend(loc="upper right") + plt.savefig(os.path.join(output_dir, f"fig_{key}")) + plt.close() -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--bunch-length", type=float, default=120.0) - parser.add_argument("--kin-energy", type=float, default=1.300) - parser.add_argument("--intensity", type=float, default=2e14) - - parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) - parser.add_argument("--eps-x", type=float, default=25.0) - parser.add_argument("--eps-y", type=float, default=25.0) - parser.add_argument("--mismatch-x", type=float, default=0.0) - parser.add_argument("--mismatch-y", type=float, default=0.0) - parser.add_argument("--offset-x", type=float, default=0.0) - parser.add_argument("--offset-y", type=float, default=0.0) - parser.add_argument("--tilt", type=float, default=0) - - parser.add_argument("--nparts", type=int, default=100_000) - parser.add_argument("--turns", type=int, default=25) - parser.add_argument("--sol", type=int, default=0) - parser.add_argument("--sc", type=int, default=0) - parser.add_argument("--sc-grid", type=int, default=64) - - parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - main(args) +# Collect bunch/envelope data on final turn. +particles = collect_bunch(bunch)["coords"] +particles[:, :4] *= 1000.0 + +env_cov_matrix = envelope.cov_matrix +env_cov_matrix[:4, :4] *= 1000.0**2 + +env_centroid = envelope.centroid +env_centroid[:4] *= 1000.0 + +xmax = 4.0 * np.std(particles, axis=0) +limits = list(zip(-xmax, xmax)) +labels = ["x [mm]", "xp [mrad]", "y [mm]", "yp [mrad]", "z [m]", "dE [GeV]"] + +# Plot x-x' +fig, ax = plt.subplots(figsize=(4, 4)) +ax.hist2d(particles[:, 0], particles[:, 1], bins=100, range=[limits[0], limits[1]]) +plot_rms_ellipse( + env_cov_matrix[0:2, 0:2], + center=(env_centroid[0], env_centroid[1]), + level=2.0, + color="red", + ax=ax, +) +ax.set_xlabel(labels[0]) +ax.set_ylabel(labels[1]) +plt.savefig(os.path.join(output_dir, "fig_dist_x_xp")) +plt.close() + +# Plot corner +fig, axs = plot_corner( + particles, + limits=limits, + bins=100, + labels=labels, +) +for i in range(6): + for j in range(i): + env_cov_matrix_proj = project_cov_matrix(env_cov_matrix, axis=(j, i)) + plot_rms_ellipse( + env_cov_matrix_proj, + center=(env_centroid[j], env_centroid[i]), + level=2.0, + color="red", + ax=axs[i, j], + ) +plt.savefig(os.path.join(output_dir, "fig_dist_corner")) +plt.close() diff --git a/examples/Envelope/sns_ring/test_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py index f5c50598..51a5c189 100644 --- a/examples/Envelope/sns_ring/test_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -22,7 +22,6 @@ sys.path.append("..") from utils import gen_dist - parser = argparse.ArgumentParser() parser.add_argument("--bunch-length", type=float, default=120.0) parser.add_argument("--kin-energy", type=float, default=1.300) @@ -111,7 +110,9 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") +bunch_coords[:, :4] = gen_dist( + size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" +) bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) for i in range(bunch_coords.shape[0]): From d1a32f12392df43d1491abacb34ee0c1d1d21905 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 21:48:11 -0400 Subject: [PATCH 169/183] Add to-do comment --- py/orbit/envelope/envelope.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 6f9346b3..d98d1bc1 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -106,6 +106,13 @@ def track_sync_part( 7 x 7 transfer matrix or None. If None, the node can be ignored during envelope tracking. """ + + # [TO DO] + # - Add option to calculate transfer matrix by fitting routine for all + # elements, or for specified elements. + # - Add option to pre-compute + store matrices for multi-turn tracking + # in static lattice. If no space charge, multiply to get single matrix. + node_type = type(node) if node_type in IGNORE_NODE_TYPES: return None From 95f53ab3cc5770998ba401a08279198ebd1c52b2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 22:00:15 -0400 Subject: [PATCH 170/183] Add continuous linear focusing element to envelope tracking --- py/orbit/envelope/matrix.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 30e192b4..418953dc 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -179,7 +179,27 @@ def track_sync_part_solenoid(sync_part: SyncParticle, length: float, B: float, c def track_sync_part_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarray: - raise NotImplementedError() + if length <= 0: + return + + if kq == 0: + return track_sync_part_drift(sync_part=sync_part, length=length) + + sqrt_abs_kq = math.sqrt(abs(kq)) + + cx = math.cos(sqrt_abs_kq * length) + sx = math.sin(sqrt_abs_kq * length) + + M = np.identity(7) + M[0, 0] = M[2, 2] = cx + M[0, 1] = M[2, 3] = +sx / sqrt_abs_kq + M[1, 0] = M[3, 2] = -sx * sqrt_abs_kq + M[1, 1] = M[3, 3] = cx + M[4, 5] = length / (sync_part.gamma()**2) + M[4, 5] *= get_dp_p_coeff(sync_part) + + sync_part.time(sync_part.time() + length / (sync_part.beta() * speed_of_light)) + return M def track_sync_part_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, phase: float, charge: float) -> np.ndarray: From 76c68e681f2740c6733d664e305d7573400c1542 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 22:00:28 -0400 Subject: [PATCH 171/183] Add continous linear focusing element to envelope tests --- examples/Envelope/test_env.py | 40 +++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/examples/Envelope/test_env.py b/examples/Envelope/test_env.py index 18b45e64..c86a7103 100644 --- a/examples/Envelope/test_env.py +++ b/examples/Envelope/test_env.py @@ -12,6 +12,7 @@ from orbit.py_linac.lattice import TiltElement from orbit.py_linac.lattice import Solenoid from orbit.teapot import BendTEAPOT +from orbit.teapot import ContinuousLinearFocusingTEAPOT from orbit.teapot import DriftTEAPOT from orbit.teapot import KickTEAPOT from orbit.teapot import QuadTEAPOT @@ -153,8 +154,8 @@ def test_drift_teapot( cov_matrix: np.ndarray = None, nparts: int = 6, ) -> None: - nodes = [DriftTEAPOT(length=length, nparts=nparts)] - lattice = make_lattice(nodes) + node = DriftTEAPOT(length=length, nparts=nparts) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) @@ -184,13 +185,25 @@ def test_quad_teapot( cov_matrix: np.ndarray = None, nparts: int = 10, ) -> None: - nodes = [QuadTEAPOT(length=length, kq=kq, nparts=nparts)] - lattice = make_lattice(nodes) + node = QuadTEAPOT(length=length, kq=kq, nparts=nparts) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) +def test_cf_teapot( + kin_energy: float = 0.0025, + length: float = 10.0, + kq: float = 1.0, + nparts: int = 10, +) -> None: + node = ContinuousLinearFocusingTEAPOT(length=length, kq=kq, nparts=nparts) + lattice = make_lattice([node]) + cov_matrix = np.diag(np.square([1e-3, 0, 1e-3, 0.0, 0.0, 0.0])) + track_and_compare_rms(lattice, kin_energy, cov_matrix) + + def test_quad_linac( kin_energy: float = 0.0025, length: float = 1.0, @@ -217,8 +230,8 @@ def test_bend_teapot( cov_matrix: np.ndarray = None, nparts: int = 5, ) -> None: - nodes = [BendTEAPOT(length=length, theta=np.radians(theta), nparts=nparts)] - lattice = make_lattice(nodes) + node = BendTEAPOT(length=length, theta=np.radians(theta), nparts=nparts) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) @@ -252,8 +265,8 @@ def test_kick_teapot( cov_matrix: np.ndarray = None, nparts: int = 4, ) -> None: - nodes = [KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length, nparts=nparts)] - lattice = make_lattice(nodes) + node = KickTEAPOT(kx=kx, ky=ky, dE=dE, length=length, nparts=nparts) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) @@ -264,8 +277,8 @@ def test_tilt_teapot( angle: float = 0.25 * np.pi, cov_matrix: np.ndarray = None, ) -> None: - nodes = [TiltTEAPOT(angle=angle)] - lattice = make_lattice(nodes) + node = TiltTEAPOT(angle=angle) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) @@ -292,8 +305,8 @@ def test_solenoid_teapot( cov_matrix: np.ndarray = None, nparts: int = 10, ) -> None: - nodes = [SolenoidTEAPOT(length=length, B=B, nparts=nparts)] - lattice = make_lattice(nodes) + node = SolenoidTEAPOT(length=length, B=B, nparts=nparts) + lattice = make_lattice([node]) if cov_matrix is None: cov_matrix = make_default_cov_matrix() track_and_compare_rms(lattice, kin_energy, cov_matrix) @@ -371,13 +384,14 @@ def test_sc_3d_cold_expansion(): if __name__ == "__main__": - for kin_energy in [0.0025, 1.0, 10.0]: + for kin_energy in [0.0025, 0.1, 1.0]: test_drift_teapot(kin_energy=kin_energy) test_quad_teapot(kin_energy=kin_energy) test_bend_teapot(kin_energy=kin_energy) test_tilt_teapot(kin_energy=kin_energy) test_solenoid_teapot(kin_energy=kin_energy) test_kick_teapot(kin_energy=kin_energy) + test_cf_teapot(kin_energy=kin_energy) test_drift_linac(kin_energy=kin_energy) test_quad_linac(kin_energy=kin_energy) From bff8f34d6e8aff2c939c4db4cc66d24522c46d7f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Fri, 26 Jun 2026 22:42:10 -0400 Subject: [PATCH 172/183] Add envelope.copy function --- py/orbit/envelope/envelope.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index d98d1bc1..c7fdcf6c 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -313,6 +313,14 @@ def __init__( self.rms_bunch_length_factor = np.sqrt(12.0) + def copy(self): + return Envelope( + bunch=self.bunch, + cov_matrix=self.cov_matrix, + centroid=self.centroid, + intensity=self.intensity + ) + def set_intensity(self, intensity: float) -> None: self.intensity = intensity From 2db3220ad0b7f149f0dbe8d7448f912409df55c8 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 12:22:12 -0400 Subject: [PATCH 173/183] Pre-calculate matrices for ring tracking Added method `track_ring` to EnvelopeTracker. It will pre-calculate the transfer matrices in the ring and mark the locations of space charge kicks. If tracking without space charge, it will compute and apply the one-turn matrix. Otherwise it will step through each element. This assumes there is no change of the synchronous particle energy and that all nodes in the lattice are static. --- examples/Envelope/sns_ring/test_sns_ring.py | 2 +- .../Envelope/sns_ring/test_sns_ring_speed.py | 2 +- py/orbit/envelope/envelope.py | 91 ++++++++++++++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index d6fd62b8..0cdb54f3 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -181,7 +181,7 @@ for turn in range(args.turns + 1): if turn > 0: - envelope_tracker.track(envelope) + envelope_tracker.track_ring(envelope) cov_matrix = envelope.cov_matrix centroid = envelope.centroid diff --git a/examples/Envelope/sns_ring/test_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py index 51a5c189..72ba8bb9 100644 --- a/examples/Envelope/sns_ring/test_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -92,7 +92,7 @@ profiler.enable() for turn in trange(args.turns): - envelope_tracker.track(envelope) + envelope_tracker.track_ring(envelope) time_per_turn = (time.time() - start_time) / args.turns diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index c7fdcf6c..7e388fb5 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -474,9 +474,19 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: class EnvelopeTracker: def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: + """Constructor. + + Args: + lattice: The accelerator lattice. + space_charge: Space charge model {"2d", "3d", None}. + """ self.lattice = lattice self.space_charge = space_charge + # For pre-computing elements + self.elements = [] + self.one_turn_matrix = None + for node in self.lattice.getNodes(): if type(node) in (BendTEAPOT, BendLINAC): if node.getParam("ea1") != 0.0 or node.getParam("ea2") != 0.0: @@ -531,9 +541,8 @@ def track(self, envelope: Envelope) -> None: if matrix is not None: envelope.transform(matrix) - def track_history(self, envelope: Envelope) -> dict[str, list]: - """Same as track but returns parameters vs. position in lattice.""" + """Track and return envelope parameters vs. position in lattice.""" history = {} history["position"] = [] history["rms_x"] = [] @@ -599,4 +608,80 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: if matrix is not None: envelope.transform(matrix) - return history \ No newline at end of file + return history + + def precompute_matrices(self, envelope: Envelope) -> None: + """Pre-compute transfer matrices for each node. + + For each node, return tuple (node, matrix). Mark space charge kicks as ("sc", length). + """ + sync_part = envelope.sync_part + charge = envelope.charge() + + self.elements = [] + for node_index, node in enumerate(self.lattice.getNodes()): + for child_node in node.getChildNodes(ENTRANCE): + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((child_node, matrix)) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((child_node, matrix)) + + if self.space_charge: + length = node.getLength(part_index) + if length > 0: + self.elements.append(("sc", length)) + + matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + self.elements.append((node, matrix)) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((node, matrix)) + + for child_node in node.getChildNodes(EXIT): + matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((node, matrix)) + + def track_ring(self, envelope: Envelope) -> None: + """Track using pre-computed transfer matrices. + + The method assumes that all nodes are static and that there is no + change in the synchronous particle energy. In this case the matrices + can be computed once and reused on each turn. If there is no space charge, + we track using the one-turn matrix. + """ + + # Pre-compute transfer matrices on the first turn. + if not self.elements: + self.precompute_matrices(envelope) + self.one_turn_matrix = None + + # If there is no space charge, apply the one-turn transfer matrix. + if not self.space_charge: + if self.one_turn_matrix is None: + self.one_turn_matrix = np.identity(7) + for (node, matrix) in self.elements: + self.one_turn_matrix = np.matmul(self.one_turn_matrix, matrix) + return envelope.transform(self.one_turn_matrix) + + # If there is space charge, apply the matrices one-by-one. + for element in self.elements: + if element[0] == "sc": + length = element[1] + if self.space_charge == "2d": + envelope.transform(envelope.sc_matrix_2d(length)) + elif self.space_charge == "3d": + envelope.transform(envelope.sc_matrix_3d(length)) + else: + raise ValueError + else: + node, matrix = element + envelope.transform(matrix) \ No newline at end of file From 21dc3dcd6b6562a73642ddc67e7f58bf5db1183f Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 13:37:02 -0400 Subject: [PATCH 174/183] Fix bunch length in 2D fodo example; use track_ring --- examples/Envelope/test_env_2d_fodo.py | 8 ++++---- examples/Envelope/test_env_2d_fodo_speed.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index f3a876ed..e09a4832 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -90,7 +90,7 @@ def main(args: argparse.Namespace) -> None: cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y - cov_matrix[4, 4] = args.zrms**2 + cov_matrix[4, 4] = args.bunch_length ** 2 / 12.0 cov_matrix[5, 5] = 0.0 # Tilt @@ -127,7 +127,7 @@ def main(args: argparse.Namespace) -> None: history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): if turn > 0: - tracker.track(envelope) + tracker.track_ring(envelope) cov_matrix = envelope.cov_matrix centroid = envelope.centroid @@ -158,7 +158,7 @@ def main(args: argparse.Namespace) -> None: bunch_coords[:, :4] = gen_dist( size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name=args.dist ) - bunch_coords[:, 4] = 2.0 * rng.uniform(-args.zrms, args.zrms, size=args.nparts) + bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) bunch_coords += centroid_init[None, :6] for i in range(bunch_coords.shape[0]): @@ -294,7 +294,7 @@ def main(args: argparse.Namespace) -> None: if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--zrms", type=float, default=5.0) + parser.add_argument("--bunch-length", type=float, default=5.0) parser.add_argument("--kin-energy", type=float, default=0.0025) parser.add_argument("--intensity", type=float, default=5e9) diff --git a/examples/Envelope/test_env_2d_fodo_speed.py b/examples/Envelope/test_env_2d_fodo_speed.py index 541c2ee7..46c04e19 100644 --- a/examples/Envelope/test_env_2d_fodo_speed.py +++ b/examples/Envelope/test_env_2d_fodo_speed.py @@ -24,7 +24,7 @@ parser = argparse.ArgumentParser() -parser.add_argument("--bunch-length", type=float, default=120.0) +parser.add_argument("--bunch-length", type=float, default=5.0) parser.add_argument("--kin-energy", type=float, default=1.300) parser.add_argument("--intensity", type=float, default=2e14) @@ -74,7 +74,7 @@ cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y -cov_matrix[4, 4] = (args.bunch_length / 4.0) ** 2 +cov_matrix[4, 4] = args.bunch_length**2 / 12.0 cov_matrix[5, 5] = 0.0 cov_matrix_init = np.copy(cov_matrix) @@ -95,7 +95,7 @@ profiler.enable() for turn in trange(args.turns): - envelope_tracker.track(envelope) + envelope_tracker.track_ring(envelope) time_per_turn = (time.time() - start_time) / args.turns From edbc9f17c3aeaa175dc32959f6064d4cdb385816 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 13:38:34 -0400 Subject: [PATCH 175/183] Move tests to /tests folder for use with pytest --- .../Envelope => tests/py/orbit}/test_env.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) rename {examples/Envelope => tests/py/orbit}/test_env.py (93%) diff --git a/examples/Envelope/test_env.py b/tests/py/orbit/test_env.py similarity index 93% rename from examples/Envelope/test_env.py rename to tests/py/orbit/test_env.py index c86a7103..ddd763c9 100644 --- a/examples/Envelope/test_env.py +++ b/tests/py/orbit/test_env.py @@ -380,23 +380,4 @@ def test_sc_3d_cold_expansion(): # (in rest frame). We can calculate the time to expand to # twice the initial size. (See examples from A. Shishlo or # from the ImpactX repo.) - raise NotImplementedError - - -if __name__ == "__main__": - for kin_energy in [0.0025, 0.1, 1.0]: - test_drift_teapot(kin_energy=kin_energy) - test_quad_teapot(kin_energy=kin_energy) - test_bend_teapot(kin_energy=kin_energy) - test_tilt_teapot(kin_energy=kin_energy) - test_solenoid_teapot(kin_energy=kin_energy) - test_kick_teapot(kin_energy=kin_energy) - test_cf_teapot(kin_energy=kin_energy) - - test_drift_linac(kin_energy=kin_energy) - test_quad_linac(kin_energy=kin_energy) - test_bend_linac(kin_energy=kin_energy) - test_tilt_linac(kin_energy=kin_energy) - test_solenoid_linac(kin_energy=kin_energy) - - test_rf_gap_matrix(kin_energy=kin_energy) + pass From 39fd357695723df635c7864674f15c4ac040e660 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 13:41:52 -0400 Subject: [PATCH 176/183] Add to-do comment --- py/orbit/envelope/envelope.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 7e388fb5..caf1aa80 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -486,6 +486,7 @@ def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None # For pre-computing elements self.elements = [] self.one_turn_matrix = None + # [TO DO] option to return one-turn matrix including linear space charge for node in self.lattice.getNodes(): if type(node) in (BendTEAPOT, BendLINAC): From 1532fced8b0a125af36e6dbeb6a97d3a47eaa0d4 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 17:26:07 -0400 Subject: [PATCH 177/183] Rename track_sync_part_ to get_matrix_ --- py/orbit/envelope/envelope.py | 84 +++++++++++++++++------------------ py/orbit/envelope/matrix.py | 22 ++++----- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index caf1aa80..de2885fb 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -41,14 +41,14 @@ from .matrix import get_zp_coeff from .matrix import convert_matrix_dp_p_to_dE from .matrix import convert_matrix_zp_to_dE -from .matrix import track_sync_part_tilt -from .matrix import track_sync_part_kick -from .matrix import track_sync_part_drift -from .matrix import track_sync_part_quad -from .matrix import track_sync_part_bend -from .matrix import track_sync_part_solenoid -from .matrix import track_sync_part_rf_gap -from .matrix import track_sync_part_cf +from .matrix import get_matrix_tilt +from .matrix import get_matrix_kick +from .matrix import get_matrix_drift +from .matrix import get_matrix_quad +from .matrix import get_matrix_bend +from .matrix import get_matrix_solenoid +from .matrix import get_matrix_rf_gap +from .matrix import get_matrix_cf from .utils import gen_dist from .utils import proj_cov_matrix @@ -82,7 +82,7 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A -def track_sync_part( +def get_matrix( node: AccNode, sync_part: SyncParticle, charge: float, @@ -123,7 +123,7 @@ def track_sync_part( if node_type is DriftTEAPOT: if length <= 0: return None - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) elif node_type is SolenoidTEAPOT: if length <= 0: @@ -131,13 +131,13 @@ def track_sync_part( B = node.getParam("B") if node.waveform: B *= node.waveform.getStrength() - return track_sync_part_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) elif node_type is MultipoleTEAPOT: if length <= 0: return None if np.all(np.abs(node.getParam("kls")) == 0): - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) elif node_type is QuadTEAPOT: if length <= 0: @@ -145,7 +145,7 @@ def track_sync_part( kq = node.getParam("kq") if node.waveform: kq *= node.waveform.getStrength() - return track_sync_part_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) elif node_type is BendTEAPOT: if length <= 0: @@ -153,7 +153,7 @@ def track_sync_part( theta = node.getParam("theta") / (nparts - 1) if index == 0 or index == nparts - 1: theta *= 0.5 - return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) elif node_type is KickTEAPOT: scale = 1.0 @@ -167,17 +167,17 @@ def track_sync_part( if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: return np.matmul( - track_sync_part_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), - track_sync_part_drift(sync_part=sync_part, length=length), + get_matrix_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), + get_matrix_drift(sync_part=sync_part, length=length), ) else: - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) elif node_type is TiltTEAPOT: angle = node.getTiltAngle() if angle == 0: return None - return track_sync_part_tilt(sync_part=sync_part, angle=angle) + return get_matrix_tilt(sync_part=sync_part, angle=angle) elif node_type is ContinuousLinearFocusingTEAPOT: if length <= 0: @@ -185,19 +185,19 @@ def track_sync_part( kq = node.getParam("kq") if node.waveform: kq *= node.waveform.getStrength() - return track_sync_part_cf(sync_part=sync_part, length=length, kq=kq) + return get_matrix_cf(sync_part=sync_part, length=length, kq=kq) elif node_type is DriftLINAC: if length <= 0: return None - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) elif node_type is QuadLINAC: if length <= 0: return None brho = 3.335640952 * sync_part.momentum() / charge kq = node.getParam("dB/dr") / brho - return track_sync_part_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) elif node_type is BendLINAC: if length <= 0: @@ -205,7 +205,7 @@ def track_sync_part( theta = node.getParam("theta") / (nparts - 1) if index == 0 or index == nparts - 1: theta *= 0.5 - return track_sync_part_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) elif node_type is DCorrectorHLINAC: length = node.getParam("effLength") / nparts @@ -213,7 +213,7 @@ def track_sync_part( delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() if delta_xp == 0: return None - return track_sync_part_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) + return get_matrix_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) elif node_type is DCorrectorVLINAC: length = node.getParam("effLength") / nparts @@ -221,19 +221,19 @@ def track_sync_part( delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() if delta_yp == 0: return None - return track_sync_part_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) + return get_matrix_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) elif node_type is SolenoidLINAC: if length <= 0: return None B = node.getParam("B") - return track_sync_part_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) elif node_type is TiltLINAC: angle = node.getTiltAngle() if angle == 0: return None - return track_sync_part_tilt(sync_part=sync_part, angle=angle) + return get_matrix_tilt(sync_part=sync_part, angle=angle) elif node_type is BaseRF_Gap: E0TL = node.getParam("E0TL") @@ -260,7 +260,7 @@ def track_sync_part( if amplitude == 0.0: return None - return track_sync_part_rf_gap( + return get_matrix_rf_gap( sync_part=sync_part, frequency=frequency, E0TL=(E0TL * amplitude), @@ -505,13 +505,13 @@ def track(self, envelope: Envelope) -> None: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) @@ -526,19 +526,19 @@ def track(self, envelope: Envelope) -> None: else: raise ValueError - matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) if matrix is not None: if matrix_sc is not None: matrix = matrix @ matrix_sc envelope.transform(matrix) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) @@ -563,13 +563,13 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) @@ -584,7 +584,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: else: raise ValueError - matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) if matrix is not None: if matrix_sc is not None: matrix = matrix @ matrix_sc @@ -600,12 +600,12 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["kin_energy"].append(envelope.sync_part.kinEnergy()) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: envelope.transform(matrix) @@ -622,13 +622,13 @@ def precompute_matrices(self, envelope: Envelope) -> None: self.elements = [] for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: self.elements.append((child_node, matrix)) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: self.elements.append((child_node, matrix)) @@ -637,17 +637,17 @@ def precompute_matrices(self, envelope: Envelope) -> None: if length > 0: self.elements.append(("sc", length)) - matrix = track_sync_part(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) if matrix is not None: self.elements.append((node, matrix)) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: self.elements.append((node, matrix)) for child_node in node.getChildNodes(EXIT): - matrix = track_sync_part(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) if matrix is not None: self.elements.append((node, matrix)) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 418953dc..08c939ff 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -48,7 +48,7 @@ def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.n return matrix -def track_sync_part_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: +def get_matrix_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: cos_phi = math.cos(angle) sin_phi = math.sin(angle) @@ -60,7 +60,7 @@ def track_sync_part_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: return M -def track_sync_part_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: +def get_matrix_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: M = np.identity(7) M[1, -1] = kx M[3, -1] = ky @@ -68,7 +68,7 @@ def track_sync_part_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0 return M -def track_sync_part_drift(sync_part: SyncParticle, length: float) -> np.ndarray: +def get_matrix_drift(sync_part: SyncParticle, length: float) -> np.ndarray: M = np.identity(7) M[0, 1] = length M[2, 3] = length @@ -79,9 +79,9 @@ def track_sync_part_drift(sync_part: SyncParticle, length: float) -> np.ndarray: return M -def track_sync_part_quad(sync_part: SyncParticle, length: float, kq: float, charge: float) -> np.ndarray: +def get_matrix_quad(sync_part: SyncParticle, length: float, kq: float, charge: float) -> np.ndarray: if abs(kq) == 0 or charge == 0: - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) sqrt_abs_kq = math.sqrt(abs(kq)) @@ -120,7 +120,7 @@ def track_sync_part_quad(sync_part: SyncParticle, length: float, kq: float, char return M -def track_sync_part_bend(sync_part: SyncParticle, length: float, theta: float, charge: float) -> np.ndarray: +def get_matrix_bend(sync_part: SyncParticle, length: float, theta: float, charge: float) -> np.ndarray: if length <= 0: return np.identity(7) @@ -145,9 +145,9 @@ def track_sync_part_bend(sync_part: SyncParticle, length: float, theta: float, c return M -def track_sync_part_solenoid(sync_part: SyncParticle, length: float, B: float, charge: float) -> np.ndarray: +def get_matrix_solenoid(sync_part: SyncParticle, length: float, B: float, charge: float) -> np.ndarray: if B == 0: - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) phase = B * length @@ -178,12 +178,12 @@ def track_sync_part_solenoid(sync_part: SyncParticle, length: float, B: float, c return M -def track_sync_part_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarray: +def get_matrix_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarray: if length <= 0: return if kq == 0: - return track_sync_part_drift(sync_part=sync_part, length=length) + return get_matrix_drift(sync_part=sync_part, length=length) sqrt_abs_kq = math.sqrt(abs(kq)) @@ -202,7 +202,7 @@ def track_sync_part_cf(sync_part: SyncParticle, length: float, kq: float) -> np. return M -def track_sync_part_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, phase: float, charge: float) -> np.ndarray: +def get_matrix_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, phase: float, charge: float) -> np.ndarray: gamma = sync_part.gamma() beta = sync_part.beta() mass = sync_part.mass() From 25266738402e394a7ed4c457b9bb01b0674013a2 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 17:58:58 -0400 Subject: [PATCH 178/183] Remove comment --- py/orbit/envelope/envelope.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index de2885fb..523b0310 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -107,12 +107,6 @@ def get_matrix( envelope tracking. """ - # [TO DO] - # - Add option to calculate transfer matrix by fitting routine for all - # elements, or for specified elements. - # - Add option to pre-compute + store matrices for multi-turn tracking - # in static lattice. If no space charge, multiply to get single matrix. - node_type = type(node) if node_type in IGNORE_NODE_TYPES: return None From c279777bc723f9c38d952b0eff8a8ff151796c7b Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 18:04:15 -0400 Subject: [PATCH 179/183] Add docstring --- py/orbit/envelope/matrix.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 08c939ff..f6e1ef6a 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -1,3 +1,8 @@ +"""Analytic transfer matrix definitions. + +The functions below calculate 7 x 7 transfer matrices for common elements such +as quadrupoles, drifts, and bends. +""" import math import numpy as np From db5cd494fea8e6c622112953da10710374ff31a6 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 18:47:14 -0400 Subject: [PATCH 180/183] Organize orbit.envelope module Separate files: - envelopy.py has Envelope class definition. - track.py has EnvelopeTracker class definition. - matrix.py has functions to get transfer matrices from AccNode --- examples/Envelope/sns_linac/test_sns_linac.py | 14 +- examples/Envelope/sns_ring/test_sns_ring.py | 4 +- .../Envelope/sns_ring/test_sns_ring_speed.py | 4 +- examples/Envelope/test_env_2d_fodo.py | 2 +- examples/Envelope/test_env_2d_fodo_speed.py | 4 +- examples/Envelope/test_env_3d_drift.py | 6 +- py/orbit/envelope/__init__.py | 3 +- py/orbit/envelope/envelope.py | 465 +----------------- py/orbit/envelope/matrix.py | 221 +++++++++ py/orbit/envelope/meson.build | 1 + py/orbit/envelope/track.py | 236 +++++++++ 11 files changed, 481 insertions(+), 479 deletions(-) create mode 100644 py/orbit/envelope/track.py diff --git a/examples/Envelope/sns_linac/test_sns_linac.py b/examples/Envelope/sns_linac/test_sns_linac.py index 8183128e..2b6a716a 100755 --- a/examples/Envelope/sns_linac/test_sns_linac.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -1,8 +1,16 @@ +"""SNS linac benchmark. + +Note that there is no analytic benchmark with 3D space charge since there +is no 3D KV equilibrium distribution. For comparison to particle tracking, +we use the `SpaceChargeCalcUnifEllipse` space charge calculator, which +approximates the charge distribution as a uniform-density ellipsoid with +the same x-y-z covariance matrix as the real charge distribution. (It +currently assumes an upright ellipsoid.) +""" import argparse import math import os import random -import time import sys import numpy as np @@ -164,10 +172,10 @@ envelope = Envelope(bunch=bunch, cov_matrix=cov_matrix, intensity=intensity) -envelope_tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) +tracker = EnvelopeTracker(lattice, sc=("3d" if args.sc else None)) histories = {} -histories["envelope"] = envelope_tracker.track_history(envelope) +histories["envelope"] = tracker.track_history(envelope) # Track bunch diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index 0cdb54f3..62d4b60b 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -165,7 +165,7 @@ centroid=centroid_init, intensity=args.intensity, ) -envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) +tracker = EnvelopeTracker(lattice, sc=("2d" if args.sc else None)) history_keys = [ "rms_x", @@ -181,7 +181,7 @@ for turn in range(args.turns + 1): if turn > 0: - envelope_tracker.track_ring(envelope) + tracker.track_ring(envelope) cov_matrix = envelope.cov_matrix centroid = envelope.centroid diff --git a/examples/Envelope/sns_ring/test_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py index 72ba8bb9..773febc6 100644 --- a/examples/Envelope/sns_ring/test_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -84,7 +84,7 @@ cov_matrix=cov_matrix_init, intensity=args.intensity, ) -envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) +tracker = EnvelopeTracker(lattice, sc=("2d" if args.sc else None)) start_time = time.time() @@ -92,7 +92,7 @@ profiler.enable() for turn in trange(args.turns): - envelope_tracker.track_ring(envelope) + tracker.track_ring(envelope) time_per_turn = (time.time() - start_time) / args.turns diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index e09a4832..3a139a6d 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -122,7 +122,7 @@ def main(args: argparse.Namespace) -> None: print("TRACK ENVELOPE") - tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) + tracker = EnvelopeTracker(lattice, sc=("2d" if args.sc else None)) history = {"xrms": [], "yrms": [], "xavg": [], "yavg": []} for turn in range(args.turns): diff --git a/examples/Envelope/test_env_2d_fodo_speed.py b/examples/Envelope/test_env_2d_fodo_speed.py index 46c04e19..4873c897 100644 --- a/examples/Envelope/test_env_2d_fodo_speed.py +++ b/examples/Envelope/test_env_2d_fodo_speed.py @@ -87,7 +87,7 @@ cov_matrix=cov_matrix_init, intensity=args.intensity, ) -envelope_tracker = EnvelopeTracker(lattice, space_charge=("2d" if args.sc else None)) +tracker = EnvelopeTracker(lattice, sc=("2d" if args.sc else None)) start_time = time.time() @@ -95,7 +95,7 @@ profiler.enable() for turn in trange(args.turns): - envelope_tracker.track_ring(envelope) + tracker.track_ring(envelope) time_per_turn = (time.time() - start_time) / args.turns diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index cb5f7fe6..da2fa4c6 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -34,9 +34,7 @@ def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: - return scipy.spatial.transform.Rotation.from_euler( - "xyz", [angle_x, angle_y, angle_z] - ).as_matrix() + return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: @@ -105,7 +103,7 @@ def main(args: argparse.Namespace) -> None: # ------------------------------------------------------------------------------ print("TRACK ENVELOPE") - tracker = EnvelopeTracker(lattice, space_charge=("3d" if args.sc else None)) + tracker = EnvelopeTracker(lattice, sc=("3d" if args.sc else None)) history = {"xrms": [], "yrms": [], "zrms": []} for turn in range(args.turns): diff --git a/py/orbit/envelope/__init__.py b/py/orbit/envelope/__init__.py index 428e361b..7a4586ae 100644 --- a/py/orbit/envelope/__init__.py +++ b/py/orbit/envelope/__init__.py @@ -1,3 +1,2 @@ from .envelope import Envelope -from .envelope import EnvelopeTracker -from . import matrix +from .track import EnvelopeTracker \ No newline at end of file diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 523b0310..f94f2d0b 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -1,77 +1,15 @@ -import math -import warnings - import numpy as np import scipy.constants import scipy.special from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle -from orbit.lattice import AccNode -from orbit.lattice import AccLattice - -from orbit.teapot import ApertureTEAPOT -from orbit.teapot import DriftTEAPOT -from orbit.teapot import BendTEAPOT -from orbit.teapot import KickTEAPOT -from orbit.teapot import MonitorTEAPOT -from orbit.teapot import MultipoleTEAPOT -from orbit.teapot import NodeTEAPOT -from orbit.teapot import QuadTEAPOT -from orbit.teapot import SolenoidTEAPOT -from orbit.teapot import FringeFieldTEAPOT -from orbit.teapot import BunchWrapTEAPOT -from orbit.teapot import TiltTEAPOT -from orbit.teapot import ContinuousLinearFocusingTEAPOT -from orbit.teapot import TurnCounterTEAPOT - -from orbit.py_linac.lattice import MarkerLinacNode as MarkerLINAC -from orbit.py_linac.lattice import Drift as DriftLINAC -from orbit.py_linac.lattice import Quad as QuadLINAC -from orbit.py_linac.lattice import Bend as BendLINAC -from orbit.py_linac.lattice import DCorrectorH as DCorrectorHLINAC -from orbit.py_linac.lattice import DCorrectorV as DCorrectorVLINAC -from orbit.py_linac.lattice import Solenoid as SolenoidLINAC -from orbit.py_linac.lattice import TiltElement as TiltLINAC -from orbit.py_linac.lattice import FringeField as FringeFieldLINAC -from orbit.py_linac.lattice import BaseRF_Gap as BaseRF_Gap -from orbit.py_linac.lattice import LinacApertureNode as ApertureLINAC - -from .matrix import get_dp_p_coeff -from .matrix import get_zp_coeff -from .matrix import convert_matrix_dp_p_to_dE + from .matrix import convert_matrix_zp_to_dE -from .matrix import get_matrix_tilt -from .matrix import get_matrix_kick -from .matrix import get_matrix_drift -from .matrix import get_matrix_quad -from .matrix import get_matrix_bend -from .matrix import get_matrix_solenoid -from .matrix import get_matrix_rf_gap -from .matrix import get_matrix_cf from .utils import gen_dist from .utils import proj_cov_matrix -ENTRANCE = AccNode.ENTRANCE -BODY = AccNode.BODY -EXIT = AccNode.EXIT - -BEFORE = AccNode.BEFORE -AFTER = AccNode.AFTER - -IGNORE_NODE_TYPES = [ - NodeTEAPOT, - MonitorTEAPOT, - FringeFieldTEAPOT, - ApertureTEAPOT, - BunchWrapTEAPOT, - TurnCounterTEAPOT, - MarkerLINAC, - FringeFieldLINAC, -] - - def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: A = np.eye(7) for i in range(eigenvectors.shape[0]): @@ -82,189 +20,6 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A -def get_matrix( - node: AccNode, - sync_part: SyncParticle, - charge: float, - index: int = -1, -) -> np.ndarray | None: - """Calculate transfer matrix and update synchronous particle. - - This function maps various accelerator nodes to 7 x 7 transfer matrices - for envelope tracking. For non-accelerating, finite-length nodes, the - synchronous particle time is updated as in a drift. Accelerating nodes - such as RF gaps will update the synchronous particle energy. - - Args: - node: The accelerator node. - sync_part: Synchronous particle. - charge: Particle charge. (The charge is currently an attribute of the - bunch, not the synchronous particle.) - index: Node part index. An index of -1 will return the transfer matrix - for the entire node. - Returns: - 7 x 7 transfer matrix or None. If None, the node can be ignored during - envelope tracking. - """ - - node_type = type(node) - if node_type in IGNORE_NODE_TYPES: - return None - - length = node.getLength(index) - nparts = node.getnParts() - - if node_type is DriftTEAPOT: - if length <= 0: - return None - return get_matrix_drift(sync_part=sync_part, length=length) - - elif node_type is SolenoidTEAPOT: - if length <= 0: - return None - B = node.getParam("B") - if node.waveform: - B *= node.waveform.getStrength() - return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) - - elif node_type is MultipoleTEAPOT: - if length <= 0: - return None - if np.all(np.abs(node.getParam("kls")) == 0): - return get_matrix_drift(sync_part=sync_part, length=length) - - elif node_type is QuadTEAPOT: - if length <= 0: - return None - kq = node.getParam("kq") - if node.waveform: - kq *= node.waveform.getStrength() - return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) - - elif node_type is BendTEAPOT: - if length <= 0: - return None - theta = node.getParam("theta") / (nparts - 1) - if index == 0 or index == nparts - 1: - theta *= 0.5 - return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) - - elif node_type is KickTEAPOT: - scale = 1.0 - if node.waveform is not None: - scale = node.waveform.getStrength() - - scale /= (nparts - 1) - kx = scale * node.getParam("kx") - ky = scale * node.getParam("ky") - kE = node.getParam("dE") - - if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: - return np.matmul( - get_matrix_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), - get_matrix_drift(sync_part=sync_part, length=length), - ) - else: - return get_matrix_drift(sync_part=sync_part, length=length) - - elif node_type is TiltTEAPOT: - angle = node.getTiltAngle() - if angle == 0: - return None - return get_matrix_tilt(sync_part=sync_part, angle=angle) - - elif node_type is ContinuousLinearFocusingTEAPOT: - if length <= 0: - return None - kq = node.getParam("kq") - if node.waveform: - kq *= node.waveform.getStrength() - return get_matrix_cf(sync_part=sync_part, length=length, kq=kq) - - elif node_type is DriftLINAC: - if length <= 0: - return None - return get_matrix_drift(sync_part=sync_part, length=length) - - elif node_type is QuadLINAC: - if length <= 0: - return None - brho = 3.335640952 * sync_part.momentum() / charge - kq = node.getParam("dB/dr") / brho - return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) - - elif node_type is BendLINAC: - if length <= 0: - return None - theta = node.getParam("theta") / (nparts - 1) - if index == 0 or index == nparts - 1: - theta *= 0.5 - return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) - - elif node_type is DCorrectorHLINAC: - length = node.getParam("effLength") / nparts - field = node.getParam("B") - delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() - if delta_xp == 0: - return None - return get_matrix_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) - - elif node_type is DCorrectorVLINAC: - length = node.getParam("effLength") / nparts - field = node.getParam("B") - delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() - if delta_yp == 0: - return None - return get_matrix_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) - - elif node_type is SolenoidLINAC: - if length <= 0: - return None - B = node.getParam("B") - return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) - - elif node_type is TiltLINAC: - angle = node.getTiltAngle() - if angle == 0: - return None - return get_matrix_tilt(sync_part=sync_part, angle=angle) - - elif node_type is BaseRF_Gap: - E0TL = node.getParam("E0TL") - mode_phase = node.getParam("mode") * math.pi - - cavity = node.getRF_Cavity() - frequency = cavity.getFrequency() - phase = cavity.getPhase() + mode_phase - amplitude = cavity.getAmp() - - arrival_time = sync_part.time() - arrival_time_design = cavity.getDesignArrivalTime() - - if node.isFirstRFGap(): - if cavity.isDesignSetUp(): - phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) - else: - orbitFinalize("Run `trackDesign` first to initialize cavity phases.") - else: - phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase,2.0 * math.pi) - - node.setGapPhase(phase) - - if amplitude == 0.0: - return None - - return get_matrix_rf_gap( - sync_part=sync_part, - frequency=frequency, - E0TL=(E0TL * amplitude), - phase=phase, - charge=charge, - ) - - raise NotImplementedError(str(node)) - - class Envelope: """Represents beam envelope/centroid. @@ -283,7 +38,7 @@ def __init__( intensity: float = 0.0, ) -> None: - # Eventually allow: + # [TO DO] # - setting covariance matrix from bunch particles # - tracking bunch particles as test particles empty_bunch = Bunch() @@ -464,219 +219,3 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: # Convert from z' to dE return convert_matrix_zp_to_dE(M, self.sync_part) - - -class EnvelopeTracker: - def __init__(self, lattice: AccLattice, space_charge: str | None = None) -> None: - """Constructor. - - Args: - lattice: The accelerator lattice. - space_charge: Space charge model {"2d", "3d", None}. - """ - self.lattice = lattice - self.space_charge = space_charge - - # For pre-computing elements - self.elements = [] - self.one_turn_matrix = None - # [TO DO] option to return one-turn matrix including linear space charge - - for node in self.lattice.getNodes(): - if type(node) in (BendTEAPOT, BendLINAC): - if node.getParam("ea1") != 0.0 or node.getParam("ea2") != 0.0: - message = f"Found bend ea1 or ea2 != 0.0 ({node.getName()}.)" - message += " Nonzero edge angles are not yet supported in envelope tracking." - message += " Setting ea1 and ea2 to 0.0." - warnings.warn(message) - - node.setParam("ea1", 0.0) - node.setParam("ea2", 0.0) - - def track(self, envelope: Envelope) -> None: - sync_part = envelope.sync_part - charge = envelope.charge() - - for node_index, node in enumerate(self.lattice.getNodes()): - for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - for part_index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - matrix_sc = None - if self.space_charge: - length = node.getLength(part_index) - if length > 0: - if self.space_charge == "2d": - matrix_sc = envelope.sc_matrix_2d(length) - elif self.space_charge == "3d": - matrix_sc = envelope.sc_matrix_3d(length) - else: - raise ValueError - - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) - if matrix is not None: - if matrix_sc is not None: - matrix = matrix @ matrix_sc - envelope.transform(matrix) - - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - def track_history(self, envelope: Envelope) -> dict[str, list]: - """Track and return envelope parameters vs. position in lattice.""" - history = {} - history["position"] = [] - history["rms_x"] = [] - history["rms_y"] = [] - history["rms_z"] = [] - history["kin_energy"] = [] - - sync_part = envelope.sync_part - charge = envelope.charge() - node_positions = self.lattice.getNodePositionsDict() - - history["position"].append(0.0) - history["rms_x"].append(1000.0 * envelope.rms(0)) - history["rms_y"].append(1000.0 * envelope.rms(2)) - history["rms_z"].append(1000.0 * envelope.rms(4)) - history["kin_energy"].append(envelope.sync_part.kinEnergy()) - - for node_index, node in enumerate(self.lattice.getNodes()): - for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - for part_index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - matrix_sc = None - if self.space_charge: - length = node.getLength(part_index) - if length > 0: - if self.space_charge == "2d": - matrix_sc = envelope.sc_matrix_2d(length) - elif self.space_charge == "3d": - matrix_sc = envelope.sc_matrix_3d(length) - else: - raise ValueError - - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) - if matrix is not None: - if matrix_sc is not None: - matrix = matrix @ matrix_sc - envelope.transform(matrix) - - position_start, position_stop = node_positions[node] - position = position_start + node.getLength(part_index) * (part_index + 1) - - history["position"].append(position) - history["rms_x"].append(1000.0 * envelope.rms(0)) - history["rms_y"].append(1000.0 * envelope.rms(2)) - history["rms_z"].append(1000.0 * envelope.rms(4)) - history["kin_energy"].append(envelope.sync_part.kinEnergy()) - - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - envelope.transform(matrix) - - return history - - def precompute_matrices(self, envelope: Envelope) -> None: - """Pre-compute transfer matrices for each node. - - For each node, return tuple (node, matrix). Mark space charge kicks as ("sc", length). - """ - sync_part = envelope.sync_part - charge = envelope.charge() - - self.elements = [] - for node_index, node in enumerate(self.lattice.getNodes()): - for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - self.elements.append((child_node, matrix)) - - for part_index in range(node.getnParts()): - for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - self.elements.append((child_node, matrix)) - - if self.space_charge: - length = node.getLength(part_index) - if length > 0: - self.elements.append(("sc", length)) - - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) - if matrix is not None: - self.elements.append((node, matrix)) - - for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - self.elements.append((node, matrix)) - - for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) - if matrix is not None: - self.elements.append((node, matrix)) - - def track_ring(self, envelope: Envelope) -> None: - """Track using pre-computed transfer matrices. - - The method assumes that all nodes are static and that there is no - change in the synchronous particle energy. In this case the matrices - can be computed once and reused on each turn. If there is no space charge, - we track using the one-turn matrix. - """ - - # Pre-compute transfer matrices on the first turn. - if not self.elements: - self.precompute_matrices(envelope) - self.one_turn_matrix = None - - # If there is no space charge, apply the one-turn transfer matrix. - if not self.space_charge: - if self.one_turn_matrix is None: - self.one_turn_matrix = np.identity(7) - for (node, matrix) in self.elements: - self.one_turn_matrix = np.matmul(self.one_turn_matrix, matrix) - return envelope.transform(self.one_turn_matrix) - - # If there is space charge, apply the matrices one-by-one. - for element in self.elements: - if element[0] == "sc": - length = element[1] - if self.space_charge == "2d": - envelope.transform(envelope.sc_matrix_2d(length)) - elif self.space_charge == "3d": - envelope.transform(envelope.sc_matrix_3d(length)) - else: - raise ValueError - else: - node, matrix = element - envelope.transform(matrix) \ No newline at end of file diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index f6e1ef6a..90b1a011 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -9,9 +9,47 @@ from orbit.core.bunch import Bunch from orbit.core.bunch import SyncParticle +from orbit.lattice import AccNode +from orbit.teapot import ApertureTEAPOT +from orbit.teapot import DriftTEAPOT +from orbit.teapot import BendTEAPOT +from orbit.teapot import KickTEAPOT +from orbit.teapot import MonitorTEAPOT +from orbit.teapot import MultipoleTEAPOT +from orbit.teapot import NodeTEAPOT +from orbit.teapot import QuadTEAPOT +from orbit.teapot import SolenoidTEAPOT +from orbit.teapot import FringeFieldTEAPOT +from orbit.teapot import BunchWrapTEAPOT +from orbit.teapot import TiltTEAPOT +from orbit.teapot import ContinuousLinearFocusingTEAPOT +from orbit.teapot import TurnCounterTEAPOT +from orbit.py_linac.lattice import MarkerLinacNode as MarkerLINAC +from orbit.py_linac.lattice import Drift as DriftLINAC +from orbit.py_linac.lattice import Quad as QuadLINAC +from orbit.py_linac.lattice import Bend as BendLINAC +from orbit.py_linac.lattice import DCorrectorH as DCorrectorHLINAC +from orbit.py_linac.lattice import DCorrectorV as DCorrectorVLINAC +from orbit.py_linac.lattice import Solenoid as SolenoidLINAC +from orbit.py_linac.lattice import TiltElement as TiltLINAC +from orbit.py_linac.lattice import FringeField as FringeFieldLINAC +from orbit.py_linac.lattice import BaseRF_Gap as BaseRF_Gap +from orbit.py_linac.lattice import LinacApertureNode as ApertureLINAC from orbit.utils.consts import speed_of_light +IGNORE_NODE_TYPES = [ + NodeTEAPOT, + MonitorTEAPOT, + FringeFieldTEAPOT, + ApertureTEAPOT, + BunchWrapTEAPOT, + TurnCounterTEAPOT, + MarkerLINAC, + FringeFieldLINAC, +] + + def get_dp_p_coeff(sync_part: SyncParticle) -> float: # dE/E = (beta^2) * dp/p # dE = (beta^2 * E) * dp/p @@ -246,3 +284,186 @@ def get_matrix_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, ph M[1, 0] = d_rp M[3, 2] = d_rp return M + + +def get_matrix( + node: AccNode, + sync_part: SyncParticle, + charge: float, + index: int = -1, +) -> np.ndarray | None: + """Calculate transfer matrix and update synchronous particle. + + This function maps various accelerator nodes to 7 x 7 transfer matrices + for envelope tracking. For non-accelerating, finite-length nodes, the + synchronous particle time is updated as in a drift. Accelerating nodes + such as RF gaps will update the synchronous particle energy. + + Args: + node: The accelerator node. + sync_part: Synchronous particle. + charge: Particle charge. (The charge is currently an attribute of the + bunch, not the synchronous particle.) + index: Node part index. An index of -1 will return the transfer matrix + for the entire node. + Returns: + 7 x 7 transfer matrix or None. If None, the node can be ignored during + envelope tracking. + """ + + node_type = type(node) + if node_type in IGNORE_NODE_TYPES: + return None + + length = node.getLength(index) + nparts = node.getnParts() + + if node_type is DriftTEAPOT: + if length <= 0: + return None + return get_matrix_drift(sync_part=sync_part, length=length) + + elif node_type is SolenoidTEAPOT: + if length <= 0: + return None + B = node.getParam("B") + if node.waveform: + B *= node.waveform.getStrength() + return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + + elif node_type is MultipoleTEAPOT: + if length <= 0: + return None + if np.all(np.abs(node.getParam("kls")) == 0): + return get_matrix_drift(sync_part=sync_part, length=length) + + elif node_type is QuadTEAPOT: + if length <= 0: + return None + kq = node.getParam("kq") + if node.waveform: + kq *= node.waveform.getStrength() + return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + + elif node_type is BendTEAPOT: + if length <= 0: + return None + theta = node.getParam("theta") / (nparts - 1) + if index == 0 or index == nparts - 1: + theta *= 0.5 + return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + + elif node_type is KickTEAPOT: + scale = 1.0 + if node.waveform is not None: + scale = node.waveform.getStrength() + + scale /= (nparts - 1) + kx = scale * node.getParam("kx") + ky = scale * node.getParam("ky") + kE = node.getParam("dE") + + if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: + return np.matmul( + get_matrix_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), + get_matrix_drift(sync_part=sync_part, length=length), + ) + else: + return get_matrix_drift(sync_part=sync_part, length=length) + + elif node_type is TiltTEAPOT: + angle = node.getTiltAngle() + if angle == 0: + return None + return get_matrix_tilt(sync_part=sync_part, angle=angle) + + elif node_type is ContinuousLinearFocusingTEAPOT: + if length <= 0: + return None + kq = node.getParam("kq") + if node.waveform: + kq *= node.waveform.getStrength() + return get_matrix_cf(sync_part=sync_part, length=length, kq=kq) + + elif node_type is DriftLINAC: + if length <= 0: + return None + return get_matrix_drift(sync_part=sync_part, length=length) + + elif node_type is QuadLINAC: + if length <= 0: + return None + brho = 3.335640952 * sync_part.momentum() / charge + kq = node.getParam("dB/dr") / brho + return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + + elif node_type is BendLINAC: + if length <= 0: + return None + theta = node.getParam("theta") / (nparts - 1) + if index == 0 or index == nparts - 1: + theta *= 0.5 + return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + + elif node_type is DCorrectorHLINAC: + length = node.getParam("effLength") / nparts + field = node.getParam("B") + delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() + if delta_xp == 0: + return None + return get_matrix_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) + + elif node_type is DCorrectorVLINAC: + length = node.getParam("effLength") / nparts + field = node.getParam("B") + delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() + if delta_yp == 0: + return None + return get_matrix_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) + + elif node_type is SolenoidLINAC: + if length <= 0: + return None + B = node.getParam("B") + return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + + elif node_type is TiltLINAC: + angle = node.getTiltAngle() + if angle == 0: + return None + return get_matrix_tilt(sync_part=sync_part, angle=angle) + + elif node_type is BaseRF_Gap: + E0TL = node.getParam("E0TL") + mode_phase = node.getParam("mode") * math.pi + + cavity = node.getRF_Cavity() + frequency = cavity.getFrequency() + phase = cavity.getPhase() + mode_phase + amplitude = cavity.getAmp() + + arrival_time = sync_part.time() + arrival_time_design = cavity.getDesignArrivalTime() + + if node.isFirstRFGap(): + if cavity.isDesignSetUp(): + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase, 2.0 * math.pi) + else: + orbitFinalize("Run `trackDesign` first to initialize cavity phases.") + else: + phase = math.fmod(frequency * (arrival_time - arrival_time_design) * 2.0 * math.pi + phase,2.0 * math.pi) + + node.setGapPhase(phase) + + if amplitude == 0.0: + return None + + return get_matrix_rf_gap( + sync_part=sync_part, + frequency=frequency, + E0TL=(E0TL * amplitude), + phase=phase, + charge=charge, + ) + + raise NotImplementedError(str(node)) \ No newline at end of file diff --git a/py/orbit/envelope/meson.build b/py/orbit/envelope/meson.build index 31c9033b..90eab844 100644 --- a/py/orbit/envelope/meson.build +++ b/py/orbit/envelope/meson.build @@ -2,6 +2,7 @@ py_sources = files([ '__init__.py', 'envelope.py', 'matrix.py', + 'track.py', 'utils.py' ]) diff --git a/py/orbit/envelope/track.py b/py/orbit/envelope/track.py new file mode 100644 index 00000000..c4a4b7f0 --- /dev/null +++ b/py/orbit/envelope/track.py @@ -0,0 +1,236 @@ +import numpy as np +import warnings + +from orbit.core.bunch import Bunch +from orbit.core.bunch import SyncParticle +from orbit.lattice import AccNode +from orbit.lattice import AccLattice +from orbit.teapot import BendTEAPOT +from orbit.py_linac.lattice import Bend as BendLINAC + +from .matrix import get_matrix +from .envelope import Envelope + + +ENTRANCE = AccNode.ENTRANCE +BODY = AccNode.BODY +EXIT = AccNode.EXIT + +BEFORE = AccNode.BEFORE +AFTER = AccNode.AFTER + + +class EnvelopeTracker: + def __init__(self, lattice: AccLattice, sc: str | None = None) -> None: + """Constructor. + + Args: + lattice: The accelerator lattice. + sc: Envelope space charge model {"2d", "3d", None}. + """ + self.lattice = lattice + self.sc = sc + + # For pre-computing elements + self.elements = [] + self.one_turn_matrix = None + # [TO DO] option to return one-turn matrix including linear space charge + + for node in self.lattice.getNodes(): + if type(node) in (BendTEAPOT, BendLINAC): + if node.getParam("ea1") != 0.0 or node.getParam("ea2") != 0.0: + message = f"Found bend ea1 or ea2 != 0.0 ({node.getName()}.)" + message += " Nonzero edge angles are not yet supported in envelope tracking." + message += " Setting ea1 and ea2 to 0.0." + warnings.warn(message) + + node.setParam("ea1", 0.0) + node.setParam("ea2", 0.0) + + def track(self, envelope: Envelope) -> None: + sync_part = envelope.sync_part + charge = envelope.charge() + + for node_index, node in enumerate(self.lattice.getNodes()): + for child_node in node.getChildNodes(ENTRANCE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + matrix_sc = None + if self.sc: + length = node.getLength(part_index) + if length > 0: + if self.sc == "2d": + matrix_sc = envelope.sc_matrix_2d(length) + elif self.sc == "3d": + matrix_sc = envelope.sc_matrix_3d(length) + else: + raise ValueError + + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + if matrix_sc is not None: + matrix = matrix @ matrix_sc + envelope.transform(matrix) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + for child_node in node.getChildNodes(EXIT): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + def track_history(self, envelope: Envelope) -> dict[str, list]: + """Track and return envelope parameters vs. position in lattice.""" + history = {} + history["position"] = [] + history["rms_x"] = [] + history["rms_y"] = [] + history["rms_z"] = [] + history["kin_energy"] = [] + + sync_part = envelope.sync_part + charge = envelope.charge() + node_positions = self.lattice.getNodePositionsDict() + + history["position"].append(0.0) + history["rms_x"].append(1000.0 * envelope.rms(0)) + history["rms_y"].append(1000.0 * envelope.rms(2)) + history["rms_z"].append(1000.0 * envelope.rms(4)) + history["kin_energy"].append(envelope.sync_part.kinEnergy()) + + for node_index, node in enumerate(self.lattice.getNodes()): + for child_node in node.getChildNodes(ENTRANCE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + matrix_sc = None + if self.sc: + length = node.getLength(part_index) + if length > 0: + if self.sc == "2d": + matrix_sc = envelope.sc_matrix_2d(length) + elif self.sc == "3d": + matrix_sc = envelope.sc_matrix_3d(length) + else: + raise ValueError + + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + if matrix_sc is not None: + matrix = matrix @ matrix_sc + envelope.transform(matrix) + + position_start, position_stop = node_positions[node] + position = position_start + node.getLength(part_index) * (part_index + 1) + + history["position"].append(position) + history["rms_x"].append(1000.0 * envelope.rms(0)) + history["rms_y"].append(1000.0 * envelope.rms(2)) + history["rms_z"].append(1000.0 * envelope.rms(4)) + history["kin_energy"].append(envelope.sync_part.kinEnergy()) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + for child_node in node.getChildNodes(EXIT): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + envelope.transform(matrix) + + return history + + def precompute_matrices(self, envelope: Envelope) -> None: + """Pre-compute transfer matrices for each node. + + For each node, return tuple (node, matrix). Mark space charge kicks as ("sc", length). + """ + sync_part = envelope.sync_part + charge = envelope.charge() + + self.elements = [] + for node_index, node in enumerate(self.lattice.getNodes()): + for child_node in node.getChildNodes(ENTRANCE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((child_node, matrix)) + + for part_index in range(node.getnParts()): + for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((child_node, matrix)) + + if self.sc: + length = node.getLength(part_index) + if length > 0: + self.elements.append(("sc", length)) + + matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + if matrix is not None: + self.elements.append((node, matrix)) + + for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((node, matrix)) + + for child_node in node.getChildNodes(EXIT): + matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + if matrix is not None: + self.elements.append((node, matrix)) + + def track_ring(self, envelope: Envelope) -> None: + """Track using pre-computed transfer matrices. + + The method assumes that all nodes are static and that there is no + change in the synchronous particle energy. In this case the matrices + can be computed once and reused on each turn. If there is no space charge, + we track using the one-turn matrix. + """ + + # Pre-compute transfer matrices on the first turn. + if not self.elements: + self.precompute_matrices(envelope) + self.one_turn_matrix = None + + # If there is no space charge, apply the one-turn transfer matrix. + if not self.sc: + if self.one_turn_matrix is None: + self.one_turn_matrix = np.identity(7) + for (node, matrix) in self.elements: + self.one_turn_matrix = np.matmul(self.one_turn_matrix, matrix) + return envelope.transform(self.one_turn_matrix) + + # If there is space charge, apply the matrices one-by-one. + for element in self.elements: + if element[0] == "sc": + length = element[1] + if self.sc == "2d": + envelope.transform(envelope.sc_matrix_2d(length)) + elif self.sc == "3d": + envelope.transform(envelope.sc_matrix_3d(length)) + else: + raise ValueError + else: + node, matrix = element + envelope.transform(matrix) From 0f782bf29b99cd4c56458e9b56d41a774567f236 Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 21:02:42 -0400 Subject: [PATCH 181/183] Clean up examples --- examples/Envelope/sns_linac/diagnostics.py | 4 +- examples/Envelope/sns_linac/test_sns_linac.py | 5 +- examples/Envelope/sns_ring/test_sns_ring.py | 12 +- .../Envelope/sns_ring/test_sns_ring_speed.py | 4 +- examples/Envelope/test_env_2d_fodo.py | 2 +- examples/Envelope/test_env_2d_fodo_speed.py | 1 - examples/Envelope/test_env_3d_drift.py | 7 +- py/orbit/envelope/envelope.py | 80 +++++--- py/orbit/envelope/matrix.py | 174 ++++++++---------- py/orbit/envelope/track.py | 40 ++-- py/orbit/envelope/utils.py | 43 +++++ 11 files changed, 198 insertions(+), 174 deletions(-) diff --git a/examples/Envelope/sns_linac/diagnostics.py b/examples/Envelope/sns_linac/diagnostics.py index d8f557fa..7edfebdd 100644 --- a/examples/Envelope/sns_linac/diagnostics.py +++ b/examples/Envelope/sns_linac/diagnostics.py @@ -35,9 +35,7 @@ def __call__(self, params_dict: dict) -> None: cov_matrix = np.zeros((6, 6)) for i in range(6): for j in range(6): - cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation( - i, j - ) + cov_matrix[i, j] = cov_matrix[j, i] = self.twiss_calc.getCorrelation(i, j) xrms = 1000.0 * np.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * np.sqrt(cov_matrix[2, 2]) diff --git a/examples/Envelope/sns_linac/test_sns_linac.py b/examples/Envelope/sns_linac/test_sns_linac.py index 2b6a716a..045e6155 100755 --- a/examples/Envelope/sns_linac/test_sns_linac.py +++ b/examples/Envelope/sns_linac/test_sns_linac.py @@ -7,6 +7,7 @@ the same x-y-z covariance matrix as the real charge distribution. (It currently assumes an upright ellipsoid.) """ + import argparse import math import os @@ -187,9 +188,7 @@ if args.sc_model == "ellipsoid": n_ellipsoids = 1 sc_calc = SpaceChargeCalcUnifEllipse(n_ellipsoids) - sc_nodes = setUniformEllipsesSCAccNodes( - lattice, args.sc_path_length_min, sc_calc - ) + sc_nodes = setUniformEllipsesSCAccNodes(lattice, args.sc_path_length_min, sc_calc) if args.sc_model == "3d": sc_calc = SpaceChargeCalc3D(64, 64, 64) sc_nodes = setSC3DAccNodes(lattice, args.sc_path_length_min, sc_calc) diff --git a/examples/Envelope/sns_ring/test_sns_ring.py b/examples/Envelope/sns_ring/test_sns_ring.py index 62d4b60b..b7e74716 100644 --- a/examples/Envelope/sns_ring/test_sns_ring.py +++ b/examples/Envelope/sns_ring/test_sns_ring.py @@ -42,9 +42,7 @@ parser.add_argument("--kin-energy", type=float, default=1.300) parser.add_argument("--intensity", type=float, default=2e14) -parser.add_argument( - "--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"] -) +parser.add_argument("--dist", type=str, default="kv", choices=["kv", "waterbag", "gauss"]) parser.add_argument("--eps-x", type=float, default=25.0) parser.add_argument("--eps-y", type=float, default=25.0) parser.add_argument("--mismatch-x", type=float, default=0.0) @@ -59,9 +57,7 @@ parser.add_argument("--sc", type=int, default=0) parser.add_argument("--sc-grid", type=int, default=64) -parser.add_argument( - "--handle-unknown", type=str, default=None, choices=["drift", "fit"] -) +parser.add_argument("--handle-unknown", type=str, default=None, choices=["drift", "fit"]) args = parser.parse_args() # Setup @@ -138,9 +134,7 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist -) +bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix[0:4, 0:4], name=args.dist) bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) bunch_coords[:, 5] *= 0.0 bunch_coords += centroid[None, :6] diff --git a/examples/Envelope/sns_ring/test_sns_ring_speed.py b/examples/Envelope/sns_ring/test_sns_ring_speed.py index 773febc6..73b84ff0 100644 --- a/examples/Envelope/sns_ring/test_sns_ring_speed.py +++ b/examples/Envelope/sns_ring/test_sns_ring_speed.py @@ -110,9 +110,7 @@ rng = np.random.default_rng() bunch_coords = np.zeros((args.nparts, 6)) -bunch_coords[:, :4] = gen_dist( - size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv" -) +bunch_coords[:, :4] = gen_dist(size=args.nparts, cov_matrix=cov_matrix_init[0:4, 0:4], name="kv") bunch_coords[:, 4] = args.bunch_length * rng.uniform(-0.5, 0.5, size=args.nparts) for i in range(bunch_coords.shape[0]): diff --git a/examples/Envelope/test_env_2d_fodo.py b/examples/Envelope/test_env_2d_fodo.py index 3a139a6d..de5a4444 100644 --- a/examples/Envelope/test_env_2d_fodo.py +++ b/examples/Envelope/test_env_2d_fodo.py @@ -90,7 +90,7 @@ def main(args: argparse.Namespace) -> None: cov_matrix[2, 3] = cov_matrix[3, 2] = -eps_y * alpha_y cov_matrix[1, 1] = eps_x * (1.0 + alpha_x**2) / beta_x cov_matrix[3, 3] = eps_y * (1.0 + alpha_y**2) / beta_y - cov_matrix[4, 4] = args.bunch_length ** 2 / 12.0 + cov_matrix[4, 4] = args.bunch_length**2 / 12.0 cov_matrix[5, 5] = 0.0 # Tilt diff --git a/examples/Envelope/test_env_2d_fodo_speed.py b/examples/Envelope/test_env_2d_fodo_speed.py index 4873c897..2b85d185 100644 --- a/examples/Envelope/test_env_2d_fodo_speed.py +++ b/examples/Envelope/test_env_2d_fodo_speed.py @@ -22,7 +22,6 @@ from utils import gen_dist - parser = argparse.ArgumentParser() parser.add_argument("--bunch-length", type=float, default=5.0) parser.add_argument("--kin-energy", type=float, default=1.300) diff --git a/examples/Envelope/test_env_3d_drift.py b/examples/Envelope/test_env_3d_drift.py index da2fa4c6..d66e592b 100644 --- a/examples/Envelope/test_env_3d_drift.py +++ b/examples/Envelope/test_env_3d_drift.py @@ -34,7 +34,9 @@ def rotation_matrix_3d(angle_x: float, angle_y: float, angle_z: float) -> np.ndarray: - return scipy.spatial.transform.Rotation.from_euler("xyz", [angle_x, angle_y, angle_z]).as_matrix() + return scipy.spatial.transform.Rotation.from_euler( + "xyz", [angle_x, angle_y, angle_z] + ).as_matrix() def build_cov_matrix_xyz(rms_sizes: np.ndarray, rotation_matrix: np.ndarray = None) -> np.ndarray: @@ -111,11 +113,10 @@ def main(args: argparse.Namespace) -> None: tracker.track(envelope) cov_matrix = envelope.cov_matrix - centroid = envelope.centroid xrms = 1000.0 * math.sqrt(cov_matrix[0, 0]) yrms = 1000.0 * math.sqrt(cov_matrix[2, 2]) - zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma() + zrms = 1000.0 * math.sqrt(cov_matrix[4, 4]) * envelope.gamma history["xrms"].append(xrms) history["yrms"].append(yrms) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index f94f2d0b..68691519 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -3,9 +3,10 @@ import scipy.special from orbit.core.bunch import Bunch +from orbit.core.bunch import BunchTwissAnalysis from orbit.core.bunch import SyncParticle -from .matrix import convert_matrix_zp_to_dE +from .utils import convert_matrix_zp_to_dE from .utils import gen_dist from .utils import proj_cov_matrix @@ -20,6 +21,15 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A +def get_classical_radius(charge: float, mass: float): + from orbit.utils.consts import charge_electron + from scipy.constants import epsilon_0 + import math + q = charge * charge_electron # [C] + rest_energy = mass * 1e9 * charge_electron # [J] + return q**2 / (4.0 * math.pi * epsilon_0 * rest_energy) + + class Envelope: """Represents beam envelope/centroid. @@ -38,28 +48,43 @@ def __init__( intensity: float = 0.0, ) -> None: - # [TO DO] - # - setting covariance matrix from bunch particles - # - tracking bunch particles as test particles empty_bunch = Bunch() bunch.copyEmptyBunchTo(empty_bunch) self.bunch = empty_bunch - self.sync_part = empty_bunch.getSyncParticle() - - self.classical_radius = self.bunch.classicalRadius() + self.sync_part = self.bunch.getSyncParticle() self.centroid = centroid if self.centroid is None: - self.centroid = np.zeros(6) + if bunch.getSize(): + twiss_calc = BunchTwissAnalysis() + twiss_calc.analyzeBunch(bunch) + self.centroid = np.zeros(6) + for i in range(6): + self.centroid[i] = twiss_calc.getAverage(i) + else: + self.centroid = np.zeros(6) self.cov_matrix = cov_matrix if self.cov_matrix is None: - self.cov_matrix = np.eye(6) + if bunch.getSize(): + twiss_calc = BunchTwissAnalysis() + twiss_calc.analyzeBunch(bunch) + self.cov_matrix = np.zeros((6, 6)) + for i in range(6): + for j in range(6): + self.cov_matrix[i, j] = twiss_calc.getCorrelation(i, j) + self.cov_matrix[j, i] = self.cov_matrix[i, j] + else: + self.cov_matrix = np.eye(6) - self.intensity = 0.0 - self.set_intensity(intensity) + self.intensity = intensity + self.classical_radius = get_classical_radius(self.charge, self.mass) + self.charge_sign = self.charge / abs(self.charge) + # For a uniform one-dimensional distribution over length L, the standard + # deviation is L * sqrt(12). This quantity is used to calculate the line + # density for two-dimensional space charge kicks. self.rms_bunch_length_factor = np.sqrt(12.0) def copy(self): @@ -70,30 +95,30 @@ def copy(self): intensity=self.intensity ) - def set_intensity(self, intensity: float) -> None: - self.intensity = intensity - @property - def sc_factor(self) -> float: - return ( - 2.0 - * self.intensity - * self.classical_radius - / (self.beta() ** 2 * self.gamma() ** 3) - ) - def gamma(self) -> float: return self.sync_part.gamma() + @property def beta(self) -> float: return self.sync_part.beta() + @property def mass(self) -> float: return self.sync_part.mass() + @property def charge(self) -> float: return self.bunch.charge() + @property + def momentum(self) -> float: + return self.sync_part.momentum() + + @property + def sc_factor(self) -> float: + return 2.0 * self.intensity * self.classical_radius / (self.beta ** 2 * self.gamma ** 3) + def rms(self, axis: int = None) -> float | np.ndarray: rms_arr = np.sqrt(np.diag(self.cov_matrix)) return rms_arr[axis] @@ -160,7 +185,7 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: # x' = dx/ds -> x' * gamma # y' = dy/ds -> y' * gamma # z' = dz/ds -> z' - gamma = self.gamma() + gamma = self.gamma gamma_inv = 1.0 / gamma L = np.identity(7) @@ -175,9 +200,6 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: # Get covariance matrix in rest frame. cov_matrix = L_inv[:-1, :-1] @ self.cov_matrix @ L_inv[:-1, :-1].T - # Get kick length in rest frame - length_rest = length * gamma - # Project covariance matrix onto x-y-z plane. cov_matrix_proj = proj_cov_matrix(cov_matrix, axis=(0, 2, 4)) @@ -198,9 +220,9 @@ def sc_matrix_3d(self, length: float) -> np.ndarray: kappa_z = factor * RDz M = np.identity(7) - M[1, 0] = kappa_x * length_rest - M[3, 2] = kappa_y * length_rest - M[5, 4] = kappa_z * length_rest + M[1, 0] = kappa_x * length + M[3, 2] = kappa_y * length + M[5, 4] = kappa_z * length # Build matrix to undo x-y-z diagonalization. A = build_diag_matrix_from_xyz_eig(cov_eig_vecs) diff --git a/py/orbit/envelope/matrix.py b/py/orbit/envelope/matrix.py index 90b1a011..ee0cebdb 100644 --- a/py/orbit/envelope/matrix.py +++ b/py/orbit/envelope/matrix.py @@ -1,8 +1,3 @@ -"""Analytic transfer matrix definitions. - -The functions below calculate 7 x 7 transfer matrices for common elements such -as quadrupoles, drifts, and bends. -""" import math import numpy as np @@ -37,6 +32,9 @@ from orbit.py_linac.lattice import LinacApertureNode as ApertureLINAC from orbit.utils.consts import speed_of_light +from .envelope import Envelope +from .utils import get_dp_p_coeff + IGNORE_NODE_TYPES = [ NodeTEAPOT, @@ -50,48 +48,7 @@ ] -def get_dp_p_coeff(sync_part: SyncParticle) -> float: - # dE/E = (beta^2) * dp/p - # dE = (beta^2 * E) * dp/p - # dE = (beta^2 * gamma * m * c^2) * dp/p - beta = sync_part.beta() - gamma = sync_part.gamma() - rest_energy = sync_part.mass() # GeV - return 1.0 / (beta**2 * gamma * rest_energy) - - -def get_zp_coeff(sync_part: SyncParticle) -> float: - # dE/E = (beta^2) * dp/p = (beta^2) * (gamma^2) z' - # dE = (beta^2 * gamma^2 * E) * z' - # dE = (beta^2 * gamma^3 * m * c^2) * z' - beta = sync_part.beta() - gamma = sync_part.gamma() - rest_energy = sync_part.mass() - return 1.0 / (beta**2 * gamma**3 * rest_energy) - - -def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: - # v = [x, x', y, y', z, dp/p] - # w = [x, x', y, y', z, dE] - # v = A w - # v -> M v - # w -> A M A^-1 - dp_p_coeff = get_dp_p_coeff(sync_part) - matrix[:5, 5] *= dp_p_coeff - matrix[5, :5] /= dp_p_coeff - matrix[5, 6] /= dp_p_coeff # driving term - return matrix - - -def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: - zp_coeff = get_zp_coeff(sync_part) - matrix[:5, 5] *= zp_coeff - matrix[5, :5] /= zp_coeff - matrix[5, 6] /= zp_coeff # driving term - return matrix - - -def get_matrix_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: +def get_matrix_tilt(angle: float) -> np.ndarray: cos_phi = math.cos(angle) sin_phi = math.sin(angle) @@ -103,7 +60,7 @@ def get_matrix_tilt(sync_part: SyncParticle, angle: float) -> np.ndarray: return M -def get_matrix_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: +def get_matrix_kick(kx: float = 0.0, ky: float = 0.0, kE: float = 0.0) -> np.ndarray: M = np.identity(7) M[1, -1] = kx M[3, -1] = ky @@ -111,7 +68,9 @@ def get_matrix_kick(sync_part: SyncParticle, kx: float = 0.0, ky: float = 0.0, k return M -def get_matrix_drift(sync_part: SyncParticle, length: float) -> np.ndarray: +def get_matrix_drift(envelope: Envelope, length: float) -> np.ndarray: + sync_part = envelope.sync_part + M = np.identity(7) M[0, 1] = length M[2, 3] = length @@ -122,9 +81,11 @@ def get_matrix_drift(sync_part: SyncParticle, length: float) -> np.ndarray: return M -def get_matrix_quad(sync_part: SyncParticle, length: float, kq: float, charge: float) -> np.ndarray: - if abs(kq) == 0 or charge == 0: - return get_matrix_drift(sync_part=sync_part, length=length) +def get_matrix_quad(envelope: Envelope, length: float, kq: float) -> np.ndarray: + if abs(kq) == 0: + return get_matrix_drift(envelope=envelope, length=length) + + sync_part = envelope.sync_part sqrt_abs_kq = math.sqrt(abs(kq)) @@ -163,9 +124,8 @@ def get_matrix_quad(sync_part: SyncParticle, length: float, kq: float, charge: f return M -def get_matrix_bend(sync_part: SyncParticle, length: float, theta: float, charge: float) -> np.ndarray: - if length <= 0: - return np.identity(7) +def get_matrix_bend(envelope: Envelope, length: float, theta: float) -> np.ndarray: + sync_part = envelope.sync_part rho = length / theta cx = math.cos(theta) @@ -188,9 +148,11 @@ def get_matrix_bend(sync_part: SyncParticle, length: float, theta: float, charge return M -def get_matrix_solenoid(sync_part: SyncParticle, length: float, B: float, charge: float) -> np.ndarray: +def get_matrix_solenoid(envelope: Envelope, length: float, B: float) -> np.ndarray: if B == 0: - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) + + sync_part = envelope.sync_part phase = B * length @@ -221,12 +183,11 @@ def get_matrix_solenoid(sync_part: SyncParticle, length: float, B: float, charge return M -def get_matrix_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarray: - if length <= 0: - return - +def get_matrix_cf(envelope: Envelope, length: float, kq: float) -> np.ndarray: if kq == 0: - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) + + sync_part = envelope.sync_part sqrt_abs_kq = math.sqrt(abs(kq)) @@ -245,10 +206,13 @@ def get_matrix_cf(sync_part: SyncParticle, length: float, kq: float) -> np.ndarr return M -def get_matrix_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, phase: float, charge: float) -> np.ndarray: +def get_matrix_rf_gap(envelope: Envelope, frequency: float, E0TL: float, phase: float) -> np.ndarray: + sync_part = envelope.sync_part + gamma = sync_part.gamma() beta = sync_part.beta() mass = sync_part.mass() + charge = envelope.charge kin_energy_in = sync_part.kinEnergy() charge_E0TL_sin = charge * E0TL * math.sin(phase) @@ -286,12 +250,7 @@ def get_matrix_rf_gap(sync_part: SyncParticle, frequency: float, E0TL: float, ph return M -def get_matrix( - node: AccNode, - sync_part: SyncParticle, - charge: float, - index: int = -1, -) -> np.ndarray | None: +def get_matrix(node: AccNode, envelope: Envelope, part_index: int = -1) -> np.ndarray | None: """Calculate transfer matrix and update synchronous particle. This function maps various accelerator nodes to 7 x 7 transfer matrices @@ -301,11 +260,9 @@ def get_matrix( Args: node: The accelerator node. - sync_part: Synchronous particle. - charge: Particle charge. (The charge is currently an attribute of the - bunch, not the synchronous particle.) - index: Node part index. An index of -1 will return the transfer matrix - for the entire node. + envelope: The beam envelope. + part_index: Index of the part within the node. An index of -1 returns + the transfer matrix for the entire node. Returns: 7 x 7 transfer matrix or None. If None, the node can be ignored during envelope tracking. @@ -315,43 +272,53 @@ def get_matrix( if node_type in IGNORE_NODE_TYPES: return None - length = node.getLength(index) + length = node.getLength(part_index) nparts = node.getnParts() if node_type is DriftTEAPOT: if length <= 0: return None - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) elif node_type is SolenoidTEAPOT: if length <= 0: return None + B = node.getParam("B") if node.waveform: B *= node.waveform.getStrength() - return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + B *= envelope.charge_sign + + return get_matrix_solenoid(envelope=envelope, length=length, B=B) elif node_type is MultipoleTEAPOT: if length <= 0: return None + if np.all(np.abs(node.getParam("kls")) == 0): - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) elif node_type is QuadTEAPOT: if length <= 0: return None + kq = node.getParam("kq") if node.waveform: kq *= node.waveform.getStrength() - return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + kq *= envelope.charge_sign + + return get_matrix_quad(envelope=envelope, length=length, kq=kq) elif node_type is BendTEAPOT: if length <= 0: return None + theta = node.getParam("theta") / (nparts - 1) - if index == 0 or index == nparts - 1: + if part_index == 0 or part_index == nparts - 1: theta *= 0.5 - return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + theta *= envelope.charge_sign + + return get_matrix_bend(envelope=envelope, length=length, theta=theta) elif node_type is KickTEAPOT: scale = 1.0 @@ -365,73 +332,80 @@ def get_matrix( if abs(kx) > 0 or abs(ky) > 0 or abs(kE) > 0: return np.matmul( - get_matrix_kick(sync_part=sync_part, kx=kx, ky=ky, kE=kE), - get_matrix_drift(sync_part=sync_part, length=length), + get_matrix_kick(kx=kx, ky=ky, kE=kE), + get_matrix_drift(envelope=envelope, length=length), ) else: - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) elif node_type is TiltTEAPOT: angle = node.getTiltAngle() if angle == 0: return None - return get_matrix_tilt(sync_part=sync_part, angle=angle) + return get_matrix_tilt(angle) elif node_type is ContinuousLinearFocusingTEAPOT: if length <= 0: return None + kq = node.getParam("kq") + kq *= envelope.charge_sign if node.waveform: kq *= node.waveform.getStrength() - return get_matrix_cf(sync_part=sync_part, length=length, kq=kq) + + return get_matrix_cf(envelope=envelope, length=length, kq=kq) elif node_type is DriftLINAC: if length <= 0: return None - return get_matrix_drift(sync_part=sync_part, length=length) + return get_matrix_drift(envelope=envelope, length=length) elif node_type is QuadLINAC: if length <= 0: return None - brho = 3.335640952 * sync_part.momentum() / charge + + brho = 3.335640952 * envelope.momentum / envelope.charge kq = node.getParam("dB/dr") / brho - return get_matrix_quad(sync_part=sync_part, length=length, kq=kq, charge=charge) + return get_matrix_quad(envelope=envelope, length=length, kq=kq) elif node_type is BendLINAC: if length <= 0: return None + theta = node.getParam("theta") / (nparts - 1) - if index == 0 or index == nparts - 1: + if part_index == 0 or part_index == nparts - 1: theta *= 0.5 - return get_matrix_bend(sync_part=sync_part, length=length, theta=theta, charge=charge) + theta *= envelope.charge_sign + + return get_matrix_bend(envelope=envelope, length=length, theta=theta) elif node_type is DCorrectorHLINAC: length = node.getParam("effLength") / nparts field = node.getParam("B") - delta_xp = -field * charge * length * 0.299792 / sync_part.momentum() + delta_xp = -field * envelope.charge * length * 0.299792 / envelope.momentum if delta_xp == 0: return None - return get_matrix_kick(sync_part=sync_part, kx=delta_xp, ky=0.0, kE=0.0) + return get_matrix_kick(kx=delta_xp, ky=0.0, kE=0.0) elif node_type is DCorrectorVLINAC: length = node.getParam("effLength") / nparts field = node.getParam("B") - delta_yp = -field * charge * length * 0.299792 / sync_part.momentum() + delta_yp = -field * envelope.charge * length * 0.299792 / envelope.momentum if delta_yp == 0: return None - return get_matrix_kick(sync_part=sync_part, kx=0.0, ky=delta_yp, kE=0.0) + return get_matrix_kick(kx=0.0, ky=delta_yp, kE=0.0) elif node_type is SolenoidLINAC: if length <= 0: return None - B = node.getParam("B") - return get_matrix_solenoid(sync_part=sync_part, length=length, B=B, charge=charge) + B = node.getParam("B") * envelope.charge_sign + return get_matrix_solenoid(envelope=envelope, length=length, B=B) elif node_type is TiltLINAC: angle = node.getTiltAngle() if angle == 0: return None - return get_matrix_tilt(sync_part=sync_part, angle=angle) + return get_matrix_tilt(angle=angle) elif node_type is BaseRF_Gap: E0TL = node.getParam("E0TL") @@ -442,6 +416,7 @@ def get_matrix( phase = cavity.getPhase() + mode_phase amplitude = cavity.getAmp() + sync_part = envelope.sync_part arrival_time = sync_part.time() arrival_time_design = cavity.getDesignArrivalTime() @@ -459,11 +434,10 @@ def get_matrix( return None return get_matrix_rf_gap( - sync_part=sync_part, + envelope=envelope, frequency=frequency, E0TL=(E0TL * amplitude), phase=phase, - charge=charge, ) raise NotImplementedError(str(node)) \ No newline at end of file diff --git a/py/orbit/envelope/track.py b/py/orbit/envelope/track.py index c4a4b7f0..98653fe9 100644 --- a/py/orbit/envelope/track.py +++ b/py/orbit/envelope/track.py @@ -48,18 +48,19 @@ def __init__(self, lattice: AccLattice, sc: str | None = None) -> None: node.setParam("ea2", 0.0) def track(self, envelope: Envelope) -> None: - sync_part = envelope.sync_part - charge = envelope.charge() + """Track envelope through lattice. + This is not recursive, so grandchild nodes are not tracked. + """ for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) @@ -74,19 +75,19 @@ def track(self, envelope: Envelope) -> None: else: raise ValueError - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, envelope=envelope, part_index=part_index) if matrix is not None: if matrix_sc is not None: matrix = matrix @ matrix_sc envelope.transform(matrix) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) @@ -99,8 +100,6 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["rms_z"] = [] history["kin_energy"] = [] - sync_part = envelope.sync_part - charge = envelope.charge() node_positions = self.lattice.getNodePositionsDict() history["position"].append(0.0) @@ -111,13 +110,13 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) @@ -132,7 +131,7 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: else: raise ValueError - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, envelope=envelope, part_index=part_index) if matrix is not None: if matrix_sc is not None: matrix = matrix @ matrix_sc @@ -148,12 +147,12 @@ def track_history(self, envelope: Envelope) -> dict[str, list]: history["kin_energy"].append(envelope.sync_part.kinEnergy()) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: envelope.transform(matrix) @@ -164,19 +163,16 @@ def precompute_matrices(self, envelope: Envelope) -> None: For each node, return tuple (node, matrix). Mark space charge kicks as ("sc", length). """ - sync_part = envelope.sync_part - charge = envelope.charge() - self.elements = [] for node_index, node in enumerate(self.lattice.getNodes()): for child_node in node.getChildNodes(ENTRANCE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: self.elements.append((child_node, matrix)) for part_index in range(node.getnParts()): for child_node in node.getChildNodes(BODY, part_index, place_in_part=BEFORE): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: self.elements.append((child_node, matrix)) @@ -185,17 +181,17 @@ def precompute_matrices(self, envelope: Envelope) -> None: if length > 0: self.elements.append(("sc", length)) - matrix = get_matrix(node, sync_part=sync_part, charge=charge, index=part_index) + matrix = get_matrix(node, envelope=envelope, part_index=part_index) if matrix is not None: self.elements.append((node, matrix)) for child_node in node.getChildNodes(BODY, part_index, place_in_part=AFTER): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: self.elements.append((node, matrix)) for child_node in node.getChildNodes(EXIT): - matrix = get_matrix(child_node, sync_part=sync_part, charge=charge) + matrix = get_matrix(child_node, envelope=envelope) if matrix is not None: self.elements.append((node, matrix)) diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index 5eb76799..db80b049 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -1,5 +1,48 @@ import numpy as np +from orbit.core.bunch import SyncParticle + + +def get_dp_p_coeff(sync_part: SyncParticle) -> float: + # dE/E = (beta^2) * dp/p + # dE = (beta^2 * E) * dp/p + # dE = (beta^2 * gamma * m * c^2) * dp/p + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() # GeV + return 1.0 / (beta**2 * gamma * rest_energy) + + +def get_zp_coeff(sync_part: SyncParticle) -> float: + # dE/E = (beta^2) * dp/p = (beta^2) * (gamma^2) z' + # dE = (beta^2 * gamma^2 * E) * z' + # dE = (beta^2 * gamma^3 * m * c^2) * z' + beta = sync_part.beta() + gamma = sync_part.gamma() + rest_energy = sync_part.mass() + return 1.0 / (beta**2 * gamma**3 * rest_energy) + + +def convert_matrix_dp_p_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + # v = [x, x', y, y', z, dp/p] + # w = [x, x', y, y', z, dE] + # v = A w + # v -> M v + # w -> A M A^-1 + dp_p_coeff = get_dp_p_coeff(sync_part) + matrix[:5, 5] *= dp_p_coeff + matrix[5, :5] /= dp_p_coeff + matrix[5, 6] /= dp_p_coeff # driving term + return matrix + + +def convert_matrix_zp_to_dE(matrix: np.ndarray, sync_part: SyncParticle) -> np.ndarray: + zp_coeff = get_zp_coeff(sync_part) + matrix[:5, 5] *= zp_coeff + matrix[5, :5] /= zp_coeff + matrix[5, 6] /= zp_coeff # driving term + return matrix + def gen_dist_gauss(size: int, cov_matrix: np.ndarray) -> np.ndarray: return np.random.multivariate_normal( From 1311f4e02586079fd64d25d6c90807aac8e3f2cc Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 21:06:35 -0400 Subject: [PATCH 182/183] Fix tests --- tests/py/orbit/test_env.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/py/orbit/test_env.py b/tests/py/orbit/test_env.py index ddd763c9..16f1e5c7 100644 --- a/tests/py/orbit/test_env.py +++ b/tests/py/orbit/test_env.py @@ -4,6 +4,8 @@ from orbit.core.bunch import BunchTwissAnalysis from orbit.core.linac import MatrixRfGap from orbit.bunch_utils import collect_bunch +from orbit.envelope import Envelope +from orbit.envelope import EnvelopeTracker from orbit.lattice import AccNode from orbit.lattice import AccLattice from orbit.py_linac.lattice import Drift @@ -20,8 +22,6 @@ from orbit.teapot import TiltTEAPOT from orbit.teapot import TEAPOT_Lattice from orbit.utils.consts import mass_proton -from orbit.envelope import Envelope -from orbit.envelope import EnvelopeTracker def get_lorentz_factors(kin_energy: float, mass: float) -> tuple[float, float]: @@ -357,17 +357,17 @@ def test_rf_gap_matrix( coords_out_1 = collect_bunch(bunch_out_1)["coords"] - from orbit.envelope.matrix import track_sync_part_rf_gap + from orbit.envelope.matrix import get_matrix_rf_gap bunch_out_2 = Bunch() bunch_in.copyBunchTo(bunch_out_2) - matrix = track_sync_part_rf_gap( - sync_part=bunch_in.getSyncParticle(), + envelope = Envelope(bunch=bunch_in) + matrix = get_matrix_rf_gap( + envelope=envelope, frequency=frequency, E0TL=E0TL, phase=phase, - charge=bunch_in.charge(), ) coords_in = np.column_stack([coords_in, np.ones(coords_in.shape[0])]) coords_out_2 = np.matmul(coords_in, matrix.T) From 3403d5d72fbc9272712cd962a18db93ea79fe04b Mon Sep 17 00:00:00 2001 From: austin-hoover Date: Mon, 29 Jun 2026 21:23:19 -0400 Subject: [PATCH 183/183] Move get_classical_radius to utils.py --- py/orbit/envelope/envelope.py | 10 +--------- py/orbit/envelope/utils.py | 9 +++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/py/orbit/envelope/envelope.py b/py/orbit/envelope/envelope.py index 68691519..2c181626 100644 --- a/py/orbit/envelope/envelope.py +++ b/py/orbit/envelope/envelope.py @@ -8,6 +8,7 @@ from .utils import convert_matrix_zp_to_dE from .utils import gen_dist +from .utils import get_classical_radius from .utils import proj_cov_matrix @@ -21,15 +22,6 @@ def build_diag_matrix_from_xyz_eig(eigenvectors: np.ndarray) -> np.ndarray: return A -def get_classical_radius(charge: float, mass: float): - from orbit.utils.consts import charge_electron - from scipy.constants import epsilon_0 - import math - q = charge * charge_electron # [C] - rest_energy = mass * 1e9 * charge_electron # [J] - return q**2 / (4.0 * math.pi * epsilon_0 * rest_energy) - - class Envelope: """Represents beam envelope/centroid. diff --git a/py/orbit/envelope/utils.py b/py/orbit/envelope/utils.py index db80b049..71499e31 100644 --- a/py/orbit/envelope/utils.py +++ b/py/orbit/envelope/utils.py @@ -1,6 +1,15 @@ +import math import numpy as np +from scipy.constants import epsilon_0 from orbit.core.bunch import SyncParticle +from orbit.utils.consts import charge_electron + + +def get_classical_radius(charge: float, mass: float) -> float: + q = charge * charge_electron # [C] + rest_energy = mass * 1e9 * charge_electron # [J] + return q**2 / (4.0 * math.pi * epsilon_0 * rest_energy) def get_dp_p_coeff(sync_part: SyncParticle) -> float: