Skip to content

Commit dbcc8ee

Browse files
committed
FrozenGaussianSpaceCharge25D: providing adaptive mode
The fields are automatically recomputed when the beam RMS size changes. This is controlled via sigma_rtol. Tune spreads are tested to be the same as for v1.12.0, so no bugs introduced (hopefully).
1 parent 61b7909 commit dbcc8ee

File tree

1 file changed

+97
-10
lines changed

1 file changed

+97
-10
lines changed

PyHEADTAIL/spacecharge/pypic_spacecharge.py

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,36 +135,76 @@ class FrozenGaussianSpaceCharge25D(FieldMapSliceWise):
135135
This frozen space charge model essentially acts equivalently to an
136136
external magnet and fails to provide self-consistent treatment of
137137
space charge related effects like quadrupolar envelope breathing
138-
etc.
138+
etc. Also the sum of all kicks does not vanish resulting in a net
139+
kick for the centroid as opposed to the SpaceChargePIC treatment.
140+
141+
FrozenGaussianSpaceCharge25D can continuously recompute and update
142+
the fields during tracking of the beam if a relative change of the
143+
beam RMS sizes is fixed via the argument sigma_rtol. If sigma_rtol
144+
is None, the stored fields are not updated and remain constant.
139145
'''
140-
def __init__(self, slicer, length, sigma_x, sigma_y, gamma,
146+
147+
'''A tuple of field maps in the beam rest frame.'''
148+
fields = None
149+
150+
def __init__(self, slicer, length, beam=None, sigma_x=None, sigma_y=None,
141151
n_mesh_sigma=[6, 6], mesh_size=[1024, 1024],
142-
*args, **kwargs):
152+
sigma_rtol=None, *args, **kwargs):
143153
'''Arguments:
144154
- slicer: determines the longitudinal discretisation for the
145155
local line charge density, with which the field is
146156
multiplied at each track call.
147157
- length: interaction length around the accelerator over
148158
which the force of the field is integrated.
159+
- beam: the Particles instance of which sigma_x, sigma_y and
160+
gamma are evaluated to compute the field map.
161+
Cannot provide sigma_x and sigma_y arguments when
162+
giving beam argument.
149163
- sigma_x, sigma_y: the horizontal and vertical RMS width of
150164
the transverse Gaussian distribution modelling the beam
151-
distribution.
152-
- gamma: the relativistic Lorentz factor of the beam.
165+
distribution. Either provide both sigma_x and sigma_y
166+
as an argument or the beam instead.
153167
154168
Optional arguments:
155169
- n_mesh_sigma: 2-list of number of beam RMS values in
156170
[x, y] to span across half the mesh width for the
157171
field interpolation.
158172
- mesh_size: 2-list of number of mesh nodes per transverse
159173
plane [x, y].
174+
- sigma_rtol: relative tolerance (float between 0 and 1)
175+
which is compared between the stored fields RMS size and
176+
the tracked beam RMS size, it determines when the fields
177+
are recomputed. For sigma_rtol == None (default value)
178+
the fields remain constant. E.g. sigma_rtol = 0.05 means
179+
if beam.sigma_x changes by more than 5%, the fields will
180+
be recomputed taking into account the new beam.sigma_x.
181+
A negative sigma_rtol will lead to continuous field
182+
updates at every track call.
160183
161184
NB: FrozenGaussianSpaceCharge25D instances should be initialised
162185
in the proper context. If the FrozenGaussianSpaceCharge25D will
163186
track on the GPU, it should be initiated within a GPU context:
164-
>>> with PyHEADTAIL.general.contextmanager.GPU(beam) as cmg:
187+
>>> with PyHEADTAIL.general.contextmanager.GPU(beam):
165188
>>> frozen_sc_node = FrozenGaussianSpaceCharge25D(...)
166189
'''
167190
wrt_beam_centroid = True
191+
self._n_mesh_sigma = n_mesh_sigma
192+
self.sigma_rtol = sigma_rtol
193+
194+
if beam is not None:
195+
if sigma_x is not None or sigma_y is not None:
196+
raise ValueError('FrozenGaussianSpaceCharge25D accepts either '
197+
'beam as an argument or both sigma_x '
198+
'and sigma_y. Do not mix these args!')
199+
sigma_x = pm.ensure_CPU(beam.sigma_x())
200+
sigma_y = pm.ensure_CPU(beam.sigma_y())
201+
else:
202+
if sigma_x is None or sigma_y is None:
203+
raise ValueError('FrozenGaussianSpaceCharge25D requires both '
204+
'sigma_x and sigma_y as arguments. '
205+
'Alternatively provide a beam whose sigma are '
206+
'evaluated.')
207+
168208
mesh = create_mesh(
169209
mesh_origin=[-n_mesh_sigma[0] * sigma_x,
170210
-n_mesh_sigma[1] * sigma_y],
@@ -184,11 +224,58 @@ def __init__(self, slicer, length, sigma_x, sigma_y, gamma,
184224
be_sc = TransverseGaussianSpaceCharge(None, None)
185225
fields_beamframe = be_sc.get_efieldn(xg, yg, 0, 0, sigma_x, sigma_y)
186226

227+
super(FrozenGaussianSpaceCharge25D, self).__init__(
228+
slicer=slicer, length=length, mesh=mesh,
229+
fields=fields_beamframe,
230+
wrt_beam_centroid=wrt_beam_centroid, *args, **kwargs)
231+
232+
@property
233+
def sigma_x(self):
234+
return -self.pypic.mesh.x0 / self._n_mesh_sigma[0]
235+
236+
@property
237+
def sigma_y(self):
238+
return -self.pypic.mesh.y0 / self._n_mesh_sigma[1]
239+
240+
@property
241+
def mesh_size(self):
242+
return self.pypic.mesh.shape_r
243+
244+
def update_field(self, beam=None, sigma_x=None, sigma_y=None):
245+
'''Update the stored Gaussian field to new RMS sizes.
246+
Take either the RMS values from the provided Particles instance
247+
beam or use sigma_x and sigma_y.
248+
249+
NB: make sure to call this method in the proper context
250+
(CPU/GPU), cf. comment in __init__ method.
251+
'''
252+
sigma_x = pm.ensure_CPU(sigma_x)
253+
sigma_y = pm.ensure_CPU(sigma_y)
254+
self.__init__(slicer=self.slicer, length=self.length, beam=beam,
255+
sigma_x=sigma_x, sigma_y=sigma_y,
256+
n_mesh_sigma=self._n_mesh_sigma,
257+
mesh_size=self.mesh_size,
258+
sigma_rtol=self.sigma_rtol)
259+
260+
def track(self, beam):
261+
# in contrast to FieldMap's length, here length becomes a float
262+
# storing the interaction length including the Lorentz transform,
263+
# so the integrated part of the accelerator x gamma**-2.
187264
# Lorentz trafo to lab frame + magnetic fields:
188265
# F = q E_beam / gamma = q (E_n_BassErsk * lambda_beam) / gamma
189266
# = q E_n_BassErsk * lambda_lab / gamma^2
190-
fields = [f * gamma**-2 for f in fields_beamframe]
267+
# this trick avoids multiplying the field arrays twice:
268+
interaction_length = self.length
269+
self.length *= beam.gamma**-2
191270

192-
super(FrozenGaussianSpaceCharge25D, self).__init__(
193-
slicer=slicer, length=length, mesh=mesh, fields=fields,
194-
wrt_beam_centroid=wrt_beam_centroid, *args, **kwargs)
271+
# update field?
272+
if self.sigma_rtol is not None:
273+
if (np.abs(pm.ensure_CPU(beam.sigma_x()) - self.sigma_x)
274+
> self.sigma_rtol * self.sigma_x):
275+
self.prints('FrozenGaussianSpaceCharge25D: '
276+
'updating field maps...')
277+
self.update_field(beam)
278+
279+
super(FrozenGaussianSpaceCharge25D, self).track(beam)
280+
281+
self.length = interaction_length

0 commit comments

Comments
 (0)