From 2a0216522c395afe89ab65dac0d437c315eb900c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Charleux?= Date: Thu, 21 Mar 2024 10:02:50 -0700 Subject: [PATCH 001/111] save test --- cheetah/accelerator.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index cf1c77f2..78d7b064 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -298,6 +298,68 @@ def defining_features(self) -> list[str]: def __repr__(self) -> str: return f"{self.__class__.__name__}(length={repr(self.length)})" + + +class SpaceChargeKick(Element): + """ + Simulates space charge effects on a beam. + :param length: Length in meters. + :param name: Unique identifier of the element. + """ + + def __init__( + self, + length: Union[torch.Tensor, nn.Parameter], + name: Optional[str] = None, + device=None, + dtype=torch.float32, + ) -> None: + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__(name=name) + + self.length = torch.as_tensor(length, **factory_kwargs) + + def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: + device = self.length.device + dtype = self.length.dtype + + gamma = energy / rest_energy.to(device=device, dtype=dtype) + igamma2 = ( + 1 / gamma**2 + if gamma != 0 + else torch.tensor(0.0, device=device, dtype=dtype) + ) + beta = torch.sqrt(1 - igamma2) + + tm = torch.eye(7, device=device, dtype=dtype) + tm[0, 1] = self.length + tm[2, 3] = self.length + tm[4, 5] = -self.length / beta**2 * igamma2 + + return tm + + @property + def is_skippable(self) -> bool: + return True + + def split(self, resolution: torch.Tensor) -> list[Element]: + split_elements = [] + remaining = self.length + while remaining > 0: + element = Drift(torch.min(resolution, remaining)) + split_elements.append(element) + remaining -= resolution + return split_elements + + def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: + pass + + @property + def defining_features(self) -> list[str]: + return super().defining_features + ["length"] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(length={repr(self.length)})" class Quadrupole(Element): From 8c0cbd38e65417eca9e68014871fa99f1c862619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Charleux?= Date: Fri, 5 Apr 2024 10:28:01 -0700 Subject: [PATCH 002/111] first commit --- cheetah/accelerator.py | 89 ++++++++++++++++++++++++++++----- test.py | 0 tests/test_space_charge_kick.py | 43 ++++++++++++++++ 3 files changed, 119 insertions(+), 13 deletions(-) delete mode 100644 test.py create mode 100644 tests/test_space_charge_kick.py diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 78d7b064..35a528f8 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -45,7 +45,7 @@ def __init__(self, name: Optional[str] = None) -> None: self.name = name if name is not None else generate_unique_name() def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: - r""" + """ Generates the element's transfer map that describes how the beam and its particles are transformed when traveling through the element. The state vector consists of 6 values with a physical meaning: @@ -303,13 +303,19 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ Simulates space charge effects on a beam. - :param length: Length in meters. + :param grid_points: Number of grid points in each dimension. + :param grid_dimensions: Dimensions of the grid in meters. :param name: Unique identifier of the element. """ def __init__( self, - length: Union[torch.Tensor, nn.Parameter], + nx: Union[torch.Tensor, nn.Parameter], + ny: Union[torch.Tensor, nn.Parameter], + ns: Union[torch.Tensor, nn.Parameter], + dx: Union[torch.Tensor, nn.Parameter], + dy: Union[torch.Tensor, nn.Parameter], + ds: Union[torch.Tensor, nn.Parameter], name: Optional[str] = None, device=None, dtype=torch.float32, @@ -317,7 +323,73 @@ def __init__( factory_kwargs = {"device": device, "dtype": dtype} super().__init__(name=name) - self.length = torch.as_tensor(length, **factory_kwargs) + self.nx = torch.as_tensor(nx, **factory_kwargs) + self.ny = torch.as_tensor(ny, **factory_kwargs) + self.ns = torch.as_tensor(ns, **factory_kwargs) + self.dx = torch.as_tensor(dx, **factory_kwargs) #in meters + self.dy = torch.as_tensor(dy, **factory_kwargs) + self.ds = torch.as_tensor(ds, **factory_kwargs) + + def grid_shape(self) -> torch.Tensor: + return torch.tensor([self.nx, self.ny, self.ns], device=self.nx.device) + + def grid_dimensions(self) -> torch.Tensor: + return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) + + def create_grid(self) -> torch.Tensor: + """ + Create a 3D grid for the space charge kick. + """ + x = torch.linspace(-self.dx / 2, self.dx / 2, self.nx) #here centered on 0, may need to change center? + y = torch.linspace(-self.dy / 2, self.dy / 2, self.ny) + s = torch.linspace(-self.ds / 2, self.ds / 2, self.ns) + + grid = torch.meshgrid(x, y, s) + return torch.stack(grid, dim=-1) + + def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + """ + Deposition of the beam on the grid. + """ + charge_density = torch.zeros(self.grid_shape, dtype=torch.float32) # Initialize the charge density grid + grid = self.create_grid() + + # Compute the grid cell size + cell_size = 2*self.grid_dimensions / self.grid_shape + + # Loop over each particle + n_particles = beam.num_particles + particle_pos = beam.particles[:, [0,2,4]] + particle_charge = beam.particle_charges + for p in range(n_particles): + # Compute the normalized position of the particle within the grid + part_pos = particle_pos[p] + normalized_pos = (part_pos + self.grid_dimensions) / cell_size + + # Find the index of the lower corner of the cell containing the particle + cell_index = torch.floor(normalized_pos).type(torch.long) + + # Distribute the charge to the surrounding cells + for dx in range(2): + for dy in range(2): + for ds in range(2): + # Compute the indices of the surrounding cell + idx_x = cell_index[0] + dx + idx_y = cell_index[1] + dy + idx_s = cell_index[2] + ds + index = torch.tensor([idx_x, idx_y, idx_s]) + + # Calculate the weights for the surrounding cells + weights = 1 - torch.abs(normalized_pos - index) + + # Compute the weight for this cell + weight = weights[0] * weights[1] * weights[2] + + # Add the charge contribution to the cell + if 0 <= idx_x < self.grid_shape[0] and 0 <= idx_y < self.grid_shape[1] and 0 <= idx_s < self.grid_shape[2]: + charge_density[idx_x, idx_y, idx_s] += weight * particle_charge[p] + + return charge_density def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: device = self.length.device @@ -342,15 +414,6 @@ def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: def is_skippable(self) -> bool: return True - def split(self, resolution: torch.Tensor) -> list[Element]: - split_elements = [] - remaining = self.length - while remaining > 0: - element = Drift(torch.min(resolution, remaining)) - split_elements.append(element) - remaining -= resolution - return split_elements - def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: pass diff --git a/test.py b/test.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py new file mode 100644 index 00000000..5da470c9 --- /dev/null +++ b/tests/test_space_charge_kick.py @@ -0,0 +1,43 @@ +import pytest +import torch + +import cheetah + +def test_charge_deposition(): + """ + Test that the charge deposition is correct for a particle beam. The first test checks that the total charge is preserved, and the second test checks that the charge is deposited in the correct grid cells. + """ + space_charge_kick = cheetah.SpaceChargeKick(nx=32,ny=32,ns=32,dx=3e-9,dy=3e-9,ds=2e-6) + incoming_beam = cheetah.ParticleBeam.from_parameters( + num_particles=torch.tensor(1000), + sigma_xp=torch.tensor(2e-7), + sigma_yp=torch.tensor(2e-7), + ) + total_charge = incoming_beam.total_charge + space_charge_grid = space_charge_kick.space_charge_deposition(incoming_beam) + + assert torch.isclose(space_charge_grid.sum() * space_charge_kick.grid_resolution ** 3, torch.tensor(total_charge), atol=1e-12) # grid_resolution is a parameter of the space charge kick #Total charge is preserved + + # something similar to the read function in the CIC code should be implemented + assert outgoing_beam.sigma_y > incoming_beam.sigma_y + + +@pytest.mark.skip( + reason="Requires rewriting Element and Beam member variables to be buffers." +) +def test_device_like_torch_module(): + """ + Test that when changing the device, Drift reacts like a `torch.nn.Module`. + """ + # There is no point in running this test, if there aren't two different devices to + # move between + if not torch.cuda.is_available(): + return + + element = cheetah.Drift(length=torch.tensor(0.2), device="cuda") + + assert element.length.device.type == "cuda" + + element = element.cpu() + + assert element.length.device.type == "cpu" \ No newline at end of file From 610f706c69e4e0bcfc744a7f836c1263b5174d08 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:02:09 -0700 Subject: [PATCH 003/111] maj1 --- cheetah/accelerator.py | 35 ++++++++ tests/Charge_Deposition_test.ipynb | 84 +++++++++++++++++++ tests/SimpleLatticeTest.ipynb | 129 +++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 tests/Charge_Deposition_test.ipynb create mode 100644 tests/SimpleLatticeTest.ipynb diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 35a528f8..29f9c0e3 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -299,6 +299,35 @@ def defining_features(self) -> list[str]: def __repr__(self) -> str: return f"{self.__class__.__name__}(length={repr(self.length)})" +class chancla(Element): + """ + Simulates space charge effects on a beam. + :param grid_points: Number of grid points in each dimension. + :param grid_dimensions: Dimensions of the grid in meters. + :param name: Unique identifier of the element. + """ + + def __init__( + self, + nx: Union[torch.Tensor, nn.Parameter], + ny: Union[torch.Tensor, nn.Parameter], + ns: Union[torch.Tensor, nn.Parameter], + dx: Union[torch.Tensor, nn.Parameter], + dy: Union[torch.Tensor, nn.Parameter], + ds: Union[torch.Tensor, nn.Parameter], + name: Optional[str] = None, + device=None, + dtype=torch.float32, + ) -> None: + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__(name=name) + + self.nx = torch.as_tensor(nx, **factory_kwargs) + self.ny = torch.as_tensor(ny, **factory_kwargs) + self.ns = torch.as_tensor(ns, **factory_kwargs) + self.dx = torch.as_tensor(dx, **factory_kwargs) #in meters + self.dy = torch.as_tensor(dy, **factory_kwargs) + self.ds = torch.as_tensor(ds, **factory_kwargs) class SpaceChargeKick(Element): """ @@ -390,6 +419,12 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o charge_density[idx_x, idx_y, idx_s] += weight * particle_charge[p] return charge_density + + def split(self, resolution: torch.Tensor) -> list[Element]: + # TODO: Implement splitting for cavity properly, for now just returns the + # element itself + return [self] + def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: device = self.length.device diff --git a/tests/Charge_Deposition_test.ipynb b/tests/Charge_Deposition_test.ipynb new file mode 100644 index 00000000..7f285801 --- /dev/null +++ b/tests/Charge_Deposition_test.ipynb @@ -0,0 +1,84 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "id": "46802055-9833-4d37-94c7-4b8d09844e91", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "from cheetah import (\n", + " SpaceChargeKick,\n", + " ParticleBeam,\n", + " Segment,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c6c3fec6-de93-4315-b812-5c2ab26a9445", + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Can't instantiate abstract class SpaceChargeKick without an implementation for abstract method 'split'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[9], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m space_charge \u001b[38;5;241m=\u001b[39m SpaceChargeKick(nx\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,ny\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,ns\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,dx\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m3e-9\u001b[39m,dy\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m3e-9\u001b[39m,ds\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2e-6\u001b[39m)\n\u001b[0;32m 2\u001b[0m incoming_beam \u001b[38;5;241m=\u001b[39m ParticleBeam\u001b[38;5;241m.\u001b[39mfrom_parameters(\n\u001b[0;32m 3\u001b[0m num_particles\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m1000\u001b[39m),\n\u001b[0;32m 4\u001b[0m sigma_xp\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m2e-7\u001b[39m),\n\u001b[0;32m 5\u001b[0m sigma_yp\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m2e-7\u001b[39m),\n\u001b[0;32m 6\u001b[0m )\n", + "\u001b[1;31mTypeError\u001b[0m: Can't instantiate abstract class SpaceChargeKick without an implementation for abstract method 'split'" + ] + } + ], + "source": [ + "space_charge = SpaceChargeKick(nx=32,ny=32,ns=32,dx=3e-9,dy=3e-9,ds=2e-6)\n", + "incoming_beam = ParticleBeam.from_parameters(\n", + " num_particles=torch.tensor(1000),\n", + " sigma_xp=torch.tensor(2e-7),\n", + " sigma_yp=torch.tensor(2e-7),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9479b8b9-1e8a-442b-80a7-dfa879512467", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0af05a0-dec7-45e5-95fa-17fd4af53bd9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/SimpleLatticeTest.ipynb b/tests/SimpleLatticeTest.ipynb new file mode 100644 index 00000000..9da00ba0 --- /dev/null +++ b/tests/SimpleLatticeTest.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "id": "ed5cecaa-ef2d-4707-ac21-a4259dbd54b7", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "from cheetah import (\n", + " BPM,\n", + " Drift,\n", + " HorizontalCorrector,\n", + " ParameterBeam,\n", + " Segment,\n", + " VerticalCorrector,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cf1086e9-2ae9-47a9-b1b8-1f5378cb13e4", + "metadata": {}, + "outputs": [], + "source": [ + "segment = Segment(\n", + " elements=[\n", + " BPM(name=\"BPM1SMATCH\"),\n", + " Drift(length=torch.tensor(1.0)),\n", + " BPM(name=\"BPM6SMATCH\"),\n", + " Drift(length=torch.tensor(1.0)),\n", + " VerticalCorrector(length=torch.tensor(0.3), name=\"V7SMATCH\"),\n", + " Drift(length=torch.tensor(0.2)),\n", + " HorizontalCorrector(length=torch.tensor(0.3), name=\"H10SMATCH\"),\n", + " Drift(length=torch.tensor(7.0)),\n", + " HorizontalCorrector(length=torch.tensor(0.3), name=\"H12SMATCH\"),\n", + " Drift(length=torch.tensor(0.05)),\n", + " BPM(name=\"BPM13SMATCH\"),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b84fbc0b-fa10-4882-9084-97c6601450ac", + "metadata": {}, + "outputs": [], + "source": [ + "segment.V7SMATCH.angle = torch.tensor(3.142e-3)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9a197bf6-b327-4cd2-8fed-c0a3fe60586b", + "metadata": {}, + "outputs": [], + "source": [ + "incoming_beam = ParameterBeam.from_parameters(\n", + " sigma_xp=torch.tensor(2e-7), sigma_yp=torch.tensor(2e-7)\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e1a83402-b003-4e04-a91c-ac32127883bb", + "metadata": {}, + "outputs": [], + "source": [ + "outgoing_beam = segment.track(incoming_beam)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "63a42125-9197-4ad4-bc34-765fecc8c075", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACZmklEQVR4nOzdd3hb5aE/8O/Rlrz3irM3BBLsJIxCAmSH3qaUMFIgAcoopJTm0v6gg1F6mwsUmrILLQml5UJpS0ohUEIIZaWEOIQCGWRBHDvetmRb60jn/f2hYW1LXpLl7+d59Ng65+icVz6x/M07JSGEABERERENe6pkF4CIiIiIBgaDHREREVGaYLAjIiIiShMMdkRERERpgsGOiIiIKE0w2BERERGlCQY7IiIiojTBYEdERESUJhjsiIiIiNIEgx0RRXXw4EEsWrQIOTk5kCQJmzdvTnaRRrRNmzZBkiR8+eWXCb1uzZo1GDt27KCUiYhSC4MdURrw/cH3PTQaDSoqKrBmzRrU1dX1+byrV6/Gp59+iv/5n//Bs88+i+rq6gEs9fDy9ttvB/2MtVotxo8fjyuvvBJHjhwZ0Gv98pe/TMkQ/eWXXwb9DGI9Eg2fRDQwNMkuABENnJ///OcYN24c7HY7/v3vf2PTpk1477338Nlnn8FgMCR0LpvNhh07duAnP/kJ1q5dO0glHn5uvvlmzJ49G7IsY/fu3XjyySfx6quv4tNPP0V5efmAXOOXv/wlLrroIqxYsSJo+xVXXIFLL70Uer1+QK6TqKKiIjz77LNB2x544AEcP34cv/71r8OOJaKhx2BHlEaWLl3qr1X7zne+g8LCQtx77714+eWXcfHFFyd0rubmZgBAbm7ugJXPbrdDp9NBpRq+jQVnn302LrroIgDAVVddhcmTJ+Pmm2/GM888g9tvv73P5xVCwG63w2g0Rj1GrVZDrVb3+Rr9lZGRgcsvvzxo2/PPP4/29vaw7YHieW9ENDCG76crEfXq7LPPBgAcPnw4aPv+/ftx0UUXIT8/HwaDAdXV1Xj55Zf9+++66y6MGTMGAPDDH/4QkiQF9dGqq6vD1VdfjZKSEuj1epx00kl4+umng67ha7p8/vnn8dOf/hQVFRUwmUywWCwAgA8//BBLlixBTk4OTCYT5s2bh/fffz/oHHfddRckScKhQ4ewZs0a5ObmIicnB1dddRWsVmvY+/3jH/+IOXPmwGQyIS8vD+eccw7eeOONoGNee+01nH322cjIyEBWVhaWL1+Ozz//PMGfbI/zzjsPAHD06FEAwMaNG3HeeeehuLgYer0e06dPx+OPPx72urFjx+KCCy7AP//5T1RXV8NoNOK3v/0tJElCd3c3nnnmGX+z5po1awBE72P32muvYd68ecjKykJ2djZmz56N5557Lma5FUXBhg0bcNJJJ8FgMKCkpATXX3892tvb+/yz6O29JfLzifd9xfPvqLOzE7fccgvGjh0LvV6P4uJiLFy4ELt37+73eyVKNayxI0pjvgCQl5fn3/b555/jrLPOQkVFBW677TZkZGTgz3/+M1asWIG//vWv+OY3v4kLL7wQubm5+MEPfoDLLrsMy5YtQ2ZmJgCgsbERp59+OiRJwtq1a1FUVITXXnsN11xzDSwWC2655ZagMtxzzz3Q6XS49dZb4XA4oNPp8NZbb2Hp0qWoqqrCnXfeCZVK5f+D/+6772LOnDlB57j44osxbtw4rF+/Hrt378bvfvc7FBcX49577/Ufc/fdd+Ouu+7CmWeeiZ///OfQ6XT48MMP8dZbb2HRokUAgGeffRarV6/G4sWLce+998JqteLxxx/H1772NXz88cd9GmDgC80FBQUAgMcffxwnnXQS/uu//gsajQb/+Mc/cOONN0JRFNx0001Brz1w4AAuu+wyXH/99bj22msxZcoUPPvss/jOd76DOXPm4LrrrgMATJgwIer1N23ahKuvvhonnXQSbr/9duTm5uLjjz/G66+/jlWrVkV93fXXX49Nmzbhqquuws0334yjR4/ikUcewccff4z3338fWq024Z9Fb+8tkZ9PPO8r3n9HN9xwA/7yl79g7dq1mD59OlpbW/Hee+9h3759OO200/r1PolSjiCiYW/jxo0CgHjzzTdFc3OzqK2tFX/5y19EUVGR0Ov1ora21n/s+eefL2bMmCHsdrt/m6Io4swzzxSTJk3ybzt69KgAIO6///6ga11zzTWirKxMtLS0BG2/9NJLRU5OjrBarUIIIbZv3y4AiPHjx/u3+a41adIksXjxYqEoin+71WoV48aNEwsXLvRvu/POOwUAcfXVVwdd65vf/KYoKCjwPz948KBQqVTim9/8pnC73UHH+q7R2dkpcnNzxbXXXhu0v6GhQeTk5IRtD+V7P08/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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "segment.plot_overview(beam=incoming_beam)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b900640-3478-41fe-9d82-607c74cc03a9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 99f3c53824a224bfd10fe31a71a9877be1bd2536 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:12:29 -0700 Subject: [PATCH 004/111] commit2 --- cheetah/accelerator.py | 36 +------- tests/Charge_Deposition_test.ipynb | 84 ------------------- tests/SimpleLatticeTest.ipynb | 129 ----------------------------- 3 files changed, 3 insertions(+), 246 deletions(-) delete mode 100644 tests/Charge_Deposition_test.ipynb delete mode 100644 tests/SimpleLatticeTest.ipynb diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 29f9c0e3..046866ff 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -298,36 +298,6 @@ def defining_features(self) -> list[str]: def __repr__(self) -> str: return f"{self.__class__.__name__}(length={repr(self.length)})" - -class chancla(Element): - """ - Simulates space charge effects on a beam. - :param grid_points: Number of grid points in each dimension. - :param grid_dimensions: Dimensions of the grid in meters. - :param name: Unique identifier of the element. - """ - - def __init__( - self, - nx: Union[torch.Tensor, nn.Parameter], - ny: Union[torch.Tensor, nn.Parameter], - ns: Union[torch.Tensor, nn.Parameter], - dx: Union[torch.Tensor, nn.Parameter], - dy: Union[torch.Tensor, nn.Parameter], - ds: Union[torch.Tensor, nn.Parameter], - name: Optional[str] = None, - device=None, - dtype=torch.float32, - ) -> None: - factory_kwargs = {"device": device, "dtype": dtype} - super().__init__(name=name) - - self.nx = torch.as_tensor(nx, **factory_kwargs) - self.ny = torch.as_tensor(ny, **factory_kwargs) - self.ns = torch.as_tensor(ns, **factory_kwargs) - self.dx = torch.as_tensor(dx, **factory_kwargs) #in meters - self.dy = torch.as_tensor(dy, **factory_kwargs) - self.ds = torch.as_tensor(ds, **factory_kwargs) class SpaceChargeKick(Element): """ @@ -359,8 +329,8 @@ def __init__( self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) - def grid_shape(self) -> torch.Tensor: - return torch.tensor([self.nx, self.ny, self.ns], device=self.nx.device) + def grid_shape(self) -> tuple[int]: + return (self.nx, self.ny, self.ns) def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) @@ -380,7 +350,7 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o """ Deposition of the beam on the grid. """ - charge_density = torch.zeros(self.grid_shape, dtype=torch.float32) # Initialize the charge density grid + charge_density = torch.zeros(self.grid_shape(), dtype=torch.float32) # Initialize the charge density grid grid = self.create_grid() # Compute the grid cell size diff --git a/tests/Charge_Deposition_test.ipynb b/tests/Charge_Deposition_test.ipynb deleted file mode 100644 index 7f285801..00000000 --- a/tests/Charge_Deposition_test.ipynb +++ /dev/null @@ -1,84 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 8, - "id": "46802055-9833-4d37-94c7-4b8d09844e91", - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "\n", - "from cheetah import (\n", - " SpaceChargeKick,\n", - " ParticleBeam,\n", - " Segment,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "c6c3fec6-de93-4315-b812-5c2ab26a9445", - "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "Can't instantiate abstract class SpaceChargeKick without an implementation for abstract method 'split'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[9], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m space_charge \u001b[38;5;241m=\u001b[39m SpaceChargeKick(nx\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,ny\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,ns\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m32\u001b[39m,dx\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m3e-9\u001b[39m,dy\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m3e-9\u001b[39m,ds\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2e-6\u001b[39m)\n\u001b[0;32m 2\u001b[0m incoming_beam \u001b[38;5;241m=\u001b[39m ParticleBeam\u001b[38;5;241m.\u001b[39mfrom_parameters(\n\u001b[0;32m 3\u001b[0m num_particles\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m1000\u001b[39m),\n\u001b[0;32m 4\u001b[0m sigma_xp\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m2e-7\u001b[39m),\n\u001b[0;32m 5\u001b[0m sigma_yp\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mtensor(\u001b[38;5;241m2e-7\u001b[39m),\n\u001b[0;32m 6\u001b[0m )\n", - "\u001b[1;31mTypeError\u001b[0m: Can't instantiate abstract class SpaceChargeKick without an implementation for abstract method 'split'" - ] - } - ], - "source": [ - "space_charge = SpaceChargeKick(nx=32,ny=32,ns=32,dx=3e-9,dy=3e-9,ds=2e-6)\n", - "incoming_beam = ParticleBeam.from_parameters(\n", - " num_particles=torch.tensor(1000),\n", - " sigma_xp=torch.tensor(2e-7),\n", - " sigma_yp=torch.tensor(2e-7),\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9479b8b9-1e8a-442b-80a7-dfa879512467", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f0af05a0-dec7-45e5-95fa-17fd4af53bd9", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tests/SimpleLatticeTest.ipynb b/tests/SimpleLatticeTest.ipynb deleted file mode 100644 index 9da00ba0..00000000 --- a/tests/SimpleLatticeTest.ipynb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 6, - "id": "ed5cecaa-ef2d-4707-ac21-a4259dbd54b7", - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "\n", - "from cheetah import (\n", - " BPM,\n", - " Drift,\n", - " HorizontalCorrector,\n", - " ParameterBeam,\n", - " Segment,\n", - " VerticalCorrector,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "cf1086e9-2ae9-47a9-b1b8-1f5378cb13e4", - "metadata": {}, - "outputs": [], - "source": [ - "segment = Segment(\n", - " elements=[\n", - " BPM(name=\"BPM1SMATCH\"),\n", - " Drift(length=torch.tensor(1.0)),\n", - " BPM(name=\"BPM6SMATCH\"),\n", - " Drift(length=torch.tensor(1.0)),\n", - " VerticalCorrector(length=torch.tensor(0.3), name=\"V7SMATCH\"),\n", - " Drift(length=torch.tensor(0.2)),\n", - " HorizontalCorrector(length=torch.tensor(0.3), name=\"H10SMATCH\"),\n", - " Drift(length=torch.tensor(7.0)),\n", - " HorizontalCorrector(length=torch.tensor(0.3), name=\"H12SMATCH\"),\n", - " Drift(length=torch.tensor(0.05)),\n", - " BPM(name=\"BPM13SMATCH\"),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b84fbc0b-fa10-4882-9084-97c6601450ac", - "metadata": {}, - "outputs": [], - "source": [ - "segment.V7SMATCH.angle = torch.tensor(3.142e-3)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "9a197bf6-b327-4cd2-8fed-c0a3fe60586b", - "metadata": {}, - "outputs": [], - "source": [ - "incoming_beam = ParameterBeam.from_parameters(\n", - " sigma_xp=torch.tensor(2e-7), sigma_yp=torch.tensor(2e-7)\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e1a83402-b003-4e04-a91c-ac32127883bb", - "metadata": {}, - "outputs": [], - "source": [ - "outgoing_beam = segment.track(incoming_beam)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "63a42125-9197-4ad4-bc34-765fecc8c075", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACZmklEQVR4nOzdd3hb5aE/8O/Rlrz3irM3BBLsJIxCAmSH3qaUMFIgAcoopJTm0v6gg1F6mwsUmrILLQml5UJpS0ohUEIIZaWEOIQCGWRBHDvetmRb60jn/f2hYW1LXpLl7+d59Ng65+icVz6x/M07JSGEABERERENe6pkF4CIiIiIBgaDHREREVGaYLAjIiIiShMMdkRERERpgsGOiIiIKE0w2BERERGlCQY7IiIiojTBYEdERESUJhjsiIiIiNIEgx0RRXXw4EEsWrQIOTk5kCQJmzdvTnaRRrRNmzZBkiR8+eWXCb1uzZo1GDt27KCUiYhSC4MdURrw/cH3PTQaDSoqKrBmzRrU1dX1+byrV6/Gp59+iv/5n//Bs88+i+rq6gEs9fDy9ttvB/2MtVotxo8fjyuvvBJHjhwZ0Gv98pe/TMkQ/eWXXwb9DGI9Eg2fRDQwNMkuABENnJ///OcYN24c7HY7/v3vf2PTpk1477338Nlnn8FgMCR0LpvNhh07duAnP/kJ1q5dO0glHn5uvvlmzJ49G7IsY/fu3XjyySfx6quv4tNPP0V5efmAXOOXv/wlLrroIqxYsSJo+xVXXIFLL70Uer1+QK6TqKKiIjz77LNB2x544AEcP34cv/71r8OOJaKhx2BHlEaWLl3qr1X7zne+g8LCQtx77714+eWXcfHFFyd0rubmZgBAbm7ugJXPbrdDp9NBpRq+jQVnn302LrroIgDAVVddhcmTJ+Pmm2/GM888g9tvv73P5xVCwG63w2g0Rj1GrVZDrVb3+Rr9lZGRgcsvvzxo2/PPP4/29vaw7YHieW9ENDCG76crEfXq7LPPBgAcPnw4aPv+/ftx0UUXIT8/HwaDAdXV1Xj55Zf9+++66y6MGTMGAPDDH/4QkiQF9dGqq6vD1VdfjZKSEuj1epx00kl4+umng67ha7p8/vnn8dOf/hQVFRUwmUywWCwAgA8//BBLlixBTk4OTCYT5s2bh/fffz/oHHfddRckScKhQ4ewZs0a5ObmIicnB1dddRWsVmvY+/3jH/+IOXPmwGQyIS8vD+eccw7eeOONoGNee+01nH322cjIyEBWVhaWL1+Ozz//PMGfbI/zzjsPAHD06FEAwMaNG3HeeeehuLgYer0e06dPx+OPPx72urFjx+KCCy7AP//5T1RXV8NoNOK3v/0tJElCd3c3nnnmGX+z5po1awBE72P32muvYd68ecjKykJ2djZmz56N5557Lma5FUXBhg0bcNJJJ8FgMKCkpATXX3892tvb+/yz6O29JfLzifd9xfPvqLOzE7fccgvGjh0LvV6P4uJiLFy4ELt37+73eyVKNayxI0pjvgCQl5fn3/b555/jrLPOQkVFBW677TZkZGTgz3/+M1asWIG//vWv+OY3v4kLL7wQubm5+MEPfoDLLrsMy5YtQ2ZmJgCgsbERp59+OiRJwtq1a1FUVITXXnsN11xzDSwWC2655ZagMtxzzz3Q6XS49dZb4XA4oNPp8NZbb2Hp0qWoqqrCnXfeCZVK5f+D/+6772LOnDlB57j44osxbtw4rF+/Hrt378bvfvc7FBcX49577/Ufc/fdd+Ouu+7CmWeeiZ///OfQ6XT48MMP8dZbb2HRokUAgGeffRarV6/G4sWLce+998JqteLxxx/H1772NXz88cd9GmDgC80FBQUAgMcffxwnnXQS/uu//gsajQb/+Mc/cOONN0JRFNx0001Brz1w4AAuu+wyXH/99bj22msxZcoUPPvss/jOd76DOXPm4LrrrgMATJgwIer1N23ahKuvvhonnXQSbr/9duTm5uLjjz/G66+/jlWrVkV93fXXX49Nmzbhqquuws0334yjR4/ikUcewccff4z3338fWq024Z9Fb+8tkZ9PPO8r3n9HN9xwA/7yl79g7dq1mD59OlpbW/Hee+9h3759OO200/r1PolSjiCiYW/jxo0CgHjzzTdFc3OzqK2tFX/5y19EUVGR0Ov1ora21n/s+eefL2bMmCHsdrt/m6Io4swzzxSTJk3ybzt69KgAIO6///6ga11zzTWirKxMtLS0BG2/9NJLRU5OjrBarUIIIbZv3y4AiPHjx/u3+a41adIksXjxYqEoin+71WoV48aNEwsXLvRvu/POOwUAcfXVVwdd65vf/KYoKCjwPz948KBQqVTim9/8pnC73UHH+q7R2dkpcnNzxbXXXhu0v6GhQeTk5IRtD+V7P08//bRobm4W9fX14tVXXxVjx44VkiSJjz76yP8+Qi1evFiMHz8+aNuYMWMEAPH666+HHZ+RkSFWr14dtt13n48ePSqEEKKjo0NkZWWJuXPnCpvNFvF9CyHE6tWrxZgxY/zP3333XQFA/OlPfwp6zeuvvx5xeyzLly8POndv7y2en0887yuRf0c5OTnipptuivs9EQ1nbIolSiMLFixAUVERKisrcdFFFyEjIwMvv/wyRo0aBQBoa2vDW2+9hYsvvhidnZ1oaWlBS0sLWltbsXjxYhw8eDDmKFohBP7617/i61//OoQQ/te3tLRg8eLFMJvNYc1bq1evDupbtWfPHhw8eBCrVq1Ca2ur//Xd3d04//zz8c4770BRlKBz3HDDDUHPzz77bLS2tvqbdTdv3gxFUXDHHXeE9d+TJAkAsHXrVnR0dOCyyy4LKrdarcbcuXOxffv2uH7GV199NYqKilBeXo7ly5f7m019fRsD36vZbEZLSwvmzZuHI0eOwGw2B51r3LhxWLx4cVzXjWTr1q3o7OzEbbfdFjY4xve+I3nxxReRk5ODhQsXBv0sqqqqkJmZGffPIpZo7y2en0887yuRf0e5ubn48MMPUV9f3+/3RZTq2BTbi3feeQf3338/ampqcOLECbz00kthI9UGWl1dHf7f//t/eO2112C1WjFx4kRs3LhxRE81QfF59NFHMXnyZJjNZjz99NN45513gkZQHjp0CEII/OxnP8PPfvaziOdoampCRUVFxH3Nzc3o6OjAk08+iSeffDLq6wONGzcu6PnBgwcBeAJfNGazOaj5ePTo0UH7ffva29uRnZ2Nw4cPQ6VSYfr06VHP6buur09cqOzs7KivDXTHHXfg7LPPhlqtRmFhIaZNmwaNpuej9P3338edd96JHTt2hPUDNJvNyMnJ8T8P/dkkytcMfPLJJyf0uoMHD8JsNqO4uDji/tB72BfR3ls8P5943lci/47uu+8+rF69GpWVlaiqqsKyZctw5ZVXYvz48Ym+LaKUx2DXi+7ubpx66qm4+uqrceGFFw769drb23HWWWfh3HPPxWuvvYaioiIcPHgw6I8cUTRz5szx/wdgxYoV+NrXvoZVq1bhwIEDyMzM9Ndg3HrrrVFriiZOnBj1/L7XX3755VH/oJ5yyilBz0NHQvrOcf/992PmzJkRz+Hrz+cTbSSoECJqWUP5rvvss8+itLQ0bH9gOItlxowZWLBgQcR9hw8fxvnnn4+pU6fiwQcfRGVlJXQ6HbZs2YJf//rXYTWRyRolqigKiouL8ac//Sni/oGYqiTSe0v05xNLIv+OLr74Ypx99tl46aWX8MYbb+D+++/Hvffei7/97W9YunRp4m+OKIUx2PVi6dKlMX/xHQ4HfvKTn+D//u//0NHRgZNPPhn33nsv5s+f36fr3XvvvaisrMTGjRv92/r7v3oamdRqNdavX49zzz0XjzzyCG677TZ/DYVWq40aTmIpKipCVlYW3G53n14P9AwEyM7O7vM5Ip1TURTs3bs36h9533WLi4sH7Lqh/vGPf8DhcODll18OqmVMtGkzVjNqIN97+uyzz2IG8kive/PNN3HWWWcNabiM9+cTz/tK9N9RWVkZbrzxRtx4441oamrCaaedhv/5n/9hsKO0wz52/bR27Vrs2LEDzz//PP7zn/9g5cqVWLJkib+ZIFEvv/wyqqursXLlShQXF2PWrFl46qmnBrjUNFLMnz8fc+bMwYYNG2C321FcXIz58+fjt7/9LU6cOBF2vG/uumjUajW+9a1v4a9//Ss+++yzhF8PAFVVVZgwYQJ+9atfoaurq0/nCLVixQqoVCr8/Oc/D6v18dXqLV68GNnZ2fjlL38JWZYH5LqhfDWLgTWJZrM56D9q8cjIyEBHR0evxy1atAhZWVlYv3497HZ70L5YtZkXX3wx3G437rnnnrB9Lpcrrmv3Rbw/n3jeV7z/jtxud1jfxuLiYpSXl8PhcPT/TRGlGNbY9cOxY8ewceNGHDt2zD/j/K233orXX38dGzduxC9/+cuEz3nkyBE8/vjjWLduHX784x/jo48+ws033wydThezLwlRND/84Q+xcuVKbNq0CTfccAMeffRRfO1rX8OMGTNw7bXXYvz48WhsbMSOHTtw/PhxfPLJJzHP97//+7/Yvn075s6di2uvvRbTp09HW1sbdu/ejTfffBNtbW0xX69SqfC73/0OS5cuxUknnYSrrroKFRUVqKurw/bt25GdnY1//OMfCb3HiRMn4ic/+QnuuecenH322bjwwguh1+vx0Ucfoby8HOvXr0d2djYef/xxXHHFFTjttNNw6aWXoqioCMeOHcOrr76Ks846C4888khC1w21aNEi6HQ6fP3rX8f111+Prq4uPPXUUyguLo4YpKOpqqrCm2++iQcffBDl5eUYN24c5s6dG3ZcdnY2fv3rX+M73/kOZs+ejVWrViEvLw+ffPIJrFYrnnnmmYjnnzdvHq6//nqsX78ee/bswaJFi6DVanHw4EG8+OKL+M1vfuOfhHkgxfvzied9xfvvqLOzE6NGjcJFF12EU089FZmZmXjzzTfx0Ucf4YEHHhjw90iUdEkbjzsMARAvvfSS//krr7wiAIiMjIygh0ajERdffLEQQoh9+/YJADEf/+///T//ObVarTjjjDOCrvu9731PnH766UPyHml48k2D4ZtyI5Db7RYTJkwQEyZMEC6XSwghxOHDh8WVV14pSktLhVarFRUVFeKCCy4Qf/nLX/yvizbdiRBCNDY2iptuuklUVlYKrVYrSktLxfnnny+efPJJ/zG+6UFefPHFiGX++OOPxYUXXigKCgqEXq8XY8aMERdffLHYtm2b/xjfdCfNzc0R369v2g+fp59+WsyaNUvo9XqRl5cn5s2bJ7Zu3Rp0zPbt28XixYtFTk6OMBgMYsKECWLNmjVi165dUX668b0fn5dfflmccsopwmAwiLFjx4p7771XPP3002HlHTNmjFi+fHnEc+zfv1+cc845wmg0CgD+qU+ive+XX35ZnHnmmcJoNIrs7GwxZ84c8X//93/+/aHTnfg8+eSToqqqShiNRpGVlSVmzJghfvSjH4n6+vqY7zFQtOlOor23eH8+8bwvIXr/d+RwOMQPf/hDceqpp4qsrCyRkZEhTj31VPHYY4/F/R6JhhNJiAR6H49wkiQFjYp94YUX8O1vfxuff/55WOfuzMxMlJaWwul09rpAeEFBgb+z8pgxY7Bw4UL87ne/8+9//PHH8Ytf/KJfi7kTERFR+mNTbD/MmjULbrcbTU1N/qWbQul0OkydOjXuc5511lk4cOBA0LYvvvjCv7wTERERUTQMdr3o6urCoUOH/M+PHj2KPXv2ID8/H5MnT8a3v/1tXHnllXjggQcwa9YsNDc3Y9u2bTjllFOwfPnyhK/3gx/8AGeeeSZ++ctf4uKLL8bOnTtjzhlGRERE5MOm2F68/fbbOPfcc8O2r169Gps2bYIsy/jFL36BP/zhD6irq0NhYSFOP/103H333ZgxY0afrvnKK6/g9ttvx8GDBzFu3DisW7cO1157bX/fChEREaU5BjsiIiKiNMF57IiIiIjSBIMdERERUZrg4IkIFEVBfX09srKy4l7ah4iIiGgwCCHQ2dmJ8vJyqFSx6+QY7CKor69HZWVlsotBRERE5FdbW4tRo0bFPIbBLoKsrCwAnh9gdnb2oFxDlmW88cYb/qV8KHl4L1ID70Pq4L1IDbwPqSPZ98JisaCystKfT2JhsIvA1/yanZ09qMHOZDIhOzubv7BJxnuRGngfUgfvRWrgfUgdqXIv4ukexsETRERERGmCwY6IiIgoTTDYEREREaUJ9rEjIiIiioNdUfCV1Y5jNieO2Z3erw60OF14adbElJgijcGOiIiICIBLETjhlHHM5vAHt1q7E1/ZHDiYWYGOD/ZFfW27y418bfJjVfJLQERERDQEhBBodro8oc3uDApwx+xO1DuccIkoL1Z5IpNJrcJog87zMHq/GvQw9DJx8FBJy2C3fv16/O1vf8P+/fthNBpx5pln4t5778WUKVOSXTQiIiIaRGY5MLgFN5ketzthU6IlNw+dJGFUQHCrNOhQoVXjWM1HuPT8+SgxGlKiyTWatAx2//rXv3DTTTdh9uzZcLlc+PGPf4xFixZh7969yMjISHbxiIiIqI+sbgW1gbVtdidqvQGu1u6E2eWO+XoJQLlei0p/jZs+oOZNh1K9FqqQ4CbLMrYoThRoNSkd6oA0DXavv/560PNNmzahuLgYNTU1OOecc5JUKiIiIuqNrAjUOwJq20JCXLPT1es5CrUaf1gLCnAGHSoMWuhSpNl0MKRlsAtlNpsBAPn5+UkuCRER0cimCIFGp+wfmBDUZGp3oN4uQ+nlHFlqVVBYqzQGN51mqNVD8l5SUdoHO0VRcMstt+Css87CySefHPEYh8MBh8Phf26xWAB4ql5lWR6UcvnOO1jnp/jxXqQG3ofUwXuRGobrfRBCoMPlxjG7jFqHE7V22dtM6nleZ5fhELH7ueklCaMMnubSSr3WW/PW8zxXo47eJKookJXeomFikn0vErmuJEQvP91h7rvf/S5ee+01vPfeexg1alTEY+666y7cfffdYdufe+45mEymwS4iERHRsGKHhFaVBi0qjeerFPC9SgO7FLupUyUE8oQbhYoLhYoLBd6vhcLzNUu4uYJCAKvVilWrVsFsNve6hn1aB7u1a9fi73//O9555x2MGzcu6nGRauwqKyvR0tLS6w+wr2RZxtatW7Fw4UIu7pxkvBepgfchdfBepIZk3genoqDOIXtq3exO1DoCat3sTrT1MkABAIq1mp5aNoMWo/U9tW5lOi20qtQehBAo2b8TFosFhYWFcQW7tGyKFULge9/7Hl566SW8/fbbMUMdAOj1euj1+rDtWq120G/gUFyD4sN7kRp4H1IH70VqGIz74BYCDd6w5psKJHB06QmHjN5qfXI16pD+bXrPQAXvgAWjOv3q3JL1O5HINdMy2N1000147rnn8Pe//x1ZWVloaGgAAOTk5MBoNCa5dERERINLCIEW2eUPar6pQHrmc5Mh99JgZ1RJqAyZCmR0QIjL1ozcAQqpLC2D3eOPPw4AmD9/ftD2jRs3Ys2aNUNfICIiogHW6XJHXD3BF+Ks7tgDCDQSMMo3HYghZD43ow6Fw2DONgqXlsEujbsNEhHRCGFXFDSoNNje1ol6lxLWZNoex0S8pf4RpcHLX4026lCm10LN4JZ20jLYERERpTqXbyLekNUTfAGu0ekCMiuAvceiniNfq/bWuIU3mY4y6KBP44l4KTIGOyIiokHQrwXnvfRCwbgMI8aY9GHNpZUGHTLZzy2pXK5uOJ1NcDpbkZtbneziAGCwIyIi6rMO34LzIctf1Xr7udn7sOC8b0WFco2EHVvfwPKzl3F08hDzBTaHowkORyNsthPQ6Xdi3/5tkOUW/z63u9v/mnPn74dKlfz7xGBHREQURawF54/ZHbC4Yg9QUAEo02sjL38VZcF5H1mWwR5wAys0sDmdzXA4GuHwbvPsa4bb3RX2Wp0OaG4OP6danQG9vhgulwU6XcEQvIvYGOyIiGjEkhWBusAF5721bX1dcN43FYivBq5cn94LzqeKoMDmbILTG9wc3uAWK7BFo1aboNeXQKcrhlZbiLq6bkydMgdGYxn0+mLvviJoNJmD+M4Sx2BHRERpK3DB+dDF5o/ZPBPx9raqaLZGhdGGgMl3ueD8kIkY2MJq3JoSDmw6nSeY6fXF0OuKofN+jRbYZFnG0SNbMGpU6jeLM9gREdGwJYRAm+wOmIDXETQZ73GHE45e+rkZVJJ32auA2raA6UFytfxTOdDcbqunRs3RDIezceADm67IE9B6CWzpiP9aiYgopXX7JuINWf7qmM0T4Lp6mYhXLQHl+pCVEwJCXJFOE7WfGyUmPLB5vjocwbVt/Q9sRdDrPDVuOm9wS/fAFi8GOyIiSiqHoqDOLvubR0NDXJvc+4LzJTpN8FQgAVOCVOh10AyjBedTkSewBQ4waOoJbgGDDxIJbCqVMag5NDCwBQY3BrbEMNgREdGgcguBEw45Ym3bMbsTDX1ccN5X8zYqTRecHwqRA1v44IPEA5uvhs3bf01fHBLYiqBWZ3LJskHAYEdERP0SacH5wBBXF9eC86qwlRMqA5pMueB8Ytxumz+YOR2NAx7YdN4m0eDBBwxsqYDBjoiIemVxuYNWTgicEuSYzQmbEt+C82GLzXtr4bjgfHzCAptvOo+A/mtOZxNcrs64zxkxsEUYfMDANjww2BEREey+iXjtThzttuFf+lz8fV8tjjtkHLM70cEF5weVJ7AFT+lhs52A3rAH//nPn+GUm/se2PzNoQGBLaDGjYEtvTDYERGNAC7vRLy1oasnBC44H0ifA7Ragjbla9URF5sfbdCjwqDlgvMRRApsTkfP4AOHoxlOZ2PUwKbVAh3m4G0qlSGgz1px8OADBrYRj8GOiCgNCCHQ5FtwPmD5q8AF5929jFDIUKs8gxH0WrhP1OGcqZMxLtPIBecj8AW2wCWpEglskQQHtiJoNUU4cqQVp5x6DkzGMgY2iguDHRHRMDEQC85XRhiY4Atu+Vo1JEmCLMvY8uVnWFZRkPKz7A+02IEtsA+bpfeTeYUGNl+TqM47atTXvy00sMmyjAMHtqCkOPVXO6DUwWBHRJQiut1u/4oJoYvN19qdfVpwfnTA8lcluugLzqc7t9seNJ1H6ICD/gQ2XUA46wlsvibR8MBGNJgY7IiIhkikBecDm0xb5N4XnC/SaYJWTqgM6OtWoddBO8Im4o0c2AJWO/DWvCUc2ILmX/N81QUNPiiGRpPFwJbmhBBwOBzo7OyE1WrFkSNH4HQ6YbPZgh52ux2XXnppSvx7YLAjIhogihBo8I4iDa1xS3TB+aDJeANCnGmETMQ7OIFNH2HAQXCNGwNbevIFtNBAZrVaw7aFbhcBczAeOHAg6jWcTif0ev1QvJ2YGOyIiOIUuOC8L6wFNp0etzvh7GUi3pG+4HxoYAsccOAJbp7nLpe595N5RQpsuqCaNk//Nga24S9aQIsnpIlefjdj0Wg88yzm5eXBZDLBaDSGPVQpMio8vT9BiIgS1OVdcL42YCqQwAEL3XEsOF8RuuB8QIAr0qXnRLxut8Mb2KKsdtCvwOYbcMDAli76EtB8D6WXybBj0Wg0QWEsWkgL3Q4AW7ZswbJlqT+QJS2D3TvvvIP7778fNTU1OHHiBF566SWsWLEi2cUiohTgUBQcDxtZ2jNAoT8Lzo826FCeZgvORwxsEVY7SDSwBTd/9gS3nsBWDI0mm4EtxcUKaL2FtIEMaPGGtL6GMlmW+1zWoZaWwa67uxunnnoqrr76alx44YXJLg4RDaGBWHA+T6MOCGv6oOlB0mXBeX9g803hERDY7PZGGE1H8MGOu/se2CKtdsDAlrKEEHA6nTGDWLR9/QloarU6LJBFC2iB+1K91iyZ0jLYLV26FEuXLk12MYhoEPgWnD9mC69t68+C84FNplnDeCLe8MAWafBBM1yujpjnUasBl3eQrkqlg04XuuB7eI0bA1vy+QJab7VlkfYNdECLJ6QxoA28tAx2RDS8hS44H9hkWmvvfcF5rSRhlEGL0QZ9yLqlw3fBeUVxwOENZk5H9NUOegtsgXoCW1HP4ANdMTSaAuzZcxRf+9oFyMioYGBLAl8Tp9PpRENDQ8QpNqKFtMEIaL2FNJ1ON4DvnvqDwQ6Aw+GAw+HwP7dYPMPnZVketHZ133mHU7t9uuK9GHo2t4LjDtlfy3bcIeMrqwN7M0rxox37YO5lgIIEoFSn8TaPalGp13lHmnoWoS/RaWIuOO9y9T5f3FBRFKe/hs3pbIbT0eRZ8N0RsM2ZWB82SdJ5a9E8U3nodEWeJtKA73W6Img0OREDmyzLcLu3QqcbC0CbUj+v4cZXg2a32yPOfRZrmy+gff755wlfV61WBwUvg8EQMZCFbveN/kxUun9+JvvvRCLXlUR/xv8OA5Ik9Tp44q677sLdd98dtv25556DyWQaxNIRpSc3gHZJgxZVz6NVpUGLpEaLSgOLqvf/U2YpbhQIFwoUFwoDHgXChXzFhdRvwHFBkiyeh8oMlf97CyTJDEmyQKWyQJKscZ9RCA2EyPY8lGwoIhtC5EAo3m0iB4qSBcAET/ylgSKEgKIocLvdcLlcCX3tz59ZSZKgVquh0Wig0Wj836vV6rDtgc8lSWItaxqxWq1YtWoVzGYzsrOzYx7LYIfINXaVlZVoaWnp9QfYV7IsY+vWrVi4cCH7GCQZ70XihBBokl2otftq3WTUOpz+5/UOGb2NLc1Uq1Cp1/pr2iq0arTs+xxfnzsb4zKNyFCnZj83Xw2b09kcVKPmdARvS6RJVJK0/kEHOm0RdPrItWzRatgGWjr/TgghIMtyWC2Z1WoNq0ELfd7fJs5YNWUGgwEmkylou0ajwfbt27Fo0aK0uw/DTbJ/JywWCwoLC+MKdmyKBaDX6yPOFq3Vagf9Bg7FNSg+vBc9hBDo8M7nFrr8Va13It7+LDg/2qhDnkYdtuD5lk934eSczKTcB08fthY4vX3V/CscBA4+cDZDltvjPqck6bwDDrzrh0ZZ7WCoAluiUvl3InCQQKLzoLndvU9pE01oQIu3P5pWq034HsuyDEmSUvo+jDTJuheJXDMtg11XVxcOHTrkf3706FHs2bMH+fn5GD16dBJLRpQ6Yi04f8zmRGcv/dxUAMq9AxRGhwW41FlwPlJg6xlw0BPcEg9sRd4Roj2DD4IDWzE0mtyUDGypJFZA6y2kDUVAizQPGu8ppbK0DHa7du3Cueee63++bt06AMDq1auxadOmJJWKaGg5FQV1djkorAWuqNCXBecDpwcpT/KC8z2BLXBJqoEMbL752LwrH/im+2Bgi8jXxNmXedD6E9BUKlWf50HjPaR0lJbBbv78+f3qrEo0HIQuOO+fkNc7JUg8C87naNT+oDbKoEMqLDivKE7YbE0h64mGLk/VDFlui/uckqQNmTQ3cD42BrZAvho03zQbof3RYoW0gQ5o8c6DNtLvGVGgtAx2ROlACIFW2R0U1hJdcN6okryBTR8wCW9PgMsZwgXnPYMOWvzzr4WuduCwNyIjow7vvb8u7nP6Altg82fwBLolIzawhQ4S6G2B9EgBrS/TbEQLaL31R2NAo+FECAG4BYTTDcWpQMhuaItSYxYNBjuiJOoKGqAQPCFv7TBZcD5yYAsecOBwNMVVwyZ5KwglSRt5wEFAYNPpiqDV5qV9GIg0ijPekNafGjRJkmAymRIOaTqdLu3vCQ0PQggIWYFwuiG84Us4FShOd8827/dK4HG+bYGvcbhwUkcOmj+r8Z8rtEmk4hdnQdIkf7lBBjuiQTQQC86X6rQ9i80HBLjKQV5wviewNYWNEA1c/D3hJlF/YAsecKDW5GPnhwdw/vnfgtFYlHbhIJGAFrqvv02ciYze9PU/27p1K5YvX87RmDSohCL84ckfpgKDl+z2BrHg4KU43QGhLSCwBW6TFfS6MHQCDFBDsUeYKFgtQdKqIWSFwY5ouHMLgXqHHLT8VW1AgGtw9j5beKQF5/393vQ6GAa4n1toYAtbnmqAAlvggANfTZtWmwtJivx+ZFmGonSmfC1cpIAWb0gbiICWaEjrSw2ab5oNIgAQLiV2rZbTHT2cyZGCWM9xcPV9bsCEaFRQ6VSQdGpIvq9adcC2nu0qbfA2lU4Nt0rg3zU7ceY5Z0Fr0kEV+Jok9EWOhcGOKIZoC877vq9zOOHq5X+EJrUqbLH5ykFYcF5RZH+zp3/ggbcPmzOgmbRvgS3SCFFvkOslsKWqaAEtnpA22AEt0j42cVI0QgjApYTVdkWt1eqtaVIO3oZe5qwcEBIgBQYqrcobniJtC9zu3aYNPjYoxGnVkPrZsiHLMroPuqAtz0j5WmwGOxrxzC43jqm0eK3FgjrZHVTjluiC8+FNpnoUaNX9+oMcMbBFGHyQaGDT6Qr9gwv8gw+CApuvD1vqB7a+zIM2kAEtkZDGgDYyxdPk2BPGPN+77TJGHzbB/OeDgEsEhTTFGdx3bCCbHKNSST21Wt6gFVTTFRikQrdFCmyBYUyr4u/FAGGwo7RncysBYc0RNJfbMbsTZpcbyCwH9tdGfL0EoEzvWVw+UpNpqV4bc8H5aPyBzVejFhjYAmrcEgtsGu/i7yVRBh+kdmBTFAUWiyXhps7+LFIfGtDibepkQEs/wq0E9+EKCFxB2+LqcD8wTY5FMMDe1Br/CzRShBqsgJoubYxaLV0vTZMp0H+MesdgR8OerAjUO4LDWmCIa3L2/kc/S3FjQnYmxpj0AU2mnu8rDFroVPF/oHkCW0vA/GsBy1P5A5uvSTS+/2ZHDGy+5wE1bqkS2HzBLJEpNnwB7ZNPPunTNSMFtHhCGgPa8OFpchRBtVrRarAi1ooFhbGQ2jPZDbiHZv7TeGqwVDo1FDVw8MtDmHLyNGgM2t6bJrVqSGr+Wx7pGOwo5SlCoMnp6hmgEBTgHDjhkHv9PM709XMz6sKaTMs0Ev71z39i2TnLYvadCA5sTcGDD7zBre+BrbhneSpvYAscfJCswNZbQIu2b6Br0OJp6mRASw2eJsfgzvPh00soEcJZwHEBNWFKSO3X0DQ5InoNVpwd7qM1TSbS5CjLMhq2fIbTzixL+X5dlDoGLdh1dHTgpZdewrvvvouvvvoKVqsVRUVFmDVrFhYvXowzzzxzsC5Nw0xvC87X2p1w9NJ5V6/yLDgfabH5SkP4gvOBHA4rJKkDls7/QHG3haxw4K1x8zeJJhLYvH3YQibMDRx8oNXmD0lgi2ei2kj7BiOgxZpi491338UFF1wAnU43gO+eQgm3iFmrJducKGzUo/v9E1C5EV/TpG+7PESjHNVSePDSqmKEseCmSU8Yi9zhHmqJ/0mgYWvAg119fT3uuOMO/OlPf0J5eTnmzJmDmTNnwmg0oq2tDdu3b8evfvUrjBkzBnfeeScuueSSgS4CpaButztsKpDAFRX6suB84PJXxTpN2ILzvho2p60ZLWFLUgX3YcvIFNizp/f3ERjYeppGAyfPHdzA1td50PoT0HwT1SYS0oxGI/R6fUJ/HGVZhlrdv4Em6SJmk2NYv65ItV+B01CEb4unyXEMMtB15Kt+vY/QzvXBNVuRarUC+4OpgsJaUGBjkyNRVAMe7GbNmoXVq1ejpqYG06dPj3iMzWbD5s2bsWHDBtTW1uLWW28d6GLQEIu24Lzva2scC84X+xac99a2VQZMERK44LyiuOCUW+B0HPdM59HdjKOOxqDA5nQ2welsRbw1bEKooNcXw2AoCerL5qlh6xl8MFCBLZHBAQMZ0BKdYqMvAW2kEIrwz+8VqwYrtMN9T9NkyDQUgU2OEWa1HxQSItZgQatCU1szSivLoDZog6eh0IX2CQvpnO8LXxpVv6eYIKLEDXiw27t3LwoKCmIeYzQacdlll+Gyyy5Da2sCo30oadyBC877a976vuB8aJNppUEHvaR4A1vAlB4djbA2NuHzPgY2SVJ7gpp/8feAwOb9qlLl4803d2DeORck3I8l0XnQfNsHIqAlGtJGYkATiggPXhHn9YoUxmJ3uE+JJsdINWKBtVq62E2T0ERucpRlGTu2HMWUZZPYt4tomBnwYNdbqOvv8TQ4Ii04H9hketwuQ+7jgvOjdGqUajphdLfA4ajtaQ7tbISjtQkdjmY0Ohv7GdiC1xD11bTp4qhh86x4AHR2dibc1DlQAS2R1QTSKaAFLqTttjqgt6kg13VBUVRhnebjb3LsWYao19mjB4gnaMWowdJGqdXqNZyl3qz2RJTaBn1UbH19Pd577z00NTVBCZno9eabbx7sy1OATu8AhdoIC84fszth7aWfm8a34LxvRKlegzKtA2UqC4qkFmS6GyF7F3532Jrh7PAMPmhxtqKln4EtePBBMXTaPEhS+IoNLpcLVqsVVqsNra1dsNma4wppsiz3eZqN0IAWb0gbLgFtIBfSjrQMUWBV78nIRduezwb+TUiIXoOlDenr1VuH+5B+X5KWTY5ElDoGNdht2rQJ119/PXQ6HQoKCoL+iEmSxGA3wOxuBccdzojLX9XanGh3xbfgfKVBi1F6gXKNDSWqThRLrSgSJ5DjqoVL9ta2dTXB6WwBICADqO/lvJECW89qBz2T5/oCmy+g+YKXxewLYe2w2eqjhjRZ7n1t1uhlDA9o8c6DpkpgnrvB0GuTY1i/ruQtpB2VWoILbugyDGHzdSXU5BhhpCM0nNWeiEaGQQ12P/vZz3DHHXfg9ttvT/ofvnQwMAvOS6jQCZRrHShVdaJEakMhTiDfXYs8+QiEXA+n2RPYAtm9j1C+wKaLMFmuXlcMtaYAQsmG06mD3d6z7FNrZ2CTZhNstq+SFtC0Wi3ee++9QZ9mY8AW0nYmr8mxvwtpx2qadClubNmyBcuWxZ5PkIhGDiEEFLcbQlGgKO6e791uKIrveyWOY9wQbjcU4TleuN1QvMf7v3e7IRTP98HbFLhkGa0HDuCDrjZIktRzPkXxvMbtxvnX3Ai1JvnTAw9qCaxWKy699FKGujgJIdDsdIWtnOALcPEsOG9UCZRrHChTd6FYakMRGlGg1CLfdRg58kGY5G4gSmZyBHwvSWrotIX+AQdabQFUUj6AXLiVTLhdmXA6jbDbdbDbHGhvC23urIPNdmhQAlpvgwb0en1C/+Z802wA8Aal4b6QdoQmx4iLa0dYcmiQF9KOSen7uq1Ew12kAON0OOCyWdHV1gq1WhUzwIRtU9yeAOP9GhhwAo8JDTBBrwsMRAEBJij4+L+PfP6eMgaXId6gJsQQDVKK067Pdkfdd+7q69I/2F1zzTV48cUXcdtttw3mZYYdu1vBGy0WbNVl4YPDJ3Dc4fIOUHDC1ssffy0UlKi7USK1owiNyFeOI9/9JYrRgCI0IcttgRTz76MaWm0+NJpCSFIugBwoSjZcrgzIzgzYHQbYrDp0d0uw2RwhNWhd3kdiYgW03uZBCw1oURfStrohOhQIuRNWZ0dcTY6BIW6mNQ9N//5weC6kzSZHGkaEEBFrUxINJIMeYEK2BYaZ8IAT8H3oOQcgwDz90p+G8A4NHyq1GiqVGpJKBZVaDUmthkqlgkql6vlerYakUgdsC/he7T1WpfYep/LsV/ec03d+SEBt7XGMHTcOGq024jGSOrzfdzJIQvQy1LEf3G43LrjgAthsNsyYMSOseeXBBx8crEv3i8ViQU5ODsxmM7Kzswf8/J2yjEnvfR5xnwSBAsmMIjShUKlHERpQjCYUoQlFaEQe2qGKmD5UkKQcALlQlCy4XJmQnUY4HEZYrVp0d2vR2amC1aqGZ7rfxEmSBIPB0OsUGwa9HgaNAUa1Dnq1DjpoAVeExbXj6nAfHOL6upB2wriQ9pCTZTmtmmKHMsDEd51ItTyRA4zbJaPhxAkUFRYCQgxZgKHI+hZgPCGlZ78qRsBRBwScnq+B11Sp1ZCkgO/9ISjG8aFByX+8Kuw9RQtWQa8LfF9D3BKY7M+nRHLJoNbYrV+/Hv/85z8xZcoUAAgbPDFSGWDDyWIPMtGFIjR5g1ujJ8yhBRrRM4WGEBKEkgWXOwNOZx7a7GWw2XSw2/RwOo1wOI1wOkyQZT3iDWy+gGY0GGHUG2DQGWDU6mHQ6mHQ6GFQ66BX6WBQ6WCAFjqhhV5ooXOrAFn0hLFON0RrSO2ZbPbPah+tX96AkBCleTHxJke3SsE777+L+YvOg86k9xzHUY5xSzTAhIUB7+tkpxPWE8fx5Z4aqFSS9xzRjx+IABO7iSmgBidCs1NPwInyPtMgwHQf79/KE4mKXmsSEB4CAkbCASZSAAqptQmrwQkLIKqYNTvB34eGk2jnjBx83G4Fr73+etr8Z4eGxqAGuwceeABPP/001qxZM5iXiejRRx/F/fffj4aGBpx66ql4+OGHMWfOnCEvRyRutw4/cv8v3C6NJ5g5TZ6Q5ijGUefYnsDmNEJ2GhAtsEmQoNfqkKHRw6DXwaDSw6DSQg8d9NBCDw30igZ6twY6lwZ6lxo6WQ2tLEGyxRNcnN4H4PI+EpIiC2n3RpZlOIwK1Fk6qLTRfyUCA0xvnXTjCiSJHh8lwMTu1xIYXKI1MaVWgHl5+2sDer5UFPcf914DRqwamjgDTIRaGwHg872f45RTT4VWqws6JnotTJy1NlGCjySxG0EoRfS9jzKNXIMa7PR6Pc4666zBvEREL7zwAtatW4cnnngCc+fOxYYNG7B48WIcOHAAxcXFQ16eUDqVFjvevwQCng8xCYBe0nrCmNAiQ2iRr2igFxroRc92PbQwCG9gE1rooIFk7+cHYZQmR0mr8sxKr1UBWskziZ1GAtTw/KvRSBBqAaEGoBZQ1IBQCQiVgKJSIFQKBLwBwR8G5JCwERJIbG4oXVGacQYxwCguF7q7u/D063/znjdKs1Ma1MAMtfDmlCh/3FVqQCWhq6sbuXl5/v3htTaxA1GkWpvwgKMODzjeMkQMMFH75oQ0F/Ua1Lxfh0GAkWUZx50KTpq3gDVFRMPMoAa773//+3j44Yfx0EMPDeZlwjz44IO49tprcdVVVwEAnnjiCbz66qt4+umnU2Igh4DANxxzoAsMaIj+Qa9IbiiSAkVS4Fa5oUhuOOGEDW644ep5CBfcQoZLyHALF1zCCZfb81xWnHC7nZAVJ2TFAZfLAVlxQnG7GGC8uroTHxjiIwX1d4kSEMLCRqwwEG8TToRzRqm18TRdhYaVSM1V8QSY0GAU/rpEA0yy+7AQEaWDQQ12O3fuxFtvvYVXXnkFJ510UtiH9d/+9rcBv6bT6URNTQ1uv/12/zaVSoUFCxZgx44dEV/jcDjgcPRM9mGxWAB4/tD0Z7qOaFwuGV81f+gNXzLcihMuXyBTPF9dii+gpVZVvBSx1iT0j39wLUjETrCh4SKoeShKqIlwfMwQFCG8RCqvogh8tGsXTj/jDGh1ugjHh5arfwEm3QkAbreCRFex9/2uDcbvHCWG9yI18D6kjmTfi0SuO6jBLjc3FxdeeOFgXiJMS0sL3G43SkpKgraXlJRg//79EV+zfv163H333WHb33jjDZhMpgEvo6IIfOmwAlBBggqACZBUACRA5f2qVgGSCmpI3mNUgCR5vkLqee4LFZInYHi2Sf7vg76qpJ4Q4nuu8pxLUvuO9YQ3qDzHeYZ5e4+TJKhUkufyEiBJwvsV0bepPCN9PV89zxXvfkUSIa8FEPc277UEAAHPFC8KAAhIUuLrtxoKi7Hn4OG+3VAaUFu3bk12EciL9yI18D6kjmTdC6vVGvexgxrsNm7cOJinHzC333471q1b539usVhQWVmJRYsWDcp0Jy7Zjec2OyAkFaDSQKjUEJLK+1BDQPJ870syfeUNPej5MiTTtCWbpJKgUnm/qiVIkverN8iqvA/Je4ykAjo7O5GbmwOVRtXzWlXwa/xf1RJUEjxfQ/ZLAefuOVYKOBZhx/rKGOkY/9ew94GA9xF4DPzHhJ4j1cmyjK1bt2LhwoVsik0y3ovUwPuQOpJ9L3wtifFI/hTJA6ywsBBqtRqNjY1B2xsbG1FaWhrxNXq9Hnq9Pmy7VqsdlBuodrlw5od3xXWsJ+RJnsDnD3+eGrzA5579AcdpdZD0RsBghKQ3QBiMnud6PaAzeL/qAa0B0Or8D6HRQdJqITQ6CI0W0GgBtQZCrYFQawG12nMNRUBRBIRbQBGAcCue54qnRlJxC+9M6sIzqbDveO9X3/aebZ7Jhz0DH7znUAJeG3C8ooiYCVUoAm5v7V0CdwXNlr73sRsOfEEzNNz6A2iEMBkxuAaEzNghVoq5PTSsCuFGV60WR2paodFqIofboLJLUUJshHAbVubhE3iTabA+AykxvA+pI1n3IpFrDniwW7JkCe666y6cfvrpMY/r7OzEY489hszMTNx0000Ddn2dToeqqips27YNK1asAAAoioJt27Zh7dq1A3ad/nBogGu+r4ZeBvQyYHACell4vvdu0zt93wvoZYEst4RMtwYZLg1MLpX/OJ1TgcYpQ+1wQWV3QnIPwcAHlQoqgwGSyQSV0eh/SCYjVEbvNpMRkjHKc1PAa8KeGxHPxJNCEVCEN1gGBUdAcXsnVPWHxZ6QGXSs23MO2enCzg93oqqqGipJ5Q2WivfcCA6gYdcLOFfYueMPvKHbA68nBLwjg3vOIyIc31vgVRQxNMua9YsB//rs4NBdTkLkMNrHwBt2rCq8Ztf/ml6CbnAoRdD5Y4bViOXwnddXG63qCcARzkFEw9eAB7uVK1fiW9/6FnJycvD1r38d1dXVKC8vh8FgQHt7O/bu3Yv33nsPW7ZswfLly3H//fcPdBGwbt06rF69GtXV1ZgzZw42bNiA7u5u/yjZZNMobqy3NKNDrYZZpYLZqII5U+X5Xq1Bo1YPi1oNsyTQ5e+ArsCzmqsjxplVULslb1D0PPJhQoHIRJ4wIlcYkKXokO3WIcOtQYZLDaM3JOqcwhMSHW6onDKEzQ7FZoNis0JYbVBsNgjfABNFgWK1AlYrBmN1T8lgiBwWEwiPGpPREz6NRqgCAqgU4X89sizj06NujJ1RMOz/VzyQgTf0HL7je7YHhNXQoBtte4zA63a70XCiAUWFxZAg9TvwBh4b/QcGKMITeLlSbQAJADLx+zffj6sbQrTm/9jbg7sTRK5Rjn2O0BAeNehGDO+xA2/EczD00jAw4MHummuuweWXX44XX3wRL7zwAp588kmYzWYAnhUPpk+fjsWLF+Ojjz7CtGnTBvryAIBLLrkEzc3NuOOOO9DQ0ICZM2fi9ddfDxtQkSySIvBZ1xIUiXaMkdpRoupAqdSBXLSHHSsDsKhUMKtVsKhU6FCpYVarYNZo0WHIhkVnhFmrh1mtRocEWOCCWedAm9uz5sMJ2ADYEisfJGTpspCjz0GOrsTzVZ+DHG0W8pGBPGFCtqJHtj8kqmGSPTWQsNshbJ4gqHgDYWA4jPzcBmGzAd7V7YTdDrfdDnd7+M+j37TasFpGyWBARVc3TmzdCk1GRq81jRGfm0yQdLqkj46VVBLU8M43OMx4pjv5EkuXnTzgATta4PUERm8NbUAAHqrA6+9uEHrOwLLEG5oHOPACEtyywsAbSEJ4KIzQP9YzWj6+LgGxujcISaD9mB4f2A5Do1HHWSvbv8AbuT+vKiiMM/CmtkFdK9bHbDbDZrOhoGB41IgM9lqxHVYnvvvHGnxR14oOWQW3t3lMCxeK0IESqR3FUgeKpXaUSO2oUJtRqbWgVNWOPKUdGa6OXq/hD4RaA8yZhTCbcmE2ZsOsM6FDq/PWCAIdwgWz2wGLqwsdDjO65e4+v6/gQJiDHIP3qz4Hufpc5OhzkK3LDnqeo8tBli7L0wRqt/sDobBZA8KhtQ9hMfg53EPw50mliljL6AmHoc/jCI+BNY0GQ1xN1MMZ57EbetECr9MhY9ub2zB//rlQq9QBNb69d0OI3d0gvrA6GIE3cs1x5PdCCZJCal+jDFbzTHUlhQ1QixQ6o4dReKbSijSALa7AG9hFQhU5rEYIuopw49333sW8+edAp9NFDOM6o2bQ/nOfMmvF+uTk5CAnJ2coLjUs5Jp0+MNV1diyZQsWLV6IFqsbte1WHG+zobbdito2K2rbbfi4zYqmTodnLa+AFtjgANiOcfpOTDB0YpTWghKpA3lKKzKcLShwtKPAYQUcx4DWY7ELpdYDWSWQM0tgySyE2ZQPszETZl0GzFo9OlQqmFWAxe1Ah9MMsyPg4fQEQgEBi9MCi9OCWtTG/fMIDIS5+lxk67ORo/OGP4Pn3062Lhu5+mJ/7WGuPheZ2kyoVb1XTQkhIGQZwmoNqyVUbDbInZ34+N//ximTJ0NyOHuCYcwgafOfTzg9y65BUaB0dwPd3SnbRB2tf6OkSbtxVBSHaDW8Gr0EtUEgK98wIkN2xHAYo4Z3UAKvW8DlcuGLAwcxYfxET7jw1b6GBOCkB15v+WPWAqeFDPz53Zqoe6/dcA50huR/lia/BCOcRq1CZb4elfkmYEL4frvsRl2HzR/2jrdZveGvAEfbrfjYKiNaa6sOMoqlDkzNtGJqRjfGGzpRobGgRGpHrrsNGc5maLobIdnaALcD6DgGbccxFAAoiFZgbwBEVhmQWQJknQqUlELOLILFkA2zr2kYSsQA2Gsg7Ew8EPprAwMDoa/5WB9Sa5ifg0xtcVAglGUZnbKMnD7WFAmXC4rdDsUaWrMY5XlgWAx67q2ptAaHT/91BrGJWtJqYw+GiSM8RhwMYzJ5zs0JnGkYkVQS1CnQvCjLMhrF55izbGxKBOyIQTdaH1hfN4BeAmZfAm+0rhIRA2+kQXBK+PWiz9jQE2wddgc0Gm1QF4fAwJsqA48Y7FKcQavGhKJMTCjKjLi/0y6jNqCm73i7LwRaUdtmw3G5CMc7gTc7I59fq5YwJkeDk7LtmJrRhbH6LlRozChGO3LdrdDbmyB1NgKdJ4CAAIiO4BpALRAcCAMDYFYpkFkKZJUDxad5nnuDoazPhNlpgcVhgdlpRoe9I2IA7C0QIsr7iyQ0EGZps9Dd3Y3Pdn2GPGNeUCAMDIm+JuOw82k0UGdmQp0Z+R71h1CUniZqmy16eIwZFntvohayDGE2Q/H2hx1QanWUsGgIaqIWej0K6uvQVl8PbUZmfE3URiNDI9EQSZXAmwyxuor4AqBKnRo/Gwa7YS7LoMX0ci2ml4e3uQsh0Nrt9Nf2eYJfT/ir67BBdgscapNxqE2NvyMHQA6ACv85TDo1RuUZUVliwthcDSaZujHW0IkKtQVFaIPB1gR0eYNfZ4PnESMAhtKq9SjMKkVhVmlAAPQGv5yTe2oGjXneJSc8ZLcMs9MMi8OCDkdH1ADY4ejwhEaH53uryxo1EH7yxScxyypB8tcKhtYI+puQEwiE8ZBUKk9N2iCsgBKxiTqsf2M8zdKRm6mFbwkctxtKVxeUrt7nCSwA0PbW9oTehxRQy9jn/oxRwqOkHoajUIhoSKVa4GWwS2OSJKEwU4/CTD1mjc4L2+9WBBot9qDgF9jXr8Fih9XpxheNXfiiMfSPciaATOSaJqAyz+QJf2NNqMwzojJHjbH6bpSp2qG3NQGdAcGvqyFCAPzK84hFrfcGPs9Dm1WGwswSFGaV9dQMFs0EDLlBATBUpEDYZmvDv/f8G+UTytEld3m2hxzjC4S+0JhoDWFgM3FoAPQ9z9XnBoXG/gTCuMolSZB0OkCngzo3d8DPL1yu8LAYo6bR1dWNo/v3YXRxCeCwxwyPwm7vuY7NBrfNNjj9GnU6f5Nyr3M2GthETUTJN6jBbvv27Tj33HMj7vvtb3+L66+/fjAvT71QqySU5xpRnmvE3Aj7HS436jvsQU27nuDnqfVr7Xaiwyqjw2rGp3WRm/CKszJQmX8yKvNmozLfhMpRJozKN6Iyz4SyDEBjbQ6v8QsKgCcAW3sfAmBZSF/AMm8g9NQOFuaM8wdAWZah2a/BslOj97HzBcLAGsEORwcszp7aQF8tYaxAeKyzl0EsAXyB0Bf4QgOgf19ISBzsQBh3+TUaqLOyoM7Kiut4WZaxc8sWzI6jr6NQlOAmZl94tNv73J8xMDxC8cwfKZxOuJ1OYEibqHvv3xi5pjHge4OBoZFohBrUYLdkyRLcfPPN+OUvf+n/oG5pacFVV12F9957j8Euxek1aowrzMC4woyI+7sdrrA+fYF9/bocLjR1OtDU6UDNV+Ed/j3B0oDKPBMq88ahMn86RhWYUDnJE/yKsvSeP06y3Rv+Qmr8fMHPFwwTCYAagz/wqTOKcXKrHaoPDgE55cHB0FsDqFVrUWgsRKGxMKGfYWgg9AW+SIEwMDSG1RAmIFIgDAyA0ZqRUyUQxkNSqSBlZECVEfnfZn8IISCczgEZDBOpmRp9aKJOmCQFN1EnGBYVnQ6mLw7CVr4H7qwsNlETDSODXmN35ZVXYuvWrXjuuedw9OhRXHPNNZgyZQr27NkzmJemIZCh12BKaRamlIbXyAgh0GGVwwKfb2Tv8XYbnG7Fs6/NBqA17Bx6jcrTxJtv8oS//EJU5o1G5SjP8xxTSK1OxAB4Irwp2NYOuOz+AKiCd0Dy9jfC32RAAAxsCg6uCSyJ2gTc10DodDvDw583EAY+DwyFHY4O2Fy2PgdClaTyzzMYKQAGPg8MjcMpEMZDkiRIej1Uej2QF96Fob+ELPc+GCZqeIw9GMbfRC0EhNUKdz9WhxkFoO73v4+4L6Em6gT6N6qMRk/3ACLqs0ENdmeeeSb27NmDG264AaeddhoURcE999yDH/3oR2wmSHOSJCEvQ4e8DB1OGZUbtl9RBJo6HVFH854w2+BwKTjc3I3DzZEnTc4yaLyBz+j96vl+VN50VJZWw6iLUqsQGAA7T8BtrsfhT97HxOIMqLqbogbAmDSG8MEfEZqCYciJ2QfQR6fWDWggDA2AvlHIgYFQEQo6HB3ocHQkdM2gQBgjFAbVGhpykKnNTKtAGC9Jq4Vaq4V6ECY/j9pEnVBNoxXubissTU3I0Ggg7Hb/ABv/6jCD2USt0YSExTgHw8Sas9FX06jX828Ppb1BHzzxxRdfYNeuXRg1ahTq6+tx4MABWK1WZAxCEwoNHyqVhNIcA0pzDJg9Nj9sv+xWcKLDHlDTF1jzZ0NLlwOddhf2nrBg7wlLxGsUZuowyhf4gmr+jCjPrYQ2bwwAQJFl7GuuwLhly6AK7Nsl2701f1EGf/hqBO0dngDY/qXnEYsvAIbW+AVNCxN/AAzV30AYa7qZwEDoC40DGQiztdno7O7E5zWfe6adCZ2TcIQHwngMVBN1pKkdhBAQDkfQ5NyJhMWwmseQCcPhcnku7nJB6eyE0pnACKV49bOJOvq0OybP9D1soqYUMKjB7n//939x55134rrrrsP999+PQ4cO4YorrsApp5yCP/7xjzjjjDMG8/I0jGnVKowuMGF0QeRpPmxON463BwQ+X62fNwha7C60dDnR0uXEntqOsNerJKAsx4hReUZU5Bpga5bg3FOPsUVZqMwzoThLD5XWAOSN9Txi8QfAKIM/fMEwoQBojFzjFzotTB8DYKj+BMJo08sEBkD/gBJvSIwVCD85EHvaGV8gDFulRJ8T1LcwqNZQz0DYX5IkeVY/MRgGp4na6QwPi6GDYWI0Uceahkc4vEv3DEATdSySXh/XYJh4p91RtFqobDbP1EEpMEExDQ+DGux+85vfYPPmzVi6dCkA4OSTT8bOnTvx4x//GPPnz4fD98tGlCCjTo1JJVmYVBJ5xKXZJvvn7Qvr49duhV1WUNdhQ12Hb2UHNV47/pn/9TqNCqNyjagIqenzNfnmmQKmqog7ANoCmoAjDP7wbbN3AC5b4gEwVlPwAAXAUDq1DkWmIhSZihJ6ncPtCAuAbdY27PxkJ8rGl6HT1Rmx1rC/NYS+wBcaAEOfB05Dk6XNYvPdEJB0Oqh1OqgHYflJ4XZDsdlDmqV7698Y55yNgU3UDgfcDgfQ0TFgZZ8I4PBddwNabXBNY9j8jWyiJo9BDXaffvopCguDawC0Wi3uv/9+XHDBBYN5aRrhcoxa5FTk4OSK8D8SQgg0dzk8K3O0W/Flcxd2fPoFpMxCHO+wob7DDqdLwZGWbhxpidy/L0OnRmW+ydvUG9zHrzLPhAx9hF8trbEPATDC4I8+B8AINX6hTcH67EEJgKH0an1YIJRlGboDOiybGX26k8BAGGm+wWjNyL5A2O5oR7sjsSXZ1JLa32QctUYwQq1hpjaTfyxThKRWQ52ZAWQO0ihq3+owYRN8JxAWveeI2kQty1BkGYolcteTflGpoDIY+j0YJuKcjUYjJBVryofSoAa70FAXaN68eYN5aaKoJElCcZYBxVkGVI3JgyzLGGvdj2XLqqHVauFyK2iw2IPm7QucwLnR4kC30439DZ3Y3xC5H1B+hg6VeUaMyvdO3hzQ168izwi9JkZfnEQCYGdD7/MA2s3eAHjU84glNABGawoeogAYKlIgjIcvEPYWAEPnKLS5bHALd78DYW+jixkIhy8poN8ewrsL94ssy9jy8stYPG8+1C65j9PuRF4ZRrHZIJxOz4UUBYrVCgxWE7XBEHefRs+x8a0MozIaIbGJOgxXniAKoVGrMCrPUxt3Rs/qt3522Y26DlvQ9C2Bgzs6rDLaup1o63bik+PhowYlCSjJMvhr90YFDu7IN6E02xDf8jRaI5A/zvOIxRcAe5sHsE8BMMrgjyQHwFD9CYRBATAkBEaapHowAmGuPjdoW6RaQwbCNKXRQJ2T3euk3X0hXC7vpN79n7MxdMJvYbP1XMduh9tuh7s9sd+FuIQ2Ucfqz5hA/0aVyQRJpxuWv1MMdkQJMmjVmFCUiQlFmRH3d9rlntq+gDV6fdusTjcaLHY0WOz46MvwDzqt2rMiiK9fn29kr6/mrzAzwQ+bfgXACE3BiQRArSn24A9fMNQP/NQfA0Gv1qPYVIxiU3FCr4sVCAMDYOgchf0NhDn6HH8A9DcNhzz3jS72BUMGwpFL0migzsyEOjPyZ1l/CEXpaaIO7dMYOB9j3P0Zg5/D7a1bHOwmal9YNBgxxiXj+HP/B3WGKeKa1IXXX++puU0yBjuiAZZl0GJ6uRbTy8PDihACbd3OoKZdX1+/2jYr6jpskN0CX7Va8VWrNeL5jVp1wMTNxuC+fvkmZBv6+D/7vgTASIM/AgOgbI07AGoyS3CWUwe1429AdnmEpuCSlKkB7E1fA6HdZQ+qAQydXiZa07HdbYdbuNFmb0ObvS2ha0YKhFnaLLTZ2lD/Wb1n6pkIzckMhBSLpFJ5+uyZIs9s0B9CCAhZDpt2J7h/YzzN0pGbqYVvdRhFgdLdDXR3ww1AD8B+oiFquQqvu27A32tfMNgRDSFJklCQqUdBph4zK3PD9rsVgUaL3d/M6wt/x721fQ0WO2yyGwebunCwKfJSVDlGbfCADm9fv8o8T62fQdvPubbiDYBOaxzzADYADk8AlNqPohAA9h6IcW1TfBNB67OGRQAMZdAYYNAY+hwIo00vE21Owt4C4Qf/+SDqNQMDYdB8g5EmqQ4YYJKhzWAgpH6RJMmzQolOB3Vu7oCfX7hcYWHR2dmJD//1L1TPmAGV0xkxPEoGw4CXpS8Y7IhSiGf9XCPKc42YG2G/06WgvsMWdam21m4nzDYZ5joZn9VFbpooytKHTdjsC4FlOQZo1AM0gk1nAvLHex6xeAOgq/04Pn73dZw2qQxqa3NIU3BPAETbEc8jFl8A7G0i6GEaAEP1JxAGBj5fIGyzteHjfR+jcFQhOuXOsDkJB6KGsLdVSnzTzfj6EzIQ0lCRNBqos7KgzuqZTksty7CeOIHM888flP6OA4nBjmgY0WlUGFuYgbGFkadt6Ha4wpZnC1y2rcvhQnOnA82dDuw+1hH2erVKQlmOIeJSbZV5JhRlDcJ8V94AKLIqUZ/XjplzlkEd6YPTXwMYax7AxgQDYEbsiaB929MkAIbyBcKSjJKg7bIso/jLYiybG3nqmdBAGG2S6sBaw/4GQo2k8Ye9eKab8W1nIKSRJu2C3f/8z//g1VdfxZ49e6DT6dAxgBNFEqW6DL0GU0qzMKU0fOJmIQQ6rHLQCh2B4e94uw1Ol4Lj7TYcb7dhR4RMpNeo/P37gqdx8YS/HKN28P6Ixl0D2B0yDUykpmBfAOxOPADGagpO0wAYKlog7E20QBg4J2FoIDQ7zHC4HXAJ14AEwki1gZGajRkIabhKu2DndDqxcuVKnHHGGfj973+f7OIQpQxJkpCXoUNehg4zRoVP3Kwovombg5dq831/wmyDw6XgcHM3DjdHnrg5S68Jnr4lYBqXUXlGmHRD8JGjywAKJngesYQFwEg1gQ2Aw5JgAIw2D2BIH8ARqL+BMNL0MhEnqbZ79jsV54AEQl8ADK0NDA2EufpcmDQmBkJKqrQLdnfffTcAYNOmTcktCNEwo1JJKMk2oCTbgOqx4TOtym4FJzrsQU27gX38mjsd6HS4sO+EBftORO7fV5ip80/fUhlS81eUMcQfR4kGwJjzAAYGwMOeRyz+ABhl8Id/HsCRGQBD9ScQBk4vExoIwyal9gbEgQiEUQNgyHQzvuMYCGmgpF2wI6LBoVWrMLrAhNEFkacvsMvu8LV5A7632F1o6XKipcuJPbUdYa9XSUC2Vo0/nvgIo/Mzwvr4lWQZoIpn4uaB1q8AGNoU3JhYANRlxh78wQAYk0FjQKmmFKUZpQm9LjQQRptuJnQUcr8CoUoTHAJ1OcjS9Uw7U2Aq8NQgBvQnZCCkSBjsADgcDjgcDv9zi3eiQ1mWIfvmsxlgvvMO1vkpfrwXA0MNYEyeAWPyDADywvZbbLJn9G67Dcc7bN4aP8/Xug4b7LKCDqeEj75sjzpxc0WuEaPyvI9co2cqF+8ybfmmQezfFw9JB2SP9jxicXYBXY2QvDV9Ulcj0NUAyRv8pC7vV0en59i2rl4DoNBlAJklEN7AJzJLgMxSiKzSnu2+PoBxGOm/E2qoUaArQIEufOWZWGwum79m0N9n0NkTBqPtcypOuBQXWu2taLW3hp33/f+8H/WavkDoX5nE22TsD4lR9jEQJibZvxOJXFcSQohBLMuAuO2223DvvffGPGbfvn2YOnWq//mmTZtwyy23xDV44q677vI34QZ67rnnYBqEyRWJKJgQQKcMtDmAVoeEVjvQ5pDQ6gBa7RLanYAiYv8R0qsE8vVAgaHna4EeyNcLFBgAQz+n7xtqarcdBrkDBleH56vcDr3s+75nm1axx31Ol8oAuzY34JEHuyY3bJtbnRrzcY0EQgjIkGETNliFFTbF5v/eKqywCVvUfe5+rOyqhhpGyQijZIRJMnm+V/V8H/rVt0+H4bnM1nBntVqxatUqmM1mZGfHXqlnWAS75uZmtLaG/y8m0Pjx46HT6fzPEwl2kWrsKisr0dLS0usPsK9kWcbWrVuxcOHClJ8TJ93xXqSGWPfB5VbQ2OkIquULfDR2OqKctUeeSeuv6fPV+vlq/MpzjdBrBmj+vqHmqwEMrfHzNgn7awadkSe0jkRoM9CtyoKxeByk7DJ/jZ/wNgOLzBJPE7Bu4Jeioh6xfieEELC77WG1gB3OjqDaQIszuNbQ7DBDVvpe6xTYZJyty+4ZVKLLDaop9O3z1RQaNcZhHQiT/XfCYrGgsLAwrmA3LJpii4qKUFSU2OLdidDr9dDr9WHbtVrtoN/AobgGxYf3IjVEug9aLTDWoMfYosgfaHbZ7Z24OXy1jto2K9qtsv/xaZSJm0uy9RFX66jMN6Isxwh1Mvr3xUObB2TkASVTYx/n6IowAjhg8IevP6CzE5LcjUx0A7XRl08C4Al2QaN/S4MHgPgngmYA7I9on0066JBtzMYojIr7XL5AGGm6mWjrG/uOkRU5ZpNxzPeg0gYNJvEPMIkyuti3PdUCYbL+TiRyzWER7BJx7NgxtLW14dixY3C73dizZw8AYOLEicgchIWOiSj5DFo1xhdlYnxR5N/xTrscMHGzzTuqt2dwh9XpRqPFgUaLA7u+Cu/fp/GuCBI4oGNUwOodhZnDoHlKn+l59DYIxNEJuf04PnzzZZx+8hhorC3Bgz8CAiCcXUDrIc8jFl1WyOCP0BHAvnkA+Rk92CRJglFjhFFjTGhQiRAiqA9htNHFkaalkRUZsiKjxdaCFltLQuUNDYRBk1Ibcnv6D6Z4IBxKaRfs7rjjDjzzzDP+57NmzQIAbN++HfPnz09SqYgombIMWkwr02JaWXiNnxACbd3O4No+f/izoa7dBqdbwbE2K461WQGE11QYteqe5t2Amj7f1C45xmFUE6zPAgomojVrKsRJyzzVpZE4Oj0BL2gEcEP4yGBnlycEtnbGGQADRvsyAKYMSZJg0ppg0pr6HAiDRhdHmqQ6YBRyh6MDLsU14IHQv0KJLjvoeToFwrQLdps2beIcdkQUN0mSUJCpR0GmHjMrc8P2K4pAY6c9bMJmT3OvFScsdthkNw42deFgU+R+bNkGTfDavCHhz6AdZiM7AE8A1GcBhRNjH+cLgGHLvwUEQMsJzxQw/gB4MPY5QwNg2BQw3jCoi7z0Hg2dgQ6EESepHoRAmKsPrg3M0mahwd4A60ErCkwFQUExW5edUoEw7YIdEdFAUqkklOV4+tnNGRc+cbPTpXj794XM4dduQ127FS1dTljsLnxeb8Hn9ZH79xVl6QNW6whu5i3LNUCrHqYDO4CBC4C+5wMWAANqAhkAU05/A2HE+QadFnTYI/Qn9K5xHE8g3PrR1ojbtSottq3chjxD+FRPQ43BjoioH3QaFcYWZmBsYeRwYHW6evr3BfTxq2234XibFZ0OF5o7HWjudGD3sY6w16tVEkqzDWETNvu+L8rUJ2fi5oGWUACM0OQbOhl0IgFQnx0w+CPScnAMgMNFYCAsQ1ncrwsNhKG1gW3WNnx+5HNkF2cHjUQODIRZutSYKJzBjohoEJl0GkwuycLkkvAPfSEEzDY5pKYvoKm33QanS0Fdh2cS538jfDUDnUblX5YtKPx5n+cYkzxx80DzB8BJsY8LDYDRagLlbs9qIA5LggEwxmogDIDDTm+BUJZlbGnYgmXnLAsaoRoYCDWq1IhUqVEKIqIRSJIk5Jp0yDXpMGNUTth+RRFo7nL0rM0bEvxOmO1wuhQcae7GkebuiNfI0mtQETKoI7Dmz6RL0z8DCQfA0OXfQkKhbE0sAEYc/MEAmG4CA2GqSNPfaCKi4U+lklCSbUBJtgHVY8P3y24FDWZ7WODzNfU2dzrQ6XBhf0Mn9jd0RrxGQYbOO2dfQL++bB2abZ7+g2k/tWM8AVAITwAMnO4lLAD6moADAmDLF71cOzvK4A9vjaChAGql98m3iQIx2BERDVNatcpb8xa5tsAuu4Pm6wut9TPbZLR2O9Ha7cQntR0hr9bgfz55E2XZBozKM2FUUDOvJwSWZBtSd+LmgSRJgCHb84gnAIbV+EVoCo4jAGoBXABA7P/vKIM/SoL7AupSp9aIkofBjogoTRm0akwszsLE4sidui122Tuow+YNgJ6avmOt3fiqtQuyIqHebEe92Y6dX4a/XquWUJFr9E7YHNrHz4j8jGEwcfNACgyARZOjHxcxAIbXBIrOBkiyFVLcNYA53rAXZfCHLxgyAKY1BjsiohEq26DFSeU5OKk8uH+fLMt49dUtmDvvfJzolCP28avvsEF2C3zZasWXrdaI5zfp1EHz9YVO4JxlSPd23ijiDIAupxNvvPI3LDrjFGhtzdHnAfTXAJo9j7gCYGn0wR8MgMMagx0REYWRJKAwU4+yvEycNjp8bi63ItBgsQdN43I8IPg1dtphdbpxoLETBxoj9+/LNWmDBnQE9vWryDUOz4mbB5IkwaU2epp/tdOjHyeEp0YvnnkAXbaAAHgg9vVjBkBvUzADYMphsCMiooSpVZ5m2IpcI04fXxC23+Fyo67dFrxUW8DgjnarjA6rjA6rGZ/WmSNeoyRbH9S0O8q3Rm+eCWU5BmiG88TNA0mSAEOO59FrE7AlvnkA+xQAo80DyAA4lBjsiIhowOk1aowvysT4osjrunY5XBGbeH19/bqdbjRaHGi0OLDrq/aw12tUEspyDZ7gF7BUm6+vX1GmfmT174tHUACcEv24SAEwWk1gIgHQkBN78IcvFGqNA/u+RxgGOyIiGnKZeg2mlWVjWll22D4hBNqtcsRpXI6321DXboPTrXjX77UBaA07h0Gr8oS8vPC1eSvzTMgxjdD+ffHoUwCMNQ+gNwDazZ5HPAEwtMYvUl9ABsCIGOyIiCilSJKE/Awd8jN0OLUyN2y/ogg0dtq9wS44/NW123DCbINdVnCoqQuHmroiXiPLoIk4YbNnrV4TjLoR3r8vHokEQLs5vnkAXfaeANi8P/b1wwJglImgR1gAZLAjIqJhRaWSUJZjRFmOEXPG5Yftd7oUnDDbwiZs9tT4WdHS5USn3YW9JyzYe8IS8RqFmfqA0Be8VFtZrgFa9u+LnyQBxlzPI54AGM88gH0JgNEmgvYtE5cmAZDBjoiI0opOo8KYggyMKYi8ZJfV6erp2xcQ+nwjezsdLrR0OdDS5cDHxzrCXq+SgLIcY1DTrq+PX2WeCcVZeqhGwsTNAy0wABZPjX5cxAAYpSYwoQCYG3UiaMlYBJOj2XO+FF+OhcGOiIhGFJNOg8klWZhcEj5xsxACFpsroKYvvI+fw6WgrsOGug4bgLawc+g0KozKNYYt1ear+cs1aTmwoz/6EgCjDf4ICoAdnkeEAKgBsBAA9v53cAAMbAqedTmgjzxYaCgx2BEREXlJkoQckxY5phycXJETtl9RBFq6HD2BLyT8nTDb4XQpONLSjSMt3RGvkanXBE3W7P/eG/wy9PzTPCASCoAdMecBFJ0noJjroRZy9AA4c9WgvZVE8F8PERFRnFQqCcXZBhRnG1A1Jny/y63ghNneM3dfQDNvbZsVTZ0OdDlc2N/Qif0NkSduzs/QoTLPiIpcAxxtKpg/qsXYwiz/xM06Dfv3DShJAox5nkeUAOiSZWx59VUsO+8saO0t4TV+3S2APvLSfUONwY6IiGiAaNQqb+1b5Ml47bLbE/bard6VOoJr/cw2GW3dTrR1O/HJcTMAFba9vM//ekkCSrMN3pU6jGETOJdmG6Bm/77B4asBzC4CiqcluzRRMdgRERENEYNWjYnFmZhYHLkvlsXunb+vzYavWjrx3p79UGcXo97smd7FJrtxwmzHCbMdO78Mf71WLaE81xg8b19AX7+CDB3796U5BjsiIqIUkW3Q4qTyHJxUngNZLkCpeS+WLTsNWq0WQgi0djvDpm8JnMNPdgt81WrFV63WiOc36dT+Zdkq801Bff0q843IMqT2iE/qXVoFuy+//BL33HMP3nrrLTQ0NKC8vByXX345fvKTn0Cn0yW7eERERH0mSRIKM/UozNRj1ui8sP1uRaDBYg+axuV4QDNvY6cdVqcbXzR24YvGyBM35xi1IfP2+Ub3ekKgQcuJm1NdWgW7/fv3Q1EU/Pa3v8XEiRPx2Wef4dprr0V3dzd+9atfJbt4REREg0atklCRa0RFrhGnjy8I2+9wuVHfYQ8byevr69fW7YTZJsNcJ+OzusgTNxdn6YOadgP7+pXlGKDhxM1Jl1bBbsmSJViyZIn/+fjx43HgwAE8/vjjDHZERDSi6TVqjCvMwLjCyBM3dzlcPU27AeHvuHcOv26nG02dDjR1OlDzVXvY69UqCeW5noEdkaZxKcrSs3/fEEirYBeJ2WxGfn74kjNERETUI1OvwdTSbEwtzQ7bJ4RAu1UOD3ze5t7j7TY43Yo3FNoAtIadQ69RhfXpC1yqLcfE/n0DIa2D3aFDh/Dwww/3WlvncDjgcDj8zy0WTxW0LMuQZXlQyuY772Cdn+LHe5EaeB9SB+9Faki1+5ClkzC9NAPTS8Nr/BRFoKnLgePtNu90Ljb/98fbbWiw2OFwKTjc3I3DzZEnbs4yaDwrduQZPX37fA/vNqMuef37kn0vErmuJIQQg1iWAXHbbbfh3nvvjXnMvn37MHVqz8SCdXV1mDdvHubPn4/f/e53MV9711134e677w7b/txzz8FkijwXEREREcXHpQAdTqDVLqHVAbQ6JLTZgTaH53mn3HsTbaZWoEAPFOgF8g2erwV6oMAgkKcD0rl7n9VqxapVq2A2m5GdHV6jGmhYBLvm5ma0toZX6wYaP368f+RrfX095s+fj9NPPx2bNm2CShX7bkeqsausrERLS0uvP8C+kmUZW7duxcKFC6FN8QWF0x3vRWrgfUgdvBepYSTdB5vTjeMdwbV8/lq/Dhs67a6Yr1d5J24eFVDTF1jrV5yph6ofEzcn+15YLBYUFhbGFeyGRVNsUVERioqK4jq2rq4O5557LqqqqrBx48ZeQx0A6PV66PX6sO1arXbQb+BQXIPiw3uRGngfUgfvRWoYCfdBq9VieoYB0yvCp3EBALNV9vbtCx7RW+vt3+dwKag321FvtmPnl+EDO3RqFSp8gS9CH788kzaugR3JuheJXHNYBLt41dXVYf78+RgzZgx+9atfobm52b+vtLQ0iSUjIiKivsoxaZFjysHJFTlh+4QQaO50BK/N6wt+7VbUd9jhdCs42tKNoy2R+/dl6NTeCZtDBnV4v9cNo2betAp2W7duxaFDh3Do0CGMGjUqaN8waHEmIiKiBEmShOJsA4qzDagaE77f5VZwwmz3ztlnC6j584TApk4Hup1u7G/oxP6GzojXyDNpkSmp8brlE4wuzAiawLkizwi9JnUmbk6rYLdmzRqsWbMm2cUgIiKiFKFRq7y1byZgQvh+u+z29umz+qduCWzu7bDKaLfKaIeE2s8bw14vSUBJlgEvrz0LxdmGIXhHsaVVsCMiIiJKhEGrxsTiTEwszoy432KX8WVTJ/6+7T2UjJ+OerMjqK+fTXajucuB/IzUWLqUwY6IiIgoimyDFtPKsnA0X2DZmWOCBjIIIdDa7USD2Z4yy6kx2BERERH1gSRJKMzUozAzfGaNZEmNeElERERE/cZgR0RERJQmGOyIiIiI0gT72EXgdrsBAMePHx+0JcVcLhdaWlpQV1cHjYa3IZl4L1ID70Pq4L1IDbwPqSPZ98JisQDoySex8F9KBIcOHQIAnHTSSUkuCREREZHHoUOHMHv27JjHSIJLMoRpb29Hfn4+amtrB63GTpZlvPHGG1i0aFHarwGY6ngvUgPvQ+rgvUgNvA+pI9n3wmKxoLKyEm1tbcjLi7yerg9r7CJQqz1Lg2RnZw9qsDOZTMjOzuYvbJLxXqQG3ofUwXuRGngfUkeq3AtfPomFgyeIiIiI0gSDHREREVGaYLAjIiIi6iOb1YrNf3022cXwYx87IiIiogT94Q+P4V9GF3blT4YzrwLndbQjOzf2wIahwGBHREREFIftb/4Dr7QeQU3JeByuPNO/XScceO4vG3HDd9YlsXQeDHZEREREUezfuwebdryG3RWV+Fw3Fe6K8QAASbgxTf4CVfXHcOm0M1CVAqEOYLAjIiIiCtLW2oLH//IkdlWU4GPTNNjHL/XvG+3+CtXNh7FYW4hvXHRlEksZGYMdERERjXgulwtPbPw1dhRmoCZnKjomL/PvK1CaMbvjAM5od+D67/x3EkvZOwY7IiIiGrFefOH3eBMW7CqYhLqJC/3bM0QXTuvai+r6Fly38jq8854Vy1Yvi3Gm1MBgR0RERCPKjnffxItHd2NX6VgcLJoFIXlmf9MIGTMc+1B1vA7fOe+bGDvhOgCelSeGCwY7IiIiSnvHvjyEJ998EbsryvGpfhrkykX+fZPlg6hq+BLfHHUyzlmaev3mEsFgR0RERGnJ2t2Nh599CLvKC7A7czq6J/QMgih312F26xc4152BS1ddl8RSDiwGOyIiIkorTz39a7yfo8Wu3ClomdIT5nJFG6rM+3F6Uxe+c8VaGE3Lk1jKwcFgR0RERMPeP/7+f3i9+wR2FU3AV+PO9W83CBtmWfei+ngjvrvyOuQXnJfEUg4+BjsiIiIalj79+N94ds+/sKt8NPZlTYbIngYAUAsXpjsPoKq+FqtnL8K0GdckuaRDh8GOiIiIho3mxno8tnkTakaVYY9xGpxjF/v3jXcdQVXjEVyQNxaLl387iaVMHgY7IiIiSmk2qxWPP/sQdpbkYnf2NFgCJg8uVhpR3XYAZ3cDV625OYmlTA0MdkRERJSS/vjs43jbIOOj/MlonLzEvz1LmFHVuQ9zTrTju1d+H0bT4hhnGVkY7IiIiChlvPXG37G56SBqSsbh8Kgz/Nt1woFTbXtRVXcC1y+/AmUV85JYytTFYEdERERJdWDff7Dpg1dRU1GJz3VT4a4YAwCQhBtT5YOoOvEVLp0yF9XnXZXkkqY+BjsiIiIacpaOdjz0wuPYXV6MjzOmwza+Z7650e5jqGo+hIWaQly4cnivBDHUGOyIiIhoyDz21P34oCADNblT0R4wCKJAaUF1x36c0ebADdf+dxJLOLwx2BEREdGg+ssLG7FVdGBX4STUTVzo324SXTitax+q65uxdtVNyMxekMRSpgcGOyIiIhpw/353G/58tAY1pWPxRdGpEJIKAKARMmY49uG0uuO4et43MGHStUkuaXphsCMiIqIBUfvVETz1zxewa1Q5PjVMg1y5yL9vknwQVQ1f4psVUzFvKfvNDRYGOyIiIuozm9WKh5/9DT4qy8fuzOnontQzCKJMqUd1yxc4VzZg1eU3JLGUIweDHRERESXs90//Bu9mq7ArbwpaJveEuRzRgWrzPsxt6sS1V6yF0bQsxllooKmSXYBHH30UY8eOhcFgwNy5c7Fz586ox37++ef41re+hbFjx0KSJGzYsKHf5yQiIqL4vPr353HTcw9g7tbN+Mm4eXi94Gy0qIphEDac3l2Dm754FTtOPRl/+uZ3cfP1P4LRZEp2kUecpNbYvfDCC1i3bh2eeOIJzJ07Fxs2bMDixYtx4MABFBcXhx1vtVoxfvx4rFy5Ej/4wQ8G5JxEREQU3Wd7PsQzH7+N3eWjsS9rMpTsqQAAlXDjJOd+VNXX4sqq8zD9vGuSXFICkhzsHnzwQVx77bW46irPTNJPPPEEXn31VTz99NO47bbbwo6fPXs2Zs+eDQAR9/flnERERBSsubEej/19E2oqSvGJcTocY3vWYh3nOoqqpiNYllOJZRd8O4mlpEiSFuycTidqampw++23+7epVCosWLAAO3bsGNJzOhwOOBwO/3OLxQIAkGUZsiz3qSy98Z13sM5P8eO9SA28D6mD9yI1DPV9sFmtePr5J7CzJBu7sqfBMqmnb1yx0ojq9gM4yyxw5ZU3hpUx3SX7dyKR6yYt2LW0tMDtdqOkpCRoe0lJCfbv3z+k51y/fj3uvvvusO1vvPEGTIPcP2Dr1q2Den6KH+9FauB9SB28F6lhsO/Dl0c+xacVWdhVMBkNk3tq5rKEBad17sUpXzZicul06AxjgEJgy5Ytg1qeVJas3wmr1Rr3sRwVC+D222/HunXr/M8tFgsqKyuxaNEiZGdnD8o1ZVnG1q1bsXDhQmi12kG5BsWH9yI18D6kDt6L1DCY9+Htt17DK80HsKtkPA7PvMC/XSucONW+F1XH63HVgpWoqDxjQK87XCX7d8LXkhiPpAW7wsJCqNVqNDY2Bm1vbGxEaWnpkJ5Tr9dDr9eHbddqtYN+A4fiGhQf3ovUwPuQOngvUsNA3YeDX3yOp9/9B3aXj8Jn+qlwV4wGAEhCwVT5C5x24itcMnk25py3pt/XSlfJ+p1I5JpJC3Y6nQ5VVVXYtm0bVqxYAQBQFAXbtm3D2rVrU+acREREw1WXxYyHnnsUuyqK8XHGdNjGL/Hvq3QfQ3XzYSzS5OGbK9ckr5A0oJLaFLtu3TqsXr0a1dXVmDNnDjZs2IDu7m7/iNYrr7wSFRUVWL9+PQDP4Ii9e/f6v6+rq8OePXuQmZmJiRMnxnVOIiKidPfYUw9gR4EBu3Kmon1KzyCIfKUV1eb9OL3FhhuvuzWJJaTBktRgd8kll6C5uRl33HEHGhoaMHPmTLz++uv+wQ/Hjh2DStUzh3J9fT1mzZrlf/6rX/0Kv/rVrzBv3jy8/fbbcZ2TiIgoHf31xU3Y6m7HrsKJOD7xfP92k+jGrO69qK5rxvdW3YTM7PNjnIWGu6QPnli7dm3UZlJfWPMZO3YshBD9OicREVG62PnBW3jh4C7UlI3BgYJTICRPZYhauDDDsQ9Vdcex5pz/wqTJ1ya5pDRUkh7siIiIKH71dcfw+JY/YXdFOf5jmAZ59CL/vknyIVQ1HMWKimmYv/SKJJaSkoXBjoiIKMXZrFY88ocN+Ki8ALszp6Nr4lL/vjKlHtWtX2C+w4BvX3FDEktJqYDBjoiIKEUdPvIJrvv7MezKm4LmgEEQOaIDVZZ9mNtowXVXfA9G07IYZ6GRhMGOiIgohWx55c941VyLmuIJ+HLmf/m364UdM217UXX8BL674ioUlcxPXiEpZTHYERERJdnn/6nBMzVvYndZJfaapkDJmAwAUAk3pjsPoKr+GK447VycPPPqJJeUUh2DHRERURK0tjbj0b88hZpRpdhjnA7H2J51Wse5jqKq6QhOapPxnRv+myuAUNwY7IiIiIaIzWrFk88+gg9LslCTPQ3myT1944qUJlS3H8DXLAquufr7kGUZW7ZsSWJpaThisCMiIhpkf/rjb7FdZ0dNwSScmNwzPUmm6MRpXXsxu74Va6+8BUbTohhnIeodgx0REdEgePutV/FS3V7UlIzDoYq5/u1a4cQp9n047Xg9rlt8CSrHnJ3EUlK6YbAjIiIaIIe/2Iun3/k7aipG4VP9NLhHVQAAJKFginwQVQ1f4eIJ1Zh73uokl5TSFYMdERFRP3RZzHj4uUexq6IIH2dMh3VCz+TBle5aVLUcwkIpB9+6hCNaafAx2BEREfXBE797AB/kG7ArZyraAiYPzlNaMdu8H3NbrLjpuh8msYQ0EjHYERERxWnzX5/FP50t2FU0AbUTzvdvNworZnXvRXVdI25etRaZ2efHOAvR4GGwIyIiiuGjHf/C8198iJrSMTiQdxKEpAIAqIULMxz7MKv+ONacuRxTpn0nySUlYrAjIiIKc6KuFk9seRY1FeX4xDAd8uieaUgmyodQ1XgU3yyZgvlLr0hiKYnCMdgRERHBM3nwY394CDvL8rA7axo6J/b0mytVTqC69QvMt2tx+ZU3JrGURLEx2BER0Yi28ZmH8W6GwK68KWiassS/PVt0oNqyD3MazLj+ypthNC2NcRai1MBgR0REI87rr7yIV8zHUFM8HkdH90wQrBN2zLTtRdXxBty4Yg2KSuYnr5BEfcBgR0REI8K+T3dj00dvoKZ8NPaapkDJmAQAkIQb0+UvUFV/DKtOORszz+N8czR8JRTsOjo68NJLL+Hdd9/FV199BavViqKiIsyaNQuLFy/GmWeeOVjlJCIiSlhbawse/cuT2DWqFJ8Yp8E+rqepdazrS1Q1HcayzHIs/8ZlSSwl0cCJK9jV19fjjjvuwJ/+9CeUl5djzpw5mDlzJoxGI9ra2rB9+3b86le/wpgxY3DnnXfikksuGexyExERRWSzWvHUs4/iw+JM1ORMRcfknkEQhUoTZrcfwFlmN75zzS3JKyTRIIkr2M2aNQurV69GTU0Npk+fHvEYm82GzZs3Y8OGDaitrcWtt946oAUlIiKK5fnnnsI2dTdqCiahfvJC//YM0YnTuvZidn0rvnflLTCaFsU4C9HwFlew27t3LwoKCmIeYzQacdlll+Gyyy5Da2vrgBSOiIgoln+9tQUvHf8cNaVjcbBstn+7Vjgxw74PVcfrcd3iS1A55uwYZyFKH3EFu95CXX+PJyIiiteXh/fjqe2bUVNRgc/0U+Gq9NTOSULBFNdBnNbwFS4eV4XTz1ud5JISDb0+jYqtr6/He++9h6amJiiKErTv5ptvHpCCERER+XRZzHjkucewq6IQuzOmwTqhZxBEhfs4ZrccxHkiGxdfdk0SS0mUfAkHu02bNuH666+HTqdDQUEBJEny75MkicGOiIgGzG9/9yDez9OhJncqWqf0TBCcJ9pQ3bEPp7dYcdN1P0xiCYlSS8LB7mc/+xnuuOMO3H777VCpVINRJiIiGsH+/rdn8bqjGTVFE3Fswnn+7UZhxazuvaiqa8J3V16H/ILzYpyFaGRKONhZrVZceumlDHVERDRgav79Dv5v/79RUzYa+3OnQ0hqAIBauHCScz+q6mqx+oylmDr9O0kuKVFqSzjYXXPNNXjxxRdx2223DUZ5iIhohDhRV4snXn0WNaPK8R/DNDjH9ExDMsF1GFUNR/FfRROxYMnlSSwl0fCScLBbv349LrjgArz++uuYMWMGtFpt0P4HH3xwwApHRETpxWa14rE/PISdZXnYnTUNnZN6Jg8uURowu/UAzrFrcOWVNyWxlETDV5+C3T//+U9MmTIFAMIGTxAREYXa9MwjeMekYFf+FDRN6RnRmi3MqLLsxewGM7575c0wmpbEOAsR9SbhYPfAAw/g6aefxpo1awahOERElC7+ueWveKX9S9QUj8OR0V/zb9cJO2ba9qHq+Anc8F9XoqRsXhJLSZReEg52er0eZ5111mCUhYiIhrn9n32MTTv/iZrySuw1TIG7fAIAQBJuTJO/QHX9V7h0+tdw2nlXJbmkROkp4WD3/e9/Hw8//DAeeuihwSgPERENM22tLXjsL09iV0UJ9pimwz6upzl1jOsrVDcfxhJTKb6+YlUSS0k0MiQc7Hbu3Im33noLr7zyCk466aSwwRN/+9vfBqxwRESUmmxWK373x0fx76IM1ORMQ8fknkEQhUozqjsO4MwOGddd84MklpJo5Ek42OXm5uLCCy8cjLIQEVGKe/753+EtqQu7CiahftJC//YM0YXTuvaiur4VN626EZnZC2OchYgGS8LBbuPGjYNRDiIiSlHvvf1P/PXYf1BTOhZflFT7t2uFEzMc+1B1vA7XnHchxk64LomlJCKgD8GOiIjSX1d7C+565n7srhiFT/XT4KrsqYGbLH+B6oYvceHoU/G1pauTWEoiChVXsFuyZAnuuusunH766TGP6+zsxGOPPYbMzEzcdBMnlyQiGk66LGY88tyj2FVehN2jp8EqVfn3lbvrMLv1C5wrMnHppdcmsZREFEtcwW7lypX41re+hZycHHz9619HdXU1ysvLYTAY0N7ejr179+K9997Dli1bsHz5ctx///2DXW4iIhogT/5+A97P1WBX7hS0TukZBJEr2lDdsR+nN3fjmitugtG0PImlJKJ4xBXsrrnmGlx++eV48cUX8cILL+DJJ5+E2WwG4FltYvr06Vi8eDE++ugjTJs2bVALTERE/fePl/6I1+xN2FU0AcfGz/dvNwgrZln3YuaX9bjhomtRUnpe8gpJRAmLu4+dXq/H5Zdfjssv9yzGbDabYbPZUFBQEDblCRERpZ49u97HHz97D7vLRmNfzjSI3JMBAGrhwknO/aiqq8WVc5Zg4tTV2LJlC/ILCpNcYiJKVJ8HT+Tk5CAnJ2cgy0JERAOs8UQdHv/HM6ipKMMnxulwjlns3zfBdQRVjUdwQd5YLFp+uX+7LMvJKCoRDQCOiiUiSjM2qxWPP/swdpbmoCZrGjon9fSbK1EaUN32Bc7pVmH1mrVJLCURDQYGOyKiNPGHPzyGfxld2JU/GY2Te2rmsoQZVZ37MOdEO7575fdhNC2JcRYiGs5UyS4AADz66KMYO3YsDAYD5s6di507d8Y8/sUXX8TUqVNhMBgwY8YMbNmyJWj/mjVrIElS0GPJEn6QEVH6efOfm3HzH3+Fs7b+DT+qPBOvFp6DRlUpdMKB2daPccPBLXhnyng8/40bsO6G22E0mZJdZCIaREmvsXvhhRewbt06PPHEE5g7dy42bNiAxYsX48CBAyguLg47/oMPPsBll12G9evX44ILLsBzzz2HFStWYPfu3Tj55JP9xy1ZsiRolQy9Xj8k74eIaLDt37sHz+x4DTUVlfhcNxXuirEAAEm4MU3+AlX1x3DptDNQdd5VyS0oEQ25hIPd6tWrcc011+Ccc84ZkAI8+OCDuPbaa3HVVZ4PoCeeeAKvvvoqnn76adx2221hx//mN7/BkiVL8MMf/hAAcM8992Dr1q145JFH8MQTT/iP0+v1KC0tHZAyEhElW1trCx7/y5PYVVGCj03TYB+/1L9vtPsrVDcfxhJ9Mf7rwstjnIWI0l3Cwc5sNmPBggUYM2YMrrrqKqxevRoVFRV9urjT6URNTQ1uv/12/zaVSoUFCxZgx44dEV+zY8cOrFu3Lmjb4sWLsXnz5qBtb7/9NoqLi5GXl4fzzjsPv/jFL1BQUNCnchIRJYPL5cITG3+NHYUZqMmZio7JPYMgCpRmVHccwJntTlz/nXUxzkJEI0nCwW7z5s1obm7Gs88+i2eeeQZ33nknFixYgGuuuQbf+MY3EprTrqWlBW63GyUlJUHbS0pKsH///oivaWhoiHh8Q0OD//mSJUtw4YUXYty4cTh8+DB+/OMfY+nSpdixYwfUanXYOR0OBxwOh/+5xWIB4BnyP1jD/n3n5bQCycd7kRp4H3q89Jc/YJvKgl0Fk1A3sWeN1gzRhdO69qKqvgXXr7wOmdnzAQz8z4z3IjXwPqSOZN+LRK7bpz52RUVFWLduHdatW4fdu3dj48aNuOKKK5CZmYnLL78cN954IyZNmtSXUw+ISy+91P/9jBkzcMopp2DChAl4++23cf7554cdv379etx9991h29944w2YBrmj8datWwf1/BQ/3ovUMFLvQ0v9UXxmcqKmbCwOlsyCkDxj2zRCxgzHPsw6VouT9KXIzCsBykrwznvvD3qZRuq9SDW8D6kjWffCarXGfWy/Bk+cOHECW7duxdatW6FWq7Fs2TJ8+umnmD59Ou677z784Ac/iPn6wsJCqNVqNDY2Bm1vbGyM2j+utLQ0oeMBYPz48SgsLMShQ4ciBrvbb789qHnXYrGgsrISixYtQnZ2dsz30FeyLGPr1q1YuHAhV+5IMt6L1DAS78OxY4ex8a2XsHtUOT6dciZkSeffN1k+iKqGL/GN8mk4a8FlQ1qukXgvUhHvQ+pI9r3wtSTGI+FgJ8syXn75ZWzcuBFvvPEGTjnlFNxyyy1YtWqVPwS99NJLuPrqq3sNdjqdDlVVVdi2bRtWrFgBAFAUBdu2bcPatZEnzjzjjDOwbds23HLLLf5tW7duxRlnnBH1OsePH0drayvKysoi7tfr9RFHzWq12kG/gUNxDYoP70VqSPf7YO3uxsPPPoRd5QXYnTkd3RN7BkGUu+tQ3XoQ57lNuHTVdUkspUe634vhgvchdSTrXiRyzYSDXVlZGRRFwWWXXYadO3di5syZYcece+65yM3Njet869atw+rVq1FdXY05c+Zgw4YN6O7u9o+SvfLKK1FRUYH169cDAL7//e9j3rx5eOCBB7B8+XI8//zz2LVrF5588kkAQFdXF+6++25861vfQmlpKQ4fPowf/ehHmDhxIhYvXhy1HEREg+mpp3+N93O02JU7BS1TesJcrmhHlXkfTm/uwncuXwujaXkSS0lEw13Cwe7Xv/41Vq5cCYPBEPWY3NxcHD16NK7zXXLJJWhubsYdd9yBhoYGzJw5E6+//rp/gMSxY8egUvXMo3zmmWfiueeew09/+lP8+Mc/xqRJk7B582b/HHZqtRr/+c9/8Mwzz6CjowPl5eVYtGgR7rnnHs5lR0RD6h+bn8Pr1gbsKpqAr8ad699uEDbMtO5FdV0jbrzoOuQXnBvjLERE8Us42F1xxRUDXoi1a9dGbXp9++23w7atXLkSK1eujHi80WjEP//5z4EsHhFR3Pbs/jee++Rf2FU+Gvuyp0DkTAcAqIUL050HUFVfi9WzF2HajGuSXFIiSkdJX3mCiGi4a26sx2ObN6FmVBn2GKfBOban28d41xFUNR7BBXljsXj5t5NYSiIaCRjsiIj6wGa14vFnH8LOklzszp4GS8DkwcVKI6rbDuDsbuCqNTcnsZRENNIw2BERJeCPf3gMbxtd+Ch/MhonL/FvzxIWnNa5D3NOtOHGK78Po4mDtYho6DHYERH14q03/o7NjQdRUzoOhyvP9G/XCQdOte3FaXUncMPyK1BWMTBraBMR9RWDHRFRBAf2/QebPngVNRWV+Fw3Fe5RYwAAknBjqnwQVSe+wqVT5qL6vKuSXFIioh4MdkREXpaOdjz0wuPYXV6MjzOmwza+Z7650e5jqGo+hMW6Iqz41sDPDkBENBAY7IhoxHvsqfvxQUEGanKnoj1gEESB0oKqjv04s92BG77z30ksIRFRfBjsiGhE+ssLG7FVdGBX4STUTVzo324S3Titey+q65qxdtVNyMxekMRSEhElhsGOiEaMf7+7DX8+WoOa0rH4ouhUCMmzqo1GyDjZsR9Vdcex5pz/wqTJ1ya5pEREfcNgR0RprfarI3jqny9g16hyfGqYBrlykX/fJPkgqhq+xIqKaZi/lP3miGj4Y7AjorRjs1rx8LO/wUdl+didOR3dk3oGQZQp9ahu+QLnygasuvyGJJaSiGjgMdgRUdr4/dO/wbvZKuzKm4KWyT1hLkd0oNq8D3ObOnHtFWthNC2LcRYiouGLwY6IhrUtLz+PV7vqsKtoAr4aN8+/3SBsONW2D9XHG3DTRdchv2B+8gpJRDREGOyIaNj5bM+H+MPHb6OmfDT2ZU6GkjUVAKASbkx3HkBV/TFccdq5OHnm1UkuKRHR0GKwI6JhobmxHo/9fRNqKkrxiXE6HGN71mId5zqKqqYjWJZTiWUXrEpiKYmIkovBjohSls1qxW+ffRg7S7KxK3saLJN6+sYVK42obj+As7uBq1bfnMRSEhGlDgY7Iko5f/rjE9iuc2BXwWQ0TO6pmcsUnajq3IvZJ9pw05Xfh9G0OMZZiIhGHgY7IkoJb297FdssB3Hf2y4crjjdv10rnDjVvhen1dXju8uuQFnF2UksJRFRamOwI6KkOfjF53j63X9gd/kofKafCveosQAASSiYIh9EVcNXuGRSNeactyap5SQiGi4Y7IhoSHVZzHjouUexq6IYH2dMh238Ev++SvcxVLUcxmJ1Hr65ck3yCklENEwx2BHRkHjsqQewo8CAXTlT0T6lZxBEvtKKavN+nN5qQ0XxJCz71lpotdoklpSIaPhisCOiQfOXP2/Em0oHdhVOxPGJ5/u3m0Q3ZnXvRXVdM7636iZkZp8PWZaxZcuWJJaWiGj4Y7AjogG184O38MLBXagpG4MDhadCSCoAgFq4MMOxD1V1x7HmnP/CpMnXJrmkRETph8GOiPrteO2X+O1r/4fdo8rxH8M0yKMX+fdNlA+hqvEovlk+DfOXXpHEUhIRpT8GOyLqE5vVikf+sAEflRdgd+Z0dE1a6t9XptSjuvULzHcY8O0rbkhiKYmIRhYGOyJKyMZND+GdTAm78qagOWAQRLboQLVlH+Y2WnDdFd+D0bQsxlmIiGgwMNgRUa+2vPJnvGquRU3xBHw55hz/dr2wY6ZtL6qOn8B3V1yFopL5ySskEREx2BFRZJ99sgt/2L0Nu8sqsdc0BUrGZACASrgx3XkAp504hitnnYuTZ16d5JISEZEPgx0R+bU2NeLRl36PmlGl2GOcDsfYnrVYx7q+RFXTYSzLrMDyb6xKYimJiCgaBjuiEc5mteLJZx/BhyVZqMmeBvPknr5xRUoTqtsP4GsWBddc/f0klpKIiOLBYEc0Qv3pj7/Fdp0dNQWTcGJyz/QkmaITp3Xtxez6Vqy98hYYTYtinIWIiFIJgx3RCPL2tlfxUv1e1JSMw6GKuf7tWuHEKfZ9OO14Pa5bfAkqx5ydxFISEVFfMdgRpbnDX+zF0+/8HTUVo/CpfhrcoyoAAJJQMMV1EFUnvsLK8VU4/bzVSS4pERH1F4MdURrqspjx8HOPYldFET7OmA7rhJ7Jg0e5a1HdcggLpFxcdMlVSSwlERENNAY7ojTyxO8ewAf5BuzKmYq2gMmD85RWzDbvx9wWK2667odJLCEREQ0mBjuiYW7zX5/FP50t2FU0AbUTzvdvNworZnXvRXVdI25etRaZ2efHOAsREaUDBjuiYeijHW/j+S92oqZ0DA7knQQhqQAAauHCyY79OK2+FmvOXI4p076T5JISEdFQYrAjGiZO1NXi8S3PYndFOT4xTIc8umcakgmuw6huPIJvFE/GeUsvT2IpiYgomRjsiFKYzWrFo394CB+V5WF31jR0TuzpN1eqnEB16xeYZ9fiiitvTGIpiYgoVTDYEaWgjc88jHczBHblTUHTlCX+7dmiA1WWfZjTYMENV34PRtPSGGchIqKRhsGOKEW89sqf8aq5FjXF43F0dM8EwTphx0zbXlTVNeDGb6xBUcn85BWSiIhSGoMdURLt+3Q3Nn30BmrKR2OvaQqUjMkAAEm4MV3+AlX1x7DqlLMx87yrk1xSIiIaDhjsiIZYW2sLHv3Lk9g1qhSfGKfBPq6nqXWM60tUNx/GElMZvr5iVRJLSUREwxGDHdEQsFmteOrZR/FhcSZqcqaiY3LPIIhCpRnV7ftxlsWFa6/+QRJLSUREwx2DHdEgev65p7BN3Y2agkmon7zQvz1DdOK0rr2YfaIN37vi+zCaFsY4CxERUXxUyS4AADz66KMYO3YsDAYD5s6di507d8Y8/sUXX8TUqVNhMBgwY8YMbNmyJWi/EAJ33HEHysrKYDQasWDBAhw8eHAw3wKR37/e2oJb/nA/zn7jRdxSNhv/KJ6PenUFtMKJ02yf4NqDr+HtCRV48b+ux4+uvx1GkynZRSYiojSR9GD3wgsvYN26dbjzzjuxe/dunHrqqVi8eDGampoiHv/BBx/gsssuwzXXXIOPP/4YK1aswIoVK/DZZ5/5j7nvvvvw0EMP4YknnsCHH36IjIwMLF68GHa7fajeFo0wXx7ej5/87n+x5LVn8W0U4fnKhTionQRJKJgif4FVtW/gRW0ntixbjXuuux2VY8Ynu8hERJSGkt4U++CDD+Laa6/FVVddBQB44okn8Oqrr+Lpp5/GbbfdFnb8b37zGyxZsgQ//KFnIfN77rkHW7duxSOPPIInnngCQghs2LABP/3pT/GNb3wDAPCHP/wBJSUl2Lx5My699NKhe3OU1r48vB9/fPNv2FNehN0Z02Cd0DMIosJ9HNUtB3E+snHxpdcksZRERDSSJDXYOZ1O1NTU4Pbbb/dvU6lUWLBgAXbs2BHxNTt27MC6deuCti1evBibN28GABw9ehQNDQ1YsGCBf39OTg7mzp2LHTt2pESw83Skfxg2mw1fPb0fAlLQfgER/Dz4adh+hB4fdL5IR4ccL4VcP/SCvZQvekkiXCv0XMFPIYW+15D9ib6X0BeEns/H5Vawe+PnYcfKKgl2rQZ2rQY2rQbdOh06dJlo0+SgSVUMOWAQRJ5oQ1XHfpzR0o2brvth5AsRERENoqQGu5aWFrjdbpSUlARtLykpwf79+yO+pqGhIeLxDQ0N/v2+bdGOCeVwOOBwOPzPLRYLAECWZciynMA7ik97WzN+OXnxgJ+Xhl626MBJ1kOoOt6Ea1dchfwCz8TCg/HvJt35fmb82SUf70Vq4H1IHcm+F4lcN+lNsalg/fr1uPvuu8O2v/HGGzANQsf27i4zcstGBW0Lq0gKqTUL3S8FVUXFPra3/VKMGrhY+0L393psWE1g38uR2HsM2Rd2meiv1QoXjIodBrcTRpcMo+xErtWOnC47ch0KRpVPh85QBJQV4d8fxh70Q/HZunVrsotAXrwXqYH3IXUk615Yrda4j01qsCssLIRarUZjY2PQ9sbGRpSWlkZ8TWlpaczjfV8bGxtRVlYWdMzMmTMjnvP2228Pat61WCyorKzEokWLkJ2dnfD7iscKWcbWrVuxcOFCaLXaQbkGxUfmvUgJvA+pg/ciNfA+pI5k3wtfS2I8khrsdDodqqqqsG3bNqxYsQIAoCgKtm3bhrVr10Z8zRlnnIFt27bhlltu8W/bunUrzjjjDADAuHHjUFpaim3btvmDnMViwYcffojvfve7Ec+p1+uh1+vDtmu12kG/gUNxDYoP70Vq4H1IHbwXqYH3IXUk614kcs2kN8WuW7cOq1evRnV1NebMmYMNGzagu7vbP0r2yiuvREVFBdavXw8A+P73v4958+bhgQcewPLly/H8889j165dePLJJwEAkiThlltuwS9+8QtMmjQJ48aNw89+9jOUl5f7wyMRERFROkp6sLvkkkvQ3NyMO+64Aw0NDZg5cyZef/11/+CHY8eOQaXqmW7vzDPPxHPPPYef/vSn+PGPf4xJkyZh8+bNOPnkk/3H/OhHP0J3dzeuu+46dHR04Gtf+xpef/11GAyGIX9/REREREMl6cEOANauXRu16fXtt98O27Zy5UqsXLky6vkkScLPf/5z/PznPx+oIhIRERGlvJQIdqnG7XYDAI4fPz5ogydcLhdaWlpQV1cHjYa3IZl4L1ID70Pq4L1IDbwPqSPZ98I3eMKXT2Lhv5QIDh06BAA46aSTklwSIiIiIo9Dhw5h9uzZMY+RRPgyAyNee3s78vPzUVtbO2g1dkRERETx8E3D1tbWhry8vJjHssYuArVaDQDIzs5msCMiIqKU4Msnsah6PYKIiIiIhgUGOyIiIqI0wWBHRERElCYY7IiIiIjSBAdPEAE4bneiTXYl/Lp8rQajDLpej3O6FLiVvg1AV6sk6DT8PxgR0WBIt89nBjsa8Y7bnTjrw31w9OEXW6+S8P7caTHDndOl4JPjHeh2JB4cASBDr8Gpo3JT7sODiGi4S8fPZwY7GvHaZFefQh0AOBSBNtkVM9i5FYFuhws6tQpadWK//LJbQbfD1ef/TRIRUXTp+PnMYEc0RLRqFQza3ucgCuV0K4NQGiIi8kmnz+fUqTskIiIion5hsCMiIiJKEwx2RERERGmCwY6IiIgoTTDYEREREaUJBjsiIiKiNMFgR0RERJQmGOyIiIiI0gSDHREREVGaYLAjIiIiShMMdkRERERpgsGOiIiIKE0w2BERERGlCQY7IiIiojTBYEdERESUJhjsiIiIiNIEg12SKIobDnsHFMWd7KL0WTq8ByIiov5Itb+FDHZJIoQbDocFQqTGP4S+SIf3QERE1B+p9reQwY6IiIgoTTDYEREREaUJBjsiIiKiNMFgR0RERJQmGOyIiIiI0gSDHREREVGaYLAjIiIiShOaZBdgJLPbbIDUDbVaDtunVqthMBj8z7u7u6OeR6VSwWg09ulYq9UKIUTEYyVJgslkinqs2+30vweNxhV0rM1mg6IoUcuRkZHRp2Ptdjvc7uhzBSVyrMlkgiRJUffHy2azoVvV83MxGo1QqTz/Z3I6nei22mG326EWGgh38P+l9Ho9VJJnm+yS4XK5gvY7XArsDhe6rd3QqTOgVqv955Xl8H83PgaDwX+sLMtwOp1Rj9Xr9dBoNAkf63K54HA4oh6r0+mg1WoTPtbtdsNut0c9VqvVQqfTJXysoiiw2WwDcqxGo4FerwcACCFgtVoH5NhEfu+Hw2dErGOH22eEw+EI+/3s67GhnxGxfpcTOTbw956fEfF9RrhEz2eyIpSYZdBoNNBqtP5j7Q477A4XrFYVDKmUpgSFMZvNAoAwm82Ddg2XyyEmTSwSer1GAAh7LFu2LOh4k8kU8TgAYt68eUHHFhYWRj22uro66NgxY8ZEPXb69OlBx06fPj1ov16v8b+HMWPGBB1bXV0d9byFhYVBx86bNy/qsSaTKejYZcuWRT029J/zRRddFPPYrq4uIYQQn1i6RclbH/f5oZk0Nei8R48e9Zfh1ltvFZJGLwzjThP6yhlCVz416PHCW7vEziOtYueRVnHDT+8L26+vnCEM404TkkYvdu7c6T/vfffdF/O9bd++3X/sI488EvPYV155xX/sxo0bYx775z//2X/sn//855jHbty40X/sK6+8EvPYRx55xH/s9u3bYx573333+Y/duXNnzGPvvPNO/7GfffZZzGNvvfVW/7FHjx6NeeyNN97oP7apqSnmsatXr/Yf29XVFfPYiy66KOjfcKxjh8NnROBjOH9GCCHE6tWrYx7b1NTkP/bGG2+MeWzoZ0SsYz/77DP/sXfeeWfMY/kZ4Xkk8hnxkzvuFtv3N4r3DzaLF97aFfYZHPhYc+vP/Z/XL3/wmf/z2ZBhEuaOr4TL5RCDJZFckkoZkygp8rUaqFwuKJrEfx2EwwHF3BH7GMUNxWmDSmeE5P3fno9NVtDt9PzPXoYaKr0p7PWK0waRIkvVEBGlEwkCGXoNuh0u2GQl4mewjwy1//PaKiuQNFooThukGLXJySAJEaXefASzWCzIycmB2WxGdnb2oFzD7XaitfkoDKZiqNW6sP3DoZnF7XbCbm2CwVQMjUY/rJtZjli60GyPXgVvMBj8xzqdTv958zVqlOuDw1qkphOnW4FbCf85Gw0hx7oiNMurJOjUKjazeLEpNvFj2RTbt2PZFOuR7p8RUGngVoTn994e4zNCE/IZYbdBrZKghgsQncjILI3493wgJJJLGOwiGKpg193VMKj/EAZbOrwHIiKi/hiKv4WJ5BKOiiUiIiJKEwx2RERERGmCwY6IiIgoTTDYEREREaUJBjsiIiKiNMFgR0RERJQmGOyIiIiI0gSDHREREVGaYLAjIiIiShMMdkkiSWro9dmQJHWyi9Jn6fAeiIiI+iPV/hYmvuo5DQiVSg29ITfZxeiXdHgPRERE/ZFqfwtZY0dERESUJhjsiIiIiNIEgx0RERFRmmCwIyIiIkoTDHZEREREaYLBjoiIiChNMNgRERERpQkGOyIiIqI0wWBHRERElCYY7IiIiIjSBIMdERERUZpgsCMiIiJKEwx2RERERGmCwY6IiIgoTTDYEREREaUJBjsiIiKiNMFgR0RERJQmGOyIiIiI0gSDHREREVGaYLAjIiIiShMMdkRERERpgsGOiIiIKE0w2BERERGlCQY7IiIiojShSXYBUpEQAgBgsViSXBIiIiIa6Xx5xJdPYmGwi6CzsxMAUFlZmeSSEBEREXl0dnYiJycn5jGSiCf+jTCKoqC+vh5ZWVmQJGlQrmGxWFBZWYna2lpkZ2cPyjUoPrwXqYH3IXXwXqQG3ofUkex7IYRAZ2cnysvLoVLF7kXHGrsIVCoVRo0aNSTXys7O5i9siuC9SA28D6mD9yI18D6kjmTei95q6nw4eIKIiIgoTTDYEREREaUJBrsk0ev1uPPOO6HX65NdlBGP9yI18D6kDt6L1MD7kDqG073g4AkiIiKiNMEaOyIiIqI0wWBHRERElCYY7IiIiIjSBINdkjz66KMYO3YsDAYD5s6di507dya7SCPK+vXrMXv2bGRlZaG4uBgrVqzAgQMHkl2sEe9///d/IUkSbrnllmQXZUSqq6vD5ZdfjoKCAhiNRsyYMQO7du1KdrFGHLfbjZ/97GcYN24cjEYjJkyYgHvuuSeu5aSo79555x18/etfR3l5OSRJwubNm4P2CyFwxx13oKysDEajEQsWLMDBgweTU9gYGOyS4IUXXsC6detw5513Yvfu3Tj11FOxePFiNDU1JbtoI8a//vUv3HTTTfj3v/+NrVu3QpZlLFq0CN3d3cku2oj10Ucf4be//S1OOeWUZBdlRGpvb8dZZ50FrVaL1157DXv37sUDDzyAvLy8ZBdtxLn33nvx+OOP45FHHsG+fftw77334r777sPDDz+c7KKlte7ubpx66ql49NFHI+6/77778NBDD+GJJ57Ahx9+iIyMDCxevBh2u32ISxobR8Umwdy5czF79mw88sgjADxLmFVWVuJ73/sebrvttiSXbmRqbm5GcXEx/vWvf+Gcc85JdnFGnK6uLpx22ml47LHH8Itf/AIzZ87Ehg0bkl2sEeW2227D+++/j3fffTfZRRnxLrjgApSUlOD3v/+9f9u3vvUtGI1G/PGPf0xiyUYOSZLw0ksvYcWKFQA8tXXl5eX47//+b9x6660AALPZjJKSEmzatAmXXnppEksbjDV2Q8zpdKKmpgYLFizwb1OpVFiwYAF27NiRxJKNbGazGQCQn5+f5JKMTDfddBOWL18e9HtBQ+vll19GdXU1Vq5cieLiYsyaNQtPPfVUsos1Ip155pnYtm0bvvjiCwDAJ598gvfeew9Lly5NcslGrqNHj6KhoSHoMyonJwdz585Nub/dXCt2iLW0tMDtdqOkpCRoe0lJCfbv35+kUo1siqLglltuwVlnnYWTTz452cUZcZ5//nns3r0bH330UbKLMqIdOXIEjz/+ONatW4cf//jH+Oijj3DzzTdDp9Nh9erVyS7eiHLbbbfBYrFg6tSpUKv/f3v3F9JU38AB/Du35kbMUVv4B5us0lJbthxW20XCllDgzchIRGQSQai4BUFUuwhyXRWRF8UMpKAlEUkUeCFmllBK6cCxKAJlEanBAmlG0Xbei5dnMJzv8z6Pezo953w/MHC/nXP2PXjx+3LOb5sSyWQSPT09aGlpETuabM3PzwNA1rn7j9d+Fyx2JHsdHR2IRCIYHx8XO4rsfPjwAd3d3RgeHoZGoxE7jqylUinYbDYEAgEAgNVqRSQSwY0bN1jsfrF79+7hzp07CIVCqK6uRjgchtfrRUlJCf8X9Kd4K/YXMxqNUCqVWFhYyBhfWFhAUVGRSKnkq7OzE48fP8bo6ChKS0vFjiM7r1+/xuLiIvbs2QOVSgWVSoWxsTFcu3YNKpUKyWRS7IiyUVxcjKqqqoyxyspKxGIxkRLJ1+nTp3HmzBkcO3YMFosFra2t8Pl8uHTpktjRZOuP+fnfMHez2P1iarUatbW1GBkZSY+lUimMjIxg//79IiaTF0EQ0NnZicHBQTx58gRms1nsSLLkdDoxMzODcDicfthsNrS0tCAcDkOpVIodUTYcDseKr/x59+4dysrKREokX8vLy8jLy5yelUolUqmUSInIbDajqKgoY+5eWlrCxMTEbzd381asCE6dOoW2tjbYbDbU1dXh6tWrSCQS8Hg8YkeTjY6ODoRCITx8+BA6nS69RkKv10Or1YqcTj50Ot2KdY3r16+HwWDgesdfzOfzwW63IxAI4OjRo5icnEQwGEQwGBQ7muw0Njaip6cHJpMJ1dXVmJ6expUrV9De3i52NEn7+vUr3r9/n34+OzuLcDiMjRs3wmQywev14uLFiygvL4fZbIbf70dJSUn6k7O/DYFE0dvbK5hMJkGtVgt1dXXCy5cvxY4kKwCyPvr7+8WOJnsHDhwQuru7xY4hS48ePRJ27twp5OfnCzt27BCCwaDYkWRpaWlJ6O7uFkwmk6DRaIQtW7YI586dE75//y52NEkbHR3NOi+0tbUJgiAIqVRK8Pv9QmFhoZCfny84nU7h7du34obOgt9jR0RERCQRXGNHREREJBEsdkREREQSwWJHREREJBEsdkREREQSwWJHREREJBEsdkREREQSwWJHREREJBEsdkREREQSwWJHRJRDfr8fJ06cWNMxotEoSktLkUgkcpSKiOSCvzxBRJQj8/PzqKiowMzMDMrKytZ0rCNHjqCmpgZ+vz9H6YhIDnjFjogoR27evAm73b7mUgcAHo8H169fx8+fP3OQjIjkgsWOiGgV9+/fh8VigVarhcFggMvl+p+3RwcGBtDY2JgxVl9fj66uLni9XmzYsAGFhYXo6+tDIpGAx+OBTqfDtm3bMDQ0lLHfwYMHEY/HMTY29o+cGxFJE4sdEVEWnz59QnNzM9rb2/HmzRs8ffoUbrcbq61eicfjiEajsNlsK167desWjEYjJicn0dXVhZMnT6KpqQl2ux1TU1NoaGhAa2srlpeX0/uo1Wrs3r0bz58//8fOkYikh2vsiIiymJqaQm1tLebm5v6vW6vhcBhWqxWxWAybN29Oj9fX1yOZTKYLWjKZhF6vh9vtxu3btwH8d21ecXExXrx4gX379qX3dbvd0Ov16O/vz/HZEZFU8YodEVEWNTU1cDqdsFgsaGpqQl9fH758+bLq9t++fQMAaDSaFa/t2rUr/bdSqYTBYIDFYkmPFRYWAgAWFxcz9tNqtRlX8YiI/gyLHRFRFkqlEsPDwxgaGkJVVRV6e3uxfft2zM7OZt3eaDQCQNbyt27duoznCoUiY0yhUAAAUqlUxnbxeBybNm1a03kQkbyw2BERrUKhUMDhcODChQuYnp6GWq3G4OBg1m23bt2KgoICRKPRnL1/JBKB1WrN2fGISPpY7IiIspiYmEAgEMCrV68Qi8Xw4MEDfP78GZWVlVm3z8vLg8vlwvj4eE7ef25uDh8/foTL5crJ8YhIHljsiIiyKCgowLNnz3D48GFUVFTg/PnzuHz5Mg4dOrTqPsePH8fAwMCKW6p/x927d9HQ0JCT78QjIvngp2KJiHJEEATs3bsXPp8Pzc3Nf/s4P378QHl5OUKhEBwORw4TEpHU8YodEVGOKBQKBIPBNf9aRCwWw9mzZ1nqiOgv4xU7IiIiIongFTsiIiIiiWCxIyIiIpIIFjsiIiIiiWCxIyIiIpIIFjsiIiIiiWCxIyIiIpIIFjsiIiIiiWCxIyIiIpIIFjsiIiIiiWCxIyIiIpKI/wByD9VNL8YAsQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "segment.plot_overview(beam=incoming_beam)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b900640-3478-41fe-9d82-607c74cc03a9", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 790003435821281d79b60ea44bb6ba7c280777cd Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:15:30 -0700 Subject: [PATCH 005/111] com --- cheetah/accelerator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 046866ff..9f6e5e48 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -309,9 +309,9 @@ class SpaceChargeKick(Element): def __init__( self, - nx: Union[torch.Tensor, nn.Parameter], - ny: Union[torch.Tensor, nn.Parameter], - ns: Union[torch.Tensor, nn.Parameter], + nx: Union[torch.Tensor, nn.Parameter,int], + ny: Union[torch.Tensor, nn.Parameter,int], + ns: Union[torch.Tensor, nn.Parameter,int], dx: Union[torch.Tensor, nn.Parameter], dy: Union[torch.Tensor, nn.Parameter], ds: Union[torch.Tensor, nn.Parameter], @@ -322,9 +322,9 @@ def __init__( factory_kwargs = {"device": device, "dtype": dtype} super().__init__(name=name) - self.nx = torch.as_tensor(nx, **factory_kwargs) - self.ny = torch.as_tensor(ny, **factory_kwargs) - self.ns = torch.as_tensor(ns, **factory_kwargs) + self.nx = int(torch.as_tensor(nx, **factory_kwargs)) + self.ny = int(torch.as_tensor(ny, **factory_kwargs)) + self.ns = int(torch.as_tensor(ns, **factory_kwargs)) self.dx = torch.as_tensor(dx, **factory_kwargs) #in meters self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) From 96e7fa384c30f2f34a2b5f3fae511059764bb24f Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:17:22 -0700 Subject: [PATCH 006/111] c --- cheetah/accelerator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 9f6e5e48..0f1d6e4d 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -329,8 +329,8 @@ def __init__( self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) - def grid_shape(self) -> tuple[int]: - return (self.nx, self.ny, self.ns) + def grid_shape(self) -> tuple[int,int,int]: + return (int(self.nx.item()), int(self.ny.item()), int(self.ns.item())) def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) From f25065e3d605213627748a80c202a3225d0b3c2f Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:21:41 -0700 Subject: [PATCH 007/111] c --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 0f1d6e4d..20cce056 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -351,7 +351,7 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o Deposition of the beam on the grid. """ charge_density = torch.zeros(self.grid_shape(), dtype=torch.float32) # Initialize the charge density grid - grid = self.create_grid() + #grid = self.create_grid() # Compute the grid cell size cell_size = 2*self.grid_dimensions / self.grid_shape From f914e2bae171cfdbf91e0c70b493d9a0a5ca6faf Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:28:00 -0700 Subject: [PATCH 008/111] c --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 20cce056..671a7945 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -330,7 +330,7 @@ def __init__( self.ds = torch.as_tensor(ds, **factory_kwargs) def grid_shape(self) -> tuple[int,int,int]: - return (int(self.nx.item()), int(self.ny.item()), int(self.ns.item())) + return (int(self.nx), int(self.ny), int(self.ns)) def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) From b01d4fe27dd623e2d54b1931e665d6905d43e004 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 15:28:46 -0700 Subject: [PATCH 009/111] c --- cheetah/accelerator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 671a7945..ed6fb3c2 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -350,6 +350,7 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o """ Deposition of the beam on the grid. """ + print(self.grid_shape()) charge_density = torch.zeros(self.grid_shape(), dtype=torch.float32) # Initialize the charge density grid #grid = self.create_grid() From 357840ef51e1f228817ba39b153e15fadc16ca11 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 5 Apr 2024 16:59:19 -0700 Subject: [PATCH 010/111] Update of charge deposition. First draft of poisson solver --- cheetah/accelerator.py | 74 ++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index ed6fb3c2..9dbe8dac 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -334,28 +334,19 @@ def grid_shape(self) -> tuple[int,int,int]: def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) - - def create_grid(self) -> torch.Tensor: - """ - Create a 3D grid for the space charge kick. - """ - x = torch.linspace(-self.dx / 2, self.dx / 2, self.nx) #here centered on 0, may need to change center? - y = torch.linspace(-self.dy / 2, self.dy / 2, self.ny) - s = torch.linspace(-self.ds / 2, self.ds / 2, self.ns) - - grid = torch.meshgrid(x, y, s) - return torch.stack(grid, dim=-1) + def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage """ Deposition of the beam on the grid. """ - print(self.grid_shape()) - charge_density = torch.zeros(self.grid_shape(), dtype=torch.float32) # Initialize the charge density grid - #grid = self.create_grid() + grid_shape = self.grid_shape() + grid_dimensions = self.grid_dimensions() + + charge_density = torch.zeros(grid_shape, dtype=torch.float32) # Initialize the charge density grid # Compute the grid cell size - cell_size = 2*self.grid_dimensions / self.grid_shape + cell_size = 2*grid_dimensions / torch.tensor(grid_shape) # Loop over each particle n_particles = beam.num_particles @@ -364,7 +355,7 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o for p in range(n_particles): # Compute the normalized position of the particle within the grid part_pos = particle_pos[p] - normalized_pos = (part_pos + self.grid_dimensions) / cell_size + normalized_pos = (part_pos + grid_dimensions) / cell_size # Find the index of the lower corner of the cell containing the particle cell_index = torch.floor(normalized_pos).type(torch.long) @@ -386,11 +377,60 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o weight = weights[0] * weights[1] * weights[2] # Add the charge contribution to the cell - if 0 <= idx_x < self.grid_shape[0] and 0 <= idx_y < self.grid_shape[1] and 0 <= idx_s < self.grid_shape[2]: + #print(idx_x, idx_y, idx_s) + if 0 <= idx_x < torch.tensor(grid_shape)[0] and 0 <= idx_y < torch.tensor(grid_shape)[1] and 0 <= idx_s < torch.tensor(grid_shape)[2]: charge_density[idx_x, idx_y, idx_s] += weight * particle_charge[p] return charge_density + def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + """ + Solves the Poisson equation for the given charge density. + """ + grid_shape = self.grid_shape() + grid_dimensions = self.grid_dimensions() + + # Compute the grid cell size + cell_size = 2*grid_dimensions / torch.tensor(grid_shape) + + # Compute the charge density + charge_density = self.space_charge_deposition(beam) + + # Compute the Fourier transform of the charge density + charge_density_ft = torch.fft.fftn(charge_density) + + # Compute the integrated Green's function + integrated_green_function = torch.zeros(grid_shape, dtype=torch.float32) + for i in range(grid_shape[0]): + for j in range(grid_shape[1]): + for k in range(grid_shape[2]): + if i != 0 or j != 0 or k != 0: + denominator = (i**2 + j**2 + k**2) * (2 * np.pi)**2 + integrated_green_function[i, j, k] = -1 / denominator + """# Compute the wave numbers + kx = torch.fft.fftfreq(grid_shape[0], cell_size[0]) + ky = torch.fft.fftfreq(grid_shape[1], cell_size[1]) + ks = torch.fft.fftfreq(grid_shape[2], cell_size[2]) + + # Compute the wave numbers squared + kx2 = kx**2 + ky2 = ky**2 + ks2 = ks**2 + + # Compute the denominator of the Green's function + denominator = kx2[:, None, None] + ky2[None, :, None] + ks2[None, None, :] + + # Compute the Green's function + green_function = 1 / denominator""" + + # Compute the Fourier transform of the potential + potential_ft = charge_density_ft * integrated_green_function + + # Compute the potential + potential = torch.fft.ifftn(potential_ft).real + + return potential + def split(self, resolution: torch.Tensor) -> list[Element]: # TODO: Implement splitting for cavity properly, for now just returns the # element itself From e1f8785fb9c75b95aa6795e7272b9dd1d75d4734 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Wed, 10 Apr 2024 15:12:10 -0700 Subject: [PATCH 011/111] added space_cherge_deposition_vec for faster computation. First draft of all the IGF solver functions. --- cheetah/accelerator.py | 136 +++++++++++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 9dbe8dac..dc329598 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -30,7 +30,7 @@ electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) - +epsilon_0 = torch.tensor(constants.epsilon_0) class Element(ABC, nn.Module): """ @@ -334,7 +334,11 @@ def grid_shape(self) -> tuple[int,int,int]: def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) - + + def cell_size(self) -> torch.Tensor: + grid_shape = self.grid_shape() + grid_dimensions = self.grid_dimensions() + return 2*grid_dimensions / torch.tensor(grid_shape) def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage """ @@ -342,12 +346,10 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o """ grid_shape = self.grid_shape() grid_dimensions = self.grid_dimensions() + cell_size = self.cell_size() charge_density = torch.zeros(grid_shape, dtype=torch.float32) # Initialize the charge density grid - # Compute the grid cell size - cell_size = 2*grid_dimensions / torch.tensor(grid_shape) - # Loop over each particle n_particles = beam.num_particles particle_pos = beam.particles[:, [0,2,4]] @@ -383,16 +385,102 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works o return charge_density - def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + def space_charge_deposition_vect(self, beam: ParticleBeam) -> torch.Tensor: """ - Solves the Poisson equation for the given charge density. + Deposition of the beam on the grid using fully vectorized computation. """ grid_shape = self.grid_shape() grid_dimensions = self.grid_dimensions() + cell_size = self.cell_size() + + # Initialize the charge density grid + charge_density = torch.zeros(grid_shape, dtype=torch.float32) + + # Get particle positions and charges + n_particles = beam.num_particles + particle_pos = beam.particles[:, [0, 2, 4]] + particle_charge = beam.particle_charges + + # Compute the normalized positions of the particles within the grid + normalized_pos = (particle_pos + grid_dimensions) / cell_size + + # Find the indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_pos).type(torch.long) - # Compute the grid cell size - cell_size = 2*grid_dimensions / torch.tensor(grid_shape) + # Calculate the weights for all surrounding cells + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices = cell_indices.unsqueeze(1) + offsets # Shape: (n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) + cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) + # Add the charge contributions to the cells + idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T + valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ + (idx_y >= 0) & (idx_y < grid_shape[1]) & \ + (idx_s >= 0) & (idx_s < grid_shape[2]) + + # Accumulate the charge contributions + indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) + repeated_charges = particle_charge.repeat_interleave(8) + values = (cell_weights.view(-1) * repeated_charges)[valid_mask] + charge_density.index_put_(tuple(indices), values, accumulate=True) + + return charge_density + + def integrated_potential(self, x, y, s) -> torch.Tensor: + r = torch.sqrt(x**2 + y**2 + s**2) + G = (-0.5 * s**2 * torch.atan(x * y / (s * r)) + -0.5 * y**2 * torch.atan(x * s / (y * r)) + -0.5 * x**2 * torch.atan(y * s / (x * r)) + + y * s * torch.asinh(x / torch.sqrt(y**2 + s**2)) + + x * s * torch.asinh(y / torch.sqrt(x**2 + s**2)) + + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2))) + return G + + def initialize_green_function(self, beam: ParticleBeam) -> torch.Tensor: + dx, dy, ds = self.cell_size()[0], self.cell_size()[1], self.cell_size()[2] + nx, ny, ns = self.grid_shape() + grid = torch.zeros(2 * nx - 1, 2 * ny - 1, 2 * ns - 1) + + for i in range(nx): + for j in range(ny): + for k in range(ns): + x = i * dx + y = j * dy + s = k * ds + G_value = 1 / ( + self.integrated_potential(x + 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) + - self.integrated_potential(x - 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) + - self.integrated_potential(x + 0.5 * dx, y - 0.5 * dy, s + 0.5 * ds) + - self.integrated_potential(x + 0.5 * dx, y + 0.5 * dy, s - 0.5 * ds) + + self.integrated_potential(x + 0.5 * dx, y - 0.5 * dy, s - 0.5 * ds) + + self.integrated_potential(x - 0.5 * dx, y + 0.5 * dy, s - 0.5 * ds) + + self.integrated_potential(x - 0.5 * dx, y - 0.5 * dy, s + 0.5 * ds) + - self.integrated_potential(x - 0.5 * dx, y - 0.5 * dy, s - 0.5 * ds) + ) + grid[i, j, k] = G_value + + # Fill the rest of the array by periodicity + if i > 0: + grid[2 * nx - 2 - i, j, k] = G_value + if j > 0: + grid[i, 2 * ny - 2 - j, k] = G_value + if k > 0: + grid[i, j, 2 * ns - 2 - k] = G_value + if i > 0 and j > 0: + grid[2 * nx - 2 - i, 2 * ny - 2 - j, k] = G_value + if j > 0 and k > 0: + grid[i, 2 * ny - 2 - j, 2 * ns - 2 - k] = G_value + if i > 0 and k > 0: + grid[2 * nx - 2 - i, j, 2 * ns - 2 - k] = G_value + if i > 0 and j > 0 and k > 0: + grid[2 * nx - 2 - i, 2 * ny - 2 - j, 2 * ns - 2 - k] = G_value + return grid + + def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + """ + Solves the Poisson equation for the given charge density. + """ # Compute the charge density charge_density = self.space_charge_deposition(beam) @@ -400,34 +488,16 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on charge_density_ft = torch.fft.fftn(charge_density) # Compute the integrated Green's function - integrated_green_function = torch.zeros(grid_shape, dtype=torch.float32) - for i in range(grid_shape[0]): - for j in range(grid_shape[1]): - for k in range(grid_shape[2]): - if i != 0 or j != 0 or k != 0: - denominator = (i**2 + j**2 + k**2) * (2 * np.pi)**2 - integrated_green_function[i, j, k] = -1 / denominator - """# Compute the wave numbers - kx = torch.fft.fftfreq(grid_shape[0], cell_size[0]) - ky = torch.fft.fftfreq(grid_shape[1], cell_size[1]) - ks = torch.fft.fftfreq(grid_shape[2], cell_size[2]) - - # Compute the wave numbers squared - kx2 = kx**2 - ky2 = ky**2 - ks2 = ks**2 - - # Compute the denominator of the Green's function - denominator = kx2[:, None, None] + ky2[None, :, None] + ks2[None, None, :] - - # Compute the Green's function - green_function = 1 / denominator""" + integrated_green_function = self.IGF(beam) + + # Compute the integrated Green's function's Fourier transform + integrated_green_function_ft = torch.fft.fftn(integrated_green_function) # Compute the Fourier transform of the potential - potential_ft = charge_density_ft * integrated_green_function + potential_ft = charge_density_ft * integrated_green_function_ft # Compute the potential - potential = torch.fft.ifftn(potential_ft).real + potential = (1/4*torch.pi*epsilon_0)*torch.fft.ifftn(potential_ft).real return potential From 456592087020a91f5aaece747cb9d9d7eb167dcb Mon Sep 17 00:00:00 2001 From: greglenerd Date: Wed, 10 Apr 2024 18:23:12 -0700 Subject: [PATCH 012/111] First version of the whole IGF solver. Works with one particle, but some bugs. --- cheetah/accelerator.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index dc329598..ccd95f40 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -437,10 +437,27 @@ def integrated_potential(self, x, y, s) -> torch.Tensor: + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2))) return G - def initialize_green_function(self, beam: ParticleBeam) -> torch.Tensor: + def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: + """ + Compute the charge density on the grid using the cyclic deposition method. + """ + grid_shape = self.grid_shape() + charge_density = self.space_charge_deposition_vect(beam) + + # Double the dimensions + new_dims = tuple(dim * 2 for dim in grid_shape) + + # Create a new tensor with the doubled dimensions, filled with zeros + cyclic_charge_density = torch.zeros(new_dims) + + # Copy the original charge_density values to the beginning of the new tensor + cyclic_charge_density[:charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density + return cyclic_charge_density + + def IGF(self) -> torch.Tensor: dx, dy, ds = self.cell_size()[0], self.cell_size()[1], self.cell_size()[2] nx, ny, ns = self.grid_shape() - grid = torch.zeros(2 * nx - 1, 2 * ny - 1, 2 * ns - 1) + grid = torch.zeros(2 * nx, 2 * ny, 2 * ns) for i in range(nx): for j in range(ny): @@ -448,7 +465,7 @@ def initialize_green_function(self, beam: ParticleBeam) -> torch.Tensor: x = i * dx y = j * dy s = k * ds - G_value = 1 / ( + G_value = ( self.integrated_potential(x + 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) - self.integrated_potential(x - 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) - self.integrated_potential(x + 0.5 * dx, y - 0.5 * dy, s + 0.5 * ds) @@ -482,13 +499,13 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on Solves the Poisson equation for the given charge density. """ # Compute the charge density - charge_density = self.space_charge_deposition(beam) + charge_density = self.cyclic_rho(beam) # Compute the Fourier transform of the charge density charge_density_ft = torch.fft.fftn(charge_density) # Compute the integrated Green's function - integrated_green_function = self.IGF(beam) + integrated_green_function = self.IGF() # Compute the integrated Green's function's Fourier transform integrated_green_function_ft = torch.fft.fftn(integrated_green_function) @@ -499,7 +516,8 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on # Compute the potential potential = (1/4*torch.pi*epsilon_0)*torch.fft.ifftn(potential_ft).real - return potential + # Return the physical potential + return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] def split(self, resolution: torch.Tensor) -> list[Element]: # TODO: Implement splitting for cavity properly, for now just returns the From 27632d7685d5cae908fc5ce171961a3aa80a7d3b Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:31:57 -0700 Subject: [PATCH 013/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index ccd95f40..9d527d9a 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -479,19 +479,19 @@ def IGF(self) -> torch.Tensor: # Fill the rest of the array by periodicity if i > 0: - grid[2 * nx - 2 - i, j, k] = G_value + grid[2 * nx - i, j, k] = G_value if j > 0: - grid[i, 2 * ny - 2 - j, k] = G_value + grid[i, 2 * ny - j, k] = G_value if k > 0: - grid[i, j, 2 * ns - 2 - k] = G_value + grid[i, j, 2 * ns - k] = G_value if i > 0 and j > 0: - grid[2 * nx - 2 - i, 2 * ny - 2 - j, k] = G_value + grid[2 * nx - i, 2 * ny - j, k] = G_value if j > 0 and k > 0: - grid[i, 2 * ny - 2 - j, 2 * ns - 2 - k] = G_value + grid[i, 2 * ny - j, 2 * ns - k] = G_value if i > 0 and k > 0: - grid[2 * nx - 2 - i, j, 2 * ns - 2 - k] = G_value + grid[2 * nx - i, j, 2 * ns - k] = G_value if i > 0 and j > 0 and k > 0: - grid[2 * nx - 2 - i, 2 * ny - 2 - j, 2 * ns - 2 - k] = G_value + grid[2 * nx - i, 2 * ny - j, 2 * ns - k] = G_value return grid def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage From 1b5535e5e771a42984bde542f823f1e2c15e0b5d Mon Sep 17 00:00:00 2001 From: greglenerd Date: Thu, 11 Apr 2024 16:58:50 -0700 Subject: [PATCH 014/111] vectorized version of the code --- cheetah/accelerator.py | 141 +++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 90 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 9d527d9a..201369f1 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -329,63 +329,22 @@ def __init__( self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) + def grid_shape(self) -> tuple[int,int,int]: return (int(self.nx), int(self.ny), int(self.ns)) + def grid_dimensions(self) -> torch.Tensor: return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) + def cell_size(self) -> torch.Tensor: grid_shape = self.grid_shape() grid_dimensions = self.grid_dimensions() return 2*grid_dimensions / torch.tensor(grid_shape) - - def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage - """ - Deposition of the beam on the grid. - """ - grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions() - cell_size = self.cell_size() - - charge_density = torch.zeros(grid_shape, dtype=torch.float32) # Initialize the charge density grid - - # Loop over each particle - n_particles = beam.num_particles - particle_pos = beam.particles[:, [0,2,4]] - particle_charge = beam.particle_charges - for p in range(n_particles): - # Compute the normalized position of the particle within the grid - part_pos = particle_pos[p] - normalized_pos = (part_pos + grid_dimensions) / cell_size - - # Find the index of the lower corner of the cell containing the particle - cell_index = torch.floor(normalized_pos).type(torch.long) - - # Distribute the charge to the surrounding cells - for dx in range(2): - for dy in range(2): - for ds in range(2): - # Compute the indices of the surrounding cell - idx_x = cell_index[0] + dx - idx_y = cell_index[1] + dy - idx_s = cell_index[2] + ds - index = torch.tensor([idx_x, idx_y, idx_s]) - - # Calculate the weights for the surrounding cells - weights = 1 - torch.abs(normalized_pos - index) - - # Compute the weight for this cell - weight = weights[0] * weights[1] * weights[2] - - # Add the charge contribution to the cell - #print(idx_x, idx_y, idx_s) - if 0 <= idx_x < torch.tensor(grid_shape)[0] and 0 <= idx_y < torch.tensor(grid_shape)[1] and 0 <= idx_s < torch.tensor(grid_shape)[2]: - charge_density[idx_x, idx_y, idx_s] += weight * particle_charge[p] - - return charge_density - def space_charge_deposition_vect(self, beam: ParticleBeam) -> torch.Tensor: + + def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: """ Deposition of the beam on the grid using fully vectorized computation. """ @@ -394,10 +353,9 @@ def space_charge_deposition_vect(self, beam: ParticleBeam) -> torch.Tensor: cell_size = self.cell_size() # Initialize the charge density grid - charge_density = torch.zeros(grid_shape, dtype=torch.float32) + charge = torch.zeros(grid_shape, dtype=torch.float32) # Get particle positions and charges - n_particles = beam.num_particles particle_pos = beam.particles[:, [0, 2, 4]] particle_charge = beam.particle_charges @@ -423,10 +381,12 @@ def space_charge_deposition_vect(self, beam: ParticleBeam) -> torch.Tensor: indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) repeated_charges = particle_charge.repeat_interleave(8) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge_density.index_put_(tuple(indices), values, accumulate=True) + charge.index_put_(tuple(indices), values, accumulate=True) + cell_volume = cell_size[0] * cell_size[1] * cell_size[2] - return charge_density + return charge/cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 + def integrated_potential(self, x, y, s) -> torch.Tensor: r = torch.sqrt(x**2 + y**2 + s**2) G = (-0.5 * s**2 * torch.atan(x * y / (s * r)) @@ -437,12 +397,13 @@ def integrated_potential(self, x, y, s) -> torch.Tensor: + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2))) return G + def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: """ Compute the charge density on the grid using the cyclic deposition method. """ grid_shape = self.grid_shape() - charge_density = self.space_charge_deposition_vect(beam) + charge_density = self.space_charge_deposition(beam) # Double the dimensions new_dims = tuple(dim * 2 for dim in grid_shape) @@ -452,48 +413,47 @@ def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: # Copy the original charge_density values to the beginning of the new tensor cyclic_charge_density[:charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density - return cyclic_charge_density - - def IGF(self) -> torch.Tensor: - dx, dy, ds = self.cell_size()[0], self.cell_size()[1], self.cell_size()[2] + return cyclic_charge_density + + def IGF(self, beam: ParticleBeam) -> torch.Tensor: + gamma = beam.energy / rest_energy + dx, dy, ds = self.cell_size()[0], self.cell_size()[1], self.cell_size()[2] * gamma # ds is scaled by gamma nx, ny, ns = self.grid_shape() + + # Create coordinate grids + x = torch.arange(nx) * dx + y = torch.arange(ny) * dy + s = torch.arange(ns) * ds + x_grid, y_grid, s_grid = torch.meshgrid(x, y, s, indexing='ij') + + # Compute the Green's function values + G_values = ( + self.integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) + - self.integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) + - self.integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) + - self.integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) + + self.integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) + + self.integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) + + self.integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) + - self.integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) + ) + + # Initialize the grid with double dimensions grid = torch.zeros(2 * nx, 2 * ny, 2 * ns) - for i in range(nx): - for j in range(ny): - for k in range(ns): - x = i * dx - y = j * dy - s = k * ds - G_value = ( - self.integrated_potential(x + 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) - - self.integrated_potential(x - 0.5 * dx, y + 0.5 * dy, s + 0.5 * ds) - - self.integrated_potential(x + 0.5 * dx, y - 0.5 * dy, s + 0.5 * ds) - - self.integrated_potential(x + 0.5 * dx, y + 0.5 * dy, s - 0.5 * ds) - + self.integrated_potential(x + 0.5 * dx, y - 0.5 * dy, s - 0.5 * ds) - + self.integrated_potential(x - 0.5 * dx, y + 0.5 * dy, s - 0.5 * ds) - + self.integrated_potential(x - 0.5 * dx, y - 0.5 * dy, s + 0.5 * ds) - - self.integrated_potential(x - 0.5 * dx, y - 0.5 * dy, s - 0.5 * ds) - ) - grid[i, j, k] = G_value - - # Fill the rest of the array by periodicity - if i > 0: - grid[2 * nx - i, j, k] = G_value - if j > 0: - grid[i, 2 * ny - j, k] = G_value - if k > 0: - grid[i, j, 2 * ns - k] = G_value - if i > 0 and j > 0: - grid[2 * nx - i, 2 * ny - j, k] = G_value - if j > 0 and k > 0: - grid[i, 2 * ny - j, 2 * ns - k] = G_value - if i > 0 and k > 0: - grid[2 * nx - i, j, 2 * ns - k] = G_value - if i > 0 and j > 0 and k > 0: - grid[2 * nx - i, 2 * ny - j, 2 * ns - k] = G_value + # Fill the grid with G_values and its periodic copies + grid[:nx, :ny, :ns] = G_values + grid[nx+1:, :ny, :ns] = G_values[1:,:,:].flip(dims=[0]) # Reverse the x dimension, excluding the first element + grid[:nx, ny+1:, :ns] = G_values[:, 1:,:].flip(dims=[1]) # Reverse the y dimension, excluding the first element + grid[:nx, :ny, ns+1:] = G_values[:, :, 1:].flip(dims=[2]) # Reverse the s dimension, excluding the first element + grid[nx+1:, ny+1:, :ns] = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions + grid[:nx, ny+1:, ns+1:] = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions + grid[nx+1:, :ny, ns+1:] = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions + grid[nx+1:, ny+1:, ns+1:] = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions + return grid + def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density. @@ -505,7 +465,7 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on charge_density_ft = torch.fft.fftn(charge_density) # Compute the integrated Green's function - integrated_green_function = self.IGF() + integrated_green_function = self.IGF(beam) # Compute the integrated Green's function's Fourier transform integrated_green_function_ft = torch.fft.fftn(integrated_green_function) @@ -518,7 +478,8 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on # Return the physical potential return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] - + + def split(self, resolution: torch.Tensor) -> list[Element]: # TODO: Implement splitting for cavity properly, for now just returns the # element itself From d7846c4db4b8139d92166530f49473bec1132f1e Mon Sep 17 00:00:00 2001 From: greglenerd Date: Thu, 25 Apr 2024 16:46:09 -0700 Subject: [PATCH 015/111] first "complete" version of the code, with the track method implemented. The code is not yet tested. (last test E+vB field) --- cheetah/accelerator.py | 232 ++++++++++++++++++++++++++++++++--------- 1 file changed, 183 insertions(+), 49 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 201369f1..df560660 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -20,8 +20,9 @@ from cheetah.track_methods import base_rmatrix, misalignment_matrix, rotation_matrix from cheetah.utils import UniqueNameGenerator +c = torch.tensor(constants.speed_of_light) generate_unique_name = UniqueNameGenerator(prefix="unnamed_element") - +elementary_charge= torch.tensor(constants.elementary_charge) rest_energy = torch.tensor( constants.electron_mass * constants.speed_of_light**2 @@ -30,6 +31,10 @@ electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) +electron_mass = torch.tensor( + physical_constants["electron mass"][0] +) + epsilon_0 = torch.tensor(constants.epsilon_0) class Element(ABC, nn.Module): @@ -302,19 +307,21 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ Simulates space charge effects on a beam. - :param grid_points: Number of grid points in each dimension. - :param grid_dimensions: Dimensions of the grid in meters. + :param grid_precision: Number of grid points in each dimension. + :param grid_dimensions: Dimensions of the grid as multiples of sigma of the beam. :param name: Unique identifier of the element. """ def __init__( self, - nx: Union[torch.Tensor, nn.Parameter,int], - ny: Union[torch.Tensor, nn.Parameter,int], - ns: Union[torch.Tensor, nn.Parameter,int], - dx: Union[torch.Tensor, nn.Parameter], - dy: Union[torch.Tensor, nn.Parameter], - ds: Union[torch.Tensor, nn.Parameter], + length: Union[torch.Tensor, nn.Parameter], + nx: Union[torch.Tensor, nn.Parameter,int]=32, + ny: Union[torch.Tensor, nn.Parameter,int]=32, + ns: Union[torch.Tensor, nn.Parameter,int]=32, + dx: Union[torch.Tensor, nn.Parameter]=3, + dy: Union[torch.Tensor, nn.Parameter]=3, + ds: Union[torch.Tensor, nn.Parameter]=3, + name: Optional[str] = None, device=None, dtype=torch.float32, @@ -322,10 +329,11 @@ def __init__( factory_kwargs = {"device": device, "dtype": dtype} super().__init__(name=name) + self.length = torch.as_tensor(length, **factory_kwargs) self.nx = int(torch.as_tensor(nx, **factory_kwargs)) self.ny = int(torch.as_tensor(ny, **factory_kwargs)) self.ns = int(torch.as_tensor(ns, **factory_kwargs)) - self.dx = torch.as_tensor(dx, **factory_kwargs) #in meters + self.dx = torch.as_tensor(dx, **factory_kwargs) # in multiples of sigma self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) @@ -334,29 +342,49 @@ def grid_shape(self) -> tuple[int,int,int]: return (int(self.nx), int(self.ny), int(self.ns)) - def grid_dimensions(self) -> torch.Tensor: - return torch.tensor([self.dx, self.dy, self.ds], device=self.dx.device) + def grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: + if beam.particles.shape[0] < 2: + sigma_x = torch.tensor(175e-9) + sigma_y = torch.tensor(175e-9) + sigma_s = torch.tensor(175e-9) + else: + sigma_x = torch.std(beam.particles[:, 0]) + sigma_y = torch.std(beam.particles[:, 2]) + sigma_s = torch.std(beam.particles[:, 4]) + return torch.tensor([self.dx*sigma_x, self.dy*sigma_y, self.ds*sigma_s], device=self.dx.device) + def delta_t(self,beam: ParticleBeam) -> torch.Tensor: + return self.length / (c*self.betaref(beam)) - def cell_size(self) -> torch.Tensor: + def cell_size(self,beam: ParticleBeam) -> torch.Tensor: grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions() + grid_dimensions = self.grid_dimensions(beam) return 2*grid_dimensions / torch.tensor(grid_shape) + def gammaref(self,beam: ParticleBeam) -> torch.Tensor: + return beam.energy / rest_energy + + def betaref(self,beam: ParticleBeam) -> torch.Tensor: + gamma = self.gammaref(beam) + if gamma == 0: + return torch.tensor(1.0) + return torch.sqrt(1 - 1 / gamma**2) def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: """ - Deposition of the beam on the grid using fully vectorized computation. + Deposition of the beam on the grid. """ grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions() - cell_size = self.cell_size() + grid_dimensions = self.grid_dimensions(beam) + cell_size = self.cell_size(beam) + betaref = self.betaref(beam) # Initialize the charge density grid charge = torch.zeros(grid_shape, dtype=torch.float32) # Get particle positions and charges particle_pos = beam.particles[:, [0, 2, 4]] + particle_pos[:,2] = -particle_pos[:,2]*betaref #modify with the tau transformation particle_charge = beam.particle_charges # Compute the normalized positions of the particles within the grid @@ -416,8 +444,9 @@ def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: return cyclic_charge_density def IGF(self, beam: ParticleBeam) -> torch.Tensor: - gamma = beam.energy / rest_energy - dx, dy, ds = self.cell_size()[0], self.cell_size()[1], self.cell_size()[2] * gamma # ds is scaled by gamma + gamma = self.gammaref(beam) + cell_size = self.cell_size(beam) + dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma # ds is scaled by gamma nx, ny, ns = self.grid_shape() # Create coordinate grids @@ -458,52 +487,157 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on """ Solves the Poisson equation for the given charge density. """ - # Compute the charge density charge_density = self.cyclic_rho(beam) - - # Compute the Fourier transform of the charge density charge_density_ft = torch.fft.fftn(charge_density) - - # Compute the integrated Green's function integrated_green_function = self.IGF(beam) - - # Compute the integrated Green's function's Fourier transform integrated_green_function_ft = torch.fft.fftn(integrated_green_function) - - # Compute the Fourier transform of the potential potential_ft = charge_density_ft * integrated_green_function_ft - - # Compute the potential - potential = (1/4*torch.pi*epsilon_0)*torch.fft.ifftn(potential_ft).real + potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft).real # Return the physical potential return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] - def split(self, resolution: torch.Tensor) -> list[Element]: - # TODO: Implement splitting for cavity properly, for now just returns the - # element itself - return [self] - - - def transfer_map(self, energy: torch.Tensor) -> torch.Tensor: - device = self.length.device - dtype = self.length.dtype - - gamma = energy / rest_energy.to(device=device, dtype=dtype) + def E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: + """ + Compute the force field from the potential and the particle positions and speeds. + """ + gamma = self.gammaref(beam) igamma2 = ( 1 / gamma**2 if gamma != 0 - else torch.tensor(0.0, device=device, dtype=dtype) + else torch.tensor(0.0) ) - beta = torch.sqrt(1 - igamma2) + potential = self.solve_poisson_equation(beam) + cell_size = self.cell_size(beam) + potential = potential.unsqueeze(0).unsqueeze(0) + + # Now apply padding + phi_padded = torch.nn.functional.pad(potential, (1, 1, 1, 1, 1, 1), mode='replicate') + phi_padded = phi_padded.squeeze(0).squeeze(0) + # Compute derivatives using central differences + grad_x = (phi_padded[2:, :, :] - phi_padded[:-2, :, :]) / (2 * cell_size[0]) + grad_y = (phi_padded[:, 2:, :] - phi_padded[:, :-2, :]) / (2 * cell_size[1]) + grad_z = (phi_padded[:, :, 2:] - phi_padded[:, :, :-2]) / (2 * cell_size[2]) + + # Crop out the padding to maintain the original shape + grad_x = -igamma2*grad_x[:, 1:-1, 1:-1] + grad_y = -igamma2*grad_y[1:-1, :, 1:-1] + grad_z = -igamma2*grad_z[1:-1, 1:-1, :] + + return grad_x, grad_y, grad_z + + def cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: + N = beam.particles.shape[0] + moments = beam.particles + gammaref = self.gammaref(beam) + betaref = self.betaref(beam) + p0 = gammaref*betaref*electron_mass*c + gamma = gammaref*(torch.ones(N)+beam.particles[:,5]*betaref) + gamma = torch.clamp(gamma, min=1.0) + beta = torch.sqrt(1 - 1 / gamma**2) + p = gamma*electron_mass*beta*c + moments[:,1] = p0*moments[:,1] + moments[:,3] = p0*moments[:,3] + moments[:,5] = torch.sqrt(p**2 - moments[:,1]**2 - moments[:,3]**2) + return moments + + def moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch.Tensor: + N = moments.shape[0] + gammaref = self.gammaref(beam) + betaref = self.betaref(beam) + p0 = gammaref*betaref*electron_mass*c + p = torch.sqrt(moments[:,1]**2 + moments[:,3]**2 + moments[:,5]**2) + gamma = torch.sqrt(1 + (p / (electron_mass*c))**2) + moments[:,1] = moments[:,1] / p0 + moments[:,3] = moments[:,3] / p0 + moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) + return moments + + def read_forces(self, beam: ParticleBeam) -> torch.Tensor: + """ + Compute the momentum kick from the force field. + """ + grad_x, grad_y, grad_z = self.E_plus_vB_field(beam) + grid_shape = self.grid_shape() + grid_dimensions = self.grid_dimensions(beam) + cell_size = self.cell_size(beam) - tm = torch.eye(7, device=device, dtype=dtype) - tm[0, 1] = self.length - tm[2, 3] = self.length - tm[4, 5] = -self.length / beta**2 * igamma2 + particle_pos = beam.particles[:, [0, 2, 4]] + particle_charge = beam.particle_charges + + normalized_pos = (particle_pos + grid_dimensions) / cell_size + + # Find the indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_pos).type(torch.long) + + # Calculate the weights for all surrounding cells + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices = cell_indices.unsqueeze(1) + offsets # Shape: (n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) + cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) + + # Extract forces from the grids + def get_force_values(force_grid): + idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T + valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ + (idx_y >= 0) & (idx_y < grid_shape[1]) & \ + (idx_s >= 0) & (idx_s < grid_shape[2]) + + valid_indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) + force_values = force_grid[tuple(valid_indices)] + return force_values, valid_mask + + Fx_values, valid_mask_x = get_force_values(grad_x) + Fy_values, valid_mask_y = get_force_values(grad_y) + Fz_values, valid_mask_z = get_force_values(grad_z) + + # Compute interpolated forces + interpolated_forces = torch.zeros((particle_pos.shape[0], 3), device=grad_x.device) + values_x = cell_weights.view(-1)[valid_mask_x] * Fx_values * particle_charge.repeat_interleave(8)[valid_mask_x] + values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * particle_charge.repeat_interleave(8)[valid_mask_y] + values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * particle_charge.repeat_interleave(8)[valid_mask_z] + + indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8) + interpolated_forces.index_add_(0, indices[valid_mask_x], torch.stack([values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) + interpolated_forces.index_add_(0, indices[valid_mask_y], torch.stack([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)], dim=1)) + interpolated_forces.index_add_(0, indices[valid_mask_z], torch.stack([torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) + + return interpolated_forces + + + def track(self, incoming: ParticleBeam) -> ParticleBeam: + """ + Track particles through the element. The input must be a `ParticleBeam`. + + :param incoming: Beam of particles entering the element. + :return: Beam of particles exiting the element. + """ + if incoming is Beam.empty: + return incoming + elif isinstance(incoming, ParticleBeam): + forces = self.read_forces(incoming) + particles = self.cheetah_to_moments(incoming) + particles[:,1] += forces[:,0]*self.delta_t(incoming) + particles[:,3] += forces[:,1]*self.delta_t(incoming) + particles[:,5] += forces[:,2]*self.delta_t(incoming) + particles = self.moments_to_cheetah(particles, incoming) + return ParticleBeam( + particles, + incoming.energy, + particle_charges=incoming.particle_charges, + device=particles.device, + dtype=particles.dtype, + ) + else: + raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") + + + def split(self, resolution: torch.Tensor) -> list[Element]: + # TODO: Implement splitting for SpaceCharge properly, for now just returns the + # element itself + return [self] - return tm @property def is_skippable(self) -> bool: From 61f67238c65128b1d2304d9c825f1ad183b0fb66 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 26 Apr 2024 13:10:20 -0700 Subject: [PATCH 016/111] version that runs correctly, need to be quantitatively tested --- cheetah/accelerator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index df560660..5c021658 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -641,7 +641,7 @@ def split(self, resolution: torch.Tensor) -> list[Element]: @property def is_skippable(self) -> bool: - return True + return False def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: pass @@ -2186,7 +2186,7 @@ def flattened(self) -> "Segment": flattened_elements.append(element) return Segment(elements=flattened_elements, name=self.name) - + def transfer_maps_merged( self, incoming_beam: Beam, except_for: Optional[list[str]] = None ) -> "Segment": @@ -2427,7 +2427,7 @@ def is_skippable(self) -> bool: @property def length(self) -> torch.Tensor: lengths = torch.stack( - [element.length for element in self.elements if hasattr(element, "length")] + [element.length for element in self.elements if hasattr(element, "length") and type(element) is not SpaceChargeKick] ) return torch.sum(lengths) @@ -2445,14 +2445,14 @@ def track(self, incoming: Beam) -> Beam: return super().track(incoming) else: todos = [] - for element in self.elements: + for element in self.elements: if not element.is_skippable: todos.append(element) elif not todos or not todos[-1].is_skippable: todos.append(Segment([element])) else: todos[-1].elements.append(element) - + print(todos) for todo in todos: incoming = todo.track(incoming) @@ -2467,7 +2467,7 @@ def split(self, resolution: torch.Tensor) -> list[Element]: def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: element_lengths = [ - element.length if hasattr(element, "length") else 0.0 + element.length if hasattr(element, "length") and type(element) is not SpaceChargeKick else 0.0 for element in self.elements ] element_ss = [0] + [ @@ -2507,7 +2507,7 @@ def plot_reference_particle_traces( splits = reference_segment.split(resolution=torch.tensor(resolution)) split_lengths = [ - split.length if hasattr(split, "length") else 0.0 for split in splits + split.length if hasattr(split, "length") and type(split) is not SpaceChargeKick else 0.0 for split in splits ] ss = [0] + [sum(split_lengths[: i + 1]) for i, _ in enumerate(split_lengths)] From 0e57d2ab75012366959e986f097e6f9aa3fbc0d6 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Tue, 30 Apr 2024 15:21:50 -0700 Subject: [PATCH 017/111] Draft version of the code, tested with the test_space_charge_kick.py test (expansion of a cold uniform beam). --- cheetah/accelerator.py | 19 +++-- cheetah/particles.py | 134 ++++++++++++++++++++++++++++++++ tests/test_space_charge_kick.py | 87 ++++++++++++--------- 3 files changed, 197 insertions(+), 43 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 5c021658..aa0f9975 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -20,7 +20,9 @@ from cheetah.track_methods import base_rmatrix, misalignment_matrix, rotation_matrix from cheetah.utils import UniqueNameGenerator +#Constants c = torch.tensor(constants.speed_of_light) +J_to_eV = torch.tensor(physical_constants["electron volt-joule relationship"][0]) generate_unique_name = UniqueNameGenerator(prefix="unnamed_element") elementary_charge= torch.tensor(constants.elementary_charge) rest_energy = torch.tensor( @@ -28,6 +30,7 @@ * constants.speed_of_light**2 / constants.elementary_charge # electron mass ) +electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) @@ -377,14 +380,12 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: grid_shape = self.grid_shape() grid_dimensions = self.grid_dimensions(beam) cell_size = self.cell_size(beam) - betaref = self.betaref(beam) # Initialize the charge density grid charge = torch.zeros(grid_shape, dtype=torch.float32) # Get particle positions and charges particle_pos = beam.particles[:, [0, 2, 4]] - particle_pos[:,2] = -particle_pos[:,2]*betaref #modify with the tau transformation particle_charge = beam.particle_charges # Compute the normalized positions of the particles within the grid @@ -534,11 +535,11 @@ def cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: betaref = self.betaref(beam) p0 = gammaref*betaref*electron_mass*c gamma = gammaref*(torch.ones(N)+beam.particles[:,5]*betaref) - gamma = torch.clamp(gamma, min=1.0) beta = torch.sqrt(1 - 1 / gamma**2) p = gamma*electron_mass*beta*c moments[:,1] = p0*moments[:,1] moments[:,3] = p0*moments[:,3] + moments[:,4] = -betaref*moments[:,4] moments[:,5] = torch.sqrt(p**2 - moments[:,1]**2 - moments[:,3]**2) return moments @@ -551,6 +552,7 @@ def moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch gamma = torch.sqrt(1 + (p / (electron_mass*c))**2) moments[:,1] = moments[:,1] / p0 moments[:,3] = moments[:,3] / p0 + moments[:,4] = -moments[:,4] / betaref moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) return moments @@ -594,9 +596,11 @@ def get_force_values(force_grid): # Compute interpolated forces interpolated_forces = torch.zeros((particle_pos.shape[0], 3), device=grad_x.device) - values_x = cell_weights.view(-1)[valid_mask_x] * Fx_values * particle_charge.repeat_interleave(8)[valid_mask_x] - values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * particle_charge.repeat_interleave(8)[valid_mask_y] - values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * particle_charge.repeat_interleave(8)[valid_mask_z] + values_x = cell_weights.view(-1)[valid_mask_x] * Fx_values * elementary_charge + values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * elementary_charge + values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * elementary_charge + + #particle_charge.repeat_interleave(8)[valid_mask_z] indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8) interpolated_forces.index_add_(0, indices[valid_mask_x], torch.stack([values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) @@ -616,8 +620,8 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: if incoming is Beam.empty: return incoming elif isinstance(incoming, ParticleBeam): - forces = self.read_forces(incoming) particles = self.cheetah_to_moments(incoming) + forces = self.read_forces(incoming) particles[:,1] += forces[:,0]*self.delta_t(incoming) particles[:,3] += forces[:,1]*self.delta_t(incoming) particles[:,5] += forces[:,2]*self.delta_t(incoming) @@ -2452,7 +2456,6 @@ def track(self, incoming: Beam) -> Beam: todos.append(Segment([element])) else: todos[-1].elements.append(element) - print(todos) for todo in todos: incoming = todo.track(incoming) diff --git a/cheetah/particles.py b/cheetah/particles.py index 3a4ddf95..28fae781 100644 --- a/cheetah/particles.py +++ b/cheetah/particles.py @@ -815,6 +815,140 @@ def from_twiss( device=device, dtype=dtype, ) + + @classmethod + def uniform_3d_ellispoid( + cls, + num_particles: Optional[torch.Tensor] = None, + radius_x: Optional[torch.Tensor] = None, + radius_y: Optional[torch.Tensor] = None, + radius_s: Optional[torch.Tensor] = None, + sigma_xp: Optional[torch.Tensor] = None, + sigma_yp: Optional[torch.Tensor] = None, + sigma_p: Optional[torch.Tensor] = None, + energy: Optional[torch.Tensor] = None, + total_charge: Optional[torch.Tensor] = None, + device=None, + dtype=torch.float32, + ): + """Generate a particle beam with spatially uniformly distributed particles + inside an ellipsoid, i.e. waterbag distribution. + Note that + - the generated particles do not have correlation in the momentum + directions, by default a cold beam with no divergence is generated. + - for batched generation, parameters that are not None + must have the same shape. + :param num_particles: Number of particles to generate. + :param radius_x: Radius of the ellipsoid in x direction in meters. + :param radius_y: Radius of the ellipsoid in y direction in meters. + :param radius_s: Radius of the ellipsoid in s (longitudinal) direction + in meters. + :param sigma_xp: Sigma of the particle distribution in x' direction in rad, + default is 0. + :param sigma_yp: Sigma of the particle distribution in y' direction in rad, + default is 0. + :param sigma_p: Sigma of the particle distribution in p, dimensionless. + :param energy: Energy of the beam in eV. + :param total_charge: Total charge of the beam in C. + :param device: Device to move the beam's particle array to. + :param dtype: Data type of the generated particles. + :return: ParticleBeam with uniformly distributed particles inside an ellipsoid. + """ + + def generate_uniform_3d_ellispoid_particles( + num_particles: torch.Tensor, + radius_x: torch.Tensor, + radius_y: torch.Tensor, + radius_s: torch.Tensor, + ) -> torch.Tensor: + """Helper function to generate uniform 3D ellipsoid particles + in a non-batched manner + """ + particles = torch.zeros((1, num_particles, 7)) + particles[0, :, 6] = 1 + + num_generated = 0 + + while num_generated < num_particles: + Xs = (torch.rand(num_particles) - 0.5) * 2 * radius_x + Ys = (torch.rand(num_particles) - 0.5) * 2 * radius_y + Zs = (torch.rand(num_particles) - 0.5) * 2 * radius_s + + indices = ( + Xs**2 / radius_x**2 + Ys**2 / radius_y**2 + Zs**2 / radius_s**2 + ) <= 1 # Rejection sampling to get the points inside the ellipsoid. + + num_new_generated = Xs[indices].shape[0] + num_to_add = min(num_new_generated, int(num_particles - num_generated)) + + particles[0, num_generated : num_generated + num_to_add, 0] = Xs[ + indices + ][:num_to_add] + particles[0, num_generated : num_generated + num_to_add, 2] = Ys[ + indices + ][:num_to_add] + particles[0, num_generated : num_generated + num_to_add, 4] = Zs[ + indices + ][:num_to_add] + num_generated += num_to_add + + return particles + + # Figure out if arguments were passed, figure out their shape + not_nones = [ + argument + for argument in [ + radius_x, + radius_y, + radius_s, + sigma_xp, + sigma_yp, + sigma_p, + energy, + total_charge, + ] + if argument is not None + ] + shape = not_nones[0].shape if len(not_nones) > 0 else torch.Size([1]) + if len(not_nones) > 1: + assert all( + argument.shape == shape for argument in not_nones + ), "Arguments must have the same shape." + + # Set default values without function call in function signature + num_particles = ( + num_particles if num_particles is not None else torch.tensor(1_000_000) + ) + radius_x = radius_x if radius_x is not None else torch.full(shape, 1e-3) + radius_y = radius_y if radius_y is not None else torch.full(shape, 1e-3) + radius_s = radius_s if radius_s is not None else torch.full(shape, 1e-3) + + # Generate a uncorrelated Gaussian Beam + parray = cls.from_parameters( + num_particles=num_particles, + mu_xp=torch.full(shape, 0.0), + mu_yp=torch.full(shape, 0.0), + sigma_xp=sigma_xp, + sigma_yp=sigma_yp, + sigma_p=sigma_p, + energy=energy, + total_charge=total_charge, + device=device, + dtype=dtype, + ) + + # Replace the with uniformly distributed particles inside the ellipsoid + particles = parray.particles.view(-1, num_particles, 7) + for i, (r_x, r_y, r_s) in enumerate( + zip(radius_x.view(-1), radius_y.view(-1), radius_s.view(-1)) + ): + particles[i] = generate_uniform_3d_ellispoid_particles( + num_particles, r_x, r_y, r_s + )[0] + parray.particles = particles.view(*shape, num_particles, 7) + parray.particles.to(device=device, dtype=dtype) + + return parray @classmethod def make_linspaced( diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 5da470c9..300121cb 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -1,43 +1,60 @@ import pytest import torch - import cheetah -def test_charge_deposition(): - """ - Test that the charge deposition is correct for a particle beam. The first test checks that the total charge is preserved, and the second test checks that the charge is deposited in the correct grid cells. - """ - space_charge_kick = cheetah.SpaceChargeKick(nx=32,ny=32,ns=32,dx=3e-9,dy=3e-9,ds=2e-6) - incoming_beam = cheetah.ParticleBeam.from_parameters( - num_particles=torch.tensor(1000), - sigma_xp=torch.tensor(2e-7), - sigma_yp=torch.tensor(2e-7), - ) - total_charge = incoming_beam.total_charge - space_charge_grid = space_charge_kick.space_charge_deposition(incoming_beam) - - assert torch.isclose(space_charge_grid.sum() * space_charge_kick.grid_resolution ** 3, torch.tensor(total_charge), atol=1e-12) # grid_resolution is a parameter of the space charge kick #Total charge is preserved - - # something similar to the read function in the CIC code should be implemented - assert outgoing_beam.sigma_y > incoming_beam.sigma_y - - -@pytest.mark.skip( - reason="Requires rewriting Element and Beam member variables to be buffers." -) -def test_device_like_torch_module(): +def test_cold_uniform_beam_expansion(): """ - Test that when changing the device, Drift reacts like a `torch.nn.Module`. + Test that that a cold uniform beam doubles in size in both dimensions when travelling through a drift section with space_charge. (cf ImpactX test) """ - # There is no point in running this test, if there aren't two different devices to - # move between - if not torch.cuda.is_available(): - return - - element = cheetah.Drift(length=torch.tensor(0.2), device="cuda") - assert element.length.device.type == "cuda" - - element = element.cpu() + # Simulation parameters + num_particles = 10000 + total_charge=torch.tensor(1e-9) + R0 = torch.tensor(0.001) + energy=torch.tensor(2.5e8) + gamma = energy/cheetah.rest_energy + beta = torch.sqrt(1-1/gamma**2) + incoming = cheetah.ParticleBeam.uniform_3d_ellispoid( + num_particles=torch.tensor(num_particles), + total_charge=total_charge, + energy = energy, + radius_x = R0, + radius_y = R0, + radius_s = R0/gamma, # radius of the beam in s direction, in the lab frame + sigma_xp = torch.tensor(1e-15), + sigma_yp = torch.tensor(1e-15), + sigma_p = torch.tensor(1e-15), + ) - assert element.length.device.type == "cpu" \ No newline at end of file + # Initial beam properties + sig_xi = incoming.sigma_x + sig_yi = incoming.sigma_y + sig_si = incoming.sigma_s + + # Compute section lenght + kappa = 1+(torch.sqrt(torch.tensor(2))/4)*torch.log(3+2*torch.sqrt(torch.tensor(2))) + Nb = total_charge/cheetah.elementary_charge + L=beta*gamma*kappa*torch.sqrt(R0**3/(Nb*cheetah.electron_radius)) + + segment_space_charge = cheetah.Segment( + elements=[ + cheetah.Drift(L/6), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/3), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/3), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/6) + ] + ) + outgoing_beam = segment_space_charge.track(incoming) + + # Final beam properties + sig_xo = outgoing_beam.sigma_x + sig_yo = outgoing_beam.sigma_y + sig_so = outgoing_beam.sigma_s + + torch.set_printoptions(precision=16) + assert torch.isclose(sig_xo,2*sig_xi,rtol=2e-2,atol=0.0) + assert torch.isclose(sig_yo,2*sig_yi,rtol=2e-2,atol=0.0) + assert torch.isclose(sig_so,2*sig_si,rtol=2e-2,atol=0.0) \ No newline at end of file From 8c9c3fc3e46e3a3b14aa3e90b561f3e85ab5b3c1 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Fri, 3 May 2024 11:50:20 -0700 Subject: [PATCH 018/111] minor written chqnges to accelerator.py --- cheetah/accelerator.py | 124 ++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 63 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index aa0f9975..37e49ecd 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -310,6 +310,7 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ Simulates space charge effects on a beam. + :param length: Length of the element in meters. :param grid_precision: Number of grid points in each dimension. :param grid_dimensions: Dimensions of the grid as multiples of sigma of the beam. :param name: Unique identifier of the element. @@ -341,11 +342,11 @@ def __init__( self.ds = torch.as_tensor(ds, **factory_kwargs) - def grid_shape(self) -> tuple[int,int,int]: + def _grid_shape(self) -> tuple[int,int,int]: return (int(self.nx), int(self.ny), int(self.ns)) - def grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: + def _grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: if beam.particles.shape[0] < 2: sigma_x = torch.tensor(175e-9) sigma_y = torch.tensor(175e-9) @@ -356,30 +357,30 @@ def grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_s = torch.std(beam.particles[:, 4]) return torch.tensor([self.dx*sigma_x, self.dy*sigma_y, self.ds*sigma_s], device=self.dx.device) - def delta_t(self,beam: ParticleBeam) -> torch.Tensor: - return self.length / (c*self.betaref(beam)) + def _delta_t(self,beam: ParticleBeam) -> torch.Tensor: + return self.length / (c*self._betaref(beam)) - def cell_size(self,beam: ParticleBeam) -> torch.Tensor: - grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions(beam) + def _cell_size(self,beam: ParticleBeam) -> torch.Tensor: + grid_shape = self._grid_shape() + grid_dimensions = self._grid_dimensions(beam) return 2*grid_dimensions / torch.tensor(grid_shape) - def gammaref(self,beam: ParticleBeam) -> torch.Tensor: + def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: return beam.energy / rest_energy - def betaref(self,beam: ParticleBeam) -> torch.Tensor: - gamma = self.gammaref(beam) + def _betaref(self,beam: ParticleBeam) -> torch.Tensor: + gamma = self._gammaref(beam) if gamma == 0: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: + def _space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: """ Deposition of the beam on the grid. """ - grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions(beam) - cell_size = self.cell_size(beam) + grid_shape = self._grid_shape() + grid_dimensions = self._grid_dimensions(beam) + cell_size = self._cell_size(beam) # Initialize the charge density grid charge = torch.zeros(grid_shape, dtype=torch.float32) @@ -416,7 +417,7 @@ def space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: return charge/cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 - def integrated_potential(self, x, y, s) -> torch.Tensor: + def _integrated_potential(self, x, y, s) -> torch.Tensor: r = torch.sqrt(x**2 + y**2 + s**2) G = (-0.5 * s**2 * torch.atan(x * y / (s * r)) -0.5 * y**2 * torch.atan(x * s / (y * r)) @@ -427,12 +428,12 @@ def integrated_potential(self, x, y, s) -> torch.Tensor: return G - def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: + def _cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: """ Compute the charge density on the grid using the cyclic deposition method. """ - grid_shape = self.grid_shape() - charge_density = self.space_charge_deposition(beam) + grid_shape = self._grid_shape() + charge_density = self._space_charge_deposition(beam) # Double the dimensions new_dims = tuple(dim * 2 for dim in grid_shape) @@ -444,11 +445,11 @@ def cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: cyclic_charge_density[:charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density return cyclic_charge_density - def IGF(self, beam: ParticleBeam) -> torch.Tensor: - gamma = self.gammaref(beam) - cell_size = self.cell_size(beam) + def _IGF(self, beam: ParticleBeam) -> torch.Tensor: + gamma = self._gammaref(beam) + cell_size = self._cell_size(beam) dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma # ds is scaled by gamma - nx, ny, ns = self.grid_shape() + nx, ny, ns = self._grid_shape() # Create coordinate grids x = torch.arange(nx) * dx @@ -458,14 +459,14 @@ def IGF(self, beam: ParticleBeam) -> torch.Tensor: # Compute the Green's function values G_values = ( - self.integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) - - self.integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) - - self.integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) - - self.integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) - + self.integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) - + self.integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) - + self.integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) - - self.integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) + self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) + - self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) + - self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) + - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) + + self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) + + self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) + + self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) + - self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) ) # Initialize the grid with double dimensions @@ -484,13 +485,13 @@ def IGF(self, beam: ParticleBeam) -> torch.Tensor: return grid - def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + def _solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density. """ - charge_density = self.cyclic_rho(beam) + charge_density = self._cyclic_rho(beam) charge_density_ft = torch.fft.fftn(charge_density) - integrated_green_function = self.IGF(beam) + integrated_green_function = self._IGF(beam) integrated_green_function_ft = torch.fft.fftn(integrated_green_function) potential_ft = charge_density_ft * integrated_green_function_ft potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft).real @@ -499,21 +500,21 @@ def solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works on return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] - def E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: + def _E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: """ Compute the force field from the potential and the particle positions and speeds. """ - gamma = self.gammaref(beam) + gamma = self._gammaref(beam) igamma2 = ( 1 / gamma**2 if gamma != 0 else torch.tensor(0.0) ) - potential = self.solve_poisson_equation(beam) - cell_size = self.cell_size(beam) + potential = self._solve_poisson_equation(beam) + cell_size = self._cell_size(beam) potential = potential.unsqueeze(0).unsqueeze(0) - # Now apply padding + # Now apply padding so that derivatives are 0 at the boundaries phi_padded = torch.nn.functional.pad(potential, (1, 1, 1, 1, 1, 1), mode='replicate') phi_padded = phi_padded.squeeze(0).squeeze(0) # Compute derivatives using central differences @@ -528,11 +529,11 @@ def E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: return grad_x, grad_y, grad_z - def cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: + def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: N = beam.particles.shape[0] moments = beam.particles - gammaref = self.gammaref(beam) - betaref = self.betaref(beam) + gammaref = self._gammaref(beam) + betaref = self._betaref(beam) p0 = gammaref*betaref*electron_mass*c gamma = gammaref*(torch.ones(N)+beam.particles[:,5]*betaref) beta = torch.sqrt(1 - 1 / gamma**2) @@ -543,10 +544,10 @@ def cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: moments[:,5] = torch.sqrt(p**2 - moments[:,1]**2 - moments[:,3]**2) return moments - def moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch.Tensor: + def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch.Tensor: N = moments.shape[0] - gammaref = self.gammaref(beam) - betaref = self.betaref(beam) + gammaref = self._gammaref(beam) + betaref = self._betaref(beam) p0 = gammaref*betaref*electron_mass*c p = torch.sqrt(moments[:,1]**2 + moments[:,3]**2 + moments[:,5]**2) gamma = torch.sqrt(1 + (p / (electron_mass*c))**2) @@ -556,18 +557,16 @@ def moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) return moments - def read_forces(self, beam: ParticleBeam) -> torch.Tensor: + def _read_forces(self, beam: ParticleBeam) -> torch.Tensor: """ Compute the momentum kick from the force field. """ - grad_x, grad_y, grad_z = self.E_plus_vB_field(beam) - grid_shape = self.grid_shape() - grid_dimensions = self.grid_dimensions(beam) - cell_size = self.cell_size(beam) + grad_x, grad_y, grad_z = self._E_plus_vB_field(beam) + grid_shape = self._grid_shape() + grid_dimensions = self._grid_dimensions(beam) + cell_size = self._cell_size(beam) particle_pos = beam.particles[:, [0, 2, 4]] - particle_charge = beam.particle_charges - normalized_pos = (particle_pos + grid_dimensions) / cell_size # Find the indices of the lower corners of the cells containing the particles @@ -580,7 +579,7 @@ def read_forces(self, beam: ParticleBeam) -> torch.Tensor: cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) # Extract forces from the grids - def get_force_values(force_grid): + def _get_force_values(force_grid): idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ (idx_y >= 0) & (idx_y < grid_shape[1]) & \ @@ -590,9 +589,9 @@ def get_force_values(force_grid): force_values = force_grid[tuple(valid_indices)] return force_values, valid_mask - Fx_values, valid_mask_x = get_force_values(grad_x) - Fy_values, valid_mask_y = get_force_values(grad_y) - Fz_values, valid_mask_z = get_force_values(grad_z) + Fx_values, valid_mask_x = _get_force_values(grad_x) + Fy_values, valid_mask_y = _get_force_values(grad_y) + Fz_values, valid_mask_z = _get_force_values(grad_z) # Compute interpolated forces interpolated_forces = torch.zeros((particle_pos.shape[0], 3), device=grad_x.device) @@ -600,8 +599,6 @@ def get_force_values(force_grid): values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * elementary_charge values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * elementary_charge - #particle_charge.repeat_interleave(8)[valid_mask_z] - indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8) interpolated_forces.index_add_(0, indices[valid_mask_x], torch.stack([values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) interpolated_forces.index_add_(0, indices[valid_mask_y], torch.stack([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)], dim=1)) @@ -620,12 +617,13 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: if incoming is Beam.empty: return incoming elif isinstance(incoming, ParticleBeam): - particles = self.cheetah_to_moments(incoming) - forces = self.read_forces(incoming) - particles[:,1] += forces[:,0]*self.delta_t(incoming) - particles[:,3] += forces[:,1]*self.delta_t(incoming) - particles[:,5] += forces[:,2]*self.delta_t(incoming) - particles = self.moments_to_cheetah(particles, incoming) + particles = self._cheetah_to_moments(incoming) + forces = self._read_forces(incoming) + dt = self._delta_t(incoming) + particles[:,1] += forces[:,0]*dt + particles[:,3] += forces[:,1]*dt + particles[:,5] += forces[:,2]*dt + particles = self._moments_to_cheetah(particles, incoming) return ParticleBeam( particles, incoming.energy, From 1c1888454b2258fd1e11687709653238fdd1b554 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:11:17 -0700 Subject: [PATCH 019/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 37e49ecd..6747a2c6 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -325,7 +325,6 @@ def __init__( dx: Union[torch.Tensor, nn.Parameter]=3, dy: Union[torch.Tensor, nn.Parameter]=3, ds: Union[torch.Tensor, nn.Parameter]=3, - name: Optional[str] = None, device=None, dtype=torch.float32, From 342e1625972d108c438cbc9afca8eedd6ec8fe97 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:12:09 -0700 Subject: [PATCH 020/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 6747a2c6..50e3a262 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -309,7 +309,9 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ - Simulates space charge effects on a beam. + Applies the effect of space charge over a length `length`, on the **momentum** (i.e. divergence and energy spread) of the beam. + + The positions are unmodified ; this is meant to be combined with another lattice element (e.g. `Drift`) that does modify the positions, but does not take into account space charge. :param length: Length of the element in meters. :param grid_precision: Number of grid points in each dimension. :param grid_dimensions: Dimensions of the grid as multiples of sigma of the beam. From 6821781b7ae78f681ee56b808e0c23cf8b29ea84 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:15:40 -0700 Subject: [PATCH 021/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 50e3a262..a115f784 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -348,14 +348,9 @@ def _grid_shape(self) -> tuple[int,int,int]: def _grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: - if beam.particles.shape[0] < 2: - sigma_x = torch.tensor(175e-9) - sigma_y = torch.tensor(175e-9) - sigma_s = torch.tensor(175e-9) - else: - sigma_x = torch.std(beam.particles[:, 0]) - sigma_y = torch.std(beam.particles[:, 2]) - sigma_s = torch.std(beam.particles[:, 4]) + sigma_x = torch.std(beam.particles[:, 0]) + sigma_y = torch.std(beam.particles[:, 2]) + sigma_s = torch.std(beam.particles[:, 4]) return torch.tensor([self.dx*sigma_x, self.dy*sigma_y, self.ds*sigma_s], device=self.dx.device) def _delta_t(self,beam: ParticleBeam) -> torch.Tensor: From 3263513f604716fdd49755493c6e004a3e1f8694 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:17:14 -0700 Subject: [PATCH 022/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index a115f784..bae2f89d 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -335,16 +335,10 @@ def __init__( super().__init__(name=name) self.length = torch.as_tensor(length, **factory_kwargs) - self.nx = int(torch.as_tensor(nx, **factory_kwargs)) - self.ny = int(torch.as_tensor(ny, **factory_kwargs)) - self.ns = int(torch.as_tensor(ns, **factory_kwargs)) + self.grid_shape = (int(nx), int(ny), int(ns)) self.dx = torch.as_tensor(dx, **factory_kwargs) # in multiples of sigma self.dy = torch.as_tensor(dy, **factory_kwargs) self.ds = torch.as_tensor(ds, **factory_kwargs) - - - def _grid_shape(self) -> tuple[int,int,int]: - return (int(self.nx), int(self.ny), int(self.ns)) def _grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: From 1b8b60d8b8a5bfb42c53e9ea0a4fa7e9b323e4cf Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:29:28 -0700 Subject: [PATCH 023/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index bae2f89d..e60adfe2 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -341,7 +341,7 @@ def __init__( self.ds = torch.as_tensor(ds, **factory_kwargs) - def _grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: + def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_x = torch.std(beam.particles[:, 0]) sigma_y = torch.std(beam.particles[:, 2]) sigma_s = torch.std(beam.particles[:, 4]) From 6220fb73ff0f48e0384ccecca970b919bb6de330 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:30:51 -0700 Subject: [PATCH 024/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index e60adfe2..1bc06da7 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -364,7 +364,7 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def _space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: + def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: """ Deposition of the beam on the grid. """ From f90cef16eb6487e0ea0ab46b0fa78c314bd508ec Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:31:29 -0700 Subject: [PATCH 025/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 1bc06da7..fa3f8988 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -366,7 +366,7 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: """ - Deposition of the beam on the grid. + Deposit the charge density of the beam onto a grid. """ grid_shape = self._grid_shape() grid_dimensions = self._grid_dimensions(beam) From be0b70695c82bbc0acebffa23e8d28e199c07306 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Mon, 6 May 2024 10:32:02 -0700 Subject: [PATCH 026/111] before pulling the suggested changes --- cheetah/accelerator.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index bae2f89d..1d40b949 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -341,7 +341,7 @@ def __init__( self.ds = torch.as_tensor(ds, **factory_kwargs) - def _grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: + def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_x = torch.std(beam.particles[:, 0]) sigma_y = torch.std(beam.particles[:, 2]) sigma_s = torch.std(beam.particles[:, 4]) @@ -351,8 +351,8 @@ def _delta_t(self,beam: ParticleBeam) -> torch.Tensor: return self.length / (c*self._betaref(beam)) def _cell_size(self,beam: ParticleBeam) -> torch.Tensor: - grid_shape = self._grid_shape() - grid_dimensions = self._grid_dimensions(beam) + grid_shape = self.grid_shape + grid_dimensions = self._compute_grid_dimensions(beam) return 2*grid_dimensions / torch.tensor(grid_shape) def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: @@ -364,12 +364,12 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def _space_charge_deposition(self, beam: ParticleBeam) -> torch.Tensor: + def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: """ Deposition of the beam on the grid. """ - grid_shape = self._grid_shape() - grid_dimensions = self._grid_dimensions(beam) + grid_shape = self.grid_shape + grid_dimensions = self._compute_grid_dimensions(beam) cell_size = self._cell_size(beam) # Initialize the charge density grid @@ -422,8 +422,8 @@ def _cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: """ Compute the charge density on the grid using the cyclic deposition method. """ - grid_shape = self._grid_shape() - charge_density = self._space_charge_deposition(beam) + grid_shape = self.grid_shape + charge_density = self._deposit_charge_on_grid(beam) # Double the dimensions new_dims = tuple(dim * 2 for dim in grid_shape) @@ -439,7 +439,7 @@ def _IGF(self, beam: ParticleBeam) -> torch.Tensor: gamma = self._gammaref(beam) cell_size = self._cell_size(beam) dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma # ds is scaled by gamma - nx, ny, ns = self._grid_shape() + nx, ny, ns = self.grid_shape # Create coordinate grids x = torch.arange(nx) * dx @@ -552,8 +552,8 @@ def _read_forces(self, beam: ParticleBeam) -> torch.Tensor: Compute the momentum kick from the force field. """ grad_x, grad_y, grad_z = self._E_plus_vB_field(beam) - grid_shape = self._grid_shape() - grid_dimensions = self._grid_dimensions(beam) + grid_shape = self.grid_shape + grid_dimensions = self._compute_grid_dimensions(beam) cell_size = self._cell_size(beam) particle_pos = beam.particles[:, [0, 2, 4]] From 6ed5a74d5d5466c7b58ce6518a32ad7b63777930 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:36:42 -0700 Subject: [PATCH 027/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index b97493c6..b979be3a 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -373,7 +373,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: cell_size = self._cell_size(beam) # Initialize the charge density grid - charge = torch.zeros(grid_shape, dtype=torch.float32) + charge = torch.zeros(grid_shape) # Get particle positions and charges particle_pos = beam.particles[:, [0, 2, 4]] From 6bc41e7e1a328290ba701b6002f6901aa597b7f5 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Mon, 6 May 2024 10:47:40 -0700 Subject: [PATCH 028/111] . --- cheetah/accelerator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index b97493c6..71a7177d 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -366,11 +366,12 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: """ - Deposit the charge density of the beam onto a grid. + Deposit the charge density of the beam onto a grid, using the nearest grid point method and weighting by the distance to the grid points. + Returns agrid of charge density in C/m^3. """ grid_shape = self.grid_shape grid_dimensions = self._compute_grid_dimensions(beam) - cell_size = self._cell_size(beam) + cell_size = 2*grid_dimensions / torch.tensor(grid_shape) # Initialize the charge density grid charge = torch.zeros(grid_shape, dtype=torch.float32) @@ -393,6 +394,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: # Add the charge contributions to the cells idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T + # Check that particles are inside the grid valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ (idx_y >= 0) & (idx_y < grid_shape[1]) & \ (idx_s >= 0) & (idx_s < grid_shape[2]) From db5876de69c3c28d18852a7d449c56e60bc9fdca Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:52:27 -0700 Subject: [PATCH 029/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 01efa020..a2106428 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -377,7 +377,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: charge = torch.zeros(grid_shape) # Get particle positions and charges - particle_pos = beam.particles[:, [0, 2, 4]] + particle_pos = beam.particles[..., [0, 2, 4]] particle_charge = beam.particle_charges # Compute the normalized positions of the particles within the grid From 96a62208ff5e90b7cfdb1d7f152989f9c608d348 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 10:52:54 -0700 Subject: [PATCH 030/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index a2106428..fa473fab 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -388,9 +388,9 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: # Calculate the weights for all surrounding cells offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(1) + offsets # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) - cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) + surrounding_indices = cell_indices.unsqueeze(-2) + offsets # Shape: (..., n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos.unsqueeze(-2) - surrounding_indices) # Shape: (..., n_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (..., n_particles, 8) - product of shapes along x, y and z # Add the charge contributions to the cells idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T From 5da3b8f293cb74ea3f6584c7be350edfd5c02728 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 11:50:24 -0700 Subject: [PATCH 031/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index fa473fab..79b88648 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -404,7 +404,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: repeated_charges = particle_charge.repeat_interleave(8) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] charge.index_put_(tuple(indices), values, accumulate=True) - cell_volume = cell_size[0] * cell_size[1] * cell_size[2] + inv_cell_volume = 1 / (cell_size[0] * cell_size[1] * cell_size[2]) return charge/cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 From 7d12f0f62a0ccbb5fa8c71146b5bdd0c07762da2 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 11:50:54 -0700 Subject: [PATCH 032/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 79b88648..1db87ee7 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -406,7 +406,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: charge.index_put_(tuple(indices), values, accumulate=True) inv_cell_volume = 1 / (cell_size[0] * cell_size[1] * cell_size[2]) - return charge/cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 + return charge * inv_cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 def _integrated_potential(self, x, y, s) -> torch.Tensor: From 4dcf3cb2d1861ceb2d3db1029a12ad94873979f2 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 12:01:07 -0700 Subject: [PATCH 033/111] Update tests/test_space_charge_kick.py Co-authored-by: Remi Lehe --- tests/test_space_charge_kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 300121cb..cd139034 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -4,7 +4,7 @@ def test_cold_uniform_beam_expansion(): """ - Test that that a cold uniform beam doubles in size in both dimensions when travelling through a drift section with space_charge. (cf ImpactX test) + Test that that a cold uniform beam doubles in size in all dimensions when traveling through a drift section having an analytically-predicted length, with space_charge. (cf ImpactX test) """ # Simulation parameters From 3534a5b9d1ec459cef13ba02610345d26c59fef0 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 12:01:58 -0700 Subject: [PATCH 034/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 1db87ee7..b7683ecd 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -2439,7 +2439,7 @@ def track(self, incoming: Beam) -> Beam: return super().track(incoming) else: todos = [] - for element in self.elements: + for element in self.elements: if not element.is_skippable: todos.append(element) elif not todos or not todos[-1].is_skippable: From 304c445e69292f0e349c4057e00f89afe5869b8e Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 12:02:49 -0700 Subject: [PATCH 035/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index b7683ecd..5f605eec 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -551,7 +551,7 @@ def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torc def _read_forces(self, beam: ParticleBeam) -> torch.Tensor: """ - Compute the momentum kick from the force field. + Interpolates the space charge force from the grid onto the macroparticles """ grad_x, grad_y, grad_z = self._E_plus_vB_field(beam) grid_shape = self.grid_shape From 2b787d39b9af078a6019539d5b8b33499f658085 Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 13:47:49 -0700 Subject: [PATCH 036/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 5f605eec..809e1d43 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -434,7 +434,7 @@ def _cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: cyclic_charge_density = torch.zeros(new_dims) # Copy the original charge_density values to the beginning of the new tensor - cyclic_charge_density[:charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density + cyclic_charge_density[..., :charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density return cyclic_charge_density def _IGF(self, beam: ParticleBeam) -> torch.Tensor: From ea8bac2b8ec1ed6dbb9c88078775c8a243aafa2b Mon Sep 17 00:00:00 2001 From: greglenerd <162642097+greglenerd@users.noreply.github.com> Date: Mon, 6 May 2024 13:57:37 -0700 Subject: [PATCH 037/111] Update cheetah/accelerator.py Co-authored-by: Remi Lehe --- cheetah/accelerator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 809e1d43..954a95e9 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -2180,7 +2180,6 @@ def flattened(self) -> "Segment": flattened_elements.append(element) return Segment(elements=flattened_elements, name=self.name) - def transfer_maps_merged( self, incoming_beam: Beam, except_for: Optional[list[str]] = None ) -> "Segment": From d9bb1527286660689ff9603160b5badb19397be1 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Mon, 6 May 2024 15:12:56 -0700 Subject: [PATCH 038/111] cleaning --- cheetah/accelerator.py | 178 ++++++++++++++++++-------------- tests/test_space_charge_kick.py | 6 +- 2 files changed, 107 insertions(+), 77 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 01efa020..a82e20db 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -309,24 +309,44 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ - Applies the effect of space charge over a length `length`, on the **momentum** (i.e. divergence and energy spread) of the beam. - - The positions are unmodified ; this is meant to be combined with another lattice element (e.g. `Drift`) that does modify the positions, but does not take into account space charge. + Apply the effect of space charge over a length `length`, on the **momentum** + (i.e. divergence and energy spread) of the beam. + The positions are unmodified ; this is meant to be combined with another lattice + element (e.g. `Drift`) that does modify the positions, but does not take into + account space charge. + This uses the integrated Green function method + (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) to compute + the effect of space charge. This is similar to the method used in Ocelot. + The main difference is that solves the Poisson equation in the beam frame, + while here we solve a modified Poisson equation in the laboratory frame + (https://pubs.aip.org/aip/pop/article-abstract/15/5/056701/1016636/Simulation-of-beams-or-plasmas-crossing-at). + The two methods are in principle equivalent. + + Overview of the method: + - Compute the beam charge density on a grid + - Convolve the charge density with a Green function (the integrated green function) + to find the potential `phi` on the grid. The convolution uses the Hockney method + for open boundaries (allocate 2x larger arrays and perform convolution using FFTs) + - Compute the corresponding electromagnetic fields and Lorentz force on the grid + - Interpolate the Lorentz force to the particles and update their momentum + :param length: Length of the element in meters. - :param grid_precision: Number of grid points in each dimension. - :param grid_dimensions: Dimensions of the grid as multiples of sigma of the beam. + :param num_grid_points_x, num_grid_points_y, num_grid_points_s: Number of grid + points in each dimension. + :param grid_extend_x, grid_extend_y, grid_extend_s: Dimensions of the grid on which + to compute space-charge, as multiples of sigma of the beam (dimensionless) :param name: Unique identifier of the element. """ def __init__( self, length: Union[torch.Tensor, nn.Parameter], - nx: Union[torch.Tensor, nn.Parameter,int]=32, - ny: Union[torch.Tensor, nn.Parameter,int]=32, - ns: Union[torch.Tensor, nn.Parameter,int]=32, - dx: Union[torch.Tensor, nn.Parameter]=3, - dy: Union[torch.Tensor, nn.Parameter]=3, - ds: Union[torch.Tensor, nn.Parameter]=3, + num_grid_points_x: Union[torch.Tensor, nn.Parameter,int]=32, + num_grid_points_y: Union[torch.Tensor, nn.Parameter,int]=32, + num_grid_points_s: Union[torch.Tensor, nn.Parameter,int]=32, + grid_extend_x: Union[torch.Tensor, nn.Parameter]=3, + grid_extend_y: Union[torch.Tensor, nn.Parameter]=3, + grid_extend_s: Union[torch.Tensor, nn.Parameter]=3, name: Optional[str] = None, device=None, dtype=torch.float32, @@ -335,25 +355,20 @@ def __init__( super().__init__(name=name) self.length = torch.as_tensor(length, **factory_kwargs) - self.grid_shape = (int(nx), int(ny), int(ns)) - self.dx = torch.as_tensor(dx, **factory_kwargs) # in multiples of sigma - self.dy = torch.as_tensor(dy, **factory_kwargs) - self.ds = torch.as_tensor(ds, **factory_kwargs) + self.grid_shape = (int(num_grid_points_x), int(num_grid_points_y), \ + int(num_grid_points_s)) + self.grid_extend_x = torch.as_tensor(grid_extend_x, **factory_kwargs) + # in multiples of sigma + self.grid_extend_y = torch.as_tensor(grid_extend_y, **factory_kwargs) + self.grid_extend_s = torch.as_tensor(grid_extend_s, **factory_kwargs) def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_x = torch.std(beam.particles[:, 0]) sigma_y = torch.std(beam.particles[:, 2]) sigma_s = torch.std(beam.particles[:, 4]) - return torch.tensor([self.dx*sigma_x, self.dy*sigma_y, self.ds*sigma_s], device=self.dx.device) - - def _delta_t(self,beam: ParticleBeam) -> torch.Tensor: - return self.length / (c*self._betaref(beam)) - - def _cell_size(self,beam: ParticleBeam) -> torch.Tensor: - grid_shape = self.grid_shape - grid_dimensions = self._compute_grid_dimensions(beam) - return 2*grid_dimensions / torch.tensor(grid_shape) + return torch.tensor([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ + , self.grid_extend_s*sigma_s]) def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: return beam.energy / rest_energy @@ -364,15 +379,13 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: + def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: """ - Deposit the charge density of the beam onto a grid, using the nearest grid point method and weighting by the distance to the grid points. - Returns agrid of charge density in C/m^3. + Deposit the charge density of the beam onto a grid, using the nearest + grid point method and weighting by the distance to the grid points. + Returns a grid of charge density in C/m^3. """ grid_shape = self.grid_shape - grid_dimensions = self._compute_grid_dimensions(beam) - cell_size = 2*grid_dimensions / torch.tensor(grid_shape) - # Initialize the charge density grid charge = torch.zeros(grid_shape) @@ -387,21 +400,25 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(1) + offsets # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ + , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices = cell_indices.unsqueeze(1) + offsets + # Shape: (n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) + # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) # Add the charge contributions to the cells - idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T + idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T + # Shape: (3, n_particles*8) # Check that particles are inside the grid valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ (idx_y >= 0) & (idx_y < grid_shape[1]) & \ (idx_s >= 0) & (idx_s < grid_shape[2]) # Accumulate the charge contributions - indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) - repeated_charges = particle_charge.repeat_interleave(8) + indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) # Shape: (3, n_valid) n_valid = number of valid particles=8*n_particles + repeated_charges = particle_charge.repeat_interleave(8) # Shape: (8*n_particles) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] charge.index_put_(tuple(indices), values, accumulate=True) cell_volume = cell_size[0] * cell_size[1] * cell_size[2] @@ -410,6 +427,11 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam) -> torch.Tensor: def _integrated_potential(self, x, y, s) -> torch.Tensor: + """ + Compute the electrostatic potential using the Integrated Green Function method + as in http://dx.doi.org/10.1103/PhysRevSTAB.9.044204. + """ + r = torch.sqrt(x**2 + y**2 + s**2) G = (-0.5 * s**2 * torch.atan(x * y / (s * r)) -0.5 * y**2 * torch.atan(x * s / (y * r)) @@ -420,14 +442,12 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: return G - def _cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: + def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: """ - Compute the charge density on the grid using the cyclic deposition method. + Allocate a 2x larger array in all dimensions (to perform Hockney's method), and copies the charge density in one of the "quadrants". """ grid_shape = self.grid_shape - charge_density = self._deposit_charge_on_grid(beam) - - # Double the dimensions + charge_density = self._deposit_charge_on_grid(beam, cell_size, grid_dimensions) new_dims = tuple(dim * 2 for dim in grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros @@ -437,16 +457,18 @@ def _cyclic_rho(self,beam: ParticleBeam) -> torch.Tensor: cyclic_charge_density[:charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density return cyclic_charge_density - def _IGF(self, beam: ParticleBeam) -> torch.Tensor: + def _IGF(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + """ + Compute the Integrated Green Function (IGF) with periodic boundary conditions (to perform Hockney's method). + """ gamma = self._gammaref(beam) - cell_size = self._cell_size(beam) dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma # ds is scaled by gamma - nx, ny, ns = self.grid_shape + num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids - x = torch.arange(nx) * dx - y = torch.arange(ny) * dy - s = torch.arange(ns) * ds + x = torch.arange(num_grid_points_x) * dx + y = torch.arange(num_grid_points_y) * dy + s = torch.arange(num_grid_points_s) * ds x_grid, y_grid, s_grid = torch.meshgrid(x, y, s, indexing='ij') # Compute the Green's function values @@ -462,26 +484,26 @@ def _IGF(self, beam: ParticleBeam) -> torch.Tensor: ) # Initialize the grid with double dimensions - grid = torch.zeros(2 * nx, 2 * ny, 2 * ns) + green_func = torch.zeros(2 * num_grid_points_x, 2 * num_grid_points_y, 2 * num_grid_points_s) # Fill the grid with G_values and its periodic copies - grid[:nx, :ny, :ns] = G_values - grid[nx+1:, :ny, :ns] = G_values[1:,:,:].flip(dims=[0]) # Reverse the x dimension, excluding the first element - grid[:nx, ny+1:, :ns] = G_values[:, 1:,:].flip(dims=[1]) # Reverse the y dimension, excluding the first element - grid[:nx, :ny, ns+1:] = G_values[:, :, 1:].flip(dims=[2]) # Reverse the s dimension, excluding the first element - grid[nx+1:, ny+1:, :ns] = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions - grid[:nx, ny+1:, ns+1:] = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions - grid[nx+1:, :ny, ns+1:] = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions - grid[nx+1:, ny+1:, ns+1:] = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions - - return grid + green_func[:num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = G_values + green_func[num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s] = G_values[1:,:,:].flip(dims=[0]) # Reverse the x dimension, excluding the first element + green_func[:num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s] = G_values[:, 1:,:].flip(dims=[1]) # Reverse the y dimension, excluding the first element + green_func[:num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:] = G_values[:, :, 1:].flip(dims=[2]) # Reverse the s dimension, excluding the first element + green_func[num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s] = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions + green_func[:num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:] = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions + green_func[num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:] = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions + green_func[num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:] = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions + + return green_func - def _solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works only for ParticleBeam at this stage + def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: #works only for ParticleBeam at this stage """ - Solves the Poisson equation for the given charge density. + Solve the Poisson equation for the given charge density. """ - charge_density = self._cyclic_rho(beam) + charge_density = self._array_rho(beam, cell_size, grid_dimensions) charge_density_ft = torch.fft.fftn(charge_density) integrated_green_function = self._IGF(beam) integrated_green_function_ft = torch.fft.fftn(integrated_green_function) @@ -492,9 +514,9 @@ def _solve_poisson_equation(self, beam: ParticleBeam) -> torch.Tensor: #works o return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] - def _E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: + def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: """ - Compute the force field from the potential and the particle positions and speeds. + Compute the force field from the potential and the particle positions and speeds, as in https://doi.org/10.1063/1.2837054. """ gamma = self._gammaref(beam) igamma2 = ( @@ -502,8 +524,7 @@ def _E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: if gamma != 0 else torch.tensor(0.0) ) - potential = self._solve_poisson_equation(beam) - cell_size = self._cell_size(beam) + potential = self._solve_poisson_equation(beam, cell_size, grid_dimensions) potential = potential.unsqueeze(0).unsqueeze(0) # Now apply padding so that derivatives are 0 at the boundaries @@ -522,6 +543,9 @@ def _E_plus_vB_field(self, beam: ParticleBeam) -> torch.Tensor: return grad_x, grad_y, grad_z def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: + """ + Convert the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. + """ N = beam.particles.shape[0] moments = beam.particles gammaref = self._gammaref(beam) @@ -537,6 +561,9 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: return moments def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch.Tensor: + """ + Convert the moments in SI units to the Cheetah particle beam parameters. + """ N = moments.shape[0] gammaref = self._gammaref(beam) betaref = self._betaref(beam) @@ -549,15 +576,12 @@ def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torc moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) return moments - def _read_forces(self, beam: ParticleBeam) -> torch.Tensor: + def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: """ - Compute the momentum kick from the force field. + Compute the forces on the particles due to the space charge. """ - grad_x, grad_y, grad_z = self._E_plus_vB_field(beam) + grad_x, grad_y, grad_z = self._E_plus_vB_field(beam, cell_size) grid_shape = self.grid_shape - grid_dimensions = self._compute_grid_dimensions(beam) - cell_size = self._cell_size(beam) - particle_pos = beam.particles[:, [0, 2, 4]] normalized_pos = (particle_pos + grid_dimensions) / cell_size @@ -586,7 +610,7 @@ def _get_force_values(force_grid): Fz_values, valid_mask_z = _get_force_values(grad_z) # Compute interpolated forces - interpolated_forces = torch.zeros((particle_pos.shape[0], 3), device=grad_x.device) + interpolated_forces = torch.zeros((particle_pos.shape[0], 3)) values_x = cell_weights.view(-1)[valid_mask_x] * Fx_values * elementary_charge values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * elementary_charge values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * elementary_charge @@ -602,16 +626,17 @@ def _get_force_values(force_grid): def track(self, incoming: ParticleBeam) -> ParticleBeam: """ Track particles through the element. The input must be a `ParticleBeam`. - :param incoming: Beam of particles entering the element. :return: Beam of particles exiting the element. """ - if incoming is Beam.empty: + if incoming is Beam.empty or incoming.particles.shape[0] == 0: return incoming elif isinstance(incoming, ParticleBeam): + grid_dimensions = self._compute_grid_dimensions(incoming) + cell_size = 2*grid_dimensions / torch.tensor(self.grid_shape) + dt = self.length / (c*self._betaref(incoming)) particles = self._cheetah_to_moments(incoming) - forces = self._read_forces(incoming) - dt = self._delta_t(incoming) + forces = self._compute_forces(incoming, cell_size, grid_dimensions) particles[:,1] += forces[:,0]*dt particles[:,3] += forces[:,1]*dt particles[:,5] += forces[:,2]*dt @@ -2446,6 +2471,7 @@ def track(self, incoming: Beam) -> Beam: todos.append(Segment([element])) else: todos[-1].elements.append(element) + for todo in todos: incoming = todo.track(incoming) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 300121cb..4b7ca995 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -4,7 +4,11 @@ def test_cold_uniform_beam_expansion(): """ - Test that that a cold uniform beam doubles in size in both dimensions when travelling through a drift section with space_charge. (cf ImpactX test) + Test that that a cold uniform beam doubles in size in both dimensions when + travelling through a drift section with space_charge. (cf ImpactX test: + https://impactx.readthedocs.io/en/latest/usage/examples/cfchannel/README.html#constant-focusing-channel-with-space-charge). + See Free Expansion of a Cold Uniform Beam in a Drift Section with Space Charge: + https://accelconf.web.cern.ch/hb2023/papers/thbp44.pdf """ # Simulation parameters From 85a6edd780bb1fa611559b13829e46e1c1198c5e Mon Sep 17 00:00:00 2001 From: greglenerd Date: Tue, 7 May 2024 14:31:53 -0700 Subject: [PATCH 039/111] new test, little change in accelerator.py --- cheetah/accelerator.py | 280 ++++++++++++++++++-------------- tests/test_space_charge_kick.py | 46 +++++- 2 files changed, 202 insertions(+), 124 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 4e766df2..6cd47e0d 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -309,7 +309,7 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ - Apply the effect of space charge over a length `length`, on the **momentum** + Applies the effect of space charge over a length `length`, on the **momentum** (i.e. divergence and energy spread) of the beam. The positions are unmodified ; this is meant to be combined with another lattice element (e.g. `Drift`) that does modify the positions, but does not take into @@ -317,9 +317,9 @@ class SpaceChargeKick(Element): This uses the integrated Green function method (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) to compute the effect of space charge. This is similar to the method used in Ocelot. - The main difference is that solves the Poisson equation in the beam frame, + The main difference is that it solves the Poisson equation in the beam frame, while here we solve a modified Poisson equation in the laboratory frame - (https://pubs.aip.org/aip/pop/article-abstract/15/5/056701/1016636/Simulation-of-beams-or-plasmas-crossing-at). + (https://pubs.aip.org/aip/pop/article-abstract/15/5/056701/1016636/Simulation-of-beams-or-plasmas-crossing-at). The two methods are in principle equivalent. Overview of the method: @@ -330,7 +330,8 @@ class SpaceChargeKick(Element): - Compute the corresponding electromagnetic fields and Lorentz force on the grid - Interpolate the Lorentz force to the particles and update their momentum - :param length: Length of the element in meters. + :param length_effect: Length over which the effect applies in meters. + :param length: Physical length of the element in meters (=0) :param num_grid_points_x, num_grid_points_y, num_grid_points_s: Number of grid points in each dimension. :param grid_extend_x, grid_extend_y, grid_extend_s: Dimensions of the grid on which @@ -340,7 +341,8 @@ class SpaceChargeKick(Element): def __init__( self, - length: Union[torch.Tensor, nn.Parameter], + length_effect: Union[torch.Tensor, nn.Parameter], + length: Union[torch.Tensor, nn.Parameter]=0.0, num_grid_points_x: Union[torch.Tensor, nn.Parameter,int]=32, num_grid_points_y: Union[torch.Tensor, nn.Parameter,int]=32, num_grid_points_s: Union[torch.Tensor, nn.Parameter,int]=32, @@ -351,16 +353,16 @@ def __init__( device=None, dtype=torch.float32, ) -> None: - factory_kwargs = {"device": device, "dtype": dtype} + self.factory_kwargs = {"device": device, "dtype": dtype} super().__init__(name=name) - - self.length = torch.as_tensor(length, **factory_kwargs) + self.length_effect = torch.as_tensor(length_effect, **self.factory_kwargs) + self.length = torch.as_tensor(length, **self.factory_kwargs) self.grid_shape = (int(num_grid_points_x), int(num_grid_points_y), \ int(num_grid_points_s)) - self.grid_extend_x = torch.as_tensor(grid_extend_x, **factory_kwargs) + self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) # in multiples of sigma - self.grid_extend_y = torch.as_tensor(grid_extend_y, **factory_kwargs) - self.grid_extend_s = torch.as_tensor(grid_extend_s, **factory_kwargs) + self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) + self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: @@ -379,28 +381,25 @@ def _betaref(self,beam: ParticleBeam) -> torch.Tensor: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions)\ + -> torch.Tensor: """ - Deposit the charge density of the beam onto a grid, using the nearest + Deposits the charge density of the beam onto a grid, using the nearest grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ grid_shape = self.grid_shape - # Initialize the charge density grid charge = torch.zeros(grid_shape) # Get particle positions and charges particle_pos = beam.particles[..., [0, 2, 4]] particle_charge = beam.particle_charges - - # Compute the normalized positions of the particles within the grid normalized_pos = (particle_pos + grid_dimensions) / cell_size # Find the indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells -<<<<<<< HEAD offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) surrounding_indices = cell_indices.unsqueeze(1) + offsets @@ -408,12 +407,6 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) -======= - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(-2) + offsets # Shape: (..., n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(-2) - surrounding_indices) # Shape: (..., n_particles, 8, 3) - cell_weights = weights.prod(dim=-1) # Shape: (..., n_particles, 8) - product of shapes along x, y and z ->>>>>>> ea8bac2b8ec1ed6dbb9c88078775c8a243aafa2b # Add the charge contributions to the cells idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T @@ -424,18 +417,19 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions (idx_s >= 0) & (idx_s < grid_shape[2]) # Accumulate the charge contributions - indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) # Shape: (3, n_valid) n_valid = number of valid particles=8*n_particles - repeated_charges = particle_charge.repeat_interleave(8) # Shape: (8*n_particles) + indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask],idx_s[valid_mask]]\ + , dim=0) # Shape: (3, n_valid) n_valid =8*n_particles + repeated_charges = particle_charge.repeat_interleave(8) # Shape:(8*n_particles) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] charge.index_put_(tuple(indices), values, accumulate=True) inv_cell_volume = 1 / (cell_size[0] * cell_size[1] * cell_size[2]) - return charge * inv_cell_volume # Normalize by the cell volume, so that the charge density is in C/m^3 + return charge * inv_cell_volume # Normalize by the cell volume def _integrated_potential(self, x, y, s) -> torch.Tensor: """ - Compute the electrostatic potential using the Integrated Green Function method + Computes the electrostatic potential using the Integrated Green Function method as in http://dx.doi.org/10.1103/PhysRevSTAB.9.044204. """ @@ -449,82 +443,107 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: return G - def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tensor: """ - Allocate a 2x larger array in all dimensions (to perform Hockney's method), and copies the charge density in one of the "quadrants". + Allocates a 2x larger array in all dimensions (to perform Hockney's method), + and copies the charge density in one of the "quadrants". """ grid_shape = self.grid_shape charge_density = self._deposit_charge_on_grid(beam, cell_size, grid_dimensions) new_dims = tuple(dim * 2 for dim in grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros - cyclic_charge_density = torch.zeros(new_dims) + new_charge_density = torch.zeros(new_dims, **self.factory_kwargs) # Copy the original charge_density values to the beginning of the new tensor - cyclic_charge_density[..., :charge_density.shape[0], :charge_density.shape[1], :charge_density.shape[2]] = charge_density - return cyclic_charge_density + new_charge_density[..., :charge_density.shape[0], :charge_density.shape[1],\ + :charge_density.shape[2]] = charge_density + return new_charge_density - def _IGF(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: """ - Compute the Integrated Green Function (IGF) with periodic boundary conditions (to perform Hockney's method). + Computes the Integrated Green Function (IGF) with periodic boundary conditions + (to perform Hockney's method). """ gamma = self._gammaref(beam) - dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma # ds is scaled by gamma + dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma #scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids - x = torch.arange(num_grid_points_x) * dx - y = torch.arange(num_grid_points_y) * dy - s = torch.arange(num_grid_points_s) * ds + x = torch.arange(num_grid_points_x, **self.factory_kwargs) * dx + y = torch.arange(num_grid_points_y, **self.factory_kwargs) * dy + s = torch.arange(num_grid_points_s, **self.factory_kwargs) * ds x_grid, y_grid, s_grid = torch.meshgrid(x, y, s, indexing='ij') # Compute the Green's function values G_values = ( - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) - - self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds) - - self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) - - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) - + self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) - + self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds) - + self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds) - - self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds) + self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ + s_grid + 0.5 * ds) + - self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy,\ + s_grid + 0.5 * ds) + - self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy,\ + s_grid + 0.5 * ds) + - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ + s_grid - 0.5 * ds) + + self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy,\ + s_grid - 0.5 * ds) + + self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy,\ + s_grid - 0.5 * ds) + + self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy,\ + s_grid + 0.5 * ds) + - self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy,\ + s_grid - 0.5 * ds) ) # Initialize the grid with double dimensions - green_func = torch.zeros(2 * num_grid_points_x, 2 * num_grid_points_y, 2 * num_grid_points_s) + green_func = torch.zeros(2 * num_grid_points_x, 2 * num_grid_points_y,\ + 2 * num_grid_points_s, **self.factory_kwargs) # Fill the grid with G_values and its periodic copies - green_func[:num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = G_values - green_func[num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s] = G_values[1:,:,:].flip(dims=[0]) # Reverse the x dimension, excluding the first element - green_func[:num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s] = G_values[:, 1:,:].flip(dims=[1]) # Reverse the y dimension, excluding the first element - green_func[:num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:] = G_values[:, :, 1:].flip(dims=[2]) # Reverse the s dimension, excluding the first element - green_func[num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s] = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions - green_func[:num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:] = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions - green_func[num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:] = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions - green_func[num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:] = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions + green_func[:num_grid_points_x, :num_grid_points_y, :num_grid_points_s]\ + = G_values + green_func[num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s]\ + = G_values[1:,:,:].flip(dims=[0]) #Reverse x, excluding the first element + green_func[:num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s]\ + = G_values[:, 1:,:].flip(dims=[1])#Reverse y, excluding the first element + green_func[:num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:]\ + = G_values[:, :, 1:].flip(dims=[2])#Reverse s,excluding the first element + green_func[num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s]\ + = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions + green_func[:num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:]\ + = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions + green_func[num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:]\ + = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions + green_func[num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:]\ + = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions return green_func - def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: #works only for ParticleBeam at this stage + def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions)\ + -> torch.Tensor: #works only for ParticleBeam at this stage """ - Solve the Poisson equation for the given charge density. + Solves the Poisson equation for the given charge density, using FFT convolution. """ charge_density = self._array_rho(beam, cell_size, grid_dimensions) charge_density_ft = torch.fft.fftn(charge_density) - integrated_green_function = self._IGF(beam) + integrated_green_function = self._IGF(beam, cell_size) integrated_green_function_ft = torch.fft.fftn(integrated_green_function) potential_ft = charge_density_ft * integrated_green_function_ft potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft).real # Return the physical potential - return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2, :charge_density.shape[2]//2] + return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2,\ + :charge_density.shape[2]//2] - def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ + -> torch.Tensor: """ - Compute the force field from the potential and the particle positions and speeds, as in https://doi.org/10.1063/1.2837054. + Computes the force field from the potential and the particle positions and + speeds, as in https://doi.org/10.1063/1.2837054. """ + inv_cell_size = 1 / cell_size gamma = self._gammaref(beam) igamma2 = ( 1 / gamma**2 @@ -532,26 +551,31 @@ def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions) -> to else torch.tensor(0.0) ) potential = self._solve_poisson_equation(beam, cell_size, grid_dimensions) - potential = potential.unsqueeze(0).unsqueeze(0) - # Now apply padding so that derivatives are 0 at the boundaries - phi_padded = torch.nn.functional.pad(potential, (1, 1, 1, 1, 1, 1), mode='replicate') - phi_padded = phi_padded.squeeze(0).squeeze(0) - # Compute derivatives using central differences - grad_x = (phi_padded[2:, :, :] - phi_padded[:-2, :, :]) / (2 * cell_size[0]) - grad_y = (phi_padded[:, 2:, :] - phi_padded[:, :-2, :]) / (2 * cell_size[1]) - grad_z = (phi_padded[:, :, 2:] - phi_padded[:, :, :-2]) / (2 * cell_size[2]) - - # Crop out the padding to maintain the original shape - grad_x = -igamma2*grad_x[:, 1:-1, 1:-1] - grad_y = -igamma2*grad_y[1:-1, :, 1:-1] - grad_z = -igamma2*grad_z[1:-1, 1:-1, :] - - return grad_x, grad_y, grad_z + grad_x = torch.zeros_like(potential) + grad_y = torch.zeros_like(potential) + grad_s = torch.zeros_like(potential) + + # Compute the gradients of the potential, using central differences, with 0 + #boundary conditions. + grad_x[1:-1, :, :] = ( potential[2:, :, :] - potential[:-2, :, :] )\ + * (0.5 * inv_cell_size[0]) + grad_y[:, 1:-1, :] = ( potential[:, 2:, :] - potential[:, :-2, :] )\ + * (0.5 * inv_cell_size[1]) + grad_s[:, :, 1:-1] = ( potential[:, :, 2:] - potential[:, :, :-2] )\ + * (0.5 * inv_cell_size[2]) + + # Scale the gradients with lorentz factor + grad_x = -igamma2*grad_x + grad_y = -igamma2*grad_y + grad_s = -igamma2*grad_s + + return grad_x, grad_y, grad_s def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: """ - Convert the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. + Converts the Cheetah particle beam parameters to the moments in SI units used + in the space charge solver. """ N = beam.particles.shape[0] moments = beam.particles @@ -565,12 +589,13 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: moments[:,3] = p0*moments[:,3] moments[:,4] = -betaref*moments[:,4] moments[:,5] = torch.sqrt(p**2 - moments[:,1]**2 - moments[:,3]**2) - return moments - def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torch.Tensor: + def _moments_to_cheetah(self, beam: ParticleBeam) \ + -> torch.Tensor: """ - Convert the moments in SI units to the Cheetah particle beam parameters. + Converts the moments in SI units to the Cheetah particle beam parameters. """ + moments = beam.particles N = moments.shape[0] gammaref = self._gammaref(beam) betaref = self._betaref(beam) @@ -581,17 +606,14 @@ def _moments_to_cheetah(self, moments: torch.Tensor, beam: ParticleBeam) -> torc moments[:,3] = moments[:,3] / p0 moments[:,4] = -moments[:,4] / betaref moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) - return moments - def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions) -> torch.Tensor: + def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ + -> torch.Tensor: """ -<<<<<<< HEAD - Compute the forces on the particles due to the space charge. -======= - Interpolates the space charge force from the grid onto the macroparticles ->>>>>>> ea8bac2b8ec1ed6dbb9c88078775c8a243aafa2b + Interpolates the space charge force from the grid onto the macroparticles. + Reciprocal function of _deposit_charge_on_grid. """ - grad_x, grad_y, grad_z = self._E_plus_vB_field(beam, cell_size) + grad_x, grad_y, grad_z = self._E_plus_vB_field(beam,cell_size, grid_dimensions) grid_shape = self.grid_shape particle_pos = beam.particles[:, [0, 2, 4]] normalized_pos = (particle_pos + grid_dimensions) / cell_size @@ -600,62 +622,80 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions) -> tor cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(1) + offsets # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0],\ + [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices =cell_indices.unsqueeze(1)+offsets #Shape:(n_particles,8,3) + + weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) + # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) # Extract forces from the grids - def _get_force_values(force_grid): - idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T - valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ - (idx_y >= 0) & (idx_y < grid_shape[1]) & \ - (idx_s >= 0) & (idx_s < grid_shape[2]) + idx_x,idx_y,idx_s = surrounding_indices.view(-1, 3).T #Shape: (3,n_particles*8) + valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ + (idx_y >= 0) & (idx_y < grid_shape[1]) & \ + (idx_s >= 0) & (idx_s < grid_shape[2]) - valid_indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]], dim=0) - force_values = force_grid[tuple(valid_indices)] - return force_values, valid_mask + valid_indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask],\ + idx_s[valid_mask]], dim=0) - Fx_values, valid_mask_x = _get_force_values(grad_x) - Fy_values, valid_mask_y = _get_force_values(grad_y) - Fz_values, valid_mask_z = _get_force_values(grad_z) + Fx_values = grad_x[tuple(valid_indices)] + Fy_values = grad_y[tuple(valid_indices)] + Fz_values = grad_z[tuple(valid_indices)] # Compute interpolated forces interpolated_forces = torch.zeros((particle_pos.shape[0], 3)) - values_x = cell_weights.view(-1)[valid_mask_x] * Fx_values * elementary_charge - values_y = cell_weights.view(-1)[valid_mask_y] * Fy_values * elementary_charge - values_z = cell_weights.view(-1)[valid_mask_z] * Fz_values * elementary_charge + valid_cell_weights = cell_weights.view(-1)[valid_mask]*elementary_charge + values_x = valid_cell_weights * Fx_values + values_y = valid_cell_weights * Fy_values + values_z = valid_cell_weights * Fz_values - indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8) - interpolated_forces.index_add_(0, indices[valid_mask_x], torch.stack([values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) - interpolated_forces.index_add_(0, indices[valid_mask_y], torch.stack([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)], dim=1)) - interpolated_forces.index_add_(0, indices[valid_mask_z], torch.stack([torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) + indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8)[valid_mask] + interpolated_forces.index_add_(0, indices, torch.stack([values_x,\ + torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) + interpolated_forces.index_add_(0,indices,torch.stack\ + ([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)],dim=1)) + interpolated_forces.index_add_(0, indices, torch.stack(\ + [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) return interpolated_forces def track(self, incoming: ParticleBeam) -> ParticleBeam: """ - Track particles through the element. The input must be a `ParticleBeam`. + Tracks particles through the element. The input must be a `ParticleBeam`. :param incoming: Beam of particles entering the element. - :return: Beam of particles exiting the element. + :returns: Beam of particles exiting the element. """ if incoming is Beam.empty or incoming.particles.shape[0] == 0: return incoming elif isinstance(incoming, ParticleBeam): - grid_dimensions = self._compute_grid_dimensions(incoming) + # Copy the array of coordinates to avoid modifying the incoming beam + outcoming_particles = torch.empty_like(incoming.particles) + outcoming_particles[...] = incoming.particles + outcoming = ParticleBeam( + outcoming_particles, + incoming.energy, + particle_charges=incoming.particle_charges, + device=incoming.particles.device, + dtype=incoming.particles.dtype, + ) + # Compute useful quantities + grid_dimensions = self._compute_grid_dimensions(outcoming) cell_size = 2*grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length / (c*self._betaref(incoming)) - particles = self._cheetah_to_moments(incoming) - forces = self._compute_forces(incoming, cell_size, grid_dimensions) + dt = self.length_effect / (c*self._betaref(outcoming)) + # Change coordinates to apply the space charge effect + self._cheetah_to_moments(outcoming) + particles = outcoming.particles + forces = self._compute_forces(outcoming, cell_size, grid_dimensions) particles[:,1] += forces[:,0]*dt particles[:,3] += forces[:,1]*dt particles[:,5] += forces[:,2]*dt - particles = self._moments_to_cheetah(particles, incoming) + self._moments_to_cheetah(outcoming) return ParticleBeam( - particles, + outcoming.particles, incoming.energy, - particle_charges=incoming.particle_charges, + particle_charges=outcoming.particle_charges, device=particles.device, dtype=particles.dtype, ) @@ -2496,7 +2536,7 @@ def split(self, resolution: torch.Tensor) -> list[Element]: def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: element_lengths = [ - element.length if hasattr(element, "length") and type(element) is not SpaceChargeKick else 0.0 + element.length if hasattr(element, "length") else 0.0 for element in self.elements ] element_ss = [0] + [ @@ -2536,7 +2576,7 @@ def plot_reference_particle_traces( splits = reference_segment.split(resolution=torch.tensor(resolution)) split_lengths = [ - split.length if hasattr(split, "length") and type(split) is not SpaceChargeKick else 0.0 for split in splits + split.length if hasattr(split, "length") else 0.0 for split in splits ] ss = [0] + [sum(split_lengths[: i + 1]) for i, _ in enumerate(split_lengths)] diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 300121cb..d547a73e 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -4,7 +4,11 @@ def test_cold_uniform_beam_expansion(): """ - Test that that a cold uniform beam doubles in size in both dimensions when travelling through a drift section with space_charge. (cf ImpactX test) + Tests that that a cold uniform beam doubles in size in both dimensions when + travelling through a drift section with space_charge. (cf ImpactX test: + https://impactx.readthedocs.io/en/latest/usage/examples/cfchannel/README.html#constant-focusing-channel-with-space-charge) + See Free Expansion of a Cold Uniform Bunch in + https://accelconf.web.cern.ch/hb2023/papers/thbp44.pdf. """ # Simulation parameters @@ -20,7 +24,7 @@ def test_cold_uniform_beam_expansion(): energy = energy, radius_x = R0, radius_y = R0, - radius_s = R0/gamma, # radius of the beam in s direction, in the lab frame + radius_s = R0/gamma, # radius of the beam in s direction, in the lab frame. sigma_xp = torch.tensor(1e-15), sigma_yp = torch.tensor(1e-15), sigma_p = torch.tensor(1e-15), @@ -32,7 +36,7 @@ def test_cold_uniform_beam_expansion(): sig_si = incoming.sigma_s # Compute section lenght - kappa = 1+(torch.sqrt(torch.tensor(2))/4)*torch.log(3+2*torch.sqrt(torch.tensor(2))) + kappa= 1+(torch.sqrt(torch.tensor(2))/4)*torch.log(3+2*torch.sqrt(torch.tensor(2))) Nb = total_charge/cheetah.elementary_charge L=beta*gamma*kappa*torch.sqrt(R0**3/(Nb*cheetah.electron_radius)) @@ -57,4 +61,38 @@ def test_cold_uniform_beam_expansion(): torch.set_printoptions(precision=16) assert torch.isclose(sig_xo,2*sig_xi,rtol=2e-2,atol=0.0) assert torch.isclose(sig_yo,2*sig_yi,rtol=2e-2,atol=0.0) - assert torch.isclose(sig_so,2*sig_si,rtol=2e-2,atol=0.0) \ No newline at end of file + assert torch.isclose(sig_so,2*sig_si,rtol=2e-2,atol=0.0) + +def test_incoming_beam_not_modified(): + """ + Tests that the incoming beam is not modified when calling the track method. + """ + + incoming_beam = cheetah.ParticleBeam.from_parameters( + num_particles=torch.tensor(10000), + sigma_xp=torch.tensor(2e-7), + sigma_yp=torch.tensor(2e-7), + ) + # Initial beam properties + incoming_particles0 = incoming_beam.particles + + L=torch.tensor(1.0) + segment_space_charge = cheetah.Segment( + elements=[ + cheetah.Drift(L/6), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/3), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/3), + cheetah.SpaceChargeKick(L/3), + cheetah.Drift(L/6) + ] + ) + # Calling the track method + outgoing_beam = segment_space_charge.track(incoming_beam) + + # Final beam properties + incoming_particles1 = incoming_beam.particles + + torch.set_printoptions(precision=16) + assert torch.allclose(incoming_particles0,incoming_particles1) \ No newline at end of file From ed861b87ee40a33510d32c9fff62eeb18de03222 Mon Sep 17 00:00:00 2001 From: greglenerd Date: Tue, 7 May 2024 16:57:10 -0700 Subject: [PATCH 040/111] start adapting to PR 116 --- cheetah/accelerator.py | 6 +++--- cheetah/particles.py | 2 +- tests/test_space_charge_kick.py | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 739b285c..b93b6647 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -394,9 +394,9 @@ def __init__( def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: - sigma_x = torch.std(beam.particles[:, 0]) - sigma_y = torch.std(beam.particles[:, 2]) - sigma_s = torch.std(beam.particles[:, 4]) + sigma_x = torch.std(beam.particles[..., 0]) + sigma_y = torch.std(beam.particles[..., 2]) + sigma_s = torch.std(beam.particles[..., 4]) return torch.tensor([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ , self.grid_extend_s*sigma_s]) diff --git a/cheetah/particles.py b/cheetah/particles.py index 2c891544..c86efdb0 100644 --- a/cheetah/particles.py +++ b/cheetah/particles.py @@ -1211,7 +1211,7 @@ def uniform_3d_ellispoid( ] if argument is not None ] - shape = not_nones[0].shape if len(not_nones) > 0 else torch.Size([1]) + shape = not_nones[0].size() if len(not_nones) > 0 else torch.Size([1]) if len(not_nones) > 1: assert all( argument.shape == shape for argument in not_nones diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index d547a73e..9a9eff21 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -13,9 +13,9 @@ def test_cold_uniform_beam_expansion(): # Simulation parameters num_particles = 10000 - total_charge=torch.tensor(1e-9) - R0 = torch.tensor(0.001) - energy=torch.tensor(2.5e8) + total_charge=torch.tensor([1e-9]) + R0 = torch.tensor([0.001]) + energy=torch.tensor([2.5e8]) gamma = energy/cheetah.rest_energy beta = torch.sqrt(1-1/gamma**2) incoming = cheetah.ParticleBeam.uniform_3d_ellispoid( @@ -25,9 +25,9 @@ def test_cold_uniform_beam_expansion(): radius_x = R0, radius_y = R0, radius_s = R0/gamma, # radius of the beam in s direction, in the lab frame. - sigma_xp = torch.tensor(1e-15), - sigma_yp = torch.tensor(1e-15), - sigma_p = torch.tensor(1e-15), + sigma_xp = torch.tensor([1e-15]), + sigma_yp = torch.tensor([1e-15]), + sigma_p = torch.tensor([1e-15]), ) # Initial beam properties @@ -69,9 +69,9 @@ def test_incoming_beam_not_modified(): """ incoming_beam = cheetah.ParticleBeam.from_parameters( - num_particles=torch.tensor(10000), - sigma_xp=torch.tensor(2e-7), - sigma_yp=torch.tensor(2e-7), + num_particles=torch.tensor([10000]), + sigma_xp=torch.tensor([2e-7]), + sigma_yp=torch.tensor([2e-7]), ) # Initial beam properties incoming_particles0 = incoming_beam.particles From 1ea0d51438ff696f9045b79a94cf60749d5d4e32 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Wed, 8 May 2024 13:24:34 -0700 Subject: [PATCH 041/111] Fix bugs associated with array shapes --- cheetah/accelerator.py | 141 +++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index b93b6647..0e3bdbda 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -337,23 +337,23 @@ def __repr__(self) -> str: class SpaceChargeKick(Element): """ - Applies the effect of space charge over a length `length`, on the **momentum** - (i.e. divergence and energy spread) of the beam. + Applies the effect of space charge over a length `length`, on the **momentum** + (i.e. divergence and energy spread) of the beam. The positions are unmodified ; this is meant to be combined with another lattice element (e.g. `Drift`) that does modify the positions, but does not take into account space charge. - This uses the integrated Green function method - (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) to compute - the effect of space charge. This is similar to the method used in Ocelot. - The main difference is that it solves the Poisson equation in the beam frame, - while here we solve a modified Poisson equation in the laboratory frame + This uses the integrated Green function method + (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) to compute + the effect of space charge. This is similar to the method used in Ocelot. + The main difference is that it solves the Poisson equation in the beam frame, + while here we solve a modified Poisson equation in the laboratory frame (https://pubs.aip.org/aip/pop/article-abstract/15/5/056701/1016636/Simulation-of-beams-or-plasmas-crossing-at). The two methods are in principle equivalent. Overview of the method: - Compute the beam charge density on a grid - Convolve the charge density with a Green function (the integrated green function) - to find the potential `phi` on the grid. The convolution uses the Hockney method + to find the potential `phi` on the grid. The convolution uses the Hockney method for open boundaries (allocate 2x larger arrays and perform convolution using FFTs) - Compute the corresponding electromagnetic fields and Lorentz force on the grid - Interpolate the Lorentz force to the particles and update their momentum @@ -387,11 +387,11 @@ def __init__( self.length = torch.as_tensor(length, **self.factory_kwargs) self.grid_shape = (int(num_grid_points_x), int(num_grid_points_y), \ int(num_grid_points_s)) - self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) + self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) # in multiples of sigma self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) - + def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_x = torch.std(beam.particles[..., 0]) @@ -399,16 +399,16 @@ def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: sigma_s = torch.std(beam.particles[..., 4]) return torch.tensor([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ , self.grid_extend_s*sigma_s]) - + def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: return beam.energy / rest_energy - + def _betaref(self,beam: ParticleBeam) -> torch.Tensor: gamma = self._gammaref(beam) if gamma == 0: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - + def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions)\ -> torch.Tensor: """ @@ -416,44 +416,48 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ - grid_shape = self.grid_shape - charge = torch.zeros(grid_shape) - - # Get particle positions and charges - particle_pos = beam.particles[..., [0, 2, 4]] - particle_charge = beam.particle_charges - normalized_pos = (particle_pos + grid_dimensions) / cell_size - - # Find the indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_pos).type(torch.long) - - # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ - , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(1) + offsets - # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) - # Shape: (n_particles, 8, 3) - cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) - - # Add the charge contributions to the cells - idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T - # Shape: (3, n_particles*8) - # Check that particles are inside the grid - valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ - (idx_y >= 0) & (idx_y < grid_shape[1]) & \ - (idx_s >= 0) & (idx_s < grid_shape[2]) - - # Accumulate the charge contributions - indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask],idx_s[valid_mask]]\ - , dim=0) # Shape: (3, n_valid) n_valid =8*n_particles - repeated_charges = particle_charge.repeat_interleave(8) # Shape:(8*n_particles) - values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge.index_put_(tuple(indices), values, accumulate=True) + n_batch = beam.particles.shape[0] + charge = torch.zeros( (n_batch,) + self.grid_shape ) + + # Loop over batch dimension + for i_batch in range(n_batch): + # Get particle positions and charges + particle_pos = beam.particles[i_batch, :, [0, 2, 4]] + particle_charge = beam.particle_charges[i_batch] + normalized_pos = (particle_pos + grid_dimensions) / cell_size + + # Find the indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_pos).type(torch.long) + + # Calculate the weights for all surrounding cells + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ + , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices = cell_indices.unsqueeze(-2) + offsets + # Shape: (n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos.unsqueeze(-2) - surrounding_indices) + # Shape: (n_batch, n_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) + + # Add the charge contributions to the cells + idx_x = surrounding_indices[:,:,0].flatten() + idx_y = surrounding_indices[:,:,1].flatten() + idx_s = surrounding_indices[:,:,2].flatten() + # Shape: (8*n_particles,) + # Check that particles are inside the grid + valid_mask = (idx_x >= 0) & (idx_x < self.grid_shape[0]) & \ + (idx_y >= 0) & (idx_y < self.grid_shape[1]) & \ + (idx_s >= 0) & (idx_s < self.grid_shape[2]) + + # Accumulate the charge contributions + repeated_charges = particle_charge.repeat_interleave(8) # Shape:(8*n_particles,) + values = (cell_weights.view(-1) * repeated_charges)[valid_mask] + charge[i_batch].index_put_( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), values, accumulate=True) + + # End of loop over batch inv_cell_volume = 1 / (cell_size[0] * cell_size[1] * cell_size[2]) return charge * inv_cell_volume # Normalize by the cell volume - + def _integrated_potential(self, x, y, s) -> torch.Tensor: """ @@ -469,7 +473,7 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: + x * s * torch.asinh(y / torch.sqrt(x**2 + s**2)) + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2))) return G - + def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tensor: """ @@ -486,8 +490,8 @@ def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tens # Copy the original charge_density values to the beginning of the new tensor new_charge_density[..., :charge_density.shape[0], :charge_density.shape[1],\ :charge_density.shape[2]] = charge_density - return new_charge_density - + return new_charge_density + def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: """ Computes the Integrated Green Function (IGF) with periodic boundary conditions @@ -496,7 +500,7 @@ def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: gamma = self._gammaref(beam) dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma #scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape - + # Create coordinate grids x = torch.arange(num_grid_points_x, **self.factory_kwargs) * dx y = torch.arange(num_grid_points_y, **self.factory_kwargs) * dy @@ -546,7 +550,7 @@ def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions return green_func - + def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions)\ -> torch.Tensor: #works only for ParticleBeam at this stage @@ -579,12 +583,12 @@ def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ else torch.tensor(0.0) ) potential = self._solve_poisson_equation(beam, cell_size, grid_dimensions) - + grad_x = torch.zeros_like(potential) grad_y = torch.zeros_like(potential) grad_s = torch.zeros_like(potential) - # Compute the gradients of the potential, using central differences, with 0 + # Compute the gradients of the potential, using central differences, with 0 #boundary conditions. grad_x[1:-1, :, :] = ( potential[2:, :, :] - potential[:-2, :, :] )\ * (0.5 * inv_cell_size[0]) @@ -602,7 +606,7 @@ def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: """ - Converts the Cheetah particle beam parameters to the moments in SI units used + Converts the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. """ N = beam.particles.shape[0] @@ -638,12 +642,12 @@ def _moments_to_cheetah(self, beam: ParticleBeam) \ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ -> torch.Tensor: """ - Interpolates the space charge force from the grid onto the macroparticles. + Interpolates the space charge force from the grid onto the macroparticles. Reciprocal function of _deposit_charge_on_grid. """ grad_x, grad_y, grad_z = self._E_plus_vB_field(beam,cell_size, grid_dimensions) grid_shape = self.grid_shape - particle_pos = beam.particles[:, [0, 2, 4]] + particle_pos = beam.particles[:, [0, 2, 4]] normalized_pos = (particle_pos + grid_dimensions) / cell_size # Find the indices of the lower corners of the cells containing the particles @@ -654,7 +658,7 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ [1, 0, 1], [1, 1, 0], [1, 1, 1]]) surrounding_indices =cell_indices.unsqueeze(1)+offsets #Shape:(n_particles,8,3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) + weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) @@ -663,7 +667,7 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ (idx_y >= 0) & (idx_y < grid_shape[1]) & \ (idx_s >= 0) & (idx_s < grid_shape[2]) - + valid_indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask],\ idx_s[valid_mask]], dim=0) @@ -677,7 +681,7 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ values_x = valid_cell_weights * Fx_values values_y = valid_cell_weights * Fy_values values_z = valid_cell_weights * Fz_values - + indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8)[valid_mask] interpolated_forces.index_add_(0, indices, torch.stack([values_x,\ torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) @@ -687,7 +691,7 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) return interpolated_forces - + def track(self, incoming: ParticleBeam) -> ParticleBeam: """ @@ -712,6 +716,9 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: grid_dimensions = self._compute_grid_dimensions(outcoming) cell_size = 2*grid_dimensions / torch.tensor(self.grid_shape) dt = self.length_effect / (c*self._betaref(outcoming)) + # Flatten the batch dimensions (to simplify later calculation, is undone at the end of `track`) + n_particles = outcoming.particles.shape[-2] + outcoming.particles.reshape( (-1, n_particles, 7) ) # Change coordinates to apply the space charge effect self._cheetah_to_moments(outcoming) particles = outcoming.particles @@ -720,13 +727,9 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: particles[:,3] += forces[:,1]*dt particles[:,5] += forces[:,2]*dt self._moments_to_cheetah(outcoming) - return ParticleBeam( - outcoming.particles, - incoming.energy, - particle_charges=outcoming.particle_charges, - device=particles.device, - dtype=particles.dtype, - ) + # Unflatten the batch dimensions + outcoming.particles.reshape( incoming.particles.shape ) + return outcoming else: raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") From d4564184fdcf73ab1f104ccfb672b790c9100784 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Wed, 8 May 2024 21:27:53 -0700 Subject: [PATCH 042/111] Fix initialization of the Green function --- cheetah/accelerator.py | 89 ++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 0e3bdbda..8d1553d8 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -394,11 +394,11 @@ def __init__( def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: - sigma_x = torch.std(beam.particles[..., 0]) - sigma_y = torch.std(beam.particles[..., 2]) - sigma_s = torch.std(beam.particles[..., 4]) - return torch.tensor([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ - , self.grid_extend_s*sigma_s]) + sigma_x = torch.std(beam.particles[:,:,0], dim=1) + sigma_y = torch.std(beam.particles[:,:,2], dim=1) + sigma_s = torch.std(beam.particles[:,:,4], dim=1) + return torch.stack([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ + , self.grid_extend_s*sigma_s], dim=-1) def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: return beam.energy / rest_energy @@ -416,11 +416,10 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ - n_batch = beam.particles.shape[0] - charge = torch.zeros( (n_batch,) + self.grid_shape ) + charge = torch.zeros( (self.n_batch,) + self.grid_shape, **self.factory_kwargs ) # Loop over batch dimension - for i_batch in range(n_batch): + for i_batch in range(self.n_batch): # Get particle positions and charges particle_pos = beam.particles[i_batch, :, [0, 2, 4]] particle_charge = beam.particle_charges[i_batch] @@ -435,7 +434,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions surrounding_indices = cell_indices.unsqueeze(-2) + offsets # Shape: (n_particles, 8, 3) weights = 1 - torch.abs(normalized_pos.unsqueeze(-2) - surrounding_indices) - # Shape: (n_batch, n_particles, 8, 3) + # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) # Add the charge contributions to the cells @@ -454,7 +453,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions charge[i_batch].index_put_( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), values, accumulate=True) # End of loop over batch - inv_cell_volume = 1 / (cell_size[0] * cell_size[1] * cell_size[2]) + inv_cell_volume = 1 / (cell_size[:,0] * cell_size[:,1] * cell_size[:,2]) return charge * inv_cell_volume # Normalize by the cell volume @@ -485,11 +484,11 @@ def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tens new_dims = tuple(dim * 2 for dim in grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros - new_charge_density = torch.zeros(new_dims, **self.factory_kwargs) + new_charge_density = torch.zeros( (self.n_batch,) + new_dims, **self.factory_kwargs) # Copy the original charge_density values to the beginning of the new tensor - new_charge_density[..., :charge_density.shape[0], :charge_density.shape[1],\ - :charge_density.shape[2]] = charge_density + new_charge_density[:, :charge_density.shape[1], :charge_density.shape[2],\ + :charge_density.shape[3]] = charge_density return new_charge_density def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: @@ -498,18 +497,21 @@ def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: (to perform Hockney's method). """ gamma = self._gammaref(beam) - dx, dy, ds = cell_size[0], cell_size[1], cell_size[2] * gamma #scaled by gamma + dx, dy, ds = cell_size[:,0], cell_size[:,1], cell_size[:,2] * gamma #scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids - x = torch.arange(num_grid_points_x, **self.factory_kwargs) * dx - y = torch.arange(num_grid_points_y, **self.factory_kwargs) * dy - s = torch.arange(num_grid_points_s, **self.factory_kwargs) * ds - x_grid, y_grid, s_grid = torch.meshgrid(x, y, s, indexing='ij') + x = torch.arange( num_grid_points_x, **self.factory_kwargs) + y = torch.arange( num_grid_points_y, **self.factory_kwargs) + s = torch.arange( num_grid_points_s, **self.factory_kwargs) + ix_grid, iy_grid, is_grid = torch.meshgrid(x, y, s, indexing='ij') + x_grid = ix_grid[None, :, :, :] * dx[:, None, None, None] # Shape: [n_batch, nx, ny, nz] + y_grid = iy_grid[None, :, :, :] * dy[:, None, None, None] # Shape: [n_batch, nx, ny, nz] + s_grid = is_grid[None, :, :, :] * ds[:, None, None, None] # Shape: [n_batch, nx, ny, nz] # Compute the Green's function values G_values = ( - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ + self._integrated_potential( x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ s_grid + 0.5 * ds) - self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy,\ s_grid + 0.5 * ds) @@ -528,26 +530,26 @@ def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: ) # Initialize the grid with double dimensions - green_func = torch.zeros(2 * num_grid_points_x, 2 * num_grid_points_y,\ + green_func = torch.zeros( self.n_batch, 2 * num_grid_points_x, 2 * num_grid_points_y,\ 2 * num_grid_points_s, **self.factory_kwargs) # Fill the grid with G_values and its periodic copies - green_func[:num_grid_points_x, :num_grid_points_y, :num_grid_points_s]\ + green_func[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s]\ = G_values - green_func[num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s]\ - = G_values[1:,:,:].flip(dims=[0]) #Reverse x, excluding the first element - green_func[:num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s]\ - = G_values[:, 1:,:].flip(dims=[1])#Reverse y, excluding the first element - green_func[:num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:]\ - = G_values[:, :, 1:].flip(dims=[2])#Reverse s,excluding the first element - green_func[num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s]\ - = G_values[1:, 1:,:].flip(dims=[0, 1]) # Reverse the x and y dimensions - green_func[:num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:]\ - = G_values[:, 1:, 1:].flip(dims=[1, 2]) # Reverse the y and s dimensions - green_func[num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:]\ - = G_values[1:, :, 1:].flip(dims=[0, 2]) # Reverse the x and s dimensions - green_func[num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:]\ - = G_values[1:, 1:, 1:].flip(dims=[0, 1, 2]) # Reverse all dimensions + green_func[:, num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s]\ + = G_values[:, 1:, :, :].flip(dims=[1]) #Reverse x, excluding the first element + green_func[:, :num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s]\ + = G_values[:, :, 1:, :].flip(dims=[2])#Reverse y, excluding the first element + green_func[:, :num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:]\ + = G_values[:, :, :, 1:].flip(dims=[3])#Reverse s,excluding the first element + green_func[:, num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s]\ + = G_values[:, 1:, 1:, :].flip(dims=[1, 2]) # Reverse the x and y dimensions + green_func[:, :num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:]\ + = G_values[:, :, 1:, 1:].flip(dims=[2, 3]) # Reverse the y and s dimensions + green_func[:, num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:]\ + = G_values[:, 1:, :, 1:].flip(dims=[1, 3]) # Reverse the x and s dimensions + green_func[:, num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:]\ + = G_values[:, 1:, 1:, 1:].flip(dims=[1, 2, 3]) # Reverse all dimensions return green_func @@ -558,15 +560,15 @@ def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions Solves the Poisson equation for the given charge density, using FFT convolution. """ charge_density = self._array_rho(beam, cell_size, grid_dimensions) - charge_density_ft = torch.fft.fftn(charge_density) + charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) integrated_green_function = self._IGF(beam, cell_size) - integrated_green_function_ft = torch.fft.fftn(integrated_green_function) + integrated_green_function_ft = torch.fft.fftn(integrated_green_function, dim=[1, 2, 3]) potential_ft = charge_density_ft * integrated_green_function_ft - potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft).real + potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft, dim=[1, 2, 3]).real # Return the physical potential - return potential[:charge_density.shape[0]//2, :charge_density.shape[1]//2,\ - :charge_density.shape[2]//2] + return potential[:, :charge_density.shape[1]//2, :charge_density.shape[2]//2,\ + :charge_density.shape[3]//2] def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ @@ -712,13 +714,14 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: device=incoming.particles.device, dtype=incoming.particles.dtype, ) + # Flatten the batch dimensions (to simplify later calculation, is undone at the end of `track`) + n_particles = outcoming.particles.shape[-2] + outcoming.particles.reshape( (-1, n_particles, 7) ) + self.n_batch = outcoming.particles.shape[0] # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(outcoming) cell_size = 2*grid_dimensions / torch.tensor(self.grid_shape) dt = self.length_effect / (c*self._betaref(outcoming)) - # Flatten the batch dimensions (to simplify later calculation, is undone at the end of `track`) - n_particles = outcoming.particles.shape[-2] - outcoming.particles.reshape( (-1, n_particles, 7) ) # Change coordinates to apply the space charge effect self._cheetah_to_moments(outcoming) particles = outcoming.particles From d8bbc5db137b3182162a2d292d6d8da8c349cb28 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Thu, 9 May 2024 05:53:15 -0700 Subject: [PATCH 043/111] Fix errors with shapes --- cheetah/accelerator.py | 121 +++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 8d1553d8..a1021928 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -423,7 +423,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions # Get particle positions and charges particle_pos = beam.particles[i_batch, :, [0, 2, 4]] particle_charge = beam.particle_charges[i_batch] - normalized_pos = (particle_pos + grid_dimensions) / cell_size + normalized_pos = (particle_pos[:, :] + grid_dimensions[i_batch, None, :]) / cell_size[i_batch, None, :] # Find the indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) @@ -431,9 +431,9 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions # Calculate the weights for all surrounding cells offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices.unsqueeze(-2) + offsets + surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos.unsqueeze(-2) - surrounding_indices) + weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) @@ -455,7 +455,7 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions # End of loop over batch inv_cell_volume = 1 / (cell_size[:,0] * cell_size[:,1] * cell_size[:,2]) - return charge * inv_cell_volume # Normalize by the cell volume + return charge * inv_cell_volume[:, None, None, None] # Normalize by the cell volume def _integrated_potential(self, x, y, s) -> torch.Tensor: @@ -592,17 +592,17 @@ def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ # Compute the gradients of the potential, using central differences, with 0 #boundary conditions. - grad_x[1:-1, :, :] = ( potential[2:, :, :] - potential[:-2, :, :] )\ - * (0.5 * inv_cell_size[0]) - grad_y[:, 1:-1, :] = ( potential[:, 2:, :] - potential[:, :-2, :] )\ - * (0.5 * inv_cell_size[1]) - grad_s[:, :, 1:-1] = ( potential[:, :, 2:] - potential[:, :, :-2] )\ - * (0.5 * inv_cell_size[2]) + grad_x[:, 1:-1, :, :] = ( potential[:, 2:, :, :] - potential[:, :-2, :, :] )\ + * (0.5 * inv_cell_size[:, 0, None, None, None]) + grad_y[:, :, 1:-1, :] = ( potential[:, :, 2:, :] - potential[:, :, :-2, :] )\ + * (0.5 * inv_cell_size[:, 1, None, None, None]) + grad_s[:, :, :, 1:-1] = ( potential[:, :, :, 2:] - potential[:, :, :, :-2] )\ + * (0.5 * inv_cell_size[:, 2, None, None, None]) # Scale the gradients with lorentz factor - grad_x = -igamma2*grad_x - grad_y = -igamma2*grad_y - grad_s = -igamma2*grad_s + grad_x = -igamma2[:, None, None, None]*grad_x + grad_y = -igamma2[:, None, None, None]*grad_y + grad_s = -igamma2[:, None, None, None]*grad_s return grad_x, grad_y, grad_s @@ -649,48 +649,53 @@ def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ """ grad_x, grad_y, grad_z = self._E_plus_vB_field(beam,cell_size, grid_dimensions) grid_shape = self.grid_shape - particle_pos = beam.particles[:, [0, 2, 4]] - normalized_pos = (particle_pos + grid_dimensions) / cell_size - - # Find the indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_pos).type(torch.long) - - # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0],\ - [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices =cell_indices.unsqueeze(1)+offsets #Shape:(n_particles,8,3) - - weights = 1 - torch.abs(normalized_pos.unsqueeze(1) - surrounding_indices) - # Shape: (n_particles, 8, 3) - cell_weights = weights.prod(dim=2) # Shape: (n_particles, 8) - - # Extract forces from the grids - idx_x,idx_y,idx_s = surrounding_indices.view(-1, 3).T #Shape: (3,n_particles*8) - valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ - (idx_y >= 0) & (idx_y < grid_shape[1]) & \ - (idx_s >= 0) & (idx_s < grid_shape[2]) - - valid_indices = torch.stack([idx_x[valid_mask], idx_y[valid_mask],\ - idx_s[valid_mask]], dim=0) - - Fx_values = grad_x[tuple(valid_indices)] - Fy_values = grad_y[tuple(valid_indices)] - Fz_values = grad_z[tuple(valid_indices)] - - # Compute interpolated forces - interpolated_forces = torch.zeros((particle_pos.shape[0], 3)) - valid_cell_weights = cell_weights.view(-1)[valid_mask]*elementary_charge - values_x = valid_cell_weights * Fx_values - values_y = valid_cell_weights * Fy_values - values_z = valid_cell_weights * Fz_values - - indices = torch.arange(particle_pos.shape[0]).repeat_interleave(8)[valid_mask] - interpolated_forces.index_add_(0, indices, torch.stack([values_x,\ - torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) - interpolated_forces.index_add_(0,indices,torch.stack\ - ([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)],dim=1)) - interpolated_forces.index_add_(0, indices, torch.stack(\ - [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) + n_particles = beam.particles.shape[1] + interpolated_forces = torch.zeros( (self.n_batch, n_particles, 3), **self.factory_kwargs ) + + # Loop over batch dimension + for i_batch in range(self.n_batch): + + # Get particle positions + particle_pos = beam.particles[i_batch, :, [0, 2, 4]] + normalized_pos = (particle_pos[:, :] + grid_dimensions[i_batch, None, :]) / cell_size[i_batch, None, :] + + # Find the indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_pos).type(torch.long) + + # Calculate the weights for all surrounding cells + offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0],\ + [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] # Shape:(n_particles,8,3) + # Shape: (n_particles, 8, 3) + weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) + # Shape: (n_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) + + # Extract forces from the grids + idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T #Shape: (3,n_particles*8) + valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ + (idx_y >= 0) & (idx_y < grid_shape[1]) & \ + (idx_s >= 0) & (idx_s < grid_shape[2]) + + valid_indices = ( idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask] ) + Fx_values = grad_x[ i_batch ][ valid_indices ] + Fy_values = grad_y[ i_batch ][ valid_indices ] + Fz_values = grad_z[ i_batch ][ valid_indices ] + + # Compute interpolated forces + valid_cell_weights = cell_weights.view(-1)[valid_mask]*elementary_charge + values_x = valid_cell_weights * Fx_values + values_y = valid_cell_weights * Fy_values + values_z = valid_cell_weights * Fz_values + + indices = torch.arange(n_particles).repeat_interleave(8)[valid_mask] + interpolated_F = interpolated_forces[i_batch] + interpolated_F.index_add_(0, indices, torch.stack([values_x,\ + torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) + interpolated_F.index_add_(0,indices,torch.stack\ + ([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)],dim=1)) + interpolated_F.index_add_(0, indices, torch.stack(\ + [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) return interpolated_forces @@ -726,9 +731,9 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: self._cheetah_to_moments(outcoming) particles = outcoming.particles forces = self._compute_forces(outcoming, cell_size, grid_dimensions) - particles[:,1] += forces[:,0]*dt - particles[:,3] += forces[:,1]*dt - particles[:,5] += forces[:,2]*dt + particles[:,:,1] += forces[:,:,0]*dt + particles[:,:,3] += forces[:,:,1]*dt + particles[:,:,5] += forces[:,:,2]*dt self._moments_to_cheetah(outcoming) # Unflatten the batch dimensions outcoming.particles.reshape( incoming.particles.shape ) From 9b2d4f1f924391f74c3654a895a465062ea3b674 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Thu, 9 May 2024 06:30:58 -0700 Subject: [PATCH 044/111] Fix remaining bugs --- cheetah/accelerator.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index a1021928..38508282 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -611,18 +611,17 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: Converts the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. """ - N = beam.particles.shape[0] moments = beam.particles gammaref = self._gammaref(beam) betaref = self._betaref(beam) p0 = gammaref*betaref*electron_mass*c - gamma = gammaref*(torch.ones(N)+beam.particles[:,5]*betaref) + gamma = gammaref[:, None] * ( torch.ones(moments.shape[:-1]) + beam.particles[:,:,5]*betaref[:, None] ) beta = torch.sqrt(1 - 1 / gamma**2) p = gamma*electron_mass*beta*c - moments[:,1] = p0*moments[:,1] - moments[:,3] = p0*moments[:,3] - moments[:,4] = -betaref*moments[:,4] - moments[:,5] = torch.sqrt(p**2 - moments[:,1]**2 - moments[:,3]**2) + moments[:,:,1] = p0[:, None] * moments[:,:,1] + moments[:,:,3] = p0[:, None] * moments[:,:,3] + moments[:,:,4] = -betaref[:, None] * moments[:,:,4] + moments[:,:,5] = torch.sqrt(p**2 - moments[:,:,1]**2 - moments[:,:,3]**2) def _moments_to_cheetah(self, beam: ParticleBeam) \ -> torch.Tensor: @@ -630,16 +629,15 @@ def _moments_to_cheetah(self, beam: ParticleBeam) \ Converts the moments in SI units to the Cheetah particle beam parameters. """ moments = beam.particles - N = moments.shape[0] gammaref = self._gammaref(beam) betaref = self._betaref(beam) p0 = gammaref*betaref*electron_mass*c - p = torch.sqrt(moments[:,1]**2 + moments[:,3]**2 + moments[:,5]**2) + p = torch.sqrt(moments[:,:,1]**2 + moments[:,:,3]**2 + moments[:,:,5]**2) gamma = torch.sqrt(1 + (p / (electron_mass*c))**2) - moments[:,1] = moments[:,1] / p0 - moments[:,3] = moments[:,3] / p0 - moments[:,4] = -moments[:,4] / betaref - moments[:,5] = (gamma-gammaref*torch.ones(N))/(betaref*gammaref) + moments[:,:,1] = moments[:,:,1] / p0[:, None] + moments[:,:,3] = moments[:,:,3] / p0[:, None] + moments[:,:,4] = -moments[:,:,4] / betaref[:, None] + moments[:,:,5] = (gamma-gammaref*torch.ones(gamma.shape))/((betaref*gammaref)[:, None]) def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ -> torch.Tensor: From 86c7d89eb0c432d73303bb4e4e03dd2f5f1a7503 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Thu, 9 May 2024 06:34:01 -0700 Subject: [PATCH 045/111] Fix test --- tests/test_space_charge_kick.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 9a9eff21..990f7944 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -4,10 +4,10 @@ def test_cold_uniform_beam_expansion(): """ - Tests that that a cold uniform beam doubles in size in both dimensions when - travelling through a drift section with space_charge. (cf ImpactX test: + Tests that that a cold uniform beam doubles in size in both dimensions when + travelling through a drift section with space_charge. (cf ImpactX test: https://impactx.readthedocs.io/en/latest/usage/examples/cfchannel/README.html#constant-focusing-channel-with-space-charge) - See Free Expansion of a Cold Uniform Bunch in + See Free Expansion of a Cold Uniform Bunch in https://accelconf.web.cern.ch/hb2023/papers/thbp44.pdf. """ @@ -51,13 +51,13 @@ def test_cold_uniform_beam_expansion(): cheetah.Drift(L/6) ] ) - outgoing_beam = segment_space_charge.track(incoming) + outgoing_beam = segment_space_charge.track(incoming) # Final beam properties sig_xo = outgoing_beam.sigma_x sig_yo = outgoing_beam.sigma_y sig_so = outgoing_beam.sigma_s - + torch.set_printoptions(precision=16) assert torch.isclose(sig_xo,2*sig_xi,rtol=2e-2,atol=0.0) assert torch.isclose(sig_yo,2*sig_yi,rtol=2e-2,atol=0.0) @@ -76,7 +76,7 @@ def test_incoming_beam_not_modified(): # Initial beam properties incoming_particles0 = incoming_beam.particles - L=torch.tensor(1.0) + L=torch.tensor([1.0]) segment_space_charge = cheetah.Segment( elements=[ cheetah.Drift(L/6), @@ -89,8 +89,8 @@ def test_incoming_beam_not_modified(): ] ) # Calling the track method - outgoing_beam = segment_space_charge.track(incoming_beam) - + outgoing_beam = segment_space_charge.track(incoming_beam) + # Final beam properties incoming_particles1 = incoming_beam.particles From 28febad3686b63220f20e590df58839b950f1fea Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Thu, 23 May 2024 05:25:44 -0700 Subject: [PATCH 046/111] Reformat code with `black` --- cheetah/accelerator.py | 479 ++++++++++++++++++++++++++--------------- 1 file changed, 310 insertions(+), 169 deletions(-) diff --git a/cheetah/accelerator.py b/cheetah/accelerator.py index 38508282..1557299a 100644 --- a/cheetah/accelerator.py +++ b/cheetah/accelerator.py @@ -20,11 +20,11 @@ from cheetah.track_methods import base_rmatrix, misalignment_matrix, rotation_matrix from cheetah.utils import UniqueNameGenerator -#Constants +# Constants c = torch.tensor(constants.speed_of_light) J_to_eV = torch.tensor(physical_constants["electron volt-joule relationship"][0]) generate_unique_name = UniqueNameGenerator(prefix="unnamed_element") -elementary_charge= torch.tensor(constants.elementary_charge) +elementary_charge = torch.tensor(constants.elementary_charge) rest_energy = torch.tensor( constants.electron_mass * constants.speed_of_light**2 @@ -34,12 +34,11 @@ electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) -electron_mass = torch.tensor( - physical_constants["electron mass"][0] -) +electron_mass = torch.tensor(physical_constants["electron mass"][0]) epsilon_0 = torch.tensor(constants.epsilon_0) + class Element(ABC, nn.Module): """ Base class for elements of particle accelerators. @@ -335,6 +334,7 @@ def defining_features(self) -> list[str]: def __repr__(self) -> str: return f"{self.__class__.__name__}(length={repr(self.length)})" + class SpaceChargeKick(Element): """ Applies the effect of space charge over a length `length`, on the **momentum** @@ -370,13 +370,13 @@ class SpaceChargeKick(Element): def __init__( self, length_effect: Union[torch.Tensor, nn.Parameter], - length: Union[torch.Tensor, nn.Parameter]=0.0, - num_grid_points_x: Union[torch.Tensor, nn.Parameter,int]=32, - num_grid_points_y: Union[torch.Tensor, nn.Parameter,int]=32, - num_grid_points_s: Union[torch.Tensor, nn.Parameter,int]=32, - grid_extend_x: Union[torch.Tensor, nn.Parameter]=3, - grid_extend_y: Union[torch.Tensor, nn.Parameter]=3, - grid_extend_s: Union[torch.Tensor, nn.Parameter]=3, + length: Union[torch.Tensor, nn.Parameter] = 0.0, + num_grid_points_x: Union[torch.Tensor, nn.Parameter, int] = 32, + num_grid_points_y: Union[torch.Tensor, nn.Parameter, int] = 32, + num_grid_points_s: Union[torch.Tensor, nn.Parameter, int] = 32, + grid_extend_x: Union[torch.Tensor, nn.Parameter] = 3, + grid_extend_y: Union[torch.Tensor, nn.Parameter] = 3, + grid_extend_s: Union[torch.Tensor, nn.Parameter] = 3, name: Optional[str] = None, device=None, dtype=torch.float32, @@ -385,52 +385,73 @@ def __init__( super().__init__(name=name) self.length_effect = torch.as_tensor(length_effect, **self.factory_kwargs) self.length = torch.as_tensor(length, **self.factory_kwargs) - self.grid_shape = (int(num_grid_points_x), int(num_grid_points_y), \ - int(num_grid_points_s)) + self.grid_shape = ( + int(num_grid_points_x), + int(num_grid_points_y), + int(num_grid_points_s), + ) self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) # in multiples of sigma self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) + def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: + sigma_x = torch.std(beam.particles[:, :, 0], dim=1) + sigma_y = torch.std(beam.particles[:, :, 2], dim=1) + sigma_s = torch.std(beam.particles[:, :, 4], dim=1) + return torch.stack( + [ + self.grid_extend_x * sigma_x, + self.grid_extend_y * sigma_y, + self.grid_extend_s * sigma_s, + ], + dim=-1, + ) - def _compute_grid_dimensions(self,beam: ParticleBeam) -> torch.Tensor: - sigma_x = torch.std(beam.particles[:,:,0], dim=1) - sigma_y = torch.std(beam.particles[:,:,2], dim=1) - sigma_s = torch.std(beam.particles[:,:,4], dim=1) - return torch.stack([self.grid_extend_x*sigma_x, self.grid_extend_y*sigma_y\ - , self.grid_extend_s*sigma_s], dim=-1) - - def _gammaref(self,beam: ParticleBeam) -> torch.Tensor: + def _gammaref(self, beam: ParticleBeam) -> torch.Tensor: return beam.energy / rest_energy - def _betaref(self,beam: ParticleBeam) -> torch.Tensor: + def _betaref(self, beam: ParticleBeam) -> torch.Tensor: gamma = self._gammaref(beam) if gamma == 0: return torch.tensor(1.0) return torch.sqrt(1 - 1 / gamma**2) - def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions)\ - -> torch.Tensor: + def _deposit_charge_on_grid( + self, beam: ParticleBeam, cell_size, grid_dimensions + ) -> torch.Tensor: """ Deposits the charge density of the beam onto a grid, using the nearest grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ - charge = torch.zeros( (self.n_batch,) + self.grid_shape, **self.factory_kwargs ) + charge = torch.zeros((self.n_batch,) + self.grid_shape, **self.factory_kwargs) # Loop over batch dimension for i_batch in range(self.n_batch): # Get particle positions and charges particle_pos = beam.particles[i_batch, :, [0, 2, 4]] particle_charge = beam.particle_charges[i_batch] - normalized_pos = (particle_pos[:, :] + grid_dimensions[i_batch, None, :]) / cell_size[i_batch, None, :] + normalized_pos = ( + particle_pos[:, :] + grid_dimensions[i_batch, None, :] + ) / cell_size[i_batch, None, :] # Find the indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0]\ - , [1, 0, 1], [1, 1, 0], [1, 1, 1]]) + offsets = torch.tensor( + [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 1, 1], + [1, 0, 0], + [1, 0, 1], + [1, 1, 0], + [1, 1, 1], + ] + ) surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] # Shape: (n_particles, 8, 3) weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) @@ -438,25 +459,37 @@ def _deposit_charge_on_grid(self, beam: ParticleBeam, cell_size, grid_dimensions cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) # Add the charge contributions to the cells - idx_x = surrounding_indices[:,:,0].flatten() - idx_y = surrounding_indices[:,:,1].flatten() - idx_s = surrounding_indices[:,:,2].flatten() + idx_x = surrounding_indices[:, :, 0].flatten() + idx_y = surrounding_indices[:, :, 1].flatten() + idx_s = surrounding_indices[:, :, 2].flatten() # Shape: (8*n_particles,) # Check that particles are inside the grid - valid_mask = (idx_x >= 0) & (idx_x < self.grid_shape[0]) & \ - (idx_y >= 0) & (idx_y < self.grid_shape[1]) & \ - (idx_s >= 0) & (idx_s < self.grid_shape[2]) + valid_mask = ( + (idx_x >= 0) + & (idx_x < self.grid_shape[0]) + & (idx_y >= 0) + & (idx_y < self.grid_shape[1]) + & (idx_s >= 0) + & (idx_s < self.grid_shape[2]) + ) # Accumulate the charge contributions - repeated_charges = particle_charge.repeat_interleave(8) # Shape:(8*n_particles,) + repeated_charges = particle_charge.repeat_interleave( + 8 + ) # Shape:(8*n_particles,) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge[i_batch].index_put_( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), values, accumulate=True) + charge[i_batch].index_put_( + (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), + values, + accumulate=True, + ) # End of loop over batch - inv_cell_volume = 1 / (cell_size[:,0] * cell_size[:,1] * cell_size[:,2]) - - return charge * inv_cell_volume[:, None, None, None] # Normalize by the cell volume + inv_cell_volume = 1 / (cell_size[:, 0] * cell_size[:, 1] * cell_size[:, 2]) + return ( + charge * inv_cell_volume[:, None, None, None] + ) # Normalize by the cell volume def _integrated_potential(self, x, y, s) -> torch.Tensor: """ @@ -465,16 +498,19 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: """ r = torch.sqrt(x**2 + y**2 + s**2) - G = (-0.5 * s**2 * torch.atan(x * y / (s * r)) - -0.5 * y**2 * torch.atan(x * s / (y * r)) - -0.5 * x**2 * torch.atan(y * s / (x * r)) + G = ( + -0.5 * s**2 * torch.atan(x * y / (s * r)) + - 0.5 * y**2 * torch.atan(x * s / (y * r)) + - 0.5 * x**2 * torch.atan(y * s / (x * r)) + y * s * torch.asinh(x / torch.sqrt(y**2 + s**2)) + x * s * torch.asinh(y / torch.sqrt(x**2 + s**2)) - + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2))) + + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2)) + ) return G - - def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tensor: + def _array_rho( + self, beam: ParticleBeam, cell_size, grid_dimensions + ) -> torch.Tensor: """ Allocates a 2x larger array in all dimensions (to perform Hockney's method), and copies the charge density in one of the "quadrants". @@ -484,11 +520,17 @@ def _array_rho(self,beam: ParticleBeam, cell_size, grid_dimensions) ->torch.Tens new_dims = tuple(dim * 2 for dim in grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros - new_charge_density = torch.zeros( (self.n_batch,) + new_dims, **self.factory_kwargs) + new_charge_density = torch.zeros( + (self.n_batch,) + new_dims, **self.factory_kwargs + ) # Copy the original charge_density values to the beginning of the new tensor - new_charge_density[:, :charge_density.shape[1], :charge_density.shape[2],\ - :charge_density.shape[3]] = charge_density + new_charge_density[ + :, + : charge_density.shape[1], + : charge_density.shape[2], + : charge_density.shape[3], + ] = charge_density return new_charge_density def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: @@ -497,93 +539,142 @@ def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: (to perform Hockney's method). """ gamma = self._gammaref(beam) - dx, dy, ds = cell_size[:,0], cell_size[:,1], cell_size[:,2] * gamma #scaled by gamma + dx, dy, ds = ( + cell_size[:, 0], + cell_size[:, 1], + cell_size[:, 2] * gamma, + ) # scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids - x = torch.arange( num_grid_points_x, **self.factory_kwargs) - y = torch.arange( num_grid_points_y, **self.factory_kwargs) - s = torch.arange( num_grid_points_s, **self.factory_kwargs) - ix_grid, iy_grid, is_grid = torch.meshgrid(x, y, s, indexing='ij') - x_grid = ix_grid[None, :, :, :] * dx[:, None, None, None] # Shape: [n_batch, nx, ny, nz] - y_grid = iy_grid[None, :, :, :] * dy[:, None, None, None] # Shape: [n_batch, nx, ny, nz] - s_grid = is_grid[None, :, :, :] * ds[:, None, None, None] # Shape: [n_batch, nx, ny, nz] + x = torch.arange(num_grid_points_x, **self.factory_kwargs) + y = torch.arange(num_grid_points_y, **self.factory_kwargs) + s = torch.arange(num_grid_points_s, **self.factory_kwargs) + ix_grid, iy_grid, is_grid = torch.meshgrid(x, y, s, indexing="ij") + x_grid = ( + ix_grid[None, :, :, :] * dx[:, None, None, None] + ) # Shape: [n_batch, nx, ny, nz] + y_grid = ( + iy_grid[None, :, :, :] * dy[:, None, None, None] + ) # Shape: [n_batch, nx, ny, nz] + s_grid = ( + is_grid[None, :, :, :] * ds[:, None, None, None] + ) # Shape: [n_batch, nx, ny, nz] # Compute the Green's function values G_values = ( - self._integrated_potential( x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ - s_grid + 0.5 * ds) - - self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy,\ - s_grid + 0.5 * ds) - - self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy,\ - s_grid + 0.5 * ds) - - self._integrated_potential(x_grid + 0.5 * dx, y_grid + 0.5 * dy,\ - s_grid - 0.5 * ds) - + self._integrated_potential(x_grid + 0.5 * dx, y_grid - 0.5 * dy,\ - s_grid - 0.5 * ds) - + self._integrated_potential(x_grid - 0.5 * dx, y_grid + 0.5 * dy,\ - s_grid - 0.5 * ds) - + self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy,\ - s_grid + 0.5 * ds) - - self._integrated_potential(x_grid - 0.5 * dx, y_grid - 0.5 * dy,\ - s_grid - 0.5 * ds) + self._integrated_potential( + x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds + ) + - self._integrated_potential( + x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds + ) + - self._integrated_potential( + x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds + ) + - self._integrated_potential( + x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds + ) + + self._integrated_potential( + x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds + ) + + self._integrated_potential( + x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds + ) + + self._integrated_potential( + x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds + ) + - self._integrated_potential( + x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds + ) ) # Initialize the grid with double dimensions - green_func = torch.zeros( self.n_batch, 2 * num_grid_points_x, 2 * num_grid_points_y,\ - 2 * num_grid_points_s, **self.factory_kwargs) + green_func = torch.zeros( + self.n_batch, + 2 * num_grid_points_x, + 2 * num_grid_points_y, + 2 * num_grid_points_s, + **self.factory_kwargs, + ) # Fill the grid with G_values and its periodic copies - green_func[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s]\ - = G_values - green_func[:, num_grid_points_x+1:, :num_grid_points_y, :num_grid_points_s]\ - = G_values[:, 1:, :, :].flip(dims=[1]) #Reverse x, excluding the first element - green_func[:, :num_grid_points_x, num_grid_points_y+1:, :num_grid_points_s]\ - = G_values[:, :, 1:, :].flip(dims=[2])#Reverse y, excluding the first element - green_func[:, :num_grid_points_x, :num_grid_points_y, num_grid_points_s+1:]\ - = G_values[:, :, :, 1:].flip(dims=[3])#Reverse s,excluding the first element - green_func[:, num_grid_points_x+1:, num_grid_points_y+1:, :num_grid_points_s]\ - = G_values[:, 1:, 1:, :].flip(dims=[1, 2]) # Reverse the x and y dimensions - green_func[:, :num_grid_points_x, num_grid_points_y+1:, num_grid_points_s+1:]\ - = G_values[:, :, 1:, 1:].flip(dims=[2, 3]) # Reverse the y and s dimensions - green_func[:, num_grid_points_x+1:, :num_grid_points_y, num_grid_points_s+1:]\ - = G_values[:, 1:, :, 1:].flip(dims=[1, 3]) # Reverse the x and s dimensions - green_func[:, num_grid_points_x+1:, num_grid_points_y+1:, num_grid_points_s+1:]\ - = G_values[:, 1:, 1:, 1:].flip(dims=[1, 2, 3]) # Reverse all dimensions + green_func[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = ( + G_values + ) + green_func[ + :, num_grid_points_x + 1 :, :num_grid_points_y, :num_grid_points_s + ] = G_values[:, 1:, :, :].flip( + dims=[1] + ) # Reverse x, excluding the first element + green_func[ + :, :num_grid_points_x, num_grid_points_y + 1 :, :num_grid_points_s + ] = G_values[:, :, 1:, :].flip( + dims=[2] + ) # Reverse y, excluding the first element + green_func[ + :, :num_grid_points_x, :num_grid_points_y, num_grid_points_s + 1 : + ] = G_values[:, :, :, 1:].flip( + dims=[3] + ) # Reverse s,excluding the first element + green_func[ + :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, :num_grid_points_s + ] = G_values[:, 1:, 1:, :].flip( + dims=[1, 2] + ) # Reverse the x and y dimensions + green_func[ + :, :num_grid_points_x, num_grid_points_y + 1 :, num_grid_points_s + 1 : + ] = G_values[:, :, 1:, 1:].flip( + dims=[2, 3] + ) # Reverse the y and s dimensions + green_func[ + :, num_grid_points_x + 1 :, :num_grid_points_y, num_grid_points_s + 1 : + ] = G_values[:, 1:, :, 1:].flip( + dims=[1, 3] + ) # Reverse the x and s dimensions + green_func[ + :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, num_grid_points_s + 1 : + ] = G_values[:, 1:, 1:, 1:].flip( + dims=[1, 2, 3] + ) # Reverse all dimensions return green_func - - def _solve_poisson_equation(self, beam: ParticleBeam, cell_size, grid_dimensions)\ - -> torch.Tensor: #works only for ParticleBeam at this stage + def _solve_poisson_equation( + self, beam: ParticleBeam, cell_size, grid_dimensions + ) -> torch.Tensor: # works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density, using FFT convolution. """ charge_density = self._array_rho(beam, cell_size, grid_dimensions) charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) integrated_green_function = self._IGF(beam, cell_size) - integrated_green_function_ft = torch.fft.fftn(integrated_green_function, dim=[1, 2, 3]) + integrated_green_function_ft = torch.fft.fftn( + integrated_green_function, dim=[1, 2, 3] + ) potential_ft = charge_density_ft * integrated_green_function_ft - potential = (1/(4*torch.pi*epsilon_0))*torch.fft.ifftn(potential_ft, dim=[1, 2, 3]).real + potential = (1 / (4 * torch.pi * epsilon_0)) * torch.fft.ifftn( + potential_ft, dim=[1, 2, 3] + ).real # Return the physical potential - return potential[:, :charge_density.shape[1]//2, :charge_density.shape[2]//2,\ - :charge_density.shape[3]//2] - + return potential[ + :, + : charge_density.shape[1] // 2, + : charge_density.shape[2] // 2, + : charge_density.shape[3] // 2, + ] - def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ - -> torch.Tensor: + def _E_plus_vB_field( + self, beam: ParticleBeam, cell_size, grid_dimensions + ) -> torch.Tensor: """ Computes the force field from the potential and the particle positions and speeds, as in https://doi.org/10.1063/1.2837054. """ inv_cell_size = 1 / cell_size gamma = self._gammaref(beam) - igamma2 = ( - 1 / gamma**2 - if gamma != 0 - else torch.tensor(0.0) - ) + igamma2 = 1 / gamma**2 if gamma != 0 else torch.tensor(0.0) potential = self._solve_poisson_equation(beam, cell_size, grid_dimensions) grad_x = torch.zeros_like(potential) @@ -591,18 +682,21 @@ def _E_plus_vB_field(self, beam: ParticleBeam, cell_size, grid_dimensions)\ grad_s = torch.zeros_like(potential) # Compute the gradients of the potential, using central differences, with 0 - #boundary conditions. - grad_x[:, 1:-1, :, :] = ( potential[:, 2:, :, :] - potential[:, :-2, :, :] )\ - * (0.5 * inv_cell_size[:, 0, None, None, None]) - grad_y[:, :, 1:-1, :] = ( potential[:, :, 2:, :] - potential[:, :, :-2, :] )\ - * (0.5 * inv_cell_size[:, 1, None, None, None]) - grad_s[:, :, :, 1:-1] = ( potential[:, :, :, 2:] - potential[:, :, :, :-2] )\ - * (0.5 * inv_cell_size[:, 2, None, None, None]) + # boundary conditions. + grad_x[:, 1:-1, :, :] = (potential[:, 2:, :, :] - potential[:, :-2, :, :]) * ( + 0.5 * inv_cell_size[:, 0, None, None, None] + ) + grad_y[:, :, 1:-1, :] = (potential[:, :, 2:, :] - potential[:, :, :-2, :]) * ( + 0.5 * inv_cell_size[:, 1, None, None, None] + ) + grad_s[:, :, :, 1:-1] = (potential[:, :, :, 2:] - potential[:, :, :, :-2]) * ( + 0.5 * inv_cell_size[:, 2, None, None, None] + ) # Scale the gradients with lorentz factor - grad_x = -igamma2[:, None, None, None]*grad_x - grad_y = -igamma2[:, None, None, None]*grad_y - grad_s = -igamma2[:, None, None, None]*grad_s + grad_x = -igamma2[:, None, None, None] * grad_x + grad_y = -igamma2[:, None, None, None] * grad_y + grad_s = -igamma2[:, None, None, None] * grad_s return grad_x, grad_y, grad_s @@ -614,90 +708,138 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: moments = beam.particles gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref*betaref*electron_mass*c - gamma = gammaref[:, None] * ( torch.ones(moments.shape[:-1]) + beam.particles[:,:,5]*betaref[:, None] ) + p0 = gammaref * betaref * electron_mass * c + gamma = gammaref[:, None] * ( + torch.ones(moments.shape[:-1]) + beam.particles[:, :, 5] * betaref[:, None] + ) beta = torch.sqrt(1 - 1 / gamma**2) - p = gamma*electron_mass*beta*c - moments[:,:,1] = p0[:, None] * moments[:,:,1] - moments[:,:,3] = p0[:, None] * moments[:,:,3] - moments[:,:,4] = -betaref[:, None] * moments[:,:,4] - moments[:,:,5] = torch.sqrt(p**2 - moments[:,:,1]**2 - moments[:,:,3]**2) - - def _moments_to_cheetah(self, beam: ParticleBeam) \ - -> torch.Tensor: + p = gamma * electron_mass * beta * c + moments[:, :, 1] = p0[:, None] * moments[:, :, 1] + moments[:, :, 3] = p0[:, None] * moments[:, :, 3] + moments[:, :, 4] = -betaref[:, None] * moments[:, :, 4] + moments[:, :, 5] = torch.sqrt( + p**2 - moments[:, :, 1] ** 2 - moments[:, :, 3] ** 2 + ) + + def _moments_to_cheetah(self, beam: ParticleBeam) -> torch.Tensor: """ Converts the moments in SI units to the Cheetah particle beam parameters. """ moments = beam.particles gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref*betaref*electron_mass*c - p = torch.sqrt(moments[:,:,1]**2 + moments[:,:,3]**2 + moments[:,:,5]**2) - gamma = torch.sqrt(1 + (p / (electron_mass*c))**2) - moments[:,:,1] = moments[:,:,1] / p0[:, None] - moments[:,:,3] = moments[:,:,3] / p0[:, None] - moments[:,:,4] = -moments[:,:,4] / betaref[:, None] - moments[:,:,5] = (gamma-gammaref*torch.ones(gamma.shape))/((betaref*gammaref)[:, None]) - - def _compute_forces(self, beam: ParticleBeam, cell_size, grid_dimensions)\ - -> torch.Tensor: + p0 = gammaref * betaref * electron_mass * c + p = torch.sqrt( + moments[:, :, 1] ** 2 + moments[:, :, 3] ** 2 + moments[:, :, 5] ** 2 + ) + gamma = torch.sqrt(1 + (p / (electron_mass * c)) ** 2) + moments[:, :, 1] = moments[:, :, 1] / p0[:, None] + moments[:, :, 3] = moments[:, :, 3] / p0[:, None] + moments[:, :, 4] = -moments[:, :, 4] / betaref[:, None] + moments[:, :, 5] = (gamma - gammaref * torch.ones(gamma.shape)) / ( + (betaref * gammaref)[:, None] + ) + + def _compute_forces( + self, beam: ParticleBeam, cell_size, grid_dimensions + ) -> torch.Tensor: """ Interpolates the space charge force from the grid onto the macroparticles. Reciprocal function of _deposit_charge_on_grid. """ - grad_x, grad_y, grad_z = self._E_plus_vB_field(beam,cell_size, grid_dimensions) + grad_x, grad_y, grad_z = self._E_plus_vB_field(beam, cell_size, grid_dimensions) grid_shape = self.grid_shape n_particles = beam.particles.shape[1] - interpolated_forces = torch.zeros( (self.n_batch, n_particles, 3), **self.factory_kwargs ) + interpolated_forces = torch.zeros( + (self.n_batch, n_particles, 3), **self.factory_kwargs + ) # Loop over batch dimension for i_batch in range(self.n_batch): # Get particle positions particle_pos = beam.particles[i_batch, :, [0, 2, 4]] - normalized_pos = (particle_pos[:, :] + grid_dimensions[i_batch, None, :]) / cell_size[i_batch, None, :] + normalized_pos = ( + particle_pos[:, :] + grid_dimensions[i_batch, None, :] + ) / cell_size[i_batch, None, :] # Find the indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells - offsets = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0],\ - [1, 0, 1], [1, 1, 0], [1, 1, 1]]) - surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] # Shape:(n_particles,8,3) + offsets = torch.tensor( + [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 1, 1], + [1, 0, 0], + [1, 0, 1], + [1, 1, 0], + [1, 1, 1], + ] + ) + surrounding_indices = ( + cell_indices[:, None, :] + offsets[None, :, :] + ) # Shape:(n_particles,8,3) # Shape: (n_particles, 8, 3) weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) # Extract forces from the grids - idx_x, idx_y, idx_s = surrounding_indices.view(-1, 3).T #Shape: (3,n_particles*8) - valid_mask = (idx_x >= 0) & (idx_x < grid_shape[0]) & \ - (idx_y >= 0) & (idx_y < grid_shape[1]) & \ - (idx_s >= 0) & (idx_s < grid_shape[2]) + idx_x, idx_y, idx_s = surrounding_indices.view( + -1, 3 + ).T # Shape: (3,n_particles*8) + valid_mask = ( + (idx_x >= 0) + & (idx_x < grid_shape[0]) + & (idx_y >= 0) + & (idx_y < grid_shape[1]) + & (idx_s >= 0) + & (idx_s < grid_shape[2]) + ) - valid_indices = ( idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask] ) - Fx_values = grad_x[ i_batch ][ valid_indices ] - Fy_values = grad_y[ i_batch ][ valid_indices ] - Fz_values = grad_z[ i_batch ][ valid_indices ] + valid_indices = (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]) + Fx_values = grad_x[i_batch][valid_indices] + Fy_values = grad_y[i_batch][valid_indices] + Fz_values = grad_z[i_batch][valid_indices] # Compute interpolated forces - valid_cell_weights = cell_weights.view(-1)[valid_mask]*elementary_charge + valid_cell_weights = cell_weights.view(-1)[valid_mask] * elementary_charge values_x = valid_cell_weights * Fx_values values_y = valid_cell_weights * Fy_values values_z = valid_cell_weights * Fz_values indices = torch.arange(n_particles).repeat_interleave(8)[valid_mask] interpolated_F = interpolated_forces[i_batch] - interpolated_F.index_add_(0, indices, torch.stack([values_x,\ - torch.zeros_like(values_x), torch.zeros_like(values_x)], dim=1)) - interpolated_F.index_add_(0,indices,torch.stack\ - ([torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)],dim=1)) - interpolated_F.index_add_(0, indices, torch.stack(\ - [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], dim=1)) + interpolated_F.index_add_( + 0, + indices, + torch.stack( + [values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], + dim=1, + ), + ) + interpolated_F.index_add_( + 0, + indices, + torch.stack( + [torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)], + dim=1, + ), + ) + interpolated_F.index_add_( + 0, + indices, + torch.stack( + [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], + dim=1, + ), + ) return interpolated_forces - def track(self, incoming: ParticleBeam) -> ParticleBeam: """ Tracks particles through the element. The input must be a `ParticleBeam`. @@ -719,33 +861,31 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: ) # Flatten the batch dimensions (to simplify later calculation, is undone at the end of `track`) n_particles = outcoming.particles.shape[-2] - outcoming.particles.reshape( (-1, n_particles, 7) ) + outcoming.particles.reshape((-1, n_particles, 7)) self.n_batch = outcoming.particles.shape[0] # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(outcoming) - cell_size = 2*grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length_effect / (c*self._betaref(outcoming)) + cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) + dt = self.length_effect / (c * self._betaref(outcoming)) # Change coordinates to apply the space charge effect self._cheetah_to_moments(outcoming) particles = outcoming.particles forces = self._compute_forces(outcoming, cell_size, grid_dimensions) - particles[:,:,1] += forces[:,:,0]*dt - particles[:,:,3] += forces[:,:,1]*dt - particles[:,:,5] += forces[:,:,2]*dt + particles[:, :, 1] += forces[:, :, 0] * dt + particles[:, :, 3] += forces[:, :, 1] * dt + particles[:, :, 5] += forces[:, :, 2] * dt self._moments_to_cheetah(outcoming) # Unflatten the batch dimensions - outcoming.particles.reshape( incoming.particles.shape ) + outcoming.particles.reshape(incoming.particles.shape) return outcoming else: raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") - def split(self, resolution: torch.Tensor) -> list[Element]: - # TODO: Implement splitting for SpaceCharge properly, for now just returns the - # element itself + # TODO: Implement splitting for SpaceCharge properly, for now just returns the + # element itself return [self] - @property def is_skippable(self) -> bool: return False @@ -2418,6 +2558,7 @@ def flattened(self) -> "Segment": flattened_elements.append(element) return Segment(elements=flattened_elements, name=self.name) + def transfer_maps_merged( self, incoming_beam: Beam, except_for: Optional[list[str]] = None ) -> "Segment": From 7b555b8d4ba58dd17e6ac11bd2dcde3b626c5546 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Fri, 31 May 2024 05:38:10 -0700 Subject: [PATCH 047/111] Fix CI --- cheetah/__init__.py | 1 + cheetah/accelerator/__init__.py | 1 + tests/test_space_charge_kick.py | 11 ++++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cheetah/__init__.py b/cheetah/__init__.py index 857533e3..37a36995 100644 --- a/cheetah/__init__.py +++ b/cheetah/__init__.py @@ -13,6 +13,7 @@ Screen, Segment, Solenoid, + SpaceChargeKick, Undulator, VerticalCorrector, ) diff --git a/cheetah/accelerator/__init__.py b/cheetah/accelerator/__init__.py index 515cc6bf..3aa65fb3 100644 --- a/cheetah/accelerator/__init__.py +++ b/cheetah/accelerator/__init__.py @@ -12,5 +12,6 @@ from .screen import Screen # noqa: F401 from .segment import Segment # noqa: F401 from .solenoid import Solenoid # noqa: F401 +from .space_charge_kick import SpaceChargeKick # noqa: F401 from .undulator import Undulator # noqa: F401 from .vertical_corrector import VerticalCorrector # noqa: F401 diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 990f7944..9af0f498 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -1,6 +1,8 @@ import pytest import torch import cheetah +from scipy import constants +from scipy.constants import physical_constants def test_cold_uniform_beam_expansion(): """ @@ -16,7 +18,10 @@ def test_cold_uniform_beam_expansion(): total_charge=torch.tensor([1e-9]) R0 = torch.tensor([0.001]) energy=torch.tensor([2.5e8]) - gamma = energy/cheetah.rest_energy + rest_energy = torch.tensor( constants.electron_mass * constants.speed_of_light**2 / constants.elementary_charge ) + elementary_charge = torch.tensor( constants.elementary_charge ) + electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) + gamma = energy/rest_energy beta = torch.sqrt(1-1/gamma**2) incoming = cheetah.ParticleBeam.uniform_3d_ellispoid( num_particles=torch.tensor(num_particles), @@ -37,8 +42,8 @@ def test_cold_uniform_beam_expansion(): # Compute section lenght kappa= 1+(torch.sqrt(torch.tensor(2))/4)*torch.log(3+2*torch.sqrt(torch.tensor(2))) - Nb = total_charge/cheetah.elementary_charge - L=beta*gamma*kappa*torch.sqrt(R0**3/(Nb*cheetah.electron_radius)) + Nb = total_charge/elementary_charge + L=beta*gamma*kappa*torch.sqrt(R0**3/(Nb*electron_radius)) segment_space_charge = cheetah.Segment( elements=[ From d2de1fcb09887c0574f92d25e42b9685d5e95139 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Fri, 31 May 2024 05:41:27 -0700 Subject: [PATCH 048/111] Fix CI --- cheetah/particles/particle_beam.py | 2 +- tests/test_space_charge_kick.py | 86 ++++++++++++++++-------------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 2d67414c..12fd97ce 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -268,7 +268,7 @@ def from_twiss( device=device, dtype=dtype, ) - + @classmethod def uniform_3d_ellispoid( cls, diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 9af0f498..c2fff30c 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -4,6 +4,7 @@ from scipy import constants from scipy.constants import physical_constants + def test_cold_uniform_beam_expansion(): """ Tests that that a cold uniform beam doubles in size in both dimensions when @@ -15,24 +16,28 @@ def test_cold_uniform_beam_expansion(): # Simulation parameters num_particles = 10000 - total_charge=torch.tensor([1e-9]) + total_charge = torch.tensor([1e-9]) R0 = torch.tensor([0.001]) - energy=torch.tensor([2.5e8]) - rest_energy = torch.tensor( constants.electron_mass * constants.speed_of_light**2 / constants.elementary_charge ) - elementary_charge = torch.tensor( constants.elementary_charge ) + energy = torch.tensor([2.5e8]) + rest_energy = torch.tensor( + constants.electron_mass + * constants.speed_of_light**2 + / constants.elementary_charge + ) + elementary_charge = torch.tensor(constants.elementary_charge) electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) - gamma = energy/rest_energy - beta = torch.sqrt(1-1/gamma**2) + gamma = energy / rest_energy + beta = torch.sqrt(1 - 1 / gamma**2) incoming = cheetah.ParticleBeam.uniform_3d_ellispoid( num_particles=torch.tensor(num_particles), total_charge=total_charge, - energy = energy, - radius_x = R0, - radius_y = R0, - radius_s = R0/gamma, # radius of the beam in s direction, in the lab frame. - sigma_xp = torch.tensor([1e-15]), - sigma_yp = torch.tensor([1e-15]), - sigma_p = torch.tensor([1e-15]), + energy=energy, + radius_x=R0, + radius_y=R0, + radius_s=R0 / gamma, # radius of the beam in s direction, in the lab frame. + sigma_xp=torch.tensor([1e-15]), + sigma_yp=torch.tensor([1e-15]), + sigma_p=torch.tensor([1e-15]), ) # Initial beam properties @@ -41,20 +46,22 @@ def test_cold_uniform_beam_expansion(): sig_si = incoming.sigma_s # Compute section lenght - kappa= 1+(torch.sqrt(torch.tensor(2))/4)*torch.log(3+2*torch.sqrt(torch.tensor(2))) - Nb = total_charge/elementary_charge - L=beta*gamma*kappa*torch.sqrt(R0**3/(Nb*electron_radius)) + kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( + 3 + 2 * torch.sqrt(torch.tensor(2)) + ) + Nb = total_charge / elementary_charge + L = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) segment_space_charge = cheetah.Segment( - elements=[ - cheetah.Drift(L/6), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/3), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/3), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/6) - ] + elements=[ + cheetah.Drift(L / 6), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 3), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 3), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 6), + ] ) outgoing_beam = segment_space_charge.track(incoming) @@ -64,9 +71,10 @@ def test_cold_uniform_beam_expansion(): sig_so = outgoing_beam.sigma_s torch.set_printoptions(precision=16) - assert torch.isclose(sig_xo,2*sig_xi,rtol=2e-2,atol=0.0) - assert torch.isclose(sig_yo,2*sig_yi,rtol=2e-2,atol=0.0) - assert torch.isclose(sig_so,2*sig_si,rtol=2e-2,atol=0.0) + assert torch.isclose(sig_xo, 2 * sig_xi, rtol=2e-2, atol=0.0) + assert torch.isclose(sig_yo, 2 * sig_yi, rtol=2e-2, atol=0.0) + assert torch.isclose(sig_so, 2 * sig_si, rtol=2e-2, atol=0.0) + def test_incoming_beam_not_modified(): """ @@ -81,17 +89,17 @@ def test_incoming_beam_not_modified(): # Initial beam properties incoming_particles0 = incoming_beam.particles - L=torch.tensor([1.0]) + L = torch.tensor([1.0]) segment_space_charge = cheetah.Segment( - elements=[ - cheetah.Drift(L/6), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/3), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/3), - cheetah.SpaceChargeKick(L/3), - cheetah.Drift(L/6) - ] + elements=[ + cheetah.Drift(L / 6), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 3), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 3), + cheetah.SpaceChargeKick(L / 3), + cheetah.Drift(L / 6), + ] ) # Calling the track method outgoing_beam = segment_space_charge.track(incoming_beam) @@ -100,4 +108,4 @@ def test_incoming_beam_not_modified(): incoming_particles1 = incoming_beam.particles torch.set_printoptions(precision=16) - assert torch.allclose(incoming_particles0,incoming_particles1) \ No newline at end of file + assert torch.allclose(incoming_particles0, incoming_particles1) From 0da8484cb9a90d1754091475df9d005148a0056f Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Fri, 31 May 2024 10:32:07 -0700 Subject: [PATCH 049/111] Apply isort corrections --- cheetah/accelerator/space_charge_kick.py | 5 +++-- tests/test_space_charge_kick.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index d0af6975..7a674222 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -1,12 +1,13 @@ from typing import Optional, Union -import torch -from torch import nn import matplotlib +import torch from scipy import constants from scipy.constants import physical_constants +from torch import nn from cheetah.particles import Beam, ParticleBeam + from .element import Element # Constants diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index c2fff30c..dfd15a70 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -1,9 +1,10 @@ import pytest import torch -import cheetah from scipy import constants from scipy.constants import physical_constants +import cheetah + def test_cold_uniform_beam_expansion(): """ From cf5119f7599bf58ca65049db0efa1faeef558895 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Fri, 31 May 2024 10:38:40 -0700 Subject: [PATCH 050/111] Apply flake8 corrections --- cheetah/accelerator/space_charge_kick.py | 7 ++++--- tests/test_space_charge_kick.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 7a674222..5e8506e1 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -129,7 +129,7 @@ def _deposit_charge_on_grid( particle_pos[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] - # Find the indices of the lower corners of the cells containing the particles + # Find indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells @@ -456,7 +456,7 @@ def _compute_forces( particle_pos[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] - # Find the indices of the lower corners of the cells containing the particles + # Find indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_pos).type(torch.long) # Calculate the weights for all surrounding cells @@ -552,7 +552,8 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: device=incoming.particles.device, dtype=incoming.particles.dtype, ) - # Flatten the batch dimensions (to simplify later calculation, is undone at the end of `track`) + # Flatten the batch dimensions + # (to simplify later calculation, is undone at the end of `track`) n_particles = outcoming.particles.shape[-2] outcoming.particles.reshape((-1, n_particles, 7)) self.n_batch = outcoming.particles.shape[0] diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index dfd15a70..58810fb2 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -103,7 +103,7 @@ def test_incoming_beam_not_modified(): ] ) # Calling the track method - outgoing_beam = segment_space_charge.track(incoming_beam) + segment_space_charge.track(incoming_beam) # Final beam properties incoming_particles1 = incoming_beam.particles From a607d8d2e63953a509b4ed225d8c579f3414647b Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Tue, 4 Jun 2024 17:58:29 +0200 Subject: [PATCH 051/111] Apply suggestions from code review Co-authored-by: Jan Kaiser --- cheetah/accelerator/space_charge_kick.py | 10 +- cheetah/particles/particle_beam.py | 136 +---------------------- 2 files changed, 6 insertions(+), 140 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 5e8506e1..36f32e89 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -111,7 +111,7 @@ def _betaref(self, beam: ParticleBeam) -> torch.Tensor: return torch.sqrt(1 - 1 / gamma**2) def _deposit_charge_on_grid( - self, beam: ParticleBeam, cell_size, grid_dimensions + self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ Deposits the charge density of the beam onto a grid, using the nearest @@ -202,7 +202,7 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: return G def _array_rho( - self, beam: ParticleBeam, cell_size, grid_dimensions + self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ Allocates a 2x larger array in all dimensions (to perform Hockney's method), @@ -226,7 +226,7 @@ def _array_rho( ] = charge_density return new_charge_density - def _IGF(self, beam: ParticleBeam, cell_size) -> torch.Tensor: + def _integrated_green_function(self, beam: ParticleBeam, cell_size: torch.Tensor) -> torch.Tensor: """ Computes the Integrated Green Function (IGF) with periodic boundary conditions (to perform Hockney's method). @@ -359,7 +359,7 @@ def _solve_poisson_equation( ] def _E_plus_vB_field( - self, beam: ParticleBeam, cell_size, grid_dimensions + self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ Computes the force field from the potential and the particle positions and @@ -434,7 +434,7 @@ def _moments_to_cheetah(self, beam: ParticleBeam) -> torch.Tensor: ) def _compute_forces( - self, beam: ParticleBeam, cell_size, grid_dimensions + self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ Interpolates the space charge force from the grid onto the macroparticles. diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 12fd97ce..b6e014fa 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -269,140 +269,6 @@ def from_twiss( dtype=dtype, ) - @classmethod - def uniform_3d_ellispoid( - cls, - num_particles: Optional[torch.Tensor] = None, - radius_x: Optional[torch.Tensor] = None, - radius_y: Optional[torch.Tensor] = None, - radius_s: Optional[torch.Tensor] = None, - sigma_xp: Optional[torch.Tensor] = None, - sigma_yp: Optional[torch.Tensor] = None, - sigma_p: Optional[torch.Tensor] = None, - energy: Optional[torch.Tensor] = None, - total_charge: Optional[torch.Tensor] = None, - device=None, - dtype=torch.float32, - ): - """Generate a particle beam with spatially uniformly distributed particles - inside an ellipsoid, i.e. waterbag distribution. - Note that - - the generated particles do not have correlation in the momentum - directions, by default a cold beam with no divergence is generated. - - for batched generation, parameters that are not None - must have the same shape. - :param num_particles: Number of particles to generate. - :param radius_x: Radius of the ellipsoid in x direction in meters. - :param radius_y: Radius of the ellipsoid in y direction in meters. - :param radius_s: Radius of the ellipsoid in s (longitudinal) direction - in meters. - :param sigma_xp: Sigma of the particle distribution in x' direction in rad, - default is 0. - :param sigma_yp: Sigma of the particle distribution in y' direction in rad, - default is 0. - :param sigma_p: Sigma of the particle distribution in p, dimensionless. - :param energy: Energy of the beam in eV. - :param total_charge: Total charge of the beam in C. - :param device: Device to move the beam's particle array to. - :param dtype: Data type of the generated particles. - :return: ParticleBeam with uniformly distributed particles inside an ellipsoid. - """ - - def generate_uniform_3d_ellispoid_particles( - num_particles: torch.Tensor, - radius_x: torch.Tensor, - radius_y: torch.Tensor, - radius_s: torch.Tensor, - ) -> torch.Tensor: - """Helper function to generate uniform 3D ellipsoid particles - in a non-batched manner - """ - particles = torch.zeros((1, num_particles, 7)) - particles[0, :, 6] = 1 - - num_generated = 0 - - while num_generated < num_particles: - Xs = (torch.rand(num_particles) - 0.5) * 2 * radius_x - Ys = (torch.rand(num_particles) - 0.5) * 2 * radius_y - Zs = (torch.rand(num_particles) - 0.5) * 2 * radius_s - - indices = ( - Xs**2 / radius_x**2 + Ys**2 / radius_y**2 + Zs**2 / radius_s**2 - ) <= 1 # Rejection sampling to get the points inside the ellipsoid. - - num_new_generated = Xs[indices].shape[0] - num_to_add = min(num_new_generated, int(num_particles - num_generated)) - - particles[0, num_generated : num_generated + num_to_add, 0] = Xs[ - indices - ][:num_to_add] - particles[0, num_generated : num_generated + num_to_add, 2] = Ys[ - indices - ][:num_to_add] - particles[0, num_generated : num_generated + num_to_add, 4] = Zs[ - indices - ][:num_to_add] - num_generated += num_to_add - - return particles - - # Figure out if arguments were passed, figure out their shape - not_nones = [ - argument - for argument in [ - radius_x, - radius_y, - radius_s, - sigma_xp, - sigma_yp, - sigma_p, - energy, - total_charge, - ] - if argument is not None - ] - shape = not_nones[0].shape if len(not_nones) > 0 else torch.Size([1]) - if len(not_nones) > 1: - assert all( - argument.shape == shape for argument in not_nones - ), "Arguments must have the same shape." - - # Set default values without function call in function signature - num_particles = ( - num_particles if num_particles is not None else torch.tensor(1_000_000) - ) - radius_x = radius_x if radius_x is not None else torch.full(shape, 1e-3) - radius_y = radius_y if radius_y is not None else torch.full(shape, 1e-3) - radius_s = radius_s if radius_s is not None else torch.full(shape, 1e-3) - - # Generate a uncorrelated Gaussian Beam - parray = cls.from_parameters( - num_particles=num_particles, - mu_xp=torch.full(shape, 0.0), - mu_yp=torch.full(shape, 0.0), - sigma_xp=sigma_xp, - sigma_yp=sigma_yp, - sigma_p=sigma_p, - energy=energy, - total_charge=total_charge, - device=device, - dtype=dtype, - ) - - # Replace the with uniformly distributed particles inside the ellipsoid - particles = parray.particles.view(-1, num_particles, 7) - for i, (r_x, r_y, r_s) in enumerate( - zip(radius_x.view(-1), radius_y.view(-1), radius_s.view(-1)) - ): - particles[i] = generate_uniform_3d_ellispoid_particles( - num_particles, r_x, r_y, r_s - )[0] - parray.particles = particles.view(*shape, num_particles, 7) - parray.particles.to(device=device, dtype=dtype) - - return parray - @classmethod def uniform_3d_ellipsoid( cls, @@ -461,7 +327,7 @@ def uniform_3d_ellipsoid( ] if argument is not None ] - shape = not_nones[0].size() if len(not_nones) > 0 else torch.Size([1]) + shape = not_nones[0].shape if len(not_nones) > 0 else torch.Size([1]) if len(not_nones) > 1: assert all( argument.shape == shape for argument in not_nones From 527e4165684300f1b32a147d6efef941ac28890d Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Tue, 4 Jun 2024 18:01:29 +0200 Subject: [PATCH 052/111] black formatting --- cheetah/accelerator/space_charge_kick.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 36f32e89..498c168f 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -226,7 +226,9 @@ def _array_rho( ] = charge_density return new_charge_density - def _integrated_green_function(self, beam: ParticleBeam, cell_size: torch.Tensor) -> torch.Tensor: + def _integrated_green_function( + self, beam: ParticleBeam, cell_size: torch.Tensor + ) -> torch.Tensor: """ Computes the Integrated Green Function (IGF) with periodic boundary conditions (to perform Hockney's method). From 0dc127f1258808d8a96f6f30ce5684022d6088fd Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Tue, 4 Jun 2024 18:10:37 +0200 Subject: [PATCH 053/111] Apply suggestions from code review --- cheetah/accelerator/space_charge_kick.py | 2 +- tests/test_space_charge_kick.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 498c168f..45f44f64 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -343,7 +343,7 @@ def _solve_poisson_equation( """ charge_density = self._array_rho(beam, cell_size, grid_dimensions) charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) - integrated_green_function = self._IGF(beam, cell_size) + integrated_green_function = self._integrated_green_function(beam, cell_size) integrated_green_function_ft = torch.fft.fftn( integrated_green_function, dim=[1, 2, 3] ) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 58810fb2..457e6d4d 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -29,7 +29,7 @@ def test_cold_uniform_beam_expansion(): electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) gamma = energy / rest_energy beta = torch.sqrt(1 - 1 / gamma**2) - incoming = cheetah.ParticleBeam.uniform_3d_ellispoid( + incoming = cheetah.ParticleBeam.uniform_3d_ellipsoid( num_particles=torch.tensor(num_particles), total_charge=total_charge, energy=energy, From 71f421cecb4dc0be1b384639060497e44b9069d6 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 13:55:22 -0700 Subject: [PATCH 054/111] Set the random seed in space charge test --- tests/test_space_charge_kick.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 457e6d4d..c4c8e2af 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -5,6 +5,11 @@ import cheetah +# For reproducibility, we set the seed for the random number generator. +# This is used when the particles positions are initialized randomly, +# Random fluctuations in the initial density can cause the tests to fail. +torch.manual_seed(0) + def test_cold_uniform_beam_expansion(): """ From 0b644056a7bc17b0074db403a31fcd4a3aed6ed2 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 14:06:53 -0700 Subject: [PATCH 055/111] Apply suggestions from code review --- cheetah/accelerator/space_charge_kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 45f44f64..807c4f8b 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -191,7 +191,7 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: """ r = torch.sqrt(x**2 + y**2 + s**2) - G = ( + integrated_potential = ( -0.5 * s**2 * torch.atan(x * y / (s * r)) - 0.5 * y**2 * torch.atan(x * s / (y * r)) - 0.5 * x**2 * torch.atan(y * s / (x * r)) From 42c390cfec2f96a74f4b76963038eee4048c3a7d Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 14:16:06 -0700 Subject: [PATCH 056/111] Update test file --- cheetah/accelerator/space_charge_kick.py | 2 +- tests/test_space_charge_kick.py | 56 ++++++++++-------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 807c4f8b..4de95397 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -199,7 +199,7 @@ def _integrated_potential(self, x, y, s) -> torch.Tensor: + x * s * torch.asinh(y / torch.sqrt(x**2 + s**2)) + x * y * torch.asinh(s / torch.sqrt(x**2 + y**2)) ) - return G + return integrated_potential def _array_rho( self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index c4c8e2af..a5e2fef7 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -46,40 +46,29 @@ def test_cold_uniform_beam_expansion(): sigma_p=torch.tensor([1e-15]), ) - # Initial beam properties - sig_xi = incoming.sigma_x - sig_yi = incoming.sigma_y - sig_si = incoming.sigma_s - # Compute section lenght kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( 3 + 2 * torch.sqrt(torch.tensor(2)) ) Nb = total_charge / elementary_charge - L = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) + section_length = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) segment_space_charge = cheetah.Segment( elements=[ - cheetah.Drift(L / 6), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 3), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 3), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 6), + cheetah.Drift(section_length / 6), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 6), ] ) outgoing_beam = segment_space_charge.track(incoming) - # Final beam properties - sig_xo = outgoing_beam.sigma_x - sig_yo = outgoing_beam.sigma_y - sig_so = outgoing_beam.sigma_s - - torch.set_printoptions(precision=16) - assert torch.isclose(sig_xo, 2 * sig_xi, rtol=2e-2, atol=0.0) - assert torch.isclose(sig_yo, 2 * sig_yi, rtol=2e-2, atol=0.0) - assert torch.isclose(sig_so, 2 * sig_si, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing_beam.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing_beam.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing_beam.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) def test_incoming_beam_not_modified(): @@ -93,25 +82,24 @@ def test_incoming_beam_not_modified(): sigma_yp=torch.tensor([2e-7]), ) # Initial beam properties - incoming_particles0 = incoming_beam.particles + incoming_beam_before = incoming_beam.particles - L = torch.tensor([1.0]) + section_length = torch.tensor([1.0]) segment_space_charge = cheetah.Segment( elements=[ - cheetah.Drift(L / 6), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 3), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 3), - cheetah.SpaceChargeKick(L / 3), - cheetah.Drift(L / 6), + cheetah.Drift(section_length / 6), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 6), ] ) # Calling the track method segment_space_charge.track(incoming_beam) # Final beam properties - incoming_particles1 = incoming_beam.particles + incoming_beam_after = incoming_beam.particles - torch.set_printoptions(precision=16) - assert torch.allclose(incoming_particles0, incoming_particles1) + assert torch.allclose(incoming_beam_before, incoming_beam_after) From 4a756dec613822737047d46edd0f828fb9eb7cb3 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 14:19:44 -0700 Subject: [PATCH 057/111] Add docstrings --- cheetah/accelerator/space_charge_kick.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 4de95397..e4df1370 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -89,6 +89,9 @@ def __init__( self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: + """ + Computes the dimensions of the grid on which to compute the space charge effect. + """ sigma_x = torch.std(beam.particles[:, :, 0], dim=1) sigma_y = torch.std(beam.particles[:, :, 2], dim=1) sigma_s = torch.std(beam.particles[:, :, 4], dim=1) @@ -102,9 +105,15 @@ def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: ) def _gammaref(self, beam: ParticleBeam) -> torch.Tensor: + """ + Returns the Lorentz factor of the reference particle of the beam. + """ return beam.energy / rest_energy def _betaref(self, beam: ParticleBeam) -> torch.Tensor: + """ + Returns beta (i.e., normalized velocity) for the reference particle of the beam. + """ gamma = self._gammaref(beam) if gamma == 0: return torch.tensor(1.0) @@ -184,7 +193,7 @@ def _deposit_charge_on_grid( charge * inv_cell_volume[:, None, None, None] ) # Normalize by the cell volume - def _integrated_potential(self, x, y, s) -> torch.Tensor: + def _integrated_potential(self, x: torch.Tensor, y: torch.Tensor, s: torch.Tensor) -> torch.Tensor: """ Computes the electrostatic potential using the Integrated Green Function method as in http://dx.doi.org/10.1103/PhysRevSTAB.9.044204. From e38111bfeedcaabd34530790585dffb7683d9d58 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 14:25:02 -0700 Subject: [PATCH 058/111] Change a few names --- cheetah/accelerator/space_charge_kick.py | 49 ++++++++++++------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index e4df1370..26292baa 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -294,7 +294,7 @@ def _integrated_green_function( ) # Initialize the grid with double dimensions - green_func = torch.zeros( + green_func_values = torch.zeros( self.n_batch, 2 * num_grid_points_x, 2 * num_grid_points_y, @@ -303,46 +303,46 @@ def _integrated_green_function( ) # Fill the grid with G_values and its periodic copies - green_func[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = ( + green_func_values[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = ( G_values ) - green_func[ + green_func_values[ :, num_grid_points_x + 1 :, :num_grid_points_y, :num_grid_points_s ] = G_values[:, 1:, :, :].flip( dims=[1] ) # Reverse x, excluding the first element - green_func[ + green_func_values[ :, :num_grid_points_x, num_grid_points_y + 1 :, :num_grid_points_s ] = G_values[:, :, 1:, :].flip( dims=[2] ) # Reverse y, excluding the first element - green_func[ + green_func_values[ :, :num_grid_points_x, :num_grid_points_y, num_grid_points_s + 1 : ] = G_values[:, :, :, 1:].flip( dims=[3] ) # Reverse s,excluding the first element - green_func[ + green_func_values[ :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, :num_grid_points_s ] = G_values[:, 1:, 1:, :].flip( dims=[1, 2] ) # Reverse the x and y dimensions - green_func[ + green_func_values[ :, :num_grid_points_x, num_grid_points_y + 1 :, num_grid_points_s + 1 : ] = G_values[:, :, 1:, 1:].flip( dims=[2, 3] ) # Reverse the y and s dimensions - green_func[ + green_func_values[ :, num_grid_points_x + 1 :, :num_grid_points_y, num_grid_points_s + 1 : ] = G_values[:, 1:, :, 1:].flip( dims=[1, 3] ) # Reverse the x and s dimensions - green_func[ + green_func_values[ :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, num_grid_points_s + 1 : ] = G_values[:, 1:, 1:, 1:].flip( dims=[1, 2, 3] ) # Reverse all dimensions - return green_func + return green_func_values def _solve_poisson_equation( self, beam: ParticleBeam, cell_size, grid_dimensions @@ -554,10 +554,10 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: return incoming elif isinstance(incoming, ParticleBeam): # Copy the array of coordinates to avoid modifying the incoming beam - outcoming_particles = torch.empty_like(incoming.particles) - outcoming_particles[...] = incoming.particles - outcoming = ParticleBeam( - outcoming_particles, + outgoing_particles = torch.empty_like(incoming.particles) + outgoing_particles[...] = incoming.particles + outgoing = ParticleBeam( + outgoing_particles, incoming.energy, particle_charges=incoming.particle_charges, device=incoming.particles.device, @@ -565,24 +565,23 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: ) # Flatten the batch dimensions # (to simplify later calculation, is undone at the end of `track`) - n_particles = outcoming.particles.shape[-2] - outcoming.particles.reshape((-1, n_particles, 7)) - self.n_batch = outcoming.particles.shape[0] + outgoing.particles.reshape((-1, outgoing.num_particles, 7)) + self.n_batch = outgoing.particles.shape[0] # Compute useful quantities - grid_dimensions = self._compute_grid_dimensions(outcoming) + grid_dimensions = self._compute_grid_dimensions(outgoing) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length_effect / (c * self._betaref(outcoming)) + dt = self.length_effect / (c * self._betaref(outgoing)) # Change coordinates to apply the space charge effect - self._cheetah_to_moments(outcoming) - particles = outcoming.particles - forces = self._compute_forces(outcoming, cell_size, grid_dimensions) + self._cheetah_to_moments(outgoing) + particles = outgoing.particles + forces = self._compute_forces(outgoing, cell_size, grid_dimensions) particles[:, :, 1] += forces[:, :, 0] * dt particles[:, :, 3] += forces[:, :, 1] * dt particles[:, :, 5] += forces[:, :, 2] * dt - self._moments_to_cheetah(outcoming) + self._moments_to_cheetah(outgoing) # Unflatten the batch dimensions - outcoming.particles.reshape(incoming.particles.shape) - return outcoming + outgoing.particles.reshape(incoming.particles.shape) + return outgoing else: raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") From ed7c924afb277910aea34a60db70966210c3abec Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Sun, 9 Jun 2024 16:22:34 -0700 Subject: [PATCH 059/111] Reformat files --- cheetah/accelerator/space_charge_kick.py | 10 ++++++---- tests/test_space_charge_kick.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 26292baa..c19ffcbe 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -193,7 +193,9 @@ def _deposit_charge_on_grid( charge * inv_cell_volume[:, None, None, None] ) # Normalize by the cell volume - def _integrated_potential(self, x: torch.Tensor, y: torch.Tensor, s: torch.Tensor) -> torch.Tensor: + def _integrated_potential( + self, x: torch.Tensor, y: torch.Tensor, s: torch.Tensor + ) -> torch.Tensor: """ Computes the electrostatic potential using the Integrated Green Function method as in http://dx.doi.org/10.1103/PhysRevSTAB.9.044204. @@ -303,9 +305,9 @@ def _integrated_green_function( ) # Fill the grid with G_values and its periodic copies - green_func_values[:, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s] = ( - G_values - ) + green_func_values[ + :, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s + ] = G_values green_func_values[ :, num_grid_points_x + 1 :, :num_grid_points_y, :num_grid_points_s ] = G_values[:, 1:, :, :].flip( diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index a5e2fef7..26487336 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -66,9 +66,15 @@ def test_cold_uniform_beam_expansion(): ) outgoing_beam = segment_space_charge.track(incoming) - assert torch.isclose(outgoing_beam.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing_beam.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing_beam.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) + assert torch.isclose( + outgoing_beam.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0 + ) + assert torch.isclose( + outgoing_beam.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0 + ) + assert torch.isclose( + outgoing_beam.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0 + ) def test_incoming_beam_not_modified(): From 665aabc3ec87ea3259d3ef33852b20ea68d06f0d Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 11 Jun 2024 10:59:22 -0700 Subject: [PATCH 060/111] Apply suggestions from code review --- cheetah/accelerator/space_charge_kick.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index c19ffcbe..75e17004 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -127,10 +127,11 @@ def _deposit_charge_on_grid( grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ - charge = torch.zeros((self.n_batch,) + self.grid_shape, **self.factory_kwargs) + charge = torch.zeros((self.batch_size,) + self.grid_shape, **self.factory_kwargs) # Loop over batch dimension - for i_batch in range(self.n_batch): + # Loop over samples in one batch (does vectorization) + for i_batch in range(self.batch_size): # Get particle positions and charges particle_pos = beam.particles[i_batch, :, [0, 2, 4]] particle_charge = beam.particle_charges[i_batch] @@ -259,7 +260,7 @@ def _integrated_green_function( ix_grid, iy_grid, is_grid = torch.meshgrid(x, y, s, indexing="ij") x_grid = ( ix_grid[None, :, :, :] * dx[:, None, None, None] - ) # Shape: [n_batch, nx, ny, nz] + ) # Shape: [batch_size, nx, ny, nz] y_grid = ( iy_grid[None, :, :, :] * dy[:, None, None, None] ) # Shape: [n_batch, nx, ny, nz] @@ -568,7 +569,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: # Flatten the batch dimensions # (to simplify later calculation, is undone at the end of `track`) outgoing.particles.reshape((-1, outgoing.num_particles, 7)) - self.n_batch = outgoing.particles.shape[0] + self.batch_size = outgoing.particles.shape[0] # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(outgoing) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) From bcd970384a5e0ef8bff1664eeca79d6b6985128b Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 11 Jun 2024 11:00:44 -0700 Subject: [PATCH 061/111] Update name: n_batch -> batch_size --- cheetah/accelerator/space_charge_kick.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 75e17004..aa6d24d9 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -226,7 +226,7 @@ def _array_rho( # Create a new tensor with the doubled dimensions, filled with zeros new_charge_density = torch.zeros( - (self.n_batch,) + new_dims, **self.factory_kwargs + (self.batch_size,) + new_dims, **self.factory_kwargs ) # Copy the original charge_density values to the beginning of the new tensor @@ -263,10 +263,10 @@ def _integrated_green_function( ) # Shape: [batch_size, nx, ny, nz] y_grid = ( iy_grid[None, :, :, :] * dy[:, None, None, None] - ) # Shape: [n_batch, nx, ny, nz] + ) # Shape: [batch_size, nx, ny, nz] s_grid = ( is_grid[None, :, :, :] * ds[:, None, None, None] - ) # Shape: [n_batch, nx, ny, nz] + ) # Shape: [batch_size, nx, ny, nz] # Compute the Green's function values G_values = ( @@ -298,7 +298,7 @@ def _integrated_green_function( # Initialize the grid with double dimensions green_func_values = torch.zeros( - self.n_batch, + self.batch_size, 2 * num_grid_points_x, 2 * num_grid_points_y, 2 * num_grid_points_s, @@ -458,11 +458,11 @@ def _compute_forces( grid_shape = self.grid_shape n_particles = beam.particles.shape[1] interpolated_forces = torch.zeros( - (self.n_batch, n_particles, 3), **self.factory_kwargs + (self.batch_size, n_particles, 3), **self.factory_kwargs ) # Loop over batch dimension - for i_batch in range(self.n_batch): + for i_batch in range(self.batch_size): # Get particle positions particle_pos = beam.particles[i_batch, :, [0, 2, 4]] From 9947d9775b9623548adedfa5e71d58d929755406 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 11 Jun 2024 12:50:22 -0700 Subject: [PATCH 062/111] Update formatting --- cheetah/accelerator/space_charge_kick.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index aa6d24d9..fc1023c7 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -127,7 +127,9 @@ def _deposit_charge_on_grid( grid point method and weighting by the distance to the grid points. Returns a grid of charge density in C/m^3. """ - charge = torch.zeros((self.batch_size,) + self.grid_shape, **self.factory_kwargs) + charge = torch.zeros( + (self.batch_size,) + self.grid_shape, **self.factory_kwargs + ) # Loop over batch dimension # Loop over samples in one batch (does vectorization) From 3db651b0d134dcb76ff24168e4e827a30544d787 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 16:42:33 +0200 Subject: [PATCH 063/111] Move random seed to individual test functions --- cheetah/accelerator/space_charge_kick.py | 2 +- tests/test_space_charge_kick.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index fc1023c7..7ea223db 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -17,7 +17,7 @@ rest_energy = torch.tensor( constants.electron_mass * constants.speed_of_light**2 - / constants.elementary_charge # electron mass + / constants.elementary_charge # Electron mass ) electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) electron_mass_eV = torch.tensor( diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 26487336..bd2ab25f 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -1,15 +1,9 @@ -import pytest import torch from scipy import constants from scipy.constants import physical_constants import cheetah -# For reproducibility, we set the seed for the random number generator. -# This is used when the particles positions are initialized randomly, -# Random fluctuations in the initial density can cause the tests to fail. -torch.manual_seed(0) - def test_cold_uniform_beam_expansion(): """ @@ -20,8 +14,11 @@ def test_cold_uniform_beam_expansion(): https://accelconf.web.cern.ch/hb2023/papers/thbp44.pdf. """ + # Random fluctuations in the initial density can cause the tests to fail + torch.manual_seed(0) + # Simulation parameters - num_particles = 10000 + num_particles = 10_000 total_charge = torch.tensor([1e-9]) R0 = torch.tensor([0.001]) energy = torch.tensor([2.5e8]) @@ -34,13 +31,14 @@ def test_cold_uniform_beam_expansion(): electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) gamma = energy / rest_energy beta = torch.sqrt(1 - 1 / gamma**2) + incoming = cheetah.ParticleBeam.uniform_3d_ellipsoid( num_particles=torch.tensor(num_particles), total_charge=total_charge, energy=energy, radius_x=R0, radius_y=R0, - radius_s=R0 / gamma, # radius of the beam in s direction, in the lab frame. + radius_s=R0 / gamma, # Radius of the beam in s direction, in the lab frame. sigma_xp=torch.tensor([1e-15]), sigma_yp=torch.tensor([1e-15]), sigma_p=torch.tensor([1e-15]), @@ -82,6 +80,9 @@ def test_incoming_beam_not_modified(): Tests that the incoming beam is not modified when calling the track method. """ + # Random fluctuations in the initial density can cause the tests to fail + torch.manual_seed(0) + incoming_beam = cheetah.ParticleBeam.from_parameters( num_particles=torch.tensor([10000]), sigma_xp=torch.tensor([2e-7]), From bbb3057e1c9ef15c61e00458c2935a26cba88bda Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 16:47:14 +0200 Subject: [PATCH 064/111] Add test for space charge gradient computation --- tests/test_space_charge_kick.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index bd2ab25f..0300437c 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -1,6 +1,7 @@ import torch from scipy import constants from scipy.constants import physical_constants +from torch import nn import cheetah @@ -110,3 +111,34 @@ def test_incoming_beam_not_modified(): incoming_beam_after = incoming_beam.particles assert torch.allclose(incoming_beam_before, incoming_beam_after) + + +def test_gradient(): + """ + Tests that the gradient of the track method is computed withouth throwing an error. + """ + incoming_beam = cheetah.ParticleBeam.from_parameters( + num_particles=torch.tensor([10_000]), + sigma_xp=torch.tensor([2e-7]), + sigma_yp=torch.tensor([2e-7]), + ) + + segment_length = nn.Parameter(torch.tensor([1.0])) + segment = cheetah.Segment( + elements=[ + cheetah.Drift(segment_length / 6), + cheetah.SpaceChargeKick(segment_length / 3), + cheetah.Drift(segment_length / 3), + cheetah.SpaceChargeKick(segment_length / 3), + cheetah.Drift(segment_length / 3), + cheetah.SpaceChargeKick(segment_length / 3), + cheetah.Drift(segment_length / 6), + ] + ) + + # Track the beam + outgoing_beam = segment.track(incoming_beam) + + # Compute and check the gradient + outgoing_beam.sigma_x.mean().backward() + assert isinstance(incoming_beam.sigma_x.grad, torch.Tensor) From f7dd7a2e9b466069146b2f944dd2e9e103c178ba Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 17:10:13 +0200 Subject: [PATCH 065/111] Minor formating changes --- cheetah/accelerator/space_charge_kick.py | 67 ++++++++++++++---------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 7ea223db..8245d75f 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -75,7 +75,9 @@ def __init__( dtype=torch.float32, ) -> None: self.factory_kwargs = {"device": device, "dtype": dtype} + super().__init__(name=name) + self.length_effect = torch.as_tensor(length_effect, **self.factory_kwargs) self.length = torch.as_tensor(length, **self.factory_kwargs) self.grid_shape = ( @@ -84,7 +86,7 @@ def __init__( int(num_grid_points_s), ) self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) - # in multiples of sigma + # In multiples of sigma self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) @@ -123,26 +125,26 @@ def _deposit_charge_on_grid( self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ - Deposits the charge density of the beam onto a grid, using the nearest - grid point method and weighting by the distance to the grid points. - Returns a grid of charge density in C/m^3. + Deposits the charge density of the beam onto a grid, using the nearest grid + point method and weighting by the distance to the grid points. Returns a grid of + charge density in C/m^3. """ charge = torch.zeros( (self.batch_size,) + self.grid_shape, **self.factory_kwargs ) - # Loop over batch dimension - # Loop over samples in one batch (does vectorization) + # Loop over vectorisation dimension, i.e. the samples in one batch (does + # vectorization) for i_batch in range(self.batch_size): # Get particle positions and charges - particle_pos = beam.particles[i_batch, :, [0, 2, 4]] + particle_positions = beam.particles[i_batch, :, [0, 2, 4]] particle_charge = beam.particle_charges[i_batch] - normalized_pos = ( - particle_pos[:, :] + grid_dimensions[i_batch, None, :] + normalized_positions = ( + particle_positions[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_pos).type(torch.long) + cell_indices = torch.floor(normalized_positions).type(torch.long) # Calculate the weights for all surrounding cells offsets = torch.tensor( @@ -159,7 +161,9 @@ def _deposit_charge_on_grid( ) surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) + weights = 1 - torch.abs( + normalized_positions[:, None, :] - surrounding_indices + ) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) @@ -167,7 +171,8 @@ def _deposit_charge_on_grid( idx_x = surrounding_indices[:, :, 0].flatten() idx_y = surrounding_indices[:, :, 1].flatten() idx_s = surrounding_indices[:, :, 2].flatten() - # Shape: (8*n_particles,) + # Shape: (8 * n_particles,) + # Check that particles are inside the grid valid_mask = ( (idx_x >= 0) @@ -181,7 +186,7 @@ def _deposit_charge_on_grid( # Accumulate the charge contributions repeated_charges = particle_charge.repeat_interleave( 8 - ) # Shape:(8*n_particles,) + ) # Shape:(8 * n_particles,) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] charge[i_batch].index_put_( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), @@ -219,8 +224,8 @@ def _array_rho( self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor ) -> torch.Tensor: """ - Allocates a 2x larger array in all dimensions (to perform Hockney's method), - and copies the charge density in one of the "quadrants". + Allocates a 2x larger array in all dimensions (to perform Hockney's method), and + copies the charge density in one of the "quadrants". """ grid_shape = self.grid_shape charge_density = self._deposit_charge_on_grid(beam, cell_size, grid_dimensions) @@ -238,6 +243,7 @@ def _array_rho( : charge_density.shape[2], : charge_density.shape[3], ] = charge_density + return new_charge_density def _integrated_green_function( @@ -325,7 +331,7 @@ def _integrated_green_function( :, :num_grid_points_x, :num_grid_points_y, num_grid_points_s + 1 : ] = G_values[:, :, :, 1:].flip( dims=[3] - ) # Reverse s,excluding the first element + ) # Reverse s, excluding the first element green_func_values[ :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, :num_grid_points_s ] = G_values[:, 1:, 1:, :].flip( @@ -351,7 +357,7 @@ def _integrated_green_function( def _solve_poisson_equation( self, beam: ParticleBeam, cell_size, grid_dimensions - ) -> torch.Tensor: # works only for ParticleBeam at this stage + ) -> torch.Tensor: # Works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density, using FFT convolution. """ @@ -391,7 +397,7 @@ def _E_plus_vB_field( grad_s = torch.zeros_like(potential) # Compute the gradients of the potential, using central differences, with 0 - # boundary conditions. + # boundary conditions grad_x[:, 1:-1, :, :] = (potential[:, 2:, :, :] - potential[:, :-2, :, :]) * ( 0.5 * inv_cell_size[:, 0, None, None, None] ) @@ -411,8 +417,8 @@ def _E_plus_vB_field( def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: """ - Converts the Cheetah particle beam parameters to the moments in SI units used - in the space charge solver. + Converts the Cheetah particle beam parameters to the moments in SI units used in + the space charge solver. """ moments = beam.particles gammaref = self._gammaref(beam) @@ -467,13 +473,13 @@ def _compute_forces( for i_batch in range(self.batch_size): # Get particle positions - particle_pos = beam.particles[i_batch, :, [0, 2, 4]] - normalized_pos = ( - particle_pos[:, :] + grid_dimensions[i_batch, None, :] + particle_positions = beam.particles[i_batch, :, [0, 2, 4]] + normalized_positions = ( + particle_positions[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_pos).type(torch.long) + cell_indices = torch.floor(normalized_positions).type(torch.long) # Calculate the weights for all surrounding cells offsets = torch.tensor( @@ -490,16 +496,18 @@ def _compute_forces( ) surrounding_indices = ( cell_indices[:, None, :] + offsets[None, :, :] - ) # Shape:(n_particles,8,3) + ) # Shape:(n_particles, 8, 3) # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs(normalized_pos[:, None, :] - surrounding_indices) + weights = 1 - torch.abs( + normalized_positions[:, None, :] - surrounding_indices + ) # Shape: (n_particles, 8, 3) cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) # Extract forces from the grids idx_x, idx_y, idx_s = surrounding_indices.view( -1, 3 - ).T # Shape: (3,n_particles*8) + ).T # Shape: (3, n_particles * 8) valid_mask = ( (idx_x >= 0) & (idx_x < grid_shape[0]) @@ -552,6 +560,7 @@ def _compute_forces( def track(self, incoming: ParticleBeam) -> ParticleBeam: """ Tracks particles through the element. The input must be a `ParticleBeam`. + :param incoming: Beam of particles entering the element. :returns: Beam of particles exiting the element. """ @@ -568,14 +577,17 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: device=incoming.particles.device, dtype=incoming.particles.dtype, ) + # Flatten the batch dimensions # (to simplify later calculation, is undone at the end of `track`) outgoing.particles.reshape((-1, outgoing.num_particles, 7)) self.batch_size = outgoing.particles.shape[0] + # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(outgoing) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) dt = self.length_effect / (c * self._betaref(outgoing)) + # Change coordinates to apply the space charge effect self._cheetah_to_moments(outgoing) particles = outgoing.particles @@ -584,6 +596,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: particles[:, :, 3] += forces[:, :, 1] * dt particles[:, :, 5] += forces[:, :, 2] * dt self._moments_to_cheetah(outgoing) + # Unflatten the batch dimensions outgoing.particles.reshape(incoming.particles.shape) return outgoing From fd0be7a99ea173adc1e19c4339f20e6e48c8e5f9 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 17:30:42 +0200 Subject: [PATCH 066/111] Replace obvious in-place operations with out-of-place alternatives --- cheetah/accelerator/space_charge_kick.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 8245d75f..23171bb0 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -188,7 +188,7 @@ def _deposit_charge_on_grid( 8 ) # Shape:(8 * n_particles,) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge[i_batch].index_put_( + charge[i_batch] = charge[i_batch].index_put( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), values, accumulate=True, @@ -530,7 +530,7 @@ def _compute_forces( indices = torch.arange(n_particles).repeat_interleave(8)[valid_mask] interpolated_F = interpolated_forces[i_batch] - interpolated_F.index_add_( + interpolated_F = interpolated_F.index_add( 0, indices, torch.stack( @@ -538,7 +538,7 @@ def _compute_forces( dim=1, ), ) - interpolated_F.index_add_( + interpolated_F = interpolated_F.index_add( 0, indices, torch.stack( @@ -546,7 +546,7 @@ def _compute_forces( dim=1, ), ) - interpolated_F.index_add_( + interpolated_F = interpolated_F.index_add( 0, indices, torch.stack( From 98cbaf5ef4e4dd1ad05ffde0b00434557e2a0851 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 21:43:21 +0200 Subject: [PATCH 067/111] Fix in-place gradient error --- cheetah/accelerator/space_charge_kick.py | 166 ++++++++++++++--------- tests/test_space_charge_kick.py | 21 +-- 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 23171bb0..d6cb03eb 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -11,7 +11,7 @@ from .element import Element # Constants -c = torch.tensor(constants.speed_of_light) +speed_of_light = torch.tensor(constants.speed_of_light) J_to_eV = torch.tensor(physical_constants["electron volt-joule relationship"][0]) elementary_charge = torch.tensor(constants.elementary_charge) rest_energy = torch.tensor( @@ -122,7 +122,11 @@ def _betaref(self, beam: ParticleBeam) -> torch.Tensor: return torch.sqrt(1 - 1 / gamma**2) def _deposit_charge_on_grid( - self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor + self, + beam: ParticleBeam, + moments: torch.Tensor, + cell_size: torch.Tensor, + grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ Deposits the charge density of the beam onto a grid, using the nearest grid @@ -137,8 +141,8 @@ def _deposit_charge_on_grid( # vectorization) for i_batch in range(self.batch_size): # Get particle positions and charges - particle_positions = beam.particles[i_batch, :, [0, 2, 4]] - particle_charge = beam.particle_charges[i_batch] + particle_positions = moments[i_batch, :, [0, 2, 4]] + particle_charges = beam.particle_charges[i_batch] normalized_positions = ( particle_positions[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] @@ -184,7 +188,7 @@ def _deposit_charge_on_grid( ) # Accumulate the charge contributions - repeated_charges = particle_charge.repeat_interleave( + repeated_charges = particle_charges.repeat_interleave( 8 ) # Shape:(8 * n_particles,) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] @@ -221,14 +225,20 @@ def _integrated_potential( return integrated_potential def _array_rho( - self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor + self, + beam: ParticleBeam, + moments: torch.Tensor, + cell_size: torch.Tensor, + grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ Allocates a 2x larger array in all dimensions (to perform Hockney's method), and copies the charge density in one of the "quadrants". """ grid_shape = self.grid_shape - charge_density = self._deposit_charge_on_grid(beam, cell_size, grid_dimensions) + charge_density = self._deposit_charge_on_grid( + beam, moments, cell_size, grid_dimensions + ) new_dims = tuple(dim * 2 for dim in grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros @@ -356,12 +366,12 @@ def _integrated_green_function( return green_func_values def _solve_poisson_equation( - self, beam: ParticleBeam, cell_size, grid_dimensions + self, beam: ParticleBeam, moments: torch.Tensor, cell_size, grid_dimensions ) -> torch.Tensor: # Works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density, using FFT convolution. """ - charge_density = self._array_rho(beam, cell_size, grid_dimensions) + charge_density = self._array_rho(beam, moments, cell_size, grid_dimensions) charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) integrated_green_function = self._integrated_green_function(beam, cell_size) integrated_green_function_ft = torch.fft.fftn( @@ -381,7 +391,11 @@ def _solve_poisson_equation( ] def _E_plus_vB_field( - self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor + self, + beam: ParticleBeam, + moments: torch.Tensor, + cell_size: torch.Tensor, + grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ Computes the force field from the potential and the particle positions and @@ -390,7 +404,9 @@ def _E_plus_vB_field( inv_cell_size = 1 / cell_size gamma = self._gammaref(beam) igamma2 = 1 / gamma**2 if gamma != 0 else torch.tensor(0.0) - potential = self._solve_poisson_equation(beam, cell_size, grid_dimensions) + potential = self._solve_poisson_equation( + beam, moments, cell_size, grid_dimensions + ) grad_x = torch.zeros_like(potential) grad_y = torch.zeros_like(potential) @@ -420,60 +436,79 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: Converts the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. """ - moments = beam.particles gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref * betaref * electron_mass * c - gamma = gammaref[:, None] * ( - torch.ones(moments.shape[:-1]) + beam.particles[:, :, 5] * betaref[:, None] + + p0 = gammaref * betaref * electron_mass * speed_of_light + gamma = gammaref.unsqueeze(-1) * ( + torch.ones(beam.particles.shape[:-1]) + + beam.particles[:, :, 5] * betaref.unsqueeze(-1) ) beta = torch.sqrt(1 - 1 / gamma**2) - p = gamma * electron_mass * beta * c - moments[:, :, 1] = p0[:, None] * moments[:, :, 1] - moments[:, :, 3] = p0[:, None] * moments[:, :, 3] - moments[:, :, 4] = -betaref[:, None] * moments[:, :, 4] - moments[:, :, 5] = torch.sqrt( - p**2 - moments[:, :, 1] ** 2 - moments[:, :, 3] ** 2 - ) + p = gamma * electron_mass * beta * speed_of_light + + moments_xp = beam.particles[:, :, 1] * p0.unsqueeze(-1) + moments_yp = beam.particles[:, :, 3] * p0.unsqueeze(-1) + moments_s = beam.particles[:, :, 4] * -betaref.unsqueeze(-1) + moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) + + moments = beam.particles.clone() + moments[:, :, 1] = moments_xp + moments[:, :, 3] = moments_yp + moments[:, :, 4] = moments_s + moments[:, :, 5] = moments_p - def _moments_to_cheetah(self, beam: ParticleBeam) -> torch.Tensor: + return moments + + def _moments_to_cheetah( + self, moments: torch.Tensor, beam: ParticleBeam + ) -> torch.Tensor: """ Converts the moments in SI units to the Cheetah particle beam parameters. """ - moments = beam.particles + particles = moments.clone() + gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref * betaref * electron_mass * c + p0 = gammaref * betaref * electron_mass * speed_of_light p = torch.sqrt( moments[:, :, 1] ** 2 + moments[:, :, 3] ** 2 + moments[:, :, 5] ** 2 ) - gamma = torch.sqrt(1 + (p / (electron_mass * c)) ** 2) - moments[:, :, 1] = moments[:, :, 1] / p0[:, None] - moments[:, :, 3] = moments[:, :, 3] / p0[:, None] - moments[:, :, 4] = -moments[:, :, 4] / betaref[:, None] - moments[:, :, 5] = (gamma - gammaref * torch.ones(gamma.shape)) / ( - (betaref * gammaref)[:, None] + gamma = torch.sqrt(1 + (p / (electron_mass * speed_of_light)) ** 2) + + particles[:, :, 1] = moments[:, :, 1] / p0.unsqueeze(-1) + particles[:, :, 3] = moments[:, :, 3] / p0.unsqueeze(-1) + particles[:, :, 4] = -moments[:, :, 4] / betaref.unsqueeze(-1) + particles[:, :, 5] = (gamma - gammaref * torch.ones(gamma.shape)) / ( + (betaref * gammaref).unsqueeze(-1) ) + return particles + def _compute_forces( - self, beam: ParticleBeam, cell_size: torch.Tensor, grid_dimensions: torch.Tensor + self, + beam: ParticleBeam, + moments: torch.Tensor, + cell_size: torch.Tensor, + grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ Interpolates the space charge force from the grid onto the macroparticles. Reciprocal function of _deposit_charge_on_grid. """ - grad_x, grad_y, grad_z = self._E_plus_vB_field(beam, cell_size, grid_dimensions) + grad_x, grad_y, grad_z = self._E_plus_vB_field( + beam, moments, cell_size, grid_dimensions + ) grid_shape = self.grid_shape - n_particles = beam.particles.shape[1] interpolated_forces = torch.zeros( - (self.batch_size, n_particles, 3), **self.factory_kwargs + (self.batch_size, beam.num_particles, 3), **self.factory_kwargs ) - # Loop over batch dimension + # Loop over vectorisation dimension, i.e. the samples in one batch (does + # vectorization) for i_batch in range(self.batch_size): - # Get particle positions - particle_positions = beam.particles[i_batch, :, [0, 2, 4]] + particle_positions = moments[i_batch, :, [0, 2, 4]] normalized_positions = ( particle_positions[:, :] + grid_dimensions[i_batch, None, :] ) / cell_size[i_batch, None, :] @@ -496,18 +531,18 @@ def _compute_forces( ) surrounding_indices = ( cell_indices[:, None, :] + offsets[None, :, :] - ) # Shape:(n_particles, 8, 3) - # Shape: (n_particles, 8, 3) + ) # Shape:(beam.num_particles, 8, 3) + # Shape: (beam.num_particles, 8, 3) weights = 1 - torch.abs( normalized_positions[:, None, :] - surrounding_indices ) - # Shape: (n_particles, 8, 3) - cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) + # Shape: (beam.num_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (beam.num_particles, 8) # Extract forces from the grids idx_x, idx_y, idx_s = surrounding_indices.view( -1, 3 - ).T # Shape: (3, n_particles * 8) + ).T # Shape: (3, beam.num_particles * 8) valid_mask = ( (idx_x >= 0) & (idx_x < grid_shape[0]) @@ -528,7 +563,7 @@ def _compute_forces( values_y = valid_cell_weights * Fy_values values_z = valid_cell_weights * Fz_values - indices = torch.arange(n_particles).repeat_interleave(8)[valid_mask] + indices = torch.arange(beam.num_particles).repeat_interleave(8)[valid_mask] interpolated_F = interpolated_forces[i_batch] interpolated_F = interpolated_F.index_add( 0, @@ -567,38 +602,35 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: if incoming is Beam.empty or incoming.particles.shape[0] == 0: return incoming elif isinstance(incoming, ParticleBeam): - # Copy the array of coordinates to avoid modifying the incoming beam - outgoing_particles = torch.empty_like(incoming.particles) - outgoing_particles[...] = incoming.particles - outgoing = ParticleBeam( - outgoing_particles, - incoming.energy, - particle_charges=incoming.particle_charges, - device=incoming.particles.device, - dtype=incoming.particles.dtype, - ) - # Flatten the batch dimensions # (to simplify later calculation, is undone at the end of `track`) - outgoing.particles.reshape((-1, outgoing.num_particles, 7)) - self.batch_size = outgoing.particles.shape[0] + original_shape = incoming.particles.shape + incoming.particles.reshape((-1, incoming.num_particles, 7)) + self.batch_size = incoming.particles.shape[0] # Compute useful quantities - grid_dimensions = self._compute_grid_dimensions(outgoing) + grid_dimensions = self._compute_grid_dimensions(incoming) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length_effect / (c * self._betaref(outgoing)) + dt = self.length_effect / (speed_of_light * self._betaref(incoming)) # Change coordinates to apply the space charge effect - self._cheetah_to_moments(outgoing) - particles = outgoing.particles - forces = self._compute_forces(outgoing, cell_size, grid_dimensions) - particles[:, :, 1] += forces[:, :, 0] * dt - particles[:, :, 3] += forces[:, :, 1] * dt - particles[:, :, 5] += forces[:, :, 2] * dt - self._moments_to_cheetah(outgoing) + moments = self._cheetah_to_moments(incoming) + forces = self._compute_forces(incoming, moments, cell_size, grid_dimensions) + moments[:, :, 1] = moments[:, :, 1] + forces[:, :, 0] * dt + moments[:, :, 3] = moments[:, :, 3] + forces[:, :, 1] * dt + moments[:, :, 5] = moments[:, :, 5] + forces[:, :, 2] * dt + + outgoing = ParticleBeam( + particles=self._moments_to_cheetah(moments, incoming), + energy=incoming.energy, + particle_charges=incoming.particle_charges, + device=incoming.particles.device, + dtype=incoming.particles.dtype, + ) # Unflatten the batch dimensions - outgoing.particles.reshape(incoming.particles.shape) + outgoing.particles.reshape(original_shape) + return outgoing else: raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 0300437c..1915a778 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -16,7 +16,7 @@ def test_cold_uniform_beam_expansion(): """ # Random fluctuations in the initial density can cause the tests to fail - torch.manual_seed(0) + torch.manual_seed(42) # Simulation parameters num_particles = 10_000 @@ -63,17 +63,11 @@ def test_cold_uniform_beam_expansion(): cheetah.Drift(section_length / 6), ] ) - outgoing_beam = segment_space_charge.track(incoming) + outgoing = segment_space_charge.track(incoming) - assert torch.isclose( - outgoing_beam.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0 - ) - assert torch.isclose( - outgoing_beam.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0 - ) - assert torch.isclose( - outgoing_beam.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0 - ) + assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) def test_incoming_beam_not_modified(): @@ -82,7 +76,7 @@ def test_incoming_beam_not_modified(): """ # Random fluctuations in the initial density can cause the tests to fail - torch.manual_seed(0) + torch.manual_seed(42) incoming_beam = cheetah.ParticleBeam.from_parameters( num_particles=torch.tensor([10000]), @@ -139,6 +133,5 @@ def test_gradient(): # Track the beam outgoing_beam = segment.track(incoming_beam) - # Compute and check the gradient + # Compute the gradient ... would throw an error if in-place operations are used outgoing_beam.sigma_x.mean().backward() - assert isinstance(incoming_beam.sigma_x.grad, torch.Tensor) From 27e58abef064943858bb13aad99fc47186e1bf9e Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 22:14:48 +0200 Subject: [PATCH 068/111] Revert "Replace obvious in-place operations with out-of-place alternatives" This reverts commit fd0be7a99ea173adc1e19c4339f20e6e48c8e5f9. --- cheetah/accelerator/space_charge_kick.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index d6cb03eb..a7e0ff02 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -192,7 +192,7 @@ def _deposit_charge_on_grid( 8 ) # Shape:(8 * n_particles,) values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge[i_batch] = charge[i_batch].index_put( + charge[i_batch].index_put_( (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), values, accumulate=True, @@ -565,7 +565,7 @@ def _compute_forces( indices = torch.arange(beam.num_particles).repeat_interleave(8)[valid_mask] interpolated_F = interpolated_forces[i_batch] - interpolated_F = interpolated_F.index_add( + interpolated_F.index_add_( 0, indices, torch.stack( @@ -573,7 +573,7 @@ def _compute_forces( dim=1, ), ) - interpolated_F = interpolated_F.index_add( + interpolated_F.index_add_( 0, indices, torch.stack( @@ -581,7 +581,7 @@ def _compute_forces( dim=1, ), ) - interpolated_F = interpolated_F.index_add( + interpolated_F.index_add_( 0, indices, torch.stack( From e9a1f47bccd66e8a140a5a94213d9710e577a20f Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 15 Jun 2024 22:30:22 +0200 Subject: [PATCH 069/111] Add test to check if space charge works vectorised accroding to #116 --- tests/test_space_charge_kick.py | 61 ++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 1915a778..7e9af197 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -19,8 +19,6 @@ def test_cold_uniform_beam_expansion(): torch.manual_seed(42) # Simulation parameters - num_particles = 10_000 - total_charge = torch.tensor([1e-9]) R0 = torch.tensor([0.001]) energy = torch.tensor([2.5e8]) rest_energy = torch.tensor( @@ -34,12 +32,12 @@ def test_cold_uniform_beam_expansion(): beta = torch.sqrt(1 - 1 / gamma**2) incoming = cheetah.ParticleBeam.uniform_3d_ellipsoid( - num_particles=torch.tensor(num_particles), - total_charge=total_charge, + num_particles=torch.tensor(10_000), + total_charge=torch.tensor([1e-9]), energy=energy, radius_x=R0, radius_y=R0, - radius_s=R0 / gamma, # Radius of the beam in s direction, in the lab frame. + radius_s=R0 / gamma, # Radius of the beam in s direction in the lab frame sigma_xp=torch.tensor([1e-15]), sigma_yp=torch.tensor([1e-15]), sigma_p=torch.tensor([1e-15]), @@ -49,10 +47,10 @@ def test_cold_uniform_beam_expansion(): kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( 3 + 2 * torch.sqrt(torch.tensor(2)) ) - Nb = total_charge / elementary_charge + Nb = incoming.total_charge / elementary_charge section_length = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) - segment_space_charge = cheetah.Segment( + segment = cheetah.Segment( elements=[ cheetah.Drift(section_length / 6), cheetah.SpaceChargeKick(section_length / 3), @@ -63,13 +61,60 @@ def test_cold_uniform_beam_expansion(): cheetah.Drift(section_length / 6), ] ) - outgoing = segment_space_charge.track(incoming) + outgoing = segment.track(incoming) assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) +def test_vectorized(): + """ + Tests that the space charge kick can be applied to a vectorized beam. + """ + + # Simulation parameters + section_length = torch.tensor([0.42]) + R0 = torch.tensor([0.001]) + energy = torch.tensor([2.5e8]) + rest_energy = torch.tensor( + constants.electron_mass + * constants.speed_of_light**2 + / constants.elementary_charge + ) + gamma = energy / rest_energy + + incoming = cheetah.ParticleBeam.uniform_3d_ellipsoid( + num_particles=torch.tensor(10_000), + total_charge=torch.tensor([[1e-9, 2e-9], [3e-9, 4e-9], [5e-9, 6e-9]]), + energy=energy.repeat(3, 2), + radius_x=R0.repeat(3, 2), + radius_y=R0.repeat(3, 2), + radius_s=(R0 / gamma).repeat( + 3, 2 + ), # Radius of the beam in s direction in the lab frame + sigma_xp=torch.tensor([1e-15]).repeat(3, 2), + sigma_yp=torch.tensor([1e-15]).repeat(3, 2), + sigma_p=torch.tensor([1e-15]).repeat(3, 2), + ) + + segment = cheetah.Segment( + elements=[ + cheetah.Drift(section_length / 6), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 6), + ] + ).broadcast(shape=(3, 2)) + + outgoing = segment.track(incoming) + + assert outgoing.particles.shape == (2, 2, 10_000, 7) + + def test_incoming_beam_not_modified(): """ Tests that the incoming beam is not modified when calling the track method. From cf629ca62c53fb54757de5529f38956e56e6a6a7 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sun, 16 Jun 2024 13:17:06 +0200 Subject: [PATCH 070/111] Implement mostly functional vectorisation for space charge --- cheetah/accelerator/space_charge_kick.py | 517 ++++++++++++----------- tests/test_space_charge_kick.py | 2 +- 2 files changed, 274 insertions(+), 245 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index a7e0ff02..d0d6f91a 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -94,14 +94,12 @@ def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: """ Computes the dimensions of the grid on which to compute the space charge effect. """ - sigma_x = torch.std(beam.particles[:, :, 0], dim=1) - sigma_y = torch.std(beam.particles[:, :, 2], dim=1) - sigma_s = torch.std(beam.particles[:, :, 4], dim=1) + # TODO: Refactor ... might not need to be a method return torch.stack( [ - self.grid_extend_x * sigma_x, - self.grid_extend_y * sigma_y, - self.grid_extend_s * sigma_s, + self.grid_extend_x * beam.sigma_x, + self.grid_extend_y * beam.sigma_y, + self.grid_extend_s * beam.sigma_s, ], dim=-1, ) @@ -117,9 +115,7 @@ def _betaref(self, beam: ParticleBeam) -> torch.Tensor: Returns beta (i.e., normalized velocity) for the reference particle of the beam. """ gamma = self._gammaref(beam) - if gamma == 0: - return torch.tensor(1.0) - return torch.sqrt(1 - 1 / gamma**2) + return torch.where(gamma == 0, torch.tensor(1.0), torch.sqrt(1 - 1 / gamma**2)) def _deposit_charge_on_grid( self, @@ -134,75 +130,67 @@ def _deposit_charge_on_grid( charge density in C/m^3. """ charge = torch.zeros( - (self.batch_size,) + self.grid_shape, **self.factory_kwargs + beam.particles.shape[:-2] + self.grid_shape, **self.factory_kwargs ) - # Loop over vectorisation dimension, i.e. the samples in one batch (does - # vectorization) - for i_batch in range(self.batch_size): - # Get particle positions and charges - particle_positions = moments[i_batch, :, [0, 2, 4]] - particle_charges = beam.particle_charges[i_batch] - normalized_positions = ( - particle_positions[:, :] + grid_dimensions[i_batch, None, :] - ) / cell_size[i_batch, None, :] - - # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_positions).type(torch.long) - - # Calculate the weights for all surrounding cells - offsets = torch.tensor( - [ - [0, 0, 0], - [0, 0, 1], - [0, 1, 0], - [0, 1, 1], - [1, 0, 0], - [1, 0, 1], - [1, 1, 0], - [1, 1, 1], - ] - ) - surrounding_indices = cell_indices[:, None, :] + offsets[None, :, :] - # Shape: (n_particles, 8, 3) - weights = 1 - torch.abs( - normalized_positions[:, None, :] - surrounding_indices - ) - # Shape: (n_particles, 8, 3) - cell_weights = weights.prod(dim=-1) # Shape: (n_particles, 8) - - # Add the charge contributions to the cells - idx_x = surrounding_indices[:, :, 0].flatten() - idx_y = surrounding_indices[:, :, 1].flatten() - idx_s = surrounding_indices[:, :, 2].flatten() - # Shape: (8 * n_particles,) - - # Check that particles are inside the grid - valid_mask = ( - (idx_x >= 0) - & (idx_x < self.grid_shape[0]) - & (idx_y >= 0) - & (idx_y < self.grid_shape[1]) - & (idx_s >= 0) - & (idx_s < self.grid_shape[2]) - ) + # Get particle positions + particle_positions = moments[..., [0, 2, 4]] + normalized_positions = ( + particle_positions + grid_dimensions.unsqueeze(-2) + ) / cell_size.unsqueeze(-2) - # Accumulate the charge contributions - repeated_charges = particle_charges.repeat_interleave( - 8 - ) # Shape:(8 * n_particles,) - values = (cell_weights.view(-1) * repeated_charges)[valid_mask] - charge[i_batch].index_put_( - (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]), - values, - accumulate=True, - ) + # Find indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_positions).type(torch.long) + + # Calculate the weights for all surrounding cells + offsets = torch.tensor( + [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 1, 1], + [1, 0, 0], + [1, 0, 1], + [1, 1, 0], + [1, 1, 1], + ] + ) + surrounding_indices = cell_indices.unsqueeze(-2) + offsets.unsqueeze(-3) + # Shape: (..., num_particles, 8, 3) + weights = 1 - torch.abs( + normalized_positions.unsqueeze(-2) - surrounding_indices + ) + # Shape: (.., num_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (.., num_particles, 8) + + # Add the charge contributions to the cells + idx_x = surrounding_indices[..., 0].flatten(start_dim=-2) + idx_y = surrounding_indices[..., 1].flatten(start_dim=-2) + idx_s = surrounding_indices[..., 2].flatten(start_dim=-2) + # Shape: (..., 8 * num_particles) + + # Check that particles are inside the grid + valid_mask = ( + (idx_x >= 0) + & (idx_x < self.grid_shape[0]) + & (idx_y >= 0) + & (idx_y < self.grid_shape[1]) + & (idx_s >= 0) + & (idx_s < self.grid_shape[2]) + ) - # End of loop over batch - inv_cell_volume = 1 / (cell_size[:, 0] * cell_size[:, 1] * cell_size[:, 2]) + # Accumulate the charge contributions + repeated_charges = beam.particle_charges.repeat_interleave( + repeats=8, dim=-1 + ) # Shape:(..., 8 * num_particles) + values = (cell_weights.flatten(start_dim=-2) * repeated_charges)[valid_mask] + charge[..., idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]] += values return ( - charge * inv_cell_volume[:, None, None, None] + charge + / (cell_size[..., 0] * cell_size[..., 1] * cell_size[..., 2])[ + ..., None, None, None + ] ) # Normalize by the cell volume def _integrated_potential( @@ -243,15 +231,15 @@ def _array_rho( # Create a new tensor with the doubled dimensions, filled with zeros new_charge_density = torch.zeros( - (self.batch_size,) + new_dims, **self.factory_kwargs + beam.particles.shape[:-2] + new_dims, **self.factory_kwargs ) # Copy the original charge_density values to the beginning of the new tensor new_charge_density[ - :, - : charge_density.shape[1], - : charge_density.shape[2], - : charge_density.shape[3], + ..., + : charge_density.shape[-3], + : charge_density.shape[-2], + : charge_density.shape[-1], ] = charge_density return new_charge_density @@ -265,10 +253,10 @@ def _integrated_green_function( """ gamma = self._gammaref(beam) dx, dy, ds = ( - cell_size[:, 0], - cell_size[:, 1], - cell_size[:, 2] * gamma, - ) # scaled by gamma + cell_size[..., 0], + cell_size[..., 1], + cell_size[..., 2] * gamma, + ) # Scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids @@ -277,90 +265,111 @@ def _integrated_green_function( s = torch.arange(num_grid_points_s, **self.factory_kwargs) ix_grid, iy_grid, is_grid = torch.meshgrid(x, y, s, indexing="ij") x_grid = ( - ix_grid[None, :, :, :] * dx[:, None, None, None] - ) # Shape: [batch_size, nx, ny, nz] + ix_grid[None, :, :, :] * dx[..., None, None, None] + ) # Shape: [..., nx, ny, nz] y_grid = ( - iy_grid[None, :, :, :] * dy[:, None, None, None] - ) # Shape: [batch_size, nx, ny, nz] + iy_grid[None, :, :, :] * dy[..., None, None, None] + ) # Shape: [..., nx, ny, nz] s_grid = ( - is_grid[None, :, :, :] * ds[:, None, None, None] - ) # Shape: [batch_size, nx, ny, nz] + is_grid[None, :, :, :] * ds[..., None, None, None] + ) # Shape: [..., nx, ny, nz] # Compute the Green's function values G_values = ( self._integrated_potential( - x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds + x_grid + 0.5 * dx[..., None, None, None], + y_grid + 0.5 * dy[..., None, None, None], + s_grid + 0.5 * ds[..., None, None, None], ) - self._integrated_potential( - x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid + 0.5 * ds + x_grid - 0.5 * dx[..., None, None, None], + y_grid + 0.5 * dy[..., None, None, None], + s_grid + 0.5 * ds[..., None, None, None], ) - self._integrated_potential( - x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds + x_grid + 0.5 * dx[..., None, None, None], + y_grid - 0.5 * dy[..., None, None, None], + s_grid + 0.5 * ds[..., None, None, None], ) - self._integrated_potential( - x_grid + 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds + x_grid + 0.5 * dx[..., None, None, None], + y_grid + 0.5 * dy[..., None, None, None], + s_grid - 0.5 * ds[..., None, None, None], ) + self._integrated_potential( - x_grid + 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds + x_grid + 0.5 * dx[..., None, None, None], + y_grid - 0.5 * dy[..., None, None, None], + s_grid - 0.5 * ds[..., None, None, None], ) + self._integrated_potential( - x_grid - 0.5 * dx, y_grid + 0.5 * dy, s_grid - 0.5 * ds + x_grid - 0.5 * dx[..., None, None, None], + y_grid + 0.5 * dy[..., None, None, None], + s_grid - 0.5 * ds[..., None, None, None], ) + self._integrated_potential( - x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid + 0.5 * ds + x_grid - 0.5 * dx[..., None, None, None], + y_grid - 0.5 * dy[..., None, None, None], + s_grid + 0.5 * ds[..., None, None, None], ) - self._integrated_potential( - x_grid - 0.5 * dx, y_grid - 0.5 * dy, s_grid - 0.5 * ds + x_grid - 0.5 * dx[..., None, None, None], + y_grid - 0.5 * dy[..., None, None, None], + s_grid - 0.5 * ds[..., None, None, None], ) ) # Initialize the grid with double dimensions green_func_values = torch.zeros( - self.batch_size, - 2 * num_grid_points_x, - 2 * num_grid_points_y, - 2 * num_grid_points_s, + ( + *beam.particles.shape[:-2], + 2 * num_grid_points_x, + 2 * num_grid_points_y, + 2 * num_grid_points_s, + ), **self.factory_kwargs, ) # Fill the grid with G_values and its periodic copies green_func_values[ - :, :num_grid_points_x, :num_grid_points_y, :num_grid_points_s + ..., :num_grid_points_x, :num_grid_points_y, :num_grid_points_s ] = G_values green_func_values[ - :, num_grid_points_x + 1 :, :num_grid_points_y, :num_grid_points_s - ] = G_values[:, 1:, :, :].flip( - dims=[1] + ..., num_grid_points_x + 1 :, :num_grid_points_y, :num_grid_points_s + ] = G_values[..., 1:, :, :].flip( + dims=[-3] ) # Reverse x, excluding the first element green_func_values[ - :, :num_grid_points_x, num_grid_points_y + 1 :, :num_grid_points_s - ] = G_values[:, :, 1:, :].flip( - dims=[2] + ..., :num_grid_points_x, num_grid_points_y + 1 :, :num_grid_points_s + ] = G_values[..., :, 1:, :].flip( + dims=[-2] ) # Reverse y, excluding the first element green_func_values[ - :, :num_grid_points_x, :num_grid_points_y, num_grid_points_s + 1 : - ] = G_values[:, :, :, 1:].flip( - dims=[3] + ..., :num_grid_points_x, :num_grid_points_y, num_grid_points_s + 1 : + ] = G_values[..., :, :, 1:].flip( + dims=[-1] ) # Reverse s, excluding the first element green_func_values[ - :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, :num_grid_points_s - ] = G_values[:, 1:, 1:, :].flip( - dims=[1, 2] + ..., num_grid_points_x + 1 :, num_grid_points_y + 1 :, :num_grid_points_s + ] = G_values[..., 1:, 1:, :].flip( + dims=[-3, -2] ) # Reverse the x and y dimensions green_func_values[ - :, :num_grid_points_x, num_grid_points_y + 1 :, num_grid_points_s + 1 : - ] = G_values[:, :, 1:, 1:].flip( - dims=[2, 3] + ..., :num_grid_points_x, num_grid_points_y + 1 :, num_grid_points_s + 1 : + ] = G_values[..., :, 1:, 1:].flip( + dims=[-2, -1] ) # Reverse the y and s dimensions green_func_values[ - :, num_grid_points_x + 1 :, :num_grid_points_y, num_grid_points_s + 1 : - ] = G_values[:, 1:, :, 1:].flip( - dims=[1, 3] + ..., num_grid_points_x + 1 :, :num_grid_points_y, num_grid_points_s + 1 : + ] = G_values[..., 1:, :, 1:].flip( + dims=[-3, -1] ) # Reverse the x and s dimensions green_func_values[ - :, num_grid_points_x + 1 :, num_grid_points_y + 1 :, num_grid_points_s + 1 : - ] = G_values[:, 1:, 1:, 1:].flip( - dims=[1, 2, 3] + ..., + num_grid_points_x + 1 :, + num_grid_points_y + 1 :, + num_grid_points_s + 1 :, + ] = G_values[..., 1:, 1:, 1:].flip( + dims=[-3, -2, -1] ) # Reverse all dimensions return green_func_values @@ -384,10 +393,10 @@ def _solve_poisson_equation( # Return the physical potential return potential[ - :, - : charge_density.shape[1] // 2, - : charge_density.shape[2] // 2, - : charge_density.shape[3] // 2, + ..., + : charge_density.shape[-3] // 2, + : charge_density.shape[-2] // 2, + : charge_density.shape[-1] // 2, ] def _E_plus_vB_field( @@ -403,7 +412,8 @@ def _E_plus_vB_field( """ inv_cell_size = 1 / cell_size gamma = self._gammaref(beam) - igamma2 = 1 / gamma**2 if gamma != 0 else torch.tensor(0.0) + igamma2 = torch.zeros_like(gamma) + igamma2[gamma != 0] = 1 / gamma[gamma != 0] ** 2 potential = self._solve_poisson_equation( beam, moments, cell_size, grid_dimensions ) @@ -414,20 +424,20 @@ def _E_plus_vB_field( # Compute the gradients of the potential, using central differences, with 0 # boundary conditions - grad_x[:, 1:-1, :, :] = (potential[:, 2:, :, :] - potential[:, :-2, :, :]) * ( - 0.5 * inv_cell_size[:, 0, None, None, None] - ) - grad_y[:, :, 1:-1, :] = (potential[:, :, 2:, :] - potential[:, :, :-2, :]) * ( - 0.5 * inv_cell_size[:, 1, None, None, None] - ) - grad_s[:, :, :, 1:-1] = (potential[:, :, :, 2:] - potential[:, :, :, :-2]) * ( - 0.5 * inv_cell_size[:, 2, None, None, None] - ) + grad_x[..., 1:-1, :, :] = ( + potential[..., 2:, :, :] - potential[..., :-2, :, :] + ) * (0.5 * inv_cell_size[..., 0, None, None, None]) + grad_y[..., :, 1:-1, :] = ( + potential[..., :, 2:, :] - potential[..., :, :-2, :] + ) * (0.5 * inv_cell_size[..., 1, None, None, None]) + grad_s[..., :, :, 1:-1] = ( + potential[..., :, :, 2:] - potential[..., :, :, :-2] + ) * (0.5 * inv_cell_size[..., 2, None, None, None]) # Scale the gradients with lorentz factor - grad_x = -igamma2[:, None, None, None] * grad_x - grad_y = -igamma2[:, None, None, None] * grad_y - grad_s = -igamma2[:, None, None, None] * grad_s + grad_x = -igamma2[..., None, None, None] * grad_x + grad_y = -igamma2[..., None, None, None] * grad_y + grad_s = -igamma2[..., None, None, None] * grad_s return grad_x, grad_y, grad_s @@ -442,21 +452,21 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: p0 = gammaref * betaref * electron_mass * speed_of_light gamma = gammaref.unsqueeze(-1) * ( torch.ones(beam.particles.shape[:-1]) - + beam.particles[:, :, 5] * betaref.unsqueeze(-1) + + beam.particles[..., 5] * betaref.unsqueeze(-1) ) beta = torch.sqrt(1 - 1 / gamma**2) p = gamma * electron_mass * beta * speed_of_light - moments_xp = beam.particles[:, :, 1] * p0.unsqueeze(-1) - moments_yp = beam.particles[:, :, 3] * p0.unsqueeze(-1) - moments_s = beam.particles[:, :, 4] * -betaref.unsqueeze(-1) + moments_xp = beam.particles[..., 1] * p0.unsqueeze(-1) + moments_yp = beam.particles[..., 3] * p0.unsqueeze(-1) + moments_s = beam.particles[..., 4] * -betaref.unsqueeze(-1) moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) moments = beam.particles.clone() - moments[:, :, 1] = moments_xp - moments[:, :, 3] = moments_yp - moments[:, :, 4] = moments_s - moments[:, :, 5] = moments_p + moments[..., 1] = moments_xp + moments[..., 3] = moments_yp + moments[..., 4] = moments_s + moments[..., 5] = moments_p return moments @@ -472,14 +482,14 @@ def _moments_to_cheetah( betaref = self._betaref(beam) p0 = gammaref * betaref * electron_mass * speed_of_light p = torch.sqrt( - moments[:, :, 1] ** 2 + moments[:, :, 3] ** 2 + moments[:, :, 5] ** 2 + moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 ) gamma = torch.sqrt(1 + (p / (electron_mass * speed_of_light)) ** 2) - particles[:, :, 1] = moments[:, :, 1] / p0.unsqueeze(-1) - particles[:, :, 3] = moments[:, :, 3] / p0.unsqueeze(-1) - particles[:, :, 4] = -moments[:, :, 4] / betaref.unsqueeze(-1) - particles[:, :, 5] = (gamma - gammaref * torch.ones(gamma.shape)) / ( + particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) + particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) + particles[..., 4] = -moments[..., 4] / betaref.unsqueeze(-1) + particles[..., 5] = (gamma - gammaref.unsqueeze(-1)) / ( (betaref * gammaref).unsqueeze(-1) ) @@ -501,94 +511,103 @@ def _compute_forces( ) grid_shape = self.grid_shape interpolated_forces = torch.zeros( - (self.batch_size, beam.num_particles, 3), **self.factory_kwargs + (*beam.particles.shape[:-1], 3), **self.factory_kwargs ) - # Loop over vectorisation dimension, i.e. the samples in one batch (does - # vectorization) - for i_batch in range(self.batch_size): - # Get particle positions - particle_positions = moments[i_batch, :, [0, 2, 4]] - normalized_positions = ( - particle_positions[:, :] + grid_dimensions[i_batch, None, :] - ) / cell_size[i_batch, None, :] - - # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_positions).type(torch.long) - - # Calculate the weights for all surrounding cells - offsets = torch.tensor( - [ - [0, 0, 0], - [0, 0, 1], - [0, 1, 0], - [0, 1, 1], - [1, 0, 0], - [1, 0, 1], - [1, 1, 0], - [1, 1, 1], - ] - ) - surrounding_indices = ( - cell_indices[:, None, :] + offsets[None, :, :] - ) # Shape:(beam.num_particles, 8, 3) - # Shape: (beam.num_particles, 8, 3) - weights = 1 - torch.abs( - normalized_positions[:, None, :] - surrounding_indices - ) - # Shape: (beam.num_particles, 8, 3) - cell_weights = weights.prod(dim=-1) # Shape: (beam.num_particles, 8) - - # Extract forces from the grids - idx_x, idx_y, idx_s = surrounding_indices.view( - -1, 3 - ).T # Shape: (3, beam.num_particles * 8) - valid_mask = ( - (idx_x >= 0) - & (idx_x < grid_shape[0]) - & (idx_y >= 0) - & (idx_y < grid_shape[1]) - & (idx_s >= 0) - & (idx_s < grid_shape[2]) + # Get particle positions + particle_positions = moments[..., [0, 2, 4]] + normalized_positions = ( + particle_positions + grid_dimensions.unsqueeze(-2) + ) / cell_size.unsqueeze(-2) + + # Find indices of the lower corners of the cells containing the particles + cell_indices = torch.floor(normalized_positions).type(torch.long) + + # Calculate the weights for all surrounding cells + offsets = torch.tensor( + [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 1, 1], + [1, 0, 0], + [1, 0, 1], + [1, 1, 0], + [1, 1, 1], + ] + ) + surrounding_indices = cell_indices.unsqueeze(-2) + offsets.unsqueeze( + -3 + ) # Shape:(.., num_particles, 8, 3) + weights = 1 - torch.abs( + normalized_positions.unsqueeze(-2) - surrounding_indices + ) # Shape: (..., num_particles, 8, 3) + cell_weights = weights.prod(dim=-1) # Shape: (..., num_particles, 8) + + # Extract forces from the grids + surrounding_indices_flattened = surrounding_indices.flatten( + start_dim=-3, end_dim=-2 + ) # Shape: (..., num_particles * 8, 3) + idx_x = surrounding_indices_flattened[..., 0] + idx_y = surrounding_indices_flattened[..., 1] + idx_s = surrounding_indices_flattened[..., 2] + valid_mask = ( + (idx_x >= 0) + & (idx_x < grid_shape[0]) + & (idx_y >= 0) + & (idx_y < grid_shape[1]) + & (idx_s >= 0) + & (idx_s < grid_shape[2]) + ) + + # TODO: Is there a better alternative to this loop? (maybe torch.vmap?) + for ( + vector_idx_x, + vector_idx_y, + vector_idx_s, + vector_valid_mask, + vector_grad_x, + vector_grad_y, + vector_grad_z, + vector_cell_weights, + vector_interpolated_forces, + ) in zip( + idx_x.flatten(end_dim=-2), + idx_y.flatten(end_dim=-2), + idx_s.flatten(end_dim=-2), + valid_mask.flatten(end_dim=-2), + grad_x.flatten(end_dim=-4), + grad_y.flatten(end_dim=-4), + grad_z.flatten(end_dim=-4), + cell_weights.flatten(end_dim=-3), + interpolated_forces.flatten(end_dim=-3), + ): + vector_valid_indices = ( + vector_idx_x[vector_valid_mask], + vector_idx_y[vector_valid_mask], + vector_idx_s[vector_valid_mask], ) - valid_indices = (idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]) - Fx_values = grad_x[i_batch][valid_indices] - Fy_values = grad_y[i_batch][valid_indices] - Fz_values = grad_z[i_batch][valid_indices] + vector_Fx_values = vector_grad_x[vector_valid_indices] + vector_Fy_values = vector_grad_y[vector_valid_indices] + vector_Fz_values = vector_grad_z[vector_valid_indices] # Compute interpolated forces - valid_cell_weights = cell_weights.view(-1)[valid_mask] * elementary_charge - values_x = valid_cell_weights * Fx_values - values_y = valid_cell_weights * Fy_values - values_z = valid_cell_weights * Fz_values - - indices = torch.arange(beam.num_particles).repeat_interleave(8)[valid_mask] - interpolated_F = interpolated_forces[i_batch] - interpolated_F.index_add_( - 0, - indices, - torch.stack( - [values_x, torch.zeros_like(values_x), torch.zeros_like(values_x)], - dim=1, - ), - ) - interpolated_F.index_add_( - 0, - indices, - torch.stack( - [torch.zeros_like(values_y), values_y, torch.zeros_like(values_y)], - dim=1, - ), - ) - interpolated_F.index_add_( - 0, - indices, - torch.stack( - [torch.zeros_like(values_z), torch.zeros_like(values_z), values_z], - dim=1, - ), + vector_valid_cell_weights = ( + vector_cell_weights.flatten(start_dim=-2)[vector_valid_mask] + * elementary_charge ) + vector_values_x = vector_valid_cell_weights * vector_Fx_values + vector_values_y = vector_valid_cell_weights * vector_Fy_values + vector_values_z = vector_valid_cell_weights * vector_Fz_values + + vector_indices = torch.arange(beam.num_particles).repeat_interleave(8)[ + vector_valid_mask + ] + + vector_interpolated_forces[vector_indices, 0] += vector_values_x + vector_interpolated_forces[vector_indices, 1] += vector_values_y + vector_interpolated_forces[vector_indices, 2] += vector_values_z return interpolated_forces @@ -602,12 +621,6 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: if incoming is Beam.empty or incoming.particles.shape[0] == 0: return incoming elif isinstance(incoming, ParticleBeam): - # Flatten the batch dimensions - # (to simplify later calculation, is undone at the end of `track`) - original_shape = incoming.particles.shape - incoming.particles.reshape((-1, incoming.num_particles, 7)) - self.batch_size = incoming.particles.shape[0] - # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(incoming) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) @@ -616,9 +629,9 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: # Change coordinates to apply the space charge effect moments = self._cheetah_to_moments(incoming) forces = self._compute_forces(incoming, moments, cell_size, grid_dimensions) - moments[:, :, 1] = moments[:, :, 1] + forces[:, :, 0] * dt - moments[:, :, 3] = moments[:, :, 3] + forces[:, :, 1] * dt - moments[:, :, 5] = moments[:, :, 5] + forces[:, :, 2] * dt + moments[..., 1] = moments[..., 1] + forces[..., 0] * dt.unsqueeze(-1) + moments[..., 3] = moments[..., 3] + forces[..., 1] * dt.unsqueeze(-1) + moments[..., 5] = moments[..., 5] + forces[..., 2] * dt.unsqueeze(-1) outgoing = ParticleBeam( particles=self._moments_to_cheetah(moments, incoming), @@ -628,13 +641,29 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: dtype=incoming.particles.dtype, ) - # Unflatten the batch dimensions - outgoing.particles.reshape(original_shape) - return outgoing else: raise TypeError(f"Parameter incoming is of invalid type {type(incoming)}") + def broadcast(self, shape: torch.Size) -> "SpaceChargeKick": + """ + Broadcast the element to higher batch dimensions. + + :param shape: Shape to broadcast the element to. + :returns: Broadcasted element. + """ + return self.__class__( + length_effect=self.length_effect, + length=self.length, + num_grid_points_x=self.grid_shape[0], + num_grid_points_y=self.grid_shape[1], + num_grid_points_s=self.grid_shape[2], + grid_extend_x=self.grid_extend_x, + grid_extend_y=self.grid_extend_y, + grid_extend_s=self.grid_extend_s, + name=self.name, + ) + def split(self, resolution: torch.Tensor) -> list[Element]: # TODO: Implement splitting for SpaceCharge properly, for now just returns the # element itself diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 7e9af197..fe08dd04 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -112,7 +112,7 @@ def test_vectorized(): outgoing = segment.track(incoming) - assert outgoing.particles.shape == (2, 2, 10_000, 7) + assert outgoing.particles.shape == (3, 2, 10_000, 7) def test_incoming_beam_not_modified(): From d505d57f3ebba53ee97aa2bc5520275f5495c967 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 19:25:07 +0200 Subject: [PATCH 071/111] First `index_put_` location where vectorisation didn't work --- cheetah/accelerator/space_charge_kick.py | 42 ++++++++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index d0d6f91a..07d8123a 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -164,10 +164,13 @@ def _deposit_charge_on_grid( cell_weights = weights.prod(dim=-1) # Shape: (.., num_particles, 8) # Add the charge contributions to the cells + # Shape: (..., 8 * num_particles) + idx_vector = ( + torch.arange(cell_indices.shape[0]).repeat(8 * beam.num_particles, 1).T + ) idx_x = surrounding_indices[..., 0].flatten(start_dim=-2) idx_y = surrounding_indices[..., 1].flatten(start_dim=-2) idx_s = surrounding_indices[..., 2].flatten(start_dim=-2) - # Shape: (..., 8 * num_particles) # Check that particles are inside the grid valid_mask = ( @@ -184,7 +187,16 @@ def _deposit_charge_on_grid( repeats=8, dim=-1 ) # Shape:(..., 8 * num_particles) values = (cell_weights.flatten(start_dim=-2) * repeated_charges)[valid_mask] - charge[..., idx_x[valid_mask], idx_y[valid_mask], idx_s[valid_mask]] += values + charge.index_put_( + ( + idx_vector[valid_mask], + idx_x[valid_mask], + idx_y[valid_mask], + idx_s[valid_mask], + ), + values, + accumulate=True, + ) return ( charge @@ -621,20 +633,36 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: if incoming is Beam.empty or incoming.particles.shape[0] == 0: return incoming elif isinstance(incoming, ParticleBeam): + # This flattening is a hack to only think about one vector dimension in the + # following code. It is reversed at the end of the function. + flattened_incoming = ParticleBeam( + particles=incoming.particles.flatten(end_dim=-3), + energy=incoming.energy.flatten(end_dim=-1), + particle_charges=incoming.particle_charges.flatten(end_dim=-2), + device=incoming.particles.device, + dtype=incoming.particles.dtype, + ) + # Compute useful quantities - grid_dimensions = self._compute_grid_dimensions(incoming) + grid_dimensions = self._compute_grid_dimensions(flattened_incoming) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length_effect / (speed_of_light * self._betaref(incoming)) + dt = self.length_effect / ( + speed_of_light * self._betaref(flattened_incoming) + ) # Change coordinates to apply the space charge effect - moments = self._cheetah_to_moments(incoming) - forces = self._compute_forces(incoming, moments, cell_size, grid_dimensions) + moments = self._cheetah_to_moments(flattened_incoming) + forces = self._compute_forces( + flattened_incoming, moments, cell_size, grid_dimensions + ) moments[..., 1] = moments[..., 1] + forces[..., 0] * dt.unsqueeze(-1) moments[..., 3] = moments[..., 3] + forces[..., 1] * dt.unsqueeze(-1) moments[..., 5] = moments[..., 5] + forces[..., 2] * dt.unsqueeze(-1) outgoing = ParticleBeam( - particles=self._moments_to_cheetah(moments, incoming), + particles=self._moments_to_cheetah( + moments, flattened_incoming + ).unflatten(dim=0, sizes=incoming.particles.shape[:-2]), energy=incoming.energy, particle_charges=incoming.particle_charges, device=incoming.particles.device, From 81921ba929e317f04880724a456ee0bf945e58a1 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 19:28:20 +0200 Subject: [PATCH 072/111] Add test to check that vectorisation doesn't just not crash but also has correct result --- tests/test_space_charge_kick.py | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index fe08dd04..f2b6f2f4 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -115,6 +115,65 @@ def test_vectorized(): assert outgoing.particles.shape == (3, 2, 10_000, 7) +def test_cold_uniform_beam_expansion_vectorized(): + """ + Same as `test_cold_uniform_beam_expansion` but testing that all results in a + vectorised setup are correct. + """ + + # Random fluctuations in the initial density can cause the tests to fail + torch.manual_seed(42) + + # Simulation parameters + R0 = torch.tensor([0.001]) + energy = torch.tensor([2.5e8]) + rest_energy = torch.tensor( + constants.electron_mass + * constants.speed_of_light**2 + / constants.elementary_charge + ) + elementary_charge = torch.tensor(constants.elementary_charge) + electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) + gamma = energy / rest_energy + beta = torch.sqrt(1 - 1 / gamma**2) + + incoming = cheetah.ParticleBeam.uniform_3d_ellipsoid( + num_particles=torch.tensor(10_000), + total_charge=torch.tensor([1e-9]), + energy=energy, + radius_x=R0, + radius_y=R0, + radius_s=R0 / gamma, # Radius of the beam in s direction in the lab frame + sigma_xp=torch.tensor([1e-15]), + sigma_yp=torch.tensor([1e-15]), + sigma_p=torch.tensor([1e-15]), + ).broadcast(shape=(2, 3)) + + # Compute section lenght + kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( + 3 + 2 * torch.sqrt(torch.tensor(2)) + ) + Nb = incoming.total_charge / elementary_charge + section_length = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) + + segment = cheetah.Segment( + elements=[ + cheetah.Drift(section_length / 6), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 6), + ] + ).broadcast(shape=(2, 3)) + outgoing = segment.track(incoming) + + assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) + + def test_incoming_beam_not_modified(): """ Tests that the incoming beam is not modified when calling the track method. From 78da8c523c9415451bd3b4c3b632c26b34f92997 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 19:29:57 +0200 Subject: [PATCH 073/111] Fix test name for better test selection --- tests/test_space_charge_kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index f2b6f2f4..055d6947 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -115,7 +115,7 @@ def test_vectorized(): assert outgoing.particles.shape == (3, 2, 10_000, 7) -def test_cold_uniform_beam_expansion_vectorized(): +def test_vectorized_cold_uniform_beam_expansion(): """ Same as `test_cold_uniform_beam_expansion` but testing that all results in a vectorised setup are correct. From b9725ffc91491c6acbc7bf1f549e38bc93733162 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 19:50:28 +0200 Subject: [PATCH 074/111] Fix shape issue in vectorised beam expansion test and add segment length test --- cheetah/accelerator/space_charge_kick.py | 7 ++++-- tests/test_space_charge_kick.py | 30 ++++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 07d8123a..e5efd68a 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -62,7 +62,9 @@ class SpaceChargeKick(Element): def __init__( self, - length_effect: Union[torch.Tensor, nn.Parameter], + length_effect: Union[ + torch.Tensor, nn.Parameter + ], # TODO: Rename to effective_length length: Union[torch.Tensor, nn.Parameter] = 0.0, num_grid_points_x: Union[torch.Tensor, nn.Parameter, int] = 32, num_grid_points_y: Union[torch.Tensor, nn.Parameter, int] = 32, @@ -642,11 +644,12 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: device=incoming.particles.device, dtype=incoming.particles.dtype, ) + flattened_length_effect = self.length_effect.flatten(end_dim=-1) # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(flattened_incoming) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) - dt = self.length_effect / ( + dt = flattened_length_effect / ( speed_of_light * self._betaref(flattened_incoming) ) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 055d6947..24895ca1 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -166,12 +166,12 @@ def test_vectorized_cold_uniform_beam_expansion(): cheetah.SpaceChargeKick(section_length / 3), cheetah.Drift(section_length / 6), ] - ).broadcast(shape=(2, 3)) + ) outgoing = segment.track(incoming) - assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) + assert torch.allclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) + assert torch.allclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) + assert torch.allclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) def test_incoming_beam_not_modified(): @@ -239,3 +239,25 @@ def test_gradient(): # Compute the gradient ... would throw an error if in-place operations are used outgoing_beam.sigma_x.mean().backward() + + +def test_does_not_break_segment_length(): + """ + Test that the computation of a `Segment`'s length does not break when + `SpaceChargeKick` is used. + """ + section_length = torch.tensor([1.0]) + segment = cheetah.Segment( + elements=[ + cheetah.Drift(section_length / 6), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 3), + cheetah.SpaceChargeKick(section_length / 3), + cheetah.Drift(section_length / 6), + ] + ).broadcast(shape=(3, 2)) + + assert segment.length.shape == (3, 2) + assert torch.allclose(segment.length, torch.tensor([1.0]).repeat(3, 2)) From ae1de55e3973b8195233117f27a2e1b440dffcd0 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 21:03:41 +0200 Subject: [PATCH 075/111] Fix gradient issue --- cheetah/accelerator/space_charge_kick.py | 78 +++++++++--------------- 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index e5efd68a..71c2d8c8 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -237,11 +237,10 @@ def _array_rho( Allocates a 2x larger array in all dimensions (to perform Hockney's method), and copies the charge density in one of the "quadrants". """ - grid_shape = self.grid_shape charge_density = self._deposit_charge_on_grid( beam, moments, cell_size, grid_dimensions ) - new_dims = tuple(dim * 2 for dim in grid_shape) + new_dims = tuple(2 * dim for dim in self.grid_shape) # Create a new tensor with the doubled dimensions, filled with zeros new_charge_density = torch.zeros( @@ -562,6 +561,9 @@ def _compute_forces( surrounding_indices_flattened = surrounding_indices.flatten( start_dim=-3, end_dim=-2 ) # Shape: (..., num_particles * 8, 3) + idx_vector = ( + torch.arange(cell_indices.shape[0]).repeat(8 * beam.num_particles, 1).T + ) idx_x = surrounding_indices_flattened[..., 0] idx_y = surrounding_indices_flattened[..., 1] idx_s = surrounding_indices_flattened[..., 2] @@ -574,54 +576,34 @@ def _compute_forces( & (idx_s < grid_shape[2]) ) - # TODO: Is there a better alternative to this loop? (maybe torch.vmap?) - for ( - vector_idx_x, - vector_idx_y, - vector_idx_s, - vector_valid_mask, - vector_grad_x, - vector_grad_y, - vector_grad_z, - vector_cell_weights, - vector_interpolated_forces, - ) in zip( - idx_x.flatten(end_dim=-2), - idx_y.flatten(end_dim=-2), - idx_s.flatten(end_dim=-2), - valid_mask.flatten(end_dim=-2), - grad_x.flatten(end_dim=-4), - grad_y.flatten(end_dim=-4), - grad_z.flatten(end_dim=-4), - cell_weights.flatten(end_dim=-3), - interpolated_forces.flatten(end_dim=-3), - ): - vector_valid_indices = ( - vector_idx_x[vector_valid_mask], - vector_idx_y[vector_valid_mask], - vector_idx_s[vector_valid_mask], - ) - - vector_Fx_values = vector_grad_x[vector_valid_indices] - vector_Fy_values = vector_grad_y[vector_valid_indices] - vector_Fz_values = vector_grad_z[vector_valid_indices] + valid_indices = ( + idx_vector[valid_mask], + idx_x[valid_mask], + idx_y[valid_mask], + idx_s[valid_mask], + ) - # Compute interpolated forces - vector_valid_cell_weights = ( - vector_cell_weights.flatten(start_dim=-2)[vector_valid_mask] - * elementary_charge - ) - vector_values_x = vector_valid_cell_weights * vector_Fx_values - vector_values_y = vector_valid_cell_weights * vector_Fy_values - vector_values_z = vector_valid_cell_weights * vector_Fz_values + Fx_values = grad_x[valid_indices] + Fy_values = grad_y[valid_indices] + Fz_values = grad_z[valid_indices] - vector_indices = torch.arange(beam.num_particles).repeat_interleave(8)[ - vector_valid_mask - ] - - vector_interpolated_forces[vector_indices, 0] += vector_values_x - vector_interpolated_forces[vector_indices, 1] += vector_values_y - vector_interpolated_forces[vector_indices, 2] += vector_values_z + # Compute interpolated forces + valid_cell_weights = ( + cell_weights.flatten(start_dim=-2)[valid_mask] * elementary_charge + ) + values_x = valid_cell_weights * Fx_values + values_y = valid_cell_weights * Fy_values + values_z = valid_cell_weights * Fz_values + + indices = ( + torch.arange(beam.num_particles) + .repeat_interleave(8) + .repeat(cell_weights.shape[0], 1)[valid_mask] + ) # TODO: Indicies of what? + + interpolated_forces[:, indices, 0] += values_x + interpolated_forces[:, indices, 1] += values_y + interpolated_forces[:, indices, 2] += values_z return interpolated_forces From 161e7593ed0671e9900bf8d92434cc1511a6a430 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 21:24:11 +0200 Subject: [PATCH 076/111] Refactor `gammaref` --- cheetah/accelerator/space_charge_kick.py | 35 ++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 71c2d8c8..4bdfc187 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -106,18 +106,15 @@ def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: dim=-1, ) - def _gammaref(self, beam: ParticleBeam) -> torch.Tensor: - """ - Returns the Lorentz factor of the reference particle of the beam. - """ - return beam.energy / rest_energy - def _betaref(self, beam: ParticleBeam) -> torch.Tensor: """ Returns beta (i.e., normalized velocity) for the reference particle of the beam. """ - gamma = self._gammaref(beam) - return torch.where(gamma == 0, torch.tensor(1.0), torch.sqrt(1 - 1 / gamma**2)) + return torch.where( + beam.relativistic_gamma == 0, + torch.tensor(1.0), + torch.sqrt(1 - 1 / beam.relativistic_gamma**2), + ) def _deposit_charge_on_grid( self, @@ -264,11 +261,10 @@ def _integrated_green_function( Computes the Integrated Green Function (IGF) with periodic boundary conditions (to perform Hockney's method). """ - gamma = self._gammaref(beam) dx, dy, ds = ( cell_size[..., 0], cell_size[..., 1], - cell_size[..., 2] * gamma, + cell_size[..., 2] * beam.relativistic_gamma, ) # Scaled by gamma num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape @@ -424,9 +420,10 @@ def _E_plus_vB_field( speeds, as in https://doi.org/10.1063/1.2837054. """ inv_cell_size = 1 / cell_size - gamma = self._gammaref(beam) - igamma2 = torch.zeros_like(gamma) - igamma2[gamma != 0] = 1 / gamma[gamma != 0] ** 2 + igamma2 = torch.zeros_like(beam.relativistic_gamma) + igamma2[beam.relativistic_gamma != 0] = ( + 1 / beam.relativistic_gamma[beam.relativistic_gamma != 0] ** 2 + ) potential = self._solve_poisson_equation( beam, moments, cell_size, grid_dimensions ) @@ -459,11 +456,10 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: Converts the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. """ - gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref * betaref * electron_mass * speed_of_light - gamma = gammaref.unsqueeze(-1) * ( + p0 = beam.relativistic_gamma * betaref * electron_mass * speed_of_light + gamma = beam.relativistic_gamma.unsqueeze(-1) * ( torch.ones(beam.particles.shape[:-1]) + beam.particles[..., 5] * betaref.unsqueeze(-1) ) @@ -491,9 +487,8 @@ def _moments_to_cheetah( """ particles = moments.clone() - gammaref = self._gammaref(beam) betaref = self._betaref(beam) - p0 = gammaref * betaref * electron_mass * speed_of_light + p0 = beam.relativistic_gamma * betaref * electron_mass * speed_of_light p = torch.sqrt( moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 ) @@ -502,8 +497,8 @@ def _moments_to_cheetah( particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) particles[..., 4] = -moments[..., 4] / betaref.unsqueeze(-1) - particles[..., 5] = (gamma - gammaref.unsqueeze(-1)) / ( - (betaref * gammaref).unsqueeze(-1) + particles[..., 5] = (gamma - beam.relativistic_gamma.unsqueeze(-1)) / ( + (betaref * beam.relativistic_gamma).unsqueeze(-1) ) return particles From 20d4db0dabfea4eb0495245268a3517cd9a71578 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 21:29:33 +0200 Subject: [PATCH 077/111] Refactor `betaref` --- cheetah/accelerator/space_charge_kick.py | 37 +++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 4bdfc187..53f0d322 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -106,16 +106,6 @@ def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: dim=-1, ) - def _betaref(self, beam: ParticleBeam) -> torch.Tensor: - """ - Returns beta (i.e., normalized velocity) for the reference particle of the beam. - """ - return torch.where( - beam.relativistic_gamma == 0, - torch.tensor(1.0), - torch.sqrt(1 - 1 / beam.relativistic_gamma**2), - ) - def _deposit_charge_on_grid( self, beam: ParticleBeam, @@ -456,19 +446,22 @@ def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: Converts the Cheetah particle beam parameters to the moments in SI units used in the space charge solver. """ - betaref = self._betaref(beam) - - p0 = beam.relativistic_gamma * betaref * electron_mass * speed_of_light + p0 = ( + beam.relativistic_gamma + * beam.relativistic_beta + * electron_mass + * speed_of_light + ) gamma = beam.relativistic_gamma.unsqueeze(-1) * ( torch.ones(beam.particles.shape[:-1]) - + beam.particles[..., 5] * betaref.unsqueeze(-1) + + beam.particles[..., 5] * beam.relativistic_beta.unsqueeze(-1) ) beta = torch.sqrt(1 - 1 / gamma**2) p = gamma * electron_mass * beta * speed_of_light moments_xp = beam.particles[..., 1] * p0.unsqueeze(-1) moments_yp = beam.particles[..., 3] * p0.unsqueeze(-1) - moments_s = beam.particles[..., 4] * -betaref.unsqueeze(-1) + moments_s = beam.particles[..., 4] * -beam.relativistic_beta.unsqueeze(-1) moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) moments = beam.particles.clone() @@ -487,8 +480,12 @@ def _moments_to_cheetah( """ particles = moments.clone() - betaref = self._betaref(beam) - p0 = beam.relativistic_gamma * betaref * electron_mass * speed_of_light + p0 = ( + beam.relativistic_gamma + * beam.relativistic_beta + * electron_mass + * speed_of_light + ) p = torch.sqrt( moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 ) @@ -496,9 +493,9 @@ def _moments_to_cheetah( particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) - particles[..., 4] = -moments[..., 4] / betaref.unsqueeze(-1) + particles[..., 4] = -moments[..., 4] / beam.relativistic_beta.unsqueeze(-1) particles[..., 5] = (gamma - beam.relativistic_gamma.unsqueeze(-1)) / ( - (betaref * beam.relativistic_gamma).unsqueeze(-1) + (beam.relativistic_beta * beam.relativistic_gamma).unsqueeze(-1) ) return particles @@ -627,7 +624,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: grid_dimensions = self._compute_grid_dimensions(flattened_incoming) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) dt = flattened_length_effect / ( - speed_of_light * self._betaref(flattened_incoming) + speed_of_light * flattened_incoming.relativistic_beta ) # Change coordinates to apply the space charge effect From 83e2b6b5dd0988eaba97212a47b17baf64553f2e Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 21:57:27 +0200 Subject: [PATCH 078/111] Refactor moments computations --- cheetah/accelerator/space_charge_kick.py | 75 +++--------------------- cheetah/particles/particle_beam.py | 68 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 68 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 53f0d322..df6859d4 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -441,65 +441,6 @@ def _E_plus_vB_field( return grad_x, grad_y, grad_s - def _cheetah_to_moments(self, beam: ParticleBeam) -> torch.Tensor: - """ - Converts the Cheetah particle beam parameters to the moments in SI units used in - the space charge solver. - """ - p0 = ( - beam.relativistic_gamma - * beam.relativistic_beta - * electron_mass - * speed_of_light - ) - gamma = beam.relativistic_gamma.unsqueeze(-1) * ( - torch.ones(beam.particles.shape[:-1]) - + beam.particles[..., 5] * beam.relativistic_beta.unsqueeze(-1) - ) - beta = torch.sqrt(1 - 1 / gamma**2) - p = gamma * electron_mass * beta * speed_of_light - - moments_xp = beam.particles[..., 1] * p0.unsqueeze(-1) - moments_yp = beam.particles[..., 3] * p0.unsqueeze(-1) - moments_s = beam.particles[..., 4] * -beam.relativistic_beta.unsqueeze(-1) - moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) - - moments = beam.particles.clone() - moments[..., 1] = moments_xp - moments[..., 3] = moments_yp - moments[..., 4] = moments_s - moments[..., 5] = moments_p - - return moments - - def _moments_to_cheetah( - self, moments: torch.Tensor, beam: ParticleBeam - ) -> torch.Tensor: - """ - Converts the moments in SI units to the Cheetah particle beam parameters. - """ - particles = moments.clone() - - p0 = ( - beam.relativistic_gamma - * beam.relativistic_beta - * electron_mass - * speed_of_light - ) - p = torch.sqrt( - moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 - ) - gamma = torch.sqrt(1 + (p / (electron_mass * speed_of_light)) ** 2) - - particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) - particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) - particles[..., 4] = -moments[..., 4] / beam.relativistic_beta.unsqueeze(-1) - particles[..., 5] = (gamma - beam.relativistic_gamma.unsqueeze(-1)) / ( - (beam.relativistic_beta * beam.relativistic_gamma).unsqueeze(-1) - ) - - return particles - def _compute_forces( self, beam: ParticleBeam, @@ -628,7 +569,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: ) # Change coordinates to apply the space charge effect - moments = self._cheetah_to_moments(flattened_incoming) + moments = flattened_incoming.to_moments() forces = self._compute_forces( flattened_incoming, moments, cell_size, grid_dimensions ) @@ -636,14 +577,12 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: moments[..., 3] = moments[..., 3] + forces[..., 1] * dt.unsqueeze(-1) moments[..., 5] = moments[..., 5] + forces[..., 2] * dt.unsqueeze(-1) - outgoing = ParticleBeam( - particles=self._moments_to_cheetah( - moments, flattened_incoming - ).unflatten(dim=0, sizes=incoming.particles.shape[:-2]), - energy=incoming.energy, - particle_charges=incoming.particle_charges, - device=incoming.particles.device, - dtype=incoming.particles.dtype, + outgoing = ParticleBeam.from_moments( + moments.unflatten(dim=0, sizes=incoming.particles.shape[:-2]), + incoming.energy, + incoming.particle_charges, + incoming.particles.device, + incoming.particles.dtype, ) return outgoing diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index b6e014fa..3d71b684 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -1,6 +1,7 @@ from typing import Optional import torch +from scipy import constants from scipy.constants import physical_constants from torch.distributions import MultivariateNormal @@ -9,6 +10,7 @@ electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) +speed_of_light = torch.tensor(constants.speed_of_light) class ParticleBeam(Beam): @@ -722,6 +724,72 @@ def transformed_to( dtype=dtype, ) + @classmethod + def from_moments( + cls, + moments: torch.Tensor, + energy: torch.Tensor, + particle_charges: Optional[torch.Tensor] = None, + device=None, + dtype=torch.float32, + ) -> torch.Tensor: + """Converts the moments in SI units to a `ParticleBeam`.""" + beam = cls( + particles=moments.clone(), + energy=energy, + particle_charges=particle_charges, + device=device, + dtype=dtype, + ) + + p0 = ( + beam.relativistic_gamma + * beam.relativistic_beta + * electron_mass_eV + * speed_of_light + ) + p = torch.sqrt( + moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 + ) + gamma = torch.sqrt(1 + (p / (electron_mass_eV * speed_of_light)) ** 2) + + beam.particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) + beam.particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) + beam.particles[..., 4] = -moments[..., 4] / beam.relativistic_beta.unsqueeze(-1) + beam.particles[..., 5] = (gamma - beam.relativistic_gamma.unsqueeze(-1)) / ( + (beam.relativistic_beta * beam.relativistic_gamma).unsqueeze(-1) + ) + + return beam + + def to_moments(self) -> torch.Tensor: + """Moments in SI units as converted from the beam's `particles`.""" + p0 = ( + self.relativistic_gamma + * self.relativistic_beta + * electron_mass_eV + * speed_of_light + ) + gamma = self.relativistic_gamma.unsqueeze(-1) * ( + torch.ones(self.particles.shape[:-1]) + + self.particles[..., 5] * self.relativistic_beta.unsqueeze(-1) + ) + beta = torch.sqrt(1 - 1 / gamma**2) + p = gamma * electron_mass_eV * beta * speed_of_light + + moments_xp = self.particles[..., 1] * p0.unsqueeze(-1) + moments_yp = self.particles[..., 3] * p0.unsqueeze(-1) + moments_s = self.particles[..., 4] * -self.relativistic_beta.unsqueeze(-1) + moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) + + moments = self.particles.clone() + moments[..., 1] = moments_xp + moments[..., 3] = moments_yp + moments[..., 4] = moments_s + moments[..., 5] = moments_p + + return moments + def __len__(self) -> int: return int(self.num_particles) From e66a08e099d33c0be6ad2677c41acdb9ab484266 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 22:01:12 +0200 Subject: [PATCH 079/111] Remove unused constants --- cheetah/accelerator/space_charge_kick.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index df6859d4..67b010d4 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -11,21 +11,9 @@ from .element import Element # Constants -speed_of_light = torch.tensor(constants.speed_of_light) -J_to_eV = torch.tensor(physical_constants["electron volt-joule relationship"][0]) elementary_charge = torch.tensor(constants.elementary_charge) -rest_energy = torch.tensor( - constants.electron_mass - * constants.speed_of_light**2 - / constants.elementary_charge # Electron mass -) -electron_radius = torch.tensor(physical_constants["classical electron radius"][0]) -electron_mass_eV = torch.tensor( - physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 -) -electron_mass = torch.tensor(physical_constants["electron mass"][0]) - epsilon_0 = torch.tensor(constants.epsilon_0) +speed_of_light = torch.tensor(constants.speed_of_light) class SpaceChargeKick(Element): From c4476678eefbb652c684439a0af22c10feaa9654 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 22:05:24 +0200 Subject: [PATCH 080/111] Fix length computation --- cheetah/accelerator/space_charge_kick.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 67b010d4..8a0ae469 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -3,7 +3,6 @@ import matplotlib import torch from scipy import constants -from scipy.constants import physical_constants from torch import nn from cheetah.particles import Beam, ParticleBeam @@ -50,10 +49,9 @@ class SpaceChargeKick(Element): def __init__( self, - length_effect: Union[ + effect_length: Union[ torch.Tensor, nn.Parameter ], # TODO: Rename to effective_length - length: Union[torch.Tensor, nn.Parameter] = 0.0, num_grid_points_x: Union[torch.Tensor, nn.Parameter, int] = 32, num_grid_points_y: Union[torch.Tensor, nn.Parameter, int] = 32, num_grid_points_s: Union[torch.Tensor, nn.Parameter, int] = 32, @@ -68,8 +66,7 @@ def __init__( super().__init__(name=name) - self.length_effect = torch.as_tensor(length_effect, **self.factory_kwargs) - self.length = torch.as_tensor(length, **self.factory_kwargs) + self.effect_length = torch.as_tensor(effect_length, **self.factory_kwargs) self.grid_shape = ( int(num_grid_points_x), int(num_grid_points_y), @@ -547,7 +544,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: device=incoming.particles.device, dtype=incoming.particles.dtype, ) - flattened_length_effect = self.length_effect.flatten(end_dim=-1) + flattened_length_effect = self.effect_length.flatten(end_dim=-1) # Compute useful quantities grid_dimensions = self._compute_grid_dimensions(flattened_incoming) @@ -584,9 +581,8 @@ def broadcast(self, shape: torch.Size) -> "SpaceChargeKick": :param shape: Shape to broadcast the element to. :returns: Broadcasted element. """ - return self.__class__( - length_effect=self.length_effect, - length=self.length, + new_space_charge_kick = self.__class__( + effect_length=self.effect_length, num_grid_points_x=self.grid_shape[0], num_grid_points_y=self.grid_shape[1], num_grid_points_s=self.grid_shape[2], @@ -595,6 +591,8 @@ def broadcast(self, shape: torch.Size) -> "SpaceChargeKick": grid_extend_s=self.grid_extend_s, name=self.name, ) + new_space_charge_kick.length = self.length.repeat(shape) + return new_space_charge_kick def split(self, resolution: torch.Tensor) -> list[Element]: # TODO: Implement splitting for SpaceCharge properly, for now just returns the From 7b57805abf8d73925501bd97a47adce481f1a852 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 22:13:29 +0200 Subject: [PATCH 081/111] Remove out-of-date todo --- cheetah/accelerator/space_charge_kick.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 8a0ae469..73f4ad5b 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -49,9 +49,7 @@ class SpaceChargeKick(Element): def __init__( self, - effect_length: Union[ - torch.Tensor, nn.Parameter - ], # TODO: Rename to effective_length + effect_length: Union[torch.Tensor, nn.Parameter], num_grid_points_x: Union[torch.Tensor, nn.Parameter, int] = 32, num_grid_points_y: Union[torch.Tensor, nn.Parameter, int] = 32, num_grid_points_s: Union[torch.Tensor, nn.Parameter, int] = 32, From cc225bb8eda91c8ae8de12faee8b47b94c6603bc Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Tue, 18 Jun 2024 22:15:05 +0200 Subject: [PATCH 082/111] Add `SpaceChargeKick` to documentation --- docs/accelerator.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/accelerator.rst b/docs/accelerator.rst index 5a309e6f..cd5b998a 100644 --- a/docs/accelerator.rst +++ b/docs/accelerator.rst @@ -55,6 +55,10 @@ Accelerator :members: :undoc-members: +.. automodule:: accelerator.space_charge_kick + :members: + :undoc-members: + .. automodule:: accelerator.solenoid :members: :undoc-members: From 35d80c4aa8c667582a75efd579d65a95c1c24829 Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Wed, 19 Jun 2024 11:15:56 +0200 Subject: [PATCH 083/111] Fix issues in `_compute_forces_` --- cheetah/accelerator/space_charge_kick.py | 59 +++++++++++++----------- cheetah/particles/particle_beam.py | 9 ++-- tests/test_space_charge_kick.py | 2 +- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 73f4ad5b..6709efdf 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -138,7 +138,9 @@ def _deposit_charge_on_grid( # Add the charge contributions to the cells # Shape: (..., 8 * num_particles) idx_vector = ( - torch.arange(cell_indices.shape[0]).repeat(8 * beam.num_particles, 1).T + torch.arange(cell_indices.shape[0]) + .repeat(8 * beam.particles.shape[-2], 1) + .T ) idx_x = surrounding_indices[..., 0].flatten(start_dim=-2) idx_y = surrounding_indices[..., 1].flatten(start_dim=-2) @@ -434,6 +436,7 @@ def _compute_forces( """ Interpolates the space charge force from the grid onto the macroparticles. Reciprocal function of _deposit_charge_on_grid. + Beam needs to have a flattened batch shape. """ grad_x, grad_y, grad_z = self._E_plus_vB_field( beam, moments, cell_size, grid_dimensions @@ -441,7 +444,7 @@ def _compute_forces( grid_shape = self.grid_shape interpolated_forces = torch.zeros( (*beam.particles.shape[:-1], 3), **self.factory_kwargs - ) + ) # (n_batch, n_particles, 3) # Get particle positions particle_positions = moments[..., [0, 2, 4]] @@ -478,8 +481,10 @@ def _compute_forces( start_dim=-3, end_dim=-2 ) # Shape: (..., num_particles * 8, 3) idx_vector = ( - torch.arange(cell_indices.shape[0]).repeat(8 * beam.num_particles, 1).T - ) + torch.arange(cell_indices.shape[0]) + .repeat(8 * beam.particles.shape[-2], 1) + .T + ) # Shape: (..., num_particles * 8) idx_x = surrounding_indices_flattened[..., 0] idx_y = surrounding_indices_flattened[..., 1] idx_s = surrounding_indices_flattened[..., 2] @@ -492,36 +497,38 @@ def _compute_forces( & (idx_s < grid_shape[2]) ) - valid_indices = ( - idx_vector[valid_mask], - idx_x[valid_mask], - idx_y[valid_mask], - idx_s[valid_mask], - ) + # Keep dimensions, and set F to zero if non-valid + force_indices = (idx_vector, idx_x, idx_y, idx_s) - Fx_values = grad_x[valid_indices] - Fy_values = grad_y[valid_indices] - Fz_values = grad_z[valid_indices] + Fx_values = torch.where(valid_mask, grad_x[force_indices], 0) + Fy_values = torch.where(valid_mask, grad_y[force_indices], 0) + Fz_values = torch.where( + valid_mask, grad_z[force_indices], 0 + ) # (..., 8 * num_particles) # Compute interpolated forces - valid_cell_weights = ( - cell_weights.flatten(start_dim=-2)[valid_mask] * elementary_charge - ) - values_x = valid_cell_weights * Fx_values - values_y = valid_cell_weights * Fy_values - values_z = valid_cell_weights * Fz_values + # Cell weights validation is taken care of by the F_x, F_y, F_z values + cell_weights_with_e = cell_weights.flatten(start_dim=-2) * elementary_charge + values_x = cell_weights_with_e * Fx_values + values_y = cell_weights_with_e * Fy_values + values_z = cell_weights_with_e * Fz_values - indices = ( + forces_to_add = torch.stack([values_x, values_y, values_z], dim=-1) + + index_tensor = ( torch.arange(beam.num_particles) .repeat_interleave(8) - .repeat(cell_weights.shape[0], 1)[valid_mask] - ) # TODO: Indicies of what? + .unsqueeze(0) + .unsqueeze(-1) + .expand(beam.particles.shape[0], 8 * beam.particles.shape[-2], 3) + ) - interpolated_forces[:, indices, 0] += values_x - interpolated_forces[:, indices, 1] += values_y - interpolated_forces[:, indices, 2] += values_z + # Add the forces to the particles + accumulated_forces = torch.scatter_add( + interpolated_forces, dim=1, index=index_tensor, src=forces_to_add + ) - return interpolated_forces + return accumulated_forces def track(self, incoming: ParticleBeam) -> ParticleBeam: """ diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 3d71b684..87c8c0f3 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -10,6 +10,7 @@ electron_mass_eV = torch.tensor( physical_constants["electron mass energy equivalent in MeV"][0] * 1e6 ) +electron_mass = torch.tensor(physical_constants["electron mass"][0]) speed_of_light = torch.tensor(constants.speed_of_light) @@ -745,13 +746,13 @@ def from_moments( p0 = ( beam.relativistic_gamma * beam.relativistic_beta - * electron_mass_eV + * electron_mass * speed_of_light ) p = torch.sqrt( moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 ) - gamma = torch.sqrt(1 + (p / (electron_mass_eV * speed_of_light)) ** 2) + gamma = torch.sqrt(1 + (p / (electron_mass * speed_of_light)) ** 2) beam.particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) beam.particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) @@ -767,7 +768,7 @@ def to_moments(self) -> torch.Tensor: p0 = ( self.relativistic_gamma * self.relativistic_beta - * electron_mass_eV + * electron_mass * speed_of_light ) gamma = self.relativistic_gamma.unsqueeze(-1) * ( @@ -775,7 +776,7 @@ def to_moments(self) -> torch.Tensor: + self.particles[..., 5] * self.relativistic_beta.unsqueeze(-1) ) beta = torch.sqrt(1 - 1 / gamma**2) - p = gamma * electron_mass_eV * beta * speed_of_light + p = gamma * electron_mass * beta * speed_of_light moments_xp = self.particles[..., 1] * p0.unsqueeze(-1) moments_yp = self.particles[..., 3] * p0.unsqueeze(-1) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 24895ca1..bf26bb52 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -183,7 +183,7 @@ def test_incoming_beam_not_modified(): torch.manual_seed(42) incoming_beam = cheetah.ParticleBeam.from_parameters( - num_particles=torch.tensor([10000]), + num_particles=torch.tensor(10000), sigma_xp=torch.tensor([2e-7]), sigma_yp=torch.tensor([2e-7]), ) From 396d8405cab491d032268604486e884b01e8abf3 Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Wed, 19 Jun 2024 11:29:12 +0200 Subject: [PATCH 084/111] Fix index out of range issue --- cheetah/accelerator/space_charge_kick.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 6709efdf..f94d8610 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -498,7 +498,12 @@ def _compute_forces( ) # Keep dimensions, and set F to zero if non-valid - force_indices = (idx_vector, idx_x, idx_y, idx_s) + force_indices = ( + idx_vector, + torch.clamp(idx_x, max=grid_shape[0] - 1), + torch.clamp(idx_y, max=grid_shape[1] - 1), + torch.clamp(idx_s, max=grid_shape[2] - 1), + ) Fx_values = torch.where(valid_mask, grad_x[force_indices], 0) Fy_values = torch.where(valid_mask, grad_y[force_indices], 0) From 2b1534485c104ac87519ab6f62f67d93cf2826f8 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Wed, 19 Jun 2024 11:43:54 +0200 Subject: [PATCH 085/111] Add entry to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1169b44..b7c978f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `CustomTransferMap` elements created by combining multiple other elements will now reflect that in their `name` attribute (see #100) (@jank324) - Add a new class method for `ParticleBeam` to generate a 3D uniformly distributed ellipsoidal beam (see #146) (@cr-xu, @jank324) - Add Python 3.12 support (see #161) (@jank324) +- Implement space charge using Green's function in a `SpaceChargeKick` element (see #142) (@greglenerd, @RemiLehe, @ax3l, @cr-xu, @jank324) ### 🐛 Bug fixes From 16d3b7751c6eda17928d504bd5f8950d95b5027a Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Wed, 19 Jun 2024 13:04:45 +0200 Subject: [PATCH 086/111] Remove not-needed `atol` as per comment by @ax3l --- cheetah/accelerator/space_charge_kick.py | 2 +- tests/test_space_charge_kick.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index f94d8610..ba34fed3 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -444,7 +444,7 @@ def _compute_forces( grid_shape = self.grid_shape interpolated_forces = torch.zeros( (*beam.particles.shape[:-1], 3), **self.factory_kwargs - ) # (n_batch, n_particles, 3) + ) # (..., num_particles, 3) # Get particle positions particle_positions = moments[..., [0, 2, 4]] diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index bf26bb52..ef2e3fce 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -63,9 +63,9 @@ def test_cold_uniform_beam_expansion(): ) outgoing = segment.track(incoming) - assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) - assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) + assert torch.isclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2) + assert torch.isclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2) + assert torch.isclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2) def test_vectorized(): @@ -169,9 +169,9 @@ def test_vectorized_cold_uniform_beam_expansion(): ) outgoing = segment.track(incoming) - assert torch.allclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2, atol=0.0) - assert torch.allclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2, atol=0.0) - assert torch.allclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2, atol=0.0) + assert torch.allclose(outgoing.sigma_x, 2 * incoming.sigma_x, rtol=2e-2) + assert torch.allclose(outgoing.sigma_y, 2 * incoming.sigma_y, rtol=2e-2) + assert torch.allclose(outgoing.sigma_s, 2 * incoming.sigma_s, rtol=2e-2) def test_incoming_beam_not_modified(): From 3d8b97d7ca4a11b1f22d3891eb43be9bb362b820 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Wed, 19 Jun 2024 13:10:07 +0200 Subject: [PATCH 087/111] Remove `try_batched` notebook --- try_batched.ipynb | 1232 --------------------------------------------- 1 file changed, 1232 deletions(-) delete mode 100644 try_batched.ipynb diff --git a/try_batched.ipynb b/try_batched.ipynb deleted file mode 100644 index 625ca178..00000000 --- a/try_batched.ipynb +++ /dev/null @@ -1,1232 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "\n", - "import cheetah" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ic| mu_x: tensor([0., 0.]), mu_x.shape: torch.Size([2])\n", - "ic| mu_xp: tensor([0., 0.]), mu_xp.shape: torch.Size([2])\n", - "ic| mu_y: tensor([0., 0.]), mu_y.shape: torch.Size([2])\n", - "ic| mu_yp: tensor([0., 0.]), mu_yp.shape: torch.Size([2])\n" - ] - }, - { - "data": { - "text/plain": [ - "ParameterBeam(mu_x=tensor([0., 0.]), mu_xp=tensor([0., 0.]), mu_y=tensor([0., 0.]), mu_yp=tensor([0., 0.]), sigma_x=tensor([6.6517e-06, 7.0356e-06]), sigma_xp=tensor([1.7005e-07, 9.5611e-08]), sigma_y=tensor([3.5642e-07, 2.4495e-07]), sigma_yp=tensor([1.2088e-08, 4.5644e-09]), sigma_s=tensor([1.0000e-06, 1.0000e-06]), sigma_p=tensor([1.0000e-06, 1.0000e-06]), energy=tensor([1.5000e+08, 1.4600e+10])), total_charge=tensor([1.0000e-12, 4.0000e-12]))" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# parameter_beam = cheetah.ParameterBeam.from_parameters(\n", - "# mu_x=torch.tensor([0.0, 1e-6]),\n", - "# sigma_x=torch.tensor([175e-9, 42e-8]),\n", - "# total_charge=torch.tensor([1e-12, 2e-12]),\n", - "# )\n", - "parameter_beam = cheetah.ParameterBeam.from_twiss(\n", - " beta_x=torch.tensor([61.47503078, 99.0]),\n", - " alpha_x=torch.tensor([-1.21242463, -0.9]),\n", - " emittance_x=torch.tensor([7.1971891e-13, 5.0e-13]),\n", - " beta_y=torch.tensor([35.41897281, 60.0]),\n", - " alpha_y=torch.tensor([0.66554622, 0.5]),\n", - " emittance_y=torch.tensor([3.5866484e-15, 1.0e-15]),\n", - " total_charge=torch.tensor([1e-12, 4e-12]),\n", - " energy=torch.tensor([150e6, 14.6e9]),\n", - ")\n", - "# parameter_beam = cheetah.ParameterBeam.from_astra(\n", - "# \"tests/resources/ACHIP_EA1_2021.1351.001\"\n", - "# )\n", - "parameter_beam" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ParticleBeam(n=1000000, mu_x=tensor([-9.0306e-09, 6.7688e-09]), mu_xp=tensor([-2.0284e-11, 5.9579e-11]), mu_y=tensor([-3.2570e-10, -4.2466e-11]), mu_yp=tensor([ 1.1656e-11, -5.8969e-12]), sigma_x=tensor([6.6523e-06, 7.0303e-06]), sigma_xp=tensor([1.7008e-07, 9.5575e-08]), sigma_y=tensor([3.5672e-07, 2.4496e-07]), sigma_yp=tensor([1.2083e-08, 4.5663e-09]), sigma_s=tensor([9.9929e-07, 9.9962e-07]), sigma_p=tensor([9.9936e-07, 1.0004e-06]), energy=tensor([100000000., 100000000.])) total_charge=tensor([1.0000e-12, 4.0000e-12]))" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# particle_beam = cheetah.ParticleBeam.from_parameters(\n", - "# mu_x=torch.tensor([0.0, 1e-6]),\n", - "# sigma_x=torch.tensor([175e-9, 42e-8]),\n", - "# total_charge=torch.tensor([1e-12, 2e-12]),\n", - "# )\n", - "particle_beam = cheetah.ParticleBeam.from_twiss(\n", - " beta_x=torch.tensor([61.47503078, 99.0]),\n", - " alpha_x=torch.tensor([-1.21242463, -0.9]),\n", - " emittance_x=torch.tensor([7.1971891e-13, 5.0e-13]),\n", - " beta_y=torch.tensor([35.41897281, 60.0]),\n", - " alpha_y=torch.tensor([0.66554622, 0.5]),\n", - " emittance_y=torch.tensor([3.5866484e-15, 1.0e-15]),\n", - " total_charge=torch.tensor([1e-12, 4e-12]),\n", - ")\n", - "# particle_beam = cheetah.ParticleBeam.from_astra(\n", - "# \"tests/resources/ACHIP_EA1_2021.1351.001\"\n", - "# )\n", - "particle_beam" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1000000" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.num_particles" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.0000e-12, 4.0000e-12])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.total_charge" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.0000e-12, 4.0000e-12])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.total_charge" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([0., 0.])" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.mu_x" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([2, 1000000])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.xs.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-9.0306e-09, 6.7688e-09])" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.mu_x" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([6.6517e-06, 7.0356e-06])" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.sigma_x" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([6.6523e-06, 7.0303e-06])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.sigma_x" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([[ 7.6652e-07, -4.8972e-06, 1.0150e-05, ..., -6.0184e-06,\n", - " -2.8498e-06, -8.1820e-07],\n", - " [ 3.3290e-06, 2.8640e-06, 5.6765e-06, ..., -9.2437e-07,\n", - " 7.3396e-06, -1.5432e-06]])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.xs" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([[-9.0306e-09],\n", - " [ 6.7688e-09]])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.mu_x.view(-1, 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([[ 7.7555e-07, -4.8882e-06, 1.0159e-05, ..., -6.0094e-06,\n", - " -2.8408e-06, -8.0917e-07],\n", - " [ 3.3222e-06, 2.8573e-06, 5.6697e-06, ..., -9.3114e-07,\n", - " 7.3329e-06, -1.5500e-06]])" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.xs - particle_beam.mu_x.view(-1, 1)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([7.1972e-13, 5.0000e-13])" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.emittance_x" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([7.1935e-13, 4.9953e-13])" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.emittance_x" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([3.5866e-15, 1.0000e-15])" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.emittance_y" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([3.5869e-15, 1.0002e-15])" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.emittance_y" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.0000, 1.0000])" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.relativistic_beta" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.0000, 1.0000])" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.relativistic_beta" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([2.1127e-10, 1.4286e-08])" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.normalized_emittance_x" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.4077e-10, 9.7754e-11])" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.normalized_emittance_x" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([1.0528e-12, 2.8571e-11])" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.normalized_emittance_y" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([7.0193e-13, 1.9574e-13])" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.normalized_emittance_y" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'mu_x': tensor([0., 0.]),\n", - " 'mu_xp': tensor([0., 0.]),\n", - " 'mu_y': tensor([0., 0.]),\n", - " 'mu_yp': tensor([0., 0.]),\n", - " 'sigma_x': tensor([6.6517e-06, 7.0356e-06]),\n", - " 'sigma_xp': tensor([1.7005e-07, 9.5611e-08]),\n", - " 'sigma_y': tensor([3.5642e-07, 2.4495e-07]),\n", - " 'sigma_yp': tensor([1.2088e-08, 4.5644e-09]),\n", - " 'sigma_s': tensor([1.0000e-06, 1.0000e-06]),\n", - " 'sigma_p': tensor([1.0000e-06, 1.0000e-06]),\n", - " 'energy': tensor([1.5000e+08, 1.4600e+10])}" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'mu_x': tensor([-9.0306e-09, 6.7688e-09]),\n", - " 'mu_xp': tensor([-2.0284e-11, 5.9579e-11]),\n", - " 'mu_y': tensor([-3.2570e-10, -4.2466e-11]),\n", - " 'mu_yp': tensor([ 1.1656e-11, -5.8969e-12]),\n", - " 'sigma_x': tensor([6.6523e-06, 7.0303e-06]),\n", - " 'sigma_xp': tensor([1.7008e-07, 9.5575e-08]),\n", - " 'sigma_y': tensor([3.5672e-07, 2.4496e-07]),\n", - " 'sigma_yp': tensor([1.2083e-08, 4.5663e-09]),\n", - " 'sigma_s': tensor([9.9929e-07, 9.9962e-07]),\n", - " 'sigma_p': tensor([9.9936e-07, 1.0004e-06]),\n", - " 'energy': tensor([100000000., 100000000.])}" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([ 293.5427, 28571.4863])" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.relativistic_gamma" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([195.6951, 195.6951])" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.relativistic_gamma" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([61.4750, 99.0000])" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.beta_x" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([61.5176, 98.9439])" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.beta_x" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-1.2124, -0.9000])" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.alpha_x" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-1.2141, -0.8996])" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.alpha_x" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([35.4190, 60.0000])" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.beta_y" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([35.4752, 59.9931])" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.beta_y" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([0.6655, 0.5000])" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.alpha_y" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([0.6664, 0.5006])" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.alpha_y" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([0., 0.])" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.mu_s" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-2.3269e-09, -6.6371e-10])" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.mu_s" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([8.7260e-13, 4.5000e-13])" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.sigma_xxp" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([8.7333e-13, 4.4938e-13])" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.sigma_xxp" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-2.3871e-15, -5.0000e-16])" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.sigma_yyp" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([-2.3901e-15, -5.0073e-16])" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.sigma_yyp" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ic| mu_x: tensor([0.0000e+00, 1.0000e-06])\n", - " mu_x.shape: torch.Size([2])\n", - "ic| mu_xp: tensor([0., 0.]), mu_xp.shape: torch.Size([2])\n", - "ic| mu_y: tensor([0., 0.]), mu_y.shape: torch.Size([2])\n", - "ic| mu_yp: tensor([0., 0.]), mu_yp.shape: torch.Size([2])\n" - ] - }, - { - "data": { - "text/plain": [ - "ParameterBeam(mu_x=tensor([0.0000e+00, 1.0000e-06]), mu_xp=tensor([0., 0.]), mu_y=tensor([0., 0.]), mu_yp=tensor([0., 0.]), sigma_x=tensor([1.7500e-07, 4.2000e-07]), sigma_xp=tensor([1.7005e-07, 9.5611e-08]), sigma_y=tensor([3.5642e-07, 2.4495e-07]), sigma_yp=tensor([1.2088e-08, 4.5644e-09]), sigma_s=tensor([1.0000e-06, 1.0000e-06]), sigma_p=tensor([1.0000e-06, 1.0000e-06]), energy=tensor([1.5000e+08, 1.4600e+10])), total_charge=tensor([1.0000e-12, 2.0000e-12]))" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam.transformed_to(\n", - " mu_x=torch.tensor([0.0, 1e-6]),\n", - " sigma_x=torch.tensor([175e-9, 42e-8]),\n", - " total_charge=torch.tensor([1e-12, 2e-12]),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ParticleBeam(n=1000000, mu_x=tensor([4.7294e-16, 1.0000e-06]), mu_xp=tensor([-2.0284e-11, 5.9579e-11]), mu_y=tensor([-3.2570e-10, -4.2466e-11]), mu_yp=tensor([ 1.1656e-11, -5.8969e-12]), sigma_x=tensor([1.7500e-07, 4.2000e-07]), sigma_xp=tensor([1.7008e-07, 9.5575e-08]), sigma_y=tensor([3.5672e-07, 2.4496e-07]), sigma_yp=tensor([1.2083e-08, 4.5663e-09]), sigma_s=tensor([9.9929e-07, 9.9962e-07]), sigma_p=tensor([9.9936e-07, 1.0004e-06]), energy=tensor([100000000., 100000000.])) total_charge=tensor([1.0000e-12, 2.0000e-12]))" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam.transformed_to(\n", - " mu_x=torch.tensor([0.0, 1e-6]),\n", - " sigma_x=torch.tensor([175e-9, 42e-8]),\n", - " total_charge=torch.tensor([1e-12, 2e-12]),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ParticleBeam(n=10, mu_x=tensor([0.0000e+00, 1.0000e-06]), mu_xp=tensor([0., 0.]), mu_y=tensor([0., 0.]), mu_yp=tensor([0., 0.]), sigma_x=tensor([1.1774e-07, 2.8258e-07]), sigma_xp=tensor([1.3456e-07, 1.3456e-07]), sigma_y=tensor([1.1774e-07, 1.1774e-07]), sigma_yp=tensor([1.3456e-07, 1.3456e-07]), sigma_s=tensor([0., 0.]), sigma_p=tensor([0., 0.]), energy=tensor([100000000., 100000000.])) total_charge=tensor([1.0000e-12, 2.0000e-12]))" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "particle_beam_2 = cheetah.ParticleBeam.make_linspaced(\n", - " mu_x=torch.tensor([0.0, 1e-6]),\n", - " sigma_x=torch.tensor([175e-9, 42e-8]),\n", - " total_charge=torch.tensor([1e-12, 2e-12]),\n", - ")\n", - "particle_beam_2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [], - "source": [ - "# parameter_beam = cheetah.ParameterBeam.from_astra(\n", - "# \"tests/resources/ACHIP_EA1_2021.1351.001\"\n", - "# )\n", - "# particle_beam = cheetah.ParticleBeam.from_astra(\n", - "# \"tests/resources/ACHIP_EA1_2021.1351.001\"\n", - "# )" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Drift(length=tensor([1., 2.]))" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "drift = cheetah.Drift(length=torch.tensor([1.0, 2.0]))\n", - "drift" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ParameterBeam(mu_x=tensor([0., 0.]), mu_xp=tensor([0., 0.]), mu_y=tensor([0., 0.]), mu_yp=tensor([0., 0.]), sigma_x=tensor([6.7837e-06, 7.1650e-06]), sigma_xp=tensor([1.7005e-07, 9.5611e-08]), sigma_y=tensor([3.4987e-07, 2.4100e-07]), sigma_yp=tensor([1.2088e-08, 4.5644e-09]), sigma_s=tensor([1.0000e-06, 1.0000e-06]), sigma_p=tensor([1.0000e-06, 1.0000e-06]), energy=tensor([1.5000e+08, 1.4600e+10])), total_charge=tensor([1.0000e-12, 4.0000e-12]))" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "drift.track(parameter_beam)" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ParticleBeam(n=1000000, mu_x=tensor([-9.0509e-09, 6.8879e-09]), mu_xp=tensor([-2.0284e-11, 5.9579e-11]), mu_y=tensor([-3.1404e-10, -5.4260e-11]), mu_yp=tensor([ 1.1656e-11, -5.8969e-12]), sigma_x=tensor([6.7844e-06, 7.1596e-06]), sigma_xp=tensor([1.7008e-07, 9.5575e-08]), sigma_y=tensor([3.5016e-07, 2.4101e-07]), sigma_yp=tensor([1.2083e-08, 4.5663e-09]), sigma_s=tensor([9.9929e-07, 9.9962e-07]), sigma_p=tensor([9.9936e-07, 1.0004e-06]), energy=tensor([100000000., 100000000.])) total_charge=tensor([1.0000e-12, 4.0000e-12]))" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "drift.track(particle_beam)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ic| mu_x: tensor([0.0000e+00, 1.0000e-06, 2.0000e-06])\n", - " mu_x.shape: torch.Size([3])\n", - "ic| mu_xp: tensor([0., 0., 0.]), mu_xp.shape: torch.Size([3])\n", - "ic| mu_y: tensor([0., 0., 0.]), mu_y.shape: torch.Size([3])\n", - "ic| mu_yp: tensor([0., 0., 0.]), mu_yp.shape: torch.Size([3])\n" - ] - }, - { - "data": { - "text/plain": [ - "ParameterBeam(mu_x=tensor([0.0000e+00, 1.0000e-06, 2.0000e-06]), mu_xp=tensor([0., 0., 0.]), mu_y=tensor([0., 0., 0.]), mu_yp=tensor([0., 0., 0.]), sigma_x=tensor([1.7500e-07, 4.2000e-07, 4.2000e-07]), sigma_xp=tensor([2.0000e-07, 2.0000e-07, 2.0000e-07]), sigma_y=tensor([1.7500e-07, 1.7500e-07, 1.7500e-07]), sigma_yp=tensor([2.0000e-07, 2.0000e-07, 2.0000e-07]), sigma_s=tensor([1.0000e-06, 1.0000e-06, 1.0000e-06]), sigma_p=tensor([1.0000e-06, 1.0000e-06, 1.0000e-06]), energy=tensor([100000000., 100000000., 100000000.])), total_charge=tensor([1.0000e-12, 2.0000e-12, 2.0000e-12]))" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parameter_beam_3 = cheetah.ParameterBeam.from_parameters(\n", - " mu_x=torch.tensor([0.0, 1e-6, 2e-6]),\n", - " sigma_x=torch.tensor([175e-9, 42e-8, 42e-8]),\n", - " total_charge=torch.tensor([1e-12, 2e-12, 2e-12]),\n", - ")\n", - "parameter_beam_3" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Screen(resolution=tensor([1024, 1024]), pixel_size=tensor([0.0010, 0.0010]), binning=tensor(1), misalignment=tensor([[1.0000e-06, 2.0000e-06],\n", - " [3.0000e-06, 4.0000e-06],\n", - " [5.0000e-06, 6.0000e-06]]), is_active=True, name='unnamed_element_1')" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "screen = cheetah.Screen(\n", - " misalignment=torch.tensor([[1e-6, 2e-6], [3e-6, 4e-6], [5e-6, 6e-6]])\n", - ")\n", - "screen.is_active = True\n", - "screen" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ic| self.misalignment: tensor([[1.0000e-06, 2.0000e-06],\n", - " [3.0000e-06, 4.0000e-06],\n", - " [5.0000e-06, 6.0000e-06]])\n", - " self.misalignment.shape: torch.Size([3, 2])\n", - "ic| copy_of_incoming._mu: tensor([[0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,\n", - " 1.0000e+00],\n", - " [1.0000e-06, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,\n", - " 1.0000e+00],\n", - " [2.0000e-06, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,\n", - " 1.0000e+00]])\n", - " copy_of_incoming._mu.shape: torch.Size([3, 7])\n", - "ic| self.misalignment[:, 0]: tensor([1.0000e-06, 3.0000e-06, 5.0000e-06])\n", - " self.misalignment[:, 0].shape: torch.Size([3])\n", - "ic| copy_of_incoming._mu[:, 0]: tensor([0.0000e+00, 1.0000e-06, 2.0000e-06])\n", - " copy_of_incoming._mu[:, 0].shape: torch.Size([3])\n", - "ic| copy_of_incoming._mu: tensor([[-1.0000e-06, 0.0000e+00, -2.0000e-06, 0.0000e+00, 0.0000e+00,\n", - " 0.0000e+00, 1.0000e+00],\n", - " [-2.0000e-06, 0.0000e+00, -4.0000e-06, 0.0000e+00, 0.0000e+00,\n", - " 0.0000e+00, 1.0000e+00],\n", - " [-3.0000e-06, 0.0000e+00, -6.0000e-06, 0.0000e+00, 0.0000e+00,\n", - " 0.0000e+00, 1.0000e+00]])\n", - " copy_of_incoming._mu.shape: torch.Size([3, 7])\n" - ] - }, - { - "data": { - "text/plain": [ - "\"I'm an empty beam!\"" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "screen.track(parameter_beam_3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cheetah-dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.18" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From fea6c839100ff3cde7759c5e15dcbee8dc5dd929 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Wed, 19 Jun 2024 13:36:40 +0200 Subject: [PATCH 088/111] Implement plotting for `SpaceChargeKick` --- cheetah/accelerator/space_charge_kick.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index ba34fed3..2d96e617 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -1,6 +1,6 @@ from typing import Optional, Union -import matplotlib +import matplotlib.pyplot as plt import torch from scipy import constants from torch import nn @@ -613,8 +613,15 @@ def split(self, resolution: torch.Tensor) -> list[Element]: def is_skippable(self) -> bool: return False - def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: - pass + def plot(self, ax: plt.Axes, s: float) -> None: + ax.axvspan( + s - self.effect_length[0], + s, + facecolor="none", + edgecolor="orange", + alpha=1.0, + hatch="/", + ) @property def defining_features(self) -> list[str]: From fd9ee34df520fe33bb532bb7aae38a1fa303fe53 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Wed, 19 Jun 2024 16:10:31 -0700 Subject: [PATCH 089/111] Use int instead of long --- cheetah/accelerator/space_charge_kick.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 2d96e617..9f2adafa 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -112,7 +112,7 @@ def _deposit_charge_on_grid( ) / cell_size.unsqueeze(-2) # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_positions).type(torch.long) + cell_indices = torch.floor(normalized_positions).type(torch.int) # Calculate the weights for all surrounding cells offsets = torch.tensor( @@ -453,7 +453,7 @@ def _compute_forces( ) / cell_size.unsqueeze(-2) # Find indices of the lower corners of the cells containing the particles - cell_indices = torch.floor(normalized_positions).type(torch.long) + cell_indices = torch.floor(normalized_positions).type(torch.int) # Calculate the weights for all surrounding cells offsets = torch.tensor( From 922417b3240c9487ebb3b17ab79066e8df0f3abc Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Thu, 20 Jun 2024 12:46:11 +0200 Subject: [PATCH 090/111] Updates to and from moments conversion method names --- cheetah/accelerator/space_charge_kick.py | 4 ++-- cheetah/particles/particle_beam.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 2d96e617..89543484 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -564,7 +564,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: ) # Change coordinates to apply the space charge effect - moments = flattened_incoming.to_moments() + moments = flattened_incoming.to_xyz_pxpypz() forces = self._compute_forces( flattened_incoming, moments, cell_size, grid_dimensions ) @@ -572,7 +572,7 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: moments[..., 3] = moments[..., 3] + forces[..., 1] * dt.unsqueeze(-1) moments[..., 5] = moments[..., 5] + forces[..., 2] * dt.unsqueeze(-1) - outgoing = ParticleBeam.from_moments( + outgoing = ParticleBeam.from_xyz_pxpypz( moments.unflatten(dim=0, sizes=incoming.particles.shape[:-2]), incoming.energy, incoming.particle_charges, diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 87c8c0f3..80d4d84d 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -726,7 +726,7 @@ def transformed_to( ) @classmethod - def from_moments( + def from_xyz_pxpypz( cls, moments: torch.Tensor, energy: torch.Tensor, @@ -763,7 +763,7 @@ def from_moments( return beam - def to_moments(self) -> torch.Tensor: + def to_xyz_pxpypz(self) -> torch.Tensor: """Moments in SI units as converted from the beam's `particles`.""" p0 = ( self.relativistic_gamma From 86ffa530ffbe91e8a5bc665bbbe56da5ce7286a1 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Thu, 20 Jun 2024 15:13:41 +0200 Subject: [PATCH 091/111] Change `SpaceChargeKick` plotting to a single line --- cheetah/accelerator/space_charge_kick.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index df36f25c..767c4356 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -614,14 +614,7 @@ def is_skippable(self) -> bool: return False def plot(self, ax: plt.Axes, s: float) -> None: - ax.axvspan( - s - self.effect_length[0], - s, - facecolor="none", - edgecolor="orange", - alpha=1.0, - hatch="/", - ) + ax.axvline(s, ymin=0.01, ymax=0.99, color="orange", linestyle="-") @property def defining_features(self) -> list[str]: From 61cd0b4e98468bf0be7d37a35a70802c1231e3ae Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Thu, 20 Jun 2024 15:20:28 +0200 Subject: [PATCH 092/111] Implement `SpaceChargeKick.defining_features` --- cheetah/accelerator/space_charge_kick.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 767c4356..9061fcdd 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -618,7 +618,13 @@ def plot(self, ax: plt.Axes, s: float) -> None: @property def defining_features(self) -> list[str]: - return super().defining_features + ["length"] + return super().defining_features + [ + "effect_length", + "grid_shape", + "grid_extend_x", + "grid_extend_y", + "grid_extend_s", + ] def __repr__(self) -> str: return f"{self.__class__.__name__}(length={repr(self.length)})" From a53a45037df838f61a367b38b03a9a516a61a475 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Thu, 20 Jun 2024 15:25:43 +0200 Subject: [PATCH 093/111] Implement `SpaceChargeKick.__repr__` --- cheetah/accelerator/quadrupole.py | 2 +- cheetah/accelerator/space_charge_kick.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cheetah/accelerator/quadrupole.py b/cheetah/accelerator/quadrupole.py index 87022e87..9a68b6c9 100644 --- a/cheetah/accelerator/quadrupole.py +++ b/cheetah/accelerator/quadrupole.py @@ -114,7 +114,7 @@ def plot(self, ax: plt.Axes, s: float) -> None: def defining_features(self) -> list[str]: return super().defining_features + ["length", "k1", "misalignment", "tilt"] - def __repr__(self) -> None: + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(length={repr(self.length)}, " + f"k1={repr(self.k1)}, " diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 9061fcdd..7210b157 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -627,4 +627,13 @@ def defining_features(self) -> list[str]: ] def __repr__(self) -> str: - return f"{self.__class__.__name__}(length={repr(self.length)})" + return ( + f"{self.__class__.__name__}(effect_length={repr(self.effect_length)}, " + + f"num_grid_points_x={repr(self.grid_shape[0])}, " + + f"num_grid_points_y={repr(self.grid_shape[1])}, " + + f"num_grid_points_s={repr(self.grid_shape[2])}, " + + f"grid_extend_x={repr(self.grid_extend_x)}, " + + f"grid_extend_y={repr(self.grid_extend_y)}, " + + f"grid_extend_s={repr(self.grid_extend_s)}, " + + f"name={repr(self.name)})" + ) From 2ca6cf8846e097745dd044369a283814f859b00d Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Thu, 20 Jun 2024 15:26:13 +0200 Subject: [PATCH 094/111] Move grid extent sigma comment up by one line --- cheetah/accelerator/space_charge_kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 7210b157..e01a42dc 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -70,8 +70,8 @@ def __init__( int(num_grid_points_y), int(num_grid_points_s), ) - self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) # In multiples of sigma + self.grid_extend_x = torch.as_tensor(grid_extend_x, **self.factory_kwargs) self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) From 4092d82e6c798612af1e798f75eb9fb186670323 Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Fri, 21 Jun 2024 11:58:58 +0200 Subject: [PATCH 095/111] Improve docstring and comments --- cheetah/particles/particle_beam.py | 5 ++++- tests/test_space_charge_kick.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index f3b4245f..b6a7e22f 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -760,7 +760,10 @@ def from_xyz_pxpypz( return beam def to_xyz_pxpypz(self) -> torch.Tensor: - """Moments in SI units as converted from the beam's `particles`.""" + """Moments in SI units as converted from the beam's `particles`. + Returns the moments tensor with shape (..., n_particles, 7) + For each particle, the moment vector is $(x, p_x, y, p_y, z, p_z, 1)$ + """ p0 = ( self.relativistic_gamma * self.relativistic_beta diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index ef2e3fce..9e61a1e5 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -43,7 +43,7 @@ def test_cold_uniform_beam_expansion(): sigma_p=torch.tensor([1e-15]), ) - # Compute section lenght + # Compute section length kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( 3 + 2 * torch.sqrt(torch.tensor(2)) ) @@ -149,7 +149,7 @@ def test_vectorized_cold_uniform_beam_expansion(): sigma_p=torch.tensor([1e-15]), ).broadcast(shape=(2, 3)) - # Compute section lenght + # Compute section length kappa = 1 + (torch.sqrt(torch.tensor(2)) / 4) * torch.log( 3 + 2 * torch.sqrt(torch.tensor(2)) ) From 5e3df7869b18a7ff86a0d9a40df7b6a7c8776659 Mon Sep 17 00:00:00 2001 From: Chenran Xu Date: Fri, 21 Jun 2024 12:01:09 +0200 Subject: [PATCH 096/111] Update cheetah/particles/particle_beam.py --- cheetah/particles/particle_beam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index b6a7e22f..4505a5de 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -761,8 +761,8 @@ def from_xyz_pxpypz( def to_xyz_pxpypz(self) -> torch.Tensor: """Moments in SI units as converted from the beam's `particles`. - Returns the moments tensor with shape (..., n_particles, 7) - For each particle, the moment vector is $(x, p_x, y, p_y, z, p_z, 1)$ + Returns the moments tensor with shape (..., n_particles, 7) + For each particle, the moment vector is $(x, p_x, y, p_y, z, p_z, 1)$ """ p0 = ( self.relativistic_gamma From f3aea769e1882d34d332fcacd6df08b6d91b78c1 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Fri, 21 Jun 2024 12:11:51 +0200 Subject: [PATCH 097/111] Slight change to comment formatting --- cheetah/particles/particle_beam.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 4505a5de..f6b13151 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -760,9 +760,10 @@ def from_xyz_pxpypz( return beam def to_xyz_pxpypz(self) -> torch.Tensor: - """Moments in SI units as converted from the beam's `particles`. - Returns the moments tensor with shape (..., n_particles, 7) - For each particle, the moment vector is $(x, p_x, y, p_y, z, p_z, 1)$ + """ + Moments in SI units as converted from the beam's `particles`. Returns the + moments tensor with shape (..., n_particles, 7). For each particle, the moment + vector is \((x, p_x, y, p_y, z, p_z, 1)\). """ p0 = ( self.relativistic_gamma From 1e8235a1f61dfa1a443666757a057110ab74fbe0 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Fri, 21 Jun 2024 16:34:34 +0200 Subject: [PATCH 098/111] Fix flake8 warning --- cheetah/particles/particle_beam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index f6b13151..417cf85f 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -763,7 +763,7 @@ def to_xyz_pxpypz(self) -> torch.Tensor: """ Moments in SI units as converted from the beam's `particles`. Returns the moments tensor with shape (..., n_particles, 7). For each particle, the moment - vector is \((x, p_x, y, p_y, z, p_z, 1)\). + vector is $(x, p_x, y, p_y, z, p_z, 1)$. """ p0 = ( self.relativistic_gamma From 3e2503ad6185afac44946d5dde6c964ed864fa42 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Fri, 21 Jun 2024 16:35:51 +0200 Subject: [PATCH 099/111] Improve docstring for `from_xyz_pxpypz` as well --- cheetah/particles/particle_beam.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 417cf85f..885d80f1 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -730,7 +730,11 @@ def from_xyz_pxpypz( device=None, dtype=torch.float32, ) -> torch.Tensor: - """Converts the moments in SI units to a `ParticleBeam`.""" + """ + Create a beam from a tensor of moments in SI units. The moments tensor should + have shape (..., n_particles, 7), where the last dimension is the moment vector + $(x, p_x, y, p_y, z, p_z, 1)$. + """ beam = cls( particles=moments.clone(), energy=energy, From c8134161b0eb34cd5c8243b28b348ad2c7ae9bbb Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 22 Jun 2024 01:12:51 +0200 Subject: [PATCH 100/111] Tiny formating improvement --- tests/test_space_charge_kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_space_charge_kick.py b/tests/test_space_charge_kick.py index 9e61a1e5..d487cd8d 100644 --- a/tests/test_space_charge_kick.py +++ b/tests/test_space_charge_kick.py @@ -183,7 +183,7 @@ def test_incoming_beam_not_modified(): torch.manual_seed(42) incoming_beam = cheetah.ParticleBeam.from_parameters( - num_particles=torch.tensor(10000), + num_particles=torch.tensor(10_000), sigma_xp=torch.tensor([2e-7]), sigma_yp=torch.tensor([2e-7]), ) From d0ccaa73bb092a759da4dd243c671b0aaf5cdfec Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 22 Jun 2024 01:22:06 +0200 Subject: [PATCH 101/111] Fix main docstring --- cheetah/accelerator/space_charge_kick.py | 47 +++++++++++++----------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index e01a42dc..e1aff97d 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -17,33 +17,36 @@ class SpaceChargeKick(Element): """ - Applies the effect of space charge over a length `length`, on the **momentum** - (i.e. divergence and energy spread) of the beam. - The positions are unmodified ; this is meant to be combined with another lattice - element (e.g. `Drift`) that does modify the positions, but does not take into - account space charge. - This uses the integrated Green function method - (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) to compute - the effect of space charge. This is similar to the method used in Ocelot. - The main difference is that it solves the Poisson equation in the beam frame, - while here we solve a modified Poisson equation in the laboratory frame + Applies the effect of space charge over a length `effect_length`, on the **momentum** + (i.e. divergence and energy spread) of the beam. The positions are unmodified; this + is meant to be combined with another lattice element (e.g. `Drift`) that does modify + the positions, but does not take into account space charge. + The integrated Green function method + (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) is used to + compute the effect of space charge. This is similar to the method used in Ocelot. + The main difference is that it solves the Poisson equation in the beam frame, while + here we solve a modified Poisson equation in the laboratory frame (https://pubs.aip.org/aip/pop/article-abstract/15/5/056701/1016636/Simulation-of-beams-or-plasmas-crossing-at). The two methods are in principle equivalent. Overview of the method: - - Compute the beam charge density on a grid + - Compute the beam charge density on a grid. - Convolve the charge density with a Green function (the integrated green function) - to find the potential `phi` on the grid. The convolution uses the Hockney method - for open boundaries (allocate 2x larger arrays and perform convolution using FFTs) - - Compute the corresponding electromagnetic fields and Lorentz force on the grid - - Interpolate the Lorentz force to the particles and update their momentum - - :param length_effect: Length over which the effect applies in meters. - :param length: Physical length of the element in meters (=0) - :param num_grid_points_x, num_grid_points_y, num_grid_points_s: Number of grid - points in each dimension. - :param grid_extend_x, grid_extend_y, grid_extend_s: Dimensions of the grid on which - to compute space-charge, as multiples of sigma of the beam (dimensionless) + to find the potential `phi` on the grid. The convolution uses the Hockney method for + open boundaries (allocate 2x larger arrays and perform convolution using FFTs). + - Compute the corresponding electromagnetic fields and Lorentz force on the grid. + - Interpolate the Lorentz force to the particles and update their momentum. + + :param effect_length: Length over which the effect is applied in meters. + :param num_grid_points_x: Number of grid points in the x direction. + :param num_grid_points_y: Number of grid points in the y direction. + :param num_grid_points_s: Number of grid points in the s direction. + :param grid_extend_x: Dimensions of the grid on which to compute space-charge, as + multiples of sigma of the beam (dimensionless). + :param grid_extend_y: Dimensions of the grid on which to compute space-charge, as + multiples of sigma of the beam (dimensionless). + :param grid_extend_s: Dimensions of the grid on which to compute space-charge, as + multiples of sigma of the beam (dimensionless). :param name: Unique identifier of the element. """ From daa08b96b5da7c8f5f1a243db61565b81597189e Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 22 Jun 2024 01:23:08 +0200 Subject: [PATCH 102/111] Another fix to the docstring --- cheetah/accelerator/space_charge_kick.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index e1aff97d..a9ddc536 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -17,11 +17,11 @@ class SpaceChargeKick(Element): """ - Applies the effect of space charge over a length `effect_length`, on the **momentum** - (i.e. divergence and energy spread) of the beam. The positions are unmodified; this - is meant to be combined with another lattice element (e.g. `Drift`) that does modify - the positions, but does not take into account space charge. - The integrated Green function method + Applies the effect of space charge over a length `effect_length`, on the + **momentum** (i.e. divergence and energy spread) of the beam. The positions are + unmodified; this is meant to be combined with another lattice element (e.g. `Drift`) + that does modify the positions, but does not take into account space charge. The + integrated Green function method (https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.9.044204) is used to compute the effect of space charge. This is similar to the method used in Ocelot. The main difference is that it solves the Poisson equation in the beam frame, while From 595ab2c7ea2ee012e5be38ce0afaa7cec81fe206 Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 22 Jun 2024 01:29:59 +0200 Subject: [PATCH 103/111] Attempt to fix docstring bullet point rendering --- cheetah/accelerator/space_charge_kick.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index a9ddc536..0402bcd5 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -30,12 +30,13 @@ class SpaceChargeKick(Element): The two methods are in principle equivalent. Overview of the method: - - Compute the beam charge density on a grid. - - Convolve the charge density with a Green function (the integrated green function) - to find the potential `phi` on the grid. The convolution uses the Hockney method for - open boundaries (allocate 2x larger arrays and perform convolution using FFTs). - - Compute the corresponding electromagnetic fields and Lorentz force on the grid. - - Interpolate the Lorentz force to the particles and update their momentum. + - Compute the beam charge density on a grid. + - Convolve the charge density with a Green function (the integrated green function) + to find the potential `phi` on the grid. The convolution uses the Hockney method + for open boundaries (allocate 2x larger arrays and perform convolution using + FFTs). + - Compute the corresponding electromagnetic fields and Lorentz force on the grid. + - Interpolate the Lorentz force to the particles and update their momentum. :param effect_length: Length over which the effect is applied in meters. :param num_grid_points_x: Number of grid points in the x direction. From 4d9b3112698227b22d7156d18eb2946d6c7ff44d Mon Sep 17 00:00:00 2001 From: Jan Kaiser Date: Sat, 22 Jun 2024 15:08:46 +0200 Subject: [PATCH 104/111] Another bullet point docstring fix --- cheetah/accelerator/space_charge_kick.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 0402bcd5..475ce27c 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -32,9 +32,9 @@ class SpaceChargeKick(Element): Overview of the method: - Compute the beam charge density on a grid. - Convolve the charge density with a Green function (the integrated green function) - to find the potential `phi` on the grid. The convolution uses the Hockney method - for open boundaries (allocate 2x larger arrays and perform convolution using - FFTs). + to find the potential `phi` on the grid. The convolution uses the Hockney method + for open boundaries (allocate 2x larger arrays and perform convolution using + FFTs). - Compute the corresponding electromagnetic fields and Lorentz force on the grid. - Interpolate the Lorentz force to the particles and update their momentum. From ee07a52fed26182a8f8e960ca0f0b010253ed5d4 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:04:47 -0700 Subject: [PATCH 105/111] Some refactoring --- cheetah/accelerator/space_charge_kick.py | 28 +++++++++--------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 475ce27c..36239657 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -79,20 +79,6 @@ def __init__( self.grid_extend_y = torch.as_tensor(grid_extend_y, **self.factory_kwargs) self.grid_extend_s = torch.as_tensor(grid_extend_s, **self.factory_kwargs) - def _compute_grid_dimensions(self, beam: ParticleBeam) -> torch.Tensor: - """ - Computes the dimensions of the grid on which to compute the space charge effect. - """ - # TODO: Refactor ... might not need to be a method - return torch.stack( - [ - self.grid_extend_x * beam.sigma_x, - self.grid_extend_y * beam.sigma_y, - self.grid_extend_s * beam.sigma_s, - ], - dim=-1, - ) - def _deposit_charge_on_grid( self, beam: ParticleBeam, @@ -101,9 +87,8 @@ def _deposit_charge_on_grid( grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ - Deposits the charge density of the beam onto a grid, using the nearest grid - point method and weighting by the distance to the grid points. Returns a grid of - charge density in C/m^3. + Deposits the charge density of the beam onto a grid, using the Cloud-In-Cell (CIC) + method. Returns a grid of charge density in C/m^3. """ charge = torch.zeros( beam.particles.shape[:-2] + self.grid_shape, **self.factory_kwargs @@ -561,7 +546,14 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: flattened_length_effect = self.effect_length.flatten(end_dim=-1) # Compute useful quantities - grid_dimensions = self._compute_grid_dimensions(flattened_incoming) + grid_dimensions = torch.stack( + [ + self.grid_extend_x * flattened_incoming.sigma_x, + self.grid_extend_y * flattened_incoming.sigma_y, + self.grid_extend_s * flattened_incoming.sigma_s, + ], + dim=-1, + ) cell_size = 2 * grid_dimensions / torch.tensor(self.grid_shape) dt = flattened_length_effect / ( speed_of_light * flattened_incoming.relativistic_beta From 1a0131cd5ccb4723f7c007c411c8049201b64f76 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:14:24 -0700 Subject: [PATCH 106/111] Simplify division by cell volume --- cheetah/accelerator/space_charge_kick.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 36239657..1a24d73e 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -94,11 +94,14 @@ def _deposit_charge_on_grid( beam.particles.shape[:-2] + self.grid_shape, **self.factory_kwargs ) + # Compute inverse cell size (to avoid multiple divisions later on) + inv_cell_size = 1 / cell_size + # Get particle positions particle_positions = moments[..., [0, 2, 4]] normalized_positions = ( particle_positions + grid_dimensions.unsqueeze(-2) - ) / cell_size.unsqueeze(-2) + ) * inv_cell_size.unsqueeze(-2) # Find indices of the lower corners of the cells containing the particles cell_indices = torch.floor(normalized_positions).type(torch.int) @@ -161,12 +164,12 @@ def _deposit_charge_on_grid( accumulate=True, ) - return ( - charge - / (cell_size[..., 0] * cell_size[..., 1] * cell_size[..., 2])[ - ..., None, None, None - ] - ) # Normalize by the cell volume + # Normalize by the cell volume + inv_cell_volume = ( + inv_cell_size[..., 0] * inv_cell_size[..., 1] * inv_cell_size[..., 2] + ) + + return charge * inv_cell_volume[..., None, None, None] def _integrated_potential( self, x: torch.Tensor, y: torch.Tensor, s: torch.Tensor From 913e1a8139fc18980b7f9d9a12d41f155d5f7e8d Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:22:44 -0700 Subject: [PATCH 107/111] Update a few docstrings --- cheetah/accelerator/space_charge_kick.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 1a24d73e..6d8f65f1 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -175,8 +175,11 @@ def _integrated_potential( self, x: torch.Tensor, y: torch.Tensor, s: torch.Tensor ) -> torch.Tensor: """ - Computes the electrostatic potential using the Integrated Green Function method - as in http://dx.doi.org/10.1103/PhysRevSTAB.9.044204. + Computes the integrate potential as in + https://journals.aps.org/prab/abstract/10.1103/PhysRevSTAB.10.129901 + The formula used here is slightly different than the one used in + the above paper, but is equivalent (up to integration constants), + and is more robust to numerical errors. """ r = torch.sqrt(x**2 + y**2 + s**2) @@ -225,14 +228,16 @@ def _integrated_green_function( self, beam: ParticleBeam, cell_size: torch.Tensor ) -> torch.Tensor: """ - Computes the Integrated Green Function (IGF) with periodic boundary conditions - (to perform Hockney's method). + Computes the Integrated Green Function (IGF) in the 2x larger array, + as needed for the Hockney method. """ dx, dy, ds = ( cell_size[..., 0], cell_size[..., 1], cell_size[..., 2] * beam.relativistic_gamma, - ) # Scaled by gamma + # The longitudinal dimension is scaled by gamma, since we are solving + # a modified Poisson equation in the lab frame (see the docstring of the class) + ) num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape # Create coordinate grids From 42e4faf92f170c1eeefb3d64f660c6a06fe9043f Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:27:42 -0700 Subject: [PATCH 108/111] Reduce length of lines --- cheetah/accelerator/space_charge_kick.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 6d8f65f1..87e87179 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -87,8 +87,8 @@ def _deposit_charge_on_grid( grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ - Deposits the charge density of the beam onto a grid, using the Cloud-In-Cell (CIC) - method. Returns a grid of charge density in C/m^3. + Deposits the charge density of the beam onto a grid, using the + Cloud-In-Cell (CIC) method. Returns a grid of charge density in C/m^3. """ charge = torch.zeros( beam.particles.shape[:-2] + self.grid_shape, **self.factory_kwargs @@ -235,8 +235,8 @@ def _integrated_green_function( cell_size[..., 0], cell_size[..., 1], cell_size[..., 2] * beam.relativistic_gamma, - # The longitudinal dimension is scaled by gamma, since we are solving - # a modified Poisson equation in the lab frame (see the docstring of the class) + # The longitudinal dimension is scaled by gamma, since we are solving a + # modified Poisson equation in the lab frame (see docstring of the class) ) num_grid_points_x, num_grid_points_y, num_grid_points_s = self.grid_shape From ee2096edc9713bf12ccd526432161a26f2b5ab28 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:50:18 -0700 Subject: [PATCH 109/111] Update name from moments to xp_coordinates --- cheetah/accelerator/space_charge_kick.py | 48 +++++++++++++++--------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index 87e87179..dd0124ae 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -82,7 +82,7 @@ def __init__( def _deposit_charge_on_grid( self, beam: ParticleBeam, - moments: torch.Tensor, + xp_coordinates: torch.Tensor, cell_size: torch.Tensor, grid_dimensions: torch.Tensor, ) -> torch.Tensor: @@ -98,7 +98,7 @@ def _deposit_charge_on_grid( inv_cell_size = 1 / cell_size # Get particle positions - particle_positions = moments[..., [0, 2, 4]] + particle_positions = xp_coordinates[..., [0, 2, 4]] normalized_positions = ( particle_positions + grid_dimensions.unsqueeze(-2) ) * inv_cell_size.unsqueeze(-2) @@ -196,7 +196,7 @@ def _integrated_potential( def _array_rho( self, beam: ParticleBeam, - moments: torch.Tensor, + xp_coordinates: torch.Tensor, cell_size: torch.Tensor, grid_dimensions: torch.Tensor, ) -> torch.Tensor: @@ -205,7 +205,7 @@ def _array_rho( copies the charge density in one of the "quadrants". """ charge_density = self._deposit_charge_on_grid( - beam, moments, cell_size, grid_dimensions + beam, xp_coordinates, cell_size, grid_dimensions ) new_dims = tuple(2 * dim for dim in self.grid_shape) @@ -356,12 +356,18 @@ def _integrated_green_function( return green_func_values def _solve_poisson_equation( - self, beam: ParticleBeam, moments: torch.Tensor, cell_size, grid_dimensions + self, + beam: ParticleBeam, + xp_coordinates: torch.Tensor, + cell_size, + grid_dimensions, ) -> torch.Tensor: # Works only for ParticleBeam at this stage """ Solves the Poisson equation for the given charge density, using FFT convolution. """ - charge_density = self._array_rho(beam, moments, cell_size, grid_dimensions) + charge_density = self._array_rho( + beam, xp_coordinates, cell_size, grid_dimensions + ) charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) integrated_green_function = self._integrated_green_function(beam, cell_size) integrated_green_function_ft = torch.fft.fftn( @@ -383,13 +389,13 @@ def _solve_poisson_equation( def _E_plus_vB_field( self, beam: ParticleBeam, - moments: torch.Tensor, + xp_coordinates: torch.Tensor, cell_size: torch.Tensor, grid_dimensions: torch.Tensor, ) -> torch.Tensor: """ Computes the force field from the potential and the particle positions and - speeds, as in https://doi.org/10.1063/1.2837054. + velocities, as in https://doi.org/10.1063/1.2837054. """ inv_cell_size = 1 / cell_size igamma2 = torch.zeros_like(beam.relativistic_gamma) @@ -397,7 +403,7 @@ def _E_plus_vB_field( 1 / beam.relativistic_gamma[beam.relativistic_gamma != 0] ** 2 ) potential = self._solve_poisson_equation( - beam, moments, cell_size, grid_dimensions + beam, xp_coordinates, cell_size, grid_dimensions ) grad_x = torch.zeros_like(potential) @@ -426,7 +432,7 @@ def _E_plus_vB_field( def _compute_forces( self, beam: ParticleBeam, - moments: torch.Tensor, + xp_coordinates: torch.Tensor, cell_size: torch.Tensor, grid_dimensions: torch.Tensor, ) -> torch.Tensor: @@ -436,7 +442,7 @@ def _compute_forces( Beam needs to have a flattened batch shape. """ grad_x, grad_y, grad_z = self._E_plus_vB_field( - beam, moments, cell_size, grid_dimensions + beam, xp_coordinates, cell_size, grid_dimensions ) grid_shape = self.grid_shape interpolated_forces = torch.zeros( @@ -444,7 +450,7 @@ def _compute_forces( ) # (..., num_particles, 3) # Get particle positions - particle_positions = moments[..., [0, 2, 4]] + particle_positions = xp_coordinates[..., [0, 2, 4]] normalized_positions = ( particle_positions + grid_dimensions.unsqueeze(-2) ) / cell_size.unsqueeze(-2) @@ -568,16 +574,22 @@ def track(self, incoming: ParticleBeam) -> ParticleBeam: ) # Change coordinates to apply the space charge effect - moments = flattened_incoming.to_xyz_pxpypz() + xp_coordinates = flattened_incoming.to_xyz_pxpypz() forces = self._compute_forces( - flattened_incoming, moments, cell_size, grid_dimensions + flattened_incoming, xp_coordinates, cell_size, grid_dimensions ) - moments[..., 1] = moments[..., 1] + forces[..., 0] * dt.unsqueeze(-1) - moments[..., 3] = moments[..., 3] + forces[..., 1] * dt.unsqueeze(-1) - moments[..., 5] = moments[..., 5] + forces[..., 2] * dt.unsqueeze(-1) + xp_coordinates[..., 1] = xp_coordinates[..., 1] + forces[ + ..., 0 + ] * dt.unsqueeze(-1) + xp_coordinates[..., 3] = xp_coordinates[..., 3] + forces[ + ..., 1 + ] * dt.unsqueeze(-1) + xp_coordinates[..., 5] = xp_coordinates[..., 5] + forces[ + ..., 2 + ] * dt.unsqueeze(-1) outgoing = ParticleBeam.from_xyz_pxpypz( - moments.unflatten(dim=0, sizes=incoming.particles.shape[:-2]), + xp_coordinates.unflatten(dim=0, sizes=incoming.particles.shape[:-2]), incoming.energy, incoming.particle_charges, incoming.particles.device, From 1e1888fc6853d880d9eaa065bbe3bea6a75cac8f Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 07:55:13 -0700 Subject: [PATCH 110/111] Use rfft and irfft --- cheetah/accelerator/space_charge_kick.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cheetah/accelerator/space_charge_kick.py b/cheetah/accelerator/space_charge_kick.py index dd0124ae..a8610272 100644 --- a/cheetah/accelerator/space_charge_kick.py +++ b/cheetah/accelerator/space_charge_kick.py @@ -368,13 +368,13 @@ def _solve_poisson_equation( charge_density = self._array_rho( beam, xp_coordinates, cell_size, grid_dimensions ) - charge_density_ft = torch.fft.fftn(charge_density, dim=[1, 2, 3]) + charge_density_ft = torch.fft.rfftn(charge_density, dim=[1, 2, 3]) integrated_green_function = self._integrated_green_function(beam, cell_size) - integrated_green_function_ft = torch.fft.fftn( + integrated_green_function_ft = torch.fft.rfftn( integrated_green_function, dim=[1, 2, 3] ) potential_ft = charge_density_ft * integrated_green_function_ft - potential = (1 / (4 * torch.pi * epsilon_0)) * torch.fft.ifftn( + potential = (1 / (4 * torch.pi * epsilon_0)) * torch.fft.irfftn( potential_ft, dim=[1, 2, 3] ).real From b2e45c071fddd3c1a08b5da03c4eafc7876cce95 Mon Sep 17 00:00:00 2001 From: Remi Lehe Date: Tue, 25 Jun 2024 08:56:53 -0700 Subject: [PATCH 111/111] Change from moments to xp_coords --- cheetah/particles/particle_beam.py | 46 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/cheetah/particles/particle_beam.py b/cheetah/particles/particle_beam.py index 885d80f1..d5d464bd 100644 --- a/cheetah/particles/particle_beam.py +++ b/cheetah/particles/particle_beam.py @@ -724,19 +724,19 @@ def transformed_to( @classmethod def from_xyz_pxpypz( cls, - moments: torch.Tensor, + xp_coords: torch.Tensor, energy: torch.Tensor, particle_charges: Optional[torch.Tensor] = None, device=None, dtype=torch.float32, ) -> torch.Tensor: """ - Create a beam from a tensor of moments in SI units. The moments tensor should - have shape (..., n_particles, 7), where the last dimension is the moment vector - $(x, p_x, y, p_y, z, p_z, 1)$. + Create a beam from a tensor of position and momentum coordinates in SI units. + This tensor should have shape (..., n_particles, 7), where the last dimension + is the moment vector $(x, p_x, y, p_y, z, p_z, 1)$. """ beam = cls( - particles=moments.clone(), + particles=xp_coords.clone(), energy=energy, particle_charges=particle_charges, device=device, @@ -750,13 +750,15 @@ def from_xyz_pxpypz( * speed_of_light ) p = torch.sqrt( - moments[..., 1] ** 2 + moments[..., 3] ** 2 + moments[..., 5] ** 2 + xp_coords[..., 1] ** 2 + xp_coords[..., 3] ** 2 + xp_coords[..., 5] ** 2 ) gamma = torch.sqrt(1 + (p / (electron_mass * speed_of_light)) ** 2) - beam.particles[..., 1] = moments[..., 1] / p0.unsqueeze(-1) - beam.particles[..., 3] = moments[..., 3] / p0.unsqueeze(-1) - beam.particles[..., 4] = -moments[..., 4] / beam.relativistic_beta.unsqueeze(-1) + beam.particles[..., 1] = xp_coords[..., 1] / p0.unsqueeze(-1) + beam.particles[..., 3] = xp_coords[..., 3] / p0.unsqueeze(-1) + beam.particles[..., 4] = -xp_coords[..., 4] / beam.relativistic_beta.unsqueeze( + -1 + ) beam.particles[..., 5] = (gamma - beam.relativistic_gamma.unsqueeze(-1)) / ( (beam.relativistic_beta * beam.relativistic_gamma).unsqueeze(-1) ) @@ -765,9 +767,9 @@ def from_xyz_pxpypz( def to_xyz_pxpypz(self) -> torch.Tensor: """ - Moments in SI units as converted from the beam's `particles`. Returns the - moments tensor with shape (..., n_particles, 7). For each particle, the moment - vector is $(x, p_x, y, p_y, z, p_z, 1)$. + Extracts the position and momentum coordinates in SI units, from the + beam's `particles`, and returns it as a tensor with shape (..., n_particles, 7). + For each particle, the moment vector is $(x, p_x, y, p_y, z, p_z, 1)$. """ p0 = ( self.relativistic_gamma @@ -782,18 +784,18 @@ def to_xyz_pxpypz(self) -> torch.Tensor: beta = torch.sqrt(1 - 1 / gamma**2) p = gamma * electron_mass * beta * speed_of_light - moments_xp = self.particles[..., 1] * p0.unsqueeze(-1) - moments_yp = self.particles[..., 3] * p0.unsqueeze(-1) - moments_s = self.particles[..., 4] * -self.relativistic_beta.unsqueeze(-1) - moments_p = torch.sqrt(p**2 - moments_xp**2 - moments_yp**2) + px = self.particles[..., 1] * p0.unsqueeze(-1) + py = self.particles[..., 3] * p0.unsqueeze(-1) + s = self.particles[..., 4] * -self.relativistic_beta.unsqueeze(-1) + ps = torch.sqrt(p**2 - px**2 - py**2) - moments = self.particles.clone() - moments[..., 1] = moments_xp - moments[..., 3] = moments_yp - moments[..., 4] = moments_s - moments[..., 5] = moments_p + xp_coords = self.particles.clone() + xp_coords[..., 1] = px + xp_coords[..., 3] = py + xp_coords[..., 4] = s + xp_coords[..., 5] = ps - return moments + return xp_coords def __len__(self) -> int: return int(self.num_particles)