-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add space-charge to Cheetah #142
base: master
Are you sure you want to change the base?
Conversation
… of all the IGF solver functions.
Co-authored-by: Remi Lehe <remi.lehe@normalesup.org>
…ed. The code is not yet tested. (last test E+vB field)
…test (expansion of a cold uniform beam).
cheetah/accelerator.py
Outdated
@@ -299,6 +307,356 @@ 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think at some point in the future, we should think about creating a Effects
class and make SpaceChargeKick
a subclass of that. But right now this doesn't make sense yet.
Nevertheless, here is how I think we should integrate adding space charge to elements right now in a way that can easily be extended in the future:
Basically what I would do is to add a method to the Element
class that looks something like this:
class Element:
...
def with_space_charge(self, resolution: float = 0.01, *args, **kwargs) -> Segment:
splits = self.split(resolution)
splits_with_space_charge = # List of [split, sc, split, sc, ..., split, sc] ... probably itertools has a nice way to create this ... *args and **kwargs go into SpaceChargeKick
return Segment(elements=splits_with_space_charge, name=f"{self.name}_with_space_charge")
This should end up looking similar to what @RemiLehe proposed in the very beginning. So if you do
Drift(length=0.5, name="my_drift").with_space_charge(resolution=0.25, nx=64)
you get
Segment(
name="my_drift_with_space_charge",
elements=[
Drift(length=0.25), SpaceChargeKick(nx=64), Drift(length=0.25), SpaceChargeKick(nx=64)
],
)
This example is probably not quite correct in many ways, but I think it illustrates my idea.
The nice thing about doing it this way would be that it automatically works for all elements that implement split
correctly, and that it would be relatively straightforward to extend this for other collective effects in the future.
Does this make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative thought for the low level implementation: One could also create sub- or mixing classes for thin (L=0) and thick (L>0) elements. For elements in ImpactX, we currently use these mixin classes, e.g. Drift vs. thin Multipole.
Space charge kicks could be thin elements :)
For the high level interface, I agree on the above syntax. You want to automatically slice this up, a property or method on how many slices on the sliced element is a nice user interface.
cheetah/accelerator.py
Outdated
class SpaceChargeKick(Element): | ||
""" | ||
Simulates space charge effects on a beam. | ||
:param length: Length of the element in meters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for now, time discretization is manually implemented through the lenght. If spacechargekick objects are built automatically, it might be interesting to play on this parameter.
cheetah/accelerator.py
Outdated
def is_skippable(self) -> bool: | ||
return False | ||
|
||
def plot(self, ax: matplotlib.axes.Axes, s: float) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@greglenerd So I think we need to be careful with the length
property. In all existing elements, length
represents the physical length of the beam pipe and Cheetah actually relies on this assumption in some places. I would suggest that SpaceChargeKick.length = 0.0
all the time (because it doesn't add length to the beam pipe), and that the property of SpaceChargeKick
currently named length
is renamed to something like effect_length
. Does that make sense?
Based on this, I think SpaceChargeKick
could be plotted with something like plt.axvspan
as a background highlight from s - effect_length
to s
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this PR!
I added a few suggestions ; we can discuss them further offline.
cheetah/accelerator.py
Outdated
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I think their role is very similar to that of Screen.resolution
. So I think no one will every want get the gradient with respect to any of them (?), meaning they don't have to be nn.Parameter
. I think we could get away with making them int
and float
. But, in NumPy, if you interleave operations on NumPy types and Python types, computations slow down significantly. I don't know to what extend this is also an issue in PyTorch, but just to avoid it, I would make them all torch.Tensor
.
Weirdly all GitHub Actions except for the docs build are currently not showing up on this PR. We need to keep an eye on this. It could just be a result of the merge conflicts with |
Co-authored-by: Remi Lehe <remi.lehe@normalesup.org>
Co-authored-by: Remi Lehe <remi.lehe@normalesup.org>
tests/test_space_charge_kick.py
Outdated
outgoing_beam = segment_space_charge.track(incoming) | ||
|
||
# Final beam properties | ||
sig_xo = outgoing_beam.sigma_x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still needs to be done
tests/test_space_charge_kick.py
Outdated
sigma_yp=torch.tensor([2e-7]), | ||
) | ||
# Initial beam properties | ||
incoming_particles0 = incoming_beam.particles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also still need doing
tests/test_space_charge_kick.py
Outdated
# Final beam properties | ||
incoming_particles1 = incoming_beam.particles | ||
|
||
torch.set_printoptions(precision=16) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this done? I think this line should probably be removed. I think it's not needed and tests shouldn't change global configs when they run.
tests/test_space_charge_kick.py
Outdated
3 + 2 * torch.sqrt(torch.tensor(2)) | ||
) | ||
Nb = total_charge / elementary_charge | ||
L = beta * gamma * kappa * torch.sqrt(R0**3 / (Nb * electron_radius)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid capital and single letter variable names ... I guess this should be section_length
?
cheetah/particles/particle_beam.py
Outdated
@@ -269,6 +269,140 @@ def from_twiss( | |||
dtype=dtype, | |||
) | |||
|
|||
@classmethod | |||
def uniform_3d_ellispoid( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a duplicate of the method below.
I think @greglenerd pasted this in here while Chenran and I were still working on it, but it was since merged into master
and this PR. So this copy of the method is no longer needed.
Note that the two copies are not identical. The lower one should be kept.
charge * inv_cell_volume[:, None, None, None] | ||
) # Normalize by the cell volume | ||
|
||
def _integrated_potential(self, x, y, s) -> torch.Tensor: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add type annotations?
""" | ||
|
||
r = torch.sqrt(x**2 + y**2 + s**2) | ||
G = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's called G
in the original method. Capital single letter variable names are not very pythonic and can sometimes be confusing. Though it might still be clearer if the name from the original paper is used. So it could alternatively be called integrated_potential
? Or we just circumvent this by returning without naming it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
) | ||
|
||
# Initialize the grid with double dimensions | ||
green_func = torch.zeros( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
green_func
is probably also a bit confusing here because it's not a function, but whatever that function return, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. How about green_func_values
?
Co-authored-by: Jan Kaiser <jan.kaiser.email@googlemail.com>
There appears to be also random failing of CI due to numerical issues? |
@cr-xu Yes, the tests where failing randomly, due to stochastic fluctuations in the initial particle distribution. In order to avoid this, I fixed the random seed in the test. |
+ x * s * torch.asinh(y / torch.sqrt(x**2 + s**2)) | ||
+ x * y * torch.asinh(s / torch.sqrt(x**2 + y**2)) | ||
) | ||
return G |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return G | |
return integrated_potential |
|
||
# Create a new tensor with the doubled dimensions, filled with zeros | ||
new_charge_density = torch.zeros( | ||
(self.n_batch,) + new_dims, **self.factory_kwargs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(self.n_batch,) + new_dims, **self.factory_kwargs | |
(self.batch_size,) + new_dims, **self.factory_kwargs |
This implements space-charge as outlined in #137
Description
Draft: todo
Motivation and Context
See #137
Types of changes
Checklist
flake8
(required).pytest
tests pass (required).pytest
on a machine with a CUDA GPU and made sure all tests pass (required).Note: We are using a maximum length of 88 characters per line