Skip to content

Commit 3a3d3a5

Browse files
committed
release-script: Merge branch 'release/v1.12.3'
2 parents 4302cd5 + 1916ac3 commit 3a3d3a5

File tree

3 files changed

+218
-1
lines changed

3 files changed

+218
-1
lines changed

PyHEADTAIL/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.12.2'
1+
__version__ = '1.12.3'

PyHEADTAIL/spacecharge/pypic_spacecharge.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from ..general import pmath as pm
1919
from ..field_maps.field_map import FieldMapSliceWise
20+
from ..gpu.pypic import make_PyPIC
2021
from pypic_factory import create_mesh
2122
from spacecharge import TransverseGaussianSpaceCharge
2223

@@ -120,6 +121,220 @@ def track(self, beam, pypic_state=None):
120121
beam.dp += force_fields[2] * kick_factor/beam.gamma
121122

122123

124+
class SpaceChargePIC_Adaptive25D(SpaceChargePIC):
125+
'''Transverse slice-by-slice (2.5D) space charge using a
126+
particle-in-cell algorithm via PyPIC. Uses a 3D mesh with respect
127+
to beam.z_beamframe . The slices along the beam are continuously
128+
centred to the grid. Optionally, the grid size can be adapted to
129+
the most extreme RMS beam size along the slices if a relative change
130+
of the beam size is fixed via the argument sigma_rtol. If sigma_rtol
131+
is None, the grid size is not updated and remains constant.
132+
'''
133+
def __init__(self, length, slicer, beam,#=None, sigma_x=None, sigma_y=None,
134+
n_mesh_sigma=[6, 6], mesh_size=[1024, 1024],
135+
sigma_rtol=None, sort_particles=False, pic_dtype=np.float64,
136+
save_memory=False, *args, **kwargs):
137+
'''Arguments:
138+
- length: interaction length over which the space charge
139+
force is integrated.
140+
- beam: the Particles instance of which sigma_x, sigma_y and
141+
gamma are evaluated to compute the field map.
142+
Cannot provide sigma_x and sigma_y arguments when
143+
giving beam argument.
144+
- sigma_x, sigma_y: the horizontal and vertical RMS width of
145+
the transverse Gaussian distribution modelling the beam
146+
distribution. Either provide both sigma_x and sigma_y
147+
as an argument or the beam instead.
148+
149+
Optional arguments:
150+
- n_mesh_sigma: 2-list of number of beam RMS values in
151+
[x, y] to span across half the mesh width for the
152+
field interpolation.
153+
- mesh_size: 2-list of number of mesh nodes per transverse
154+
plane [x, y].
155+
- sigma_rtol: relative tolerance (float between 0 and 1)
156+
which is compared between the stored fields RMS size and
157+
the tracked beam RMS size, it determines when the fields
158+
are recomputed. For sigma_rtol == None (default value)
159+
the fields remain constant. E.g. sigma_rtol = 0.05 means
160+
if beam.sigma_x changes by more than 5%, the fields will
161+
be recomputed taking into account the new beam.sigma_x.
162+
A negative sigma_rtol will lead to continuous field
163+
updates at every track call.
164+
- sort_particles: determines whether to sort the particles
165+
by their mesh ID. This may speed up the PyPIC
166+
particle-to-mesh and mesh-to-particles methods
167+
due to coalesced memory access, especially on the GPU
168+
(test the timing for your parameters though!).
169+
- pic_dtype: can be np.float32 or np.float64 and determines
170+
(for sort_particles == False) which atomicAdd should be
171+
used on the GPU. On GPUs with computing capability <v6.0
172+
the double precision atomicAdd is not hardware accelerated
173+
and thus much slower.
174+
- save_memory: True means treat one slice at a time which
175+
saves memory but is slower, False means treat all slices
176+
simultaneously (faster).
177+
178+
(NB: sort_particles=True is necessarily required for the
179+
PyPIC_GPU.sorted_particles_to_mesh method.)
180+
181+
NB: SpaceChargePIC_Adaptive25D instances should be initialised
182+
in the proper context. If the SpaceChargePIC_Adaptive25D will
183+
track on the GPU, it should be initiated within a GPU context:
184+
>>> with PyHEADTAIL.general.contextmanager.GPU(beam):
185+
>>> pic_sc_node = SpaceChargePIC_Adaptive25D(...)
186+
'''
187+
188+
# TODO:
189+
# - do not check beam emittance but instead slice emittances
190+
# - do this check on the CPU/GPU staying on the same device
191+
# instead of always CPU
192+
# - update mesh and green's function of poisson solver only
193+
# - allow non-GPU poisson solvers...
194+
195+
self._wrt_beam_centroid = True
196+
self._n_mesh_sigma = n_mesh_sigma
197+
self.sigma_rtol = sigma_rtol
198+
self.save_memory = save_memory
199+
self.slicer = slicer
200+
201+
sigma_x = pm.ensure_CPU(beam.sigma_x())
202+
sigma_y = pm.ensure_CPU(beam.sigma_y())
203+
# if beam is not None:
204+
# if sigma_x is not None or sigma_y is not None:
205+
# raise ValueError('SpaceChargePIC_Adaptive25D accepts either '
206+
# 'beam as an argument or both sigma_x '
207+
# 'and sigma_y. Do not mix these args!')
208+
# sigma_x = pm.ensure_CPU(beam.sigma_x())
209+
# sigma_y = pm.ensure_CPU(beam.sigma_y())
210+
# else:
211+
# if sigma_x is None or sigma_y is None:
212+
# raise ValueError('SpaceChargePIC_Adaptive25D requires both '
213+
# 'sigma_x and sigma_y as arguments. '
214+
# 'Alternatively provide a beam whose sigma are '
215+
# 'evaluated.')
216+
217+
mesh = create_mesh(
218+
mesh_origin=[-n_mesh_sigma[0] * sigma_x,
219+
-n_mesh_sigma[1] * sigma_y],
220+
mesh_distances=[sigma_x * 2 * n_mesh_sigma[0] / mesh_size[0],
221+
sigma_y * 2 * n_mesh_sigma[1] / mesh_size[1]],
222+
mesh_size=mesh_size,
223+
slices=beam.get_slices(self.slicer)
224+
)
225+
226+
if pm.device is not 'GPU':
227+
raise NotImplementedError(
228+
'Currently only implemented for GPU, sorry!')
229+
230+
# to be removed:
231+
from PyPIC.GPU.poisson_solver.FFT_solver import GPUFFTPoissonSolver_2_5D
232+
from pycuda.autoinit import context
233+
234+
poissonsolver = GPUFFTPoissonSolver_2_5D(
235+
mesh=mesh, context=context, save_memory=self.save_memory)
236+
237+
pypic = make_PyPIC(
238+
poissonsolver=poissonsolver,
239+
mesh=mesh)
240+
241+
super(SpaceChargePIC_Adaptive25D, self).__init__(
242+
length=length, pypic_algorithm=pypic,
243+
sort_particles=sort_particles, pic_dtype=np.float64,
244+
*args, **kwargs)
245+
246+
@property
247+
def sigma_x(self):
248+
return -self.pypic.mesh.x0 / self._n_mesh_sigma[0]
249+
250+
@property
251+
def sigma_y(self):
252+
return -self.pypic.mesh.y0 / self._n_mesh_sigma[1]
253+
254+
@property
255+
def mesh_size(self):
256+
return self.pypic.mesh.shape_r
257+
258+
def update_grid(self, beam=None, sigma_x=None, sigma_y=None):
259+
'''Update the grid to new RMS sizes.
260+
Take either the RMS values from the provided Particles instance
261+
beam or use sigma_x and sigma_y.
262+
263+
NB: make sure to call this method in the proper context
264+
(CPU/GPU), cf. comment in __init__ method.
265+
'''
266+
sigma_x = pm.ensure_CPU(sigma_x)
267+
sigma_y = pm.ensure_CPU(sigma_y)
268+
self.__init__(length=self.length, slicer=self.slicer, beam=beam,
269+
sigma_x=sigma_x, sigma_y=sigma_y,
270+
n_mesh_sigma=self._n_mesh_sigma,
271+
mesh_size=self.mesh_size,
272+
sigma_rtol=self.sigma_rtol,
273+
sort_particles=self.sort_particles,
274+
pic_dtype=self.pic_dtype,
275+
save_memory=self.save_memory)
276+
277+
def track(self, beam, pypic_state=None):
278+
# update field?
279+
if self.sigma_rtol is not None:
280+
x_too_large = (
281+
np.abs(pm.ensure_CPU(beam.sigma_x()) - self.sigma_x)
282+
> self.sigma_rtol * self.sigma_x)
283+
y_too_large = (
284+
np.abs(pm.ensure_CPU(beam.sigma_y()) - self.sigma_y)
285+
> self.sigma_rtol * self.sigma_y)
286+
if x_too_large or y_too_large:
287+
self.prints('SpaceChargePIC_Adaptive25D: '
288+
'updating field maps...')
289+
self.update_grid(beam)
290+
291+
mx, my = 0, 0
292+
if self._wrt_beam_centroid:
293+
slices = beam.get_slices(
294+
self.slicer, statistics=["mean_x", "mean_y"])
295+
mx = slices.convert_to_particles(slices.mean_x)
296+
my = slices.convert_to_particles(slices.mean_y)
297+
298+
mp_coords = [beam.x - mx,
299+
beam.y - my,
300+
beam.z_beamframe] # zip will cut to #fields
301+
302+
mesh = self.pypic.mesh
303+
304+
solve_kwargs = {
305+
'charge': beam.charge_per_mp,
306+
'state': pypic_state,
307+
'dtype': self.pic_dtype,
308+
}
309+
310+
# 2.5D: macro-particle charge becomes line density in beam frame
311+
# (in 3D this is implicit via rho=mesh_charges/mesh_3d.volume_elem)
312+
solve_kwargs['charge'] /= mesh.dz
313+
314+
if self.sort_particles:
315+
align_particles(beam, mesh)
316+
317+
solve_kwargs['lower_bounds'], solve_kwargs['upper_bounds'] = \
318+
get_bounds(beam, mesh)
319+
320+
# electric fields for each particle in beam frame [V/m]
321+
force_fields = self.pypic.pic_solve(*mp_coords, **solve_kwargs)
322+
323+
# we want force F = q * (1 - beta^2) E_r where E_r is in lab frame
324+
# --> Lorentz boost E_r from beam frame to lab frame (*gamma)
325+
# --> include magnetic fields (1 - beta^2) = 1/gamma^2
326+
# ==> overall factor 1/gamma
327+
force_fields[0] /= beam.gamma
328+
force_fields[1] /= beam.gamma
329+
330+
# integrate over dt and apply the force to each charged particle,
331+
# p0 comes from kicking xp=p_x/p0 instead of p_x
332+
kick_factor = (self.length / (beam.beta*c) * beam.charge / beam.p0)
333+
334+
beam.xp += force_fields[0] * kick_factor
335+
beam.yp += force_fields[1] * kick_factor
336+
337+
123338
class FrozenGaussianSpaceCharge25D(FieldMapSliceWise):
124339
'''Transverse slice-by-slice (2.5D) frozen space charge assuming
125340
a static transverse Gaussian distribution of a fixed RMS size.

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Installation for Users
1313
For using PyHEADTAIL without modifying the source code,
1414
we recommend to install the latest version via PyPI:
1515

16+
.. code-block:: bash
17+
1618
$ pip install PyHEADTAIL
1719
1820
Installation for Developers

0 commit comments

Comments
 (0)