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