Skip to content

Commit 04c689f

Browse files
committed
Merge branch 'losses' of https://github.com/like2000/PyHEADTAIL
Conflicts: _version.py particles/particles.py Resolved manually.
2 parents b8c4843 + 6017af1 commit 04c689f

File tree

6 files changed

+948
-3
lines changed

6 files changed

+948
-3
lines changed

aperture/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .. import Element
2+
from .. import __version__

aperture/aperture.pyx

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
from __future__ import division
2+
'''
3+
@date: Created on 04.12.2014
4+
@author: Hannes Bartosik, Michael Schenk
5+
6+
TODO:
7+
- Speed up RFBucketAperture. It is currently EXTREMELY slow. Use
8+
RectangularApertureZ as an alternative.
9+
'''
10+
cimport cython
11+
import numpy as np
12+
cimport numpy as np
13+
14+
from . import Element
15+
16+
from abc import ABCMeta, abstractmethod
17+
18+
class Aperture(Element):
19+
''' Abstract base class for Aperture elements. An aperture is
20+
generally defined as a condition on the phase space coordinates.
21+
Particles not fulfilling this condition are tagged as lost. They
22+
will be removed from the tracked particles as soon as the
23+
Particles.update_losses() method is called. '''
24+
__metaclass__ = ABCMeta
25+
26+
def __init__(self, apply_losses_here, *args, **kwargs):
27+
''' The boolean apply_losses_here controls whether the
28+
Particles.update_losses(beam) method is called to relocate lost
29+
particles to the end of the bunch.u_all arrays (u = x, y, z,
30+
...) and to actually remove them from the numpy array views
31+
beam.u and leave them in an untracked state. In case there are
32+
several Aperture elements placed at a segment boundary of the
33+
accelerator ring, apply_losses_here should only be set to True
34+
for the last one to increase performance. '''
35+
self.apply_losses_here = apply_losses_here
36+
37+
def track(self, beam):
38+
''' Tag particles not passing through the aperture as lost. If
39+
there are any losses and the self.apply_losses_here is set to
40+
True, the method Particles.update_losses() is called to relocate
41+
lost particles to the end of the beam.u_all numpy arrays (u = x,
42+
y, z, ...) and to actually remove them from the numpy array
43+
views beam.u and leave them in an untracked state. Also, clean
44+
currently cached slice_sets of the bunch as losses change its
45+
state. '''
46+
losses = self.tag_lost_particles(beam)
47+
48+
if losses and self.apply_losses_here:
49+
beam.update_losses()
50+
beam.clean_slices()
51+
52+
@abstractmethod
53+
def tag_lost_particles(self, beam):
54+
''' This method is called by Aperture.track(beam) to identify
55+
particles not passing through the aperture and set their
56+
bunch.alive state to 0 (false) to mark them as lost. Return
57+
whether or not any lost particles were found. '''
58+
pass
59+
60+
61+
class RectangularApertureX(Aperture):
62+
''' Mark particles with transverse spatial coord (x) outside the
63+
interval (x_high, x_low) as lost. '''
64+
65+
def __init__(self, x_low, x_high, apply_losses_here=True,
66+
*args, **kwargs):
67+
''' The arguments x_low and x_high define the interval of
68+
horizontal spatial coordinates for which particles pass through
69+
the rectangular horizontal aperture. The boolean
70+
apply_losses_here specifies whether the
71+
Particles.update_losses(beam) method should be called after
72+
tagging lost particles to relocate them to the end of the
73+
bunch.u_all arrays (u = x, y, z, ...), remove them from the
74+
views bunch.u and leave them in an untracked state. In case
75+
there are several Aperture elements placed at a segment boundary
76+
of the accelerator ring, apply_losses_here should only be set to
77+
True for the last one to increase performance. '''
78+
self.x_low = x_low
79+
self.x_high = x_high
80+
super(RectangularApertureX, self).__init__(apply_losses_here)
81+
82+
def tag_lost_particles(self, beam):
83+
''' This method is called by Aperture.track(beam) to identify
84+
particles not passing through the aperture and set their
85+
bunch.alive state to 0 (false) to mark them as lost. The search
86+
for lost particles is done using a cython function. Return
87+
whether or not any lost particles were found. '''
88+
return cytag_lost_rectangular(
89+
beam.x, beam.alive, self.x_low, self.x_high)
90+
91+
92+
class RectangularApertureY(Aperture):
93+
''' Mark particles with transverse spatial coord (y) outside the
94+
interval (y_high, y_low) as lost. '''
95+
96+
def __init__(self, y_low, y_high, apply_losses_here=True,
97+
*args, **kwargs):
98+
''' The arguments y_low and y_high define the interval of
99+
vertical spatial coordinates for which particles pass through
100+
the rectangular vertical aperture. The boolean apply_losses_here
101+
specifies whether the Particles.update_losses(beam) method
102+
should be called after tagging lost particles to relocate them
103+
to the end of the bunch.u_all arrays (u = x, y, z, ...), remove
104+
them from the views bunch.u and leave them in an untracked
105+
state. In case there are several Aperture elements placed at a
106+
segment boundary of the accelerator ring, apply_losses_here
107+
should only be set to True for the last one to increase
108+
performance. '''
109+
self.y_low = y_low
110+
self.y_high = y_high
111+
super(RectangularApertureY, self).__init__(apply_losses_here)
112+
113+
def tag_lost_particles(self, beam):
114+
''' This method is called by Aperture.track(beam) to identify
115+
particles not passing through the aperture and set their
116+
bunch.alive state to 0 (false) to mark them as lost. The search
117+
for lost particles is done using a cython function. Return
118+
whether or not any lost particles were found. '''
119+
return cytag_lost_rectangular(
120+
beam.y, beam.alive, self.y_low, self.y_high)
121+
122+
123+
class RectangularApertureZ(Aperture):
124+
''' Mark particles with longitudinal spatial coord (z) outside the
125+
interval (z_high, z_low) as lost. '''
126+
127+
def __init__(self, z_low, z_high, apply_losses_here=True,
128+
*args, **kwargs):
129+
''' The arguments z_low and z_high define the interval of
130+
longitudinal spatial coordinates for which particles pass
131+
through the rectangular longitudinal aperture. The boolean
132+
apply_losses_here specifies whether the
133+
Particles.update_losses(beam) method should be called after
134+
tagging lost particles to relocate them to the end of the
135+
bunch.u_all arrays (u = x, y, z, ...), remove them from the
136+
views bunch.u and leave them in an untracked state. In case
137+
there are several Aperture elements placed at a segment boundary
138+
of the accelerator ring, apply_losses_here should only be set to
139+
True for the last one to increase performance. '''
140+
self.z_low = z_low
141+
self.z_high = z_high
142+
super(RectangularApertureZ, self).__init__(apply_losses_here)
143+
144+
def tag_lost_particles(self, beam):
145+
''' This method is called by Aperture.track(beam) to identify
146+
particles not passing through the aperture and set their
147+
bunch.alive state to 0 (false) to mark them as lost. The
148+
search for lost particles is done using a cython function.
149+
Return whether or not any lost particles were found. '''
150+
return cytag_lost_rectangular(
151+
beam.z, beam.alive, self.z_low, self.z_high)
152+
153+
154+
class CircularApertureXY(Aperture):
155+
''' Mark particles with transverse spatial coords (x, y) outside a
156+
circle of specified radius, i.e. x**2 + y**2 > radius**2, as lost.
157+
'''
158+
159+
def __init__(self, radius, apply_losses_here=True, *args, **kwargs):
160+
''' The argument radius defines the radius of the circular
161+
(transverse) aperture. The argument apply_losses_here specifies
162+
whether the Particles.update_losses(beam) method should be
163+
called after tagging lost particles to relocate them to the end
164+
of the bunch.u_all arrays (u = x, y, z, ...), remove them from
165+
the views bunch.u and leave them in an untracked state. In case
166+
there are several Aperture elements placed at a segment boundary
167+
of the accelerator ring, apply_losses_here should only be set to
168+
True for the last one to increase performance. '''
169+
self.radius_square = radius * radius
170+
super(CircularApertureXY, self).__init__(apply_losses_here)
171+
172+
def tag_lost_particles(self, beam):
173+
''' This method is called by Aperture.track(beam) to identify
174+
particles not passing through the aperture and set their
175+
bunch.alive state to 0 (false) to mark them as lost. The search
176+
for lost particles is done using a cython function. Return
177+
whether or not any lost particles were found. '''
178+
return cytag_lost_circular(
179+
beam.x, beam.y, beam.alive, self.radius_square)
180+
181+
182+
class RFBucketAperture(Aperture):
183+
''' Mark particles with longitudinal phase space coords (z, dp)
184+
outside the accepted region as lost.
185+
186+
NOTE: THE CURRENT IMPLEMENTATION IS EXTREMELY SLOW! For 1e6
187+
macroparticles, executing it once takes 120ms. Hence, as an
188+
alternative, one may use the RectangularApertureZ using the z-limits
189+
of the RFBucket. Like this, particles leaking from the RFBucket will
190+
at some point be marked lost and finally removed as well. '''
191+
192+
def __init__(self, is_accepted, apply_losses_here=True,
193+
*args, **kwargs):
194+
''' The argument is_accepted takes a reference to a function of
195+
the form is_accepted(z, dp) returning a boolean array saying
196+
whether the pair (z, dp) is in- or outside the specified region
197+
of the longitudinal phase space. Usually, is_accepted can be
198+
generated either using the RFSystems.RFBucket.make_is_accepted
199+
or using directly RFSystems.RFBucket.is_in_separatrix. The
200+
argument apply_losses_here specifies whether the
201+
Particles.update_losses(beam) method should be called after
202+
tagging lost particles to relocate them to the end of the
203+
bunch.u_all arrays (u = x, y, z, ...), remove them from the
204+
views bunch.u and leave them in an untracked state. In case
205+
there are several Aperture elements placed at a segment boundary
206+
of the accelerator ring, apply_losses_here should only be set to
207+
True for the last one to increase performance. '''
208+
self.is_accepted = is_accepted
209+
super(RFBucketAperture, self).__init__(apply_losses_here)
210+
211+
def tag_lost_particles(self, beam):
212+
''' This method is called by Aperture.track(beam) to identify
213+
particles not passing through the aperture and set their
214+
bunch.alive state to 0 (false) to mark them as lost. The
215+
search for lost particles is done using a cython function.
216+
Return whether or not any lost particles were found. '''
217+
mask_lost = ~self.is_accepted(beam.z, beam.dp)
218+
beam.alive[mask_lost] = 0
219+
return np.sum(beam.alive)
220+
221+
222+
''' Cython functions for fast id and tagging of lost particles. '''
223+
224+
@cython.boundscheck(False)
225+
@cython.wraparound(False)
226+
cpdef cytag_lost_rectangular(double[::1] u, unsigned int[::1] alive,
227+
double low_lim, double high_lim):
228+
''' Cython function for fast identification and tagging of particles
229+
lost at a rectangular aperture element, i.e. it tags particles with
230+
a spatial coord u (beam.x, beam.y or beam.z) lying outside the
231+
interval (low_lim, high_lim) as lost. Returns whether or not any
232+
lost particles were found. '''
233+
cdef unsigned int n = alive.shape[0]
234+
cdef unsigned int losses = 0
235+
cdef unsigned int i
236+
for i in xrange(n):
237+
if u[i] < low_lim or u[i] > high_lim:
238+
alive[i] = 0
239+
losses = 1
240+
return losses
241+
242+
@cython.boundscheck(False)
243+
@cython.wraparound(False)
244+
cpdef cytag_lost_circular(
245+
double[::1] u, double[::1] v, unsigned int[::1] alive,
246+
double radius_square):
247+
''' Cython function for fast identification and tagging of particles
248+
lost at a circular transverse aperture element of a given radius,
249+
i.e. it tags particles with spatial coords u, v (usually (beam.x,
250+
beam.y)) fulfilling u**2 + v**2 > radius_square as lost. Returns
251+
whether or not any lost particles were found. '''
252+
cdef unsigned int n = alive.shape[0]
253+
cdef unsigned int losses = 0
254+
cdef unsigned int i
255+
for i in xrange(n):
256+
if (u[i]*u[i] + v[i]*v[i]) > radius_square:
257+
alive[i] = 0
258+
losses = 1
259+
return losses

particles/cython_functions.pyx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'''
2+
@date 01.12.2014
3+
@author Michael Schenk
4+
5+
TODO:
6+
- Find fast sorting algorithm in C.
7+
- Possible to enable vectorization?!
8+
'''
9+
cimport cython
10+
import numpy as np
11+
cimport numpy as np
12+
13+
@cython.boundscheck(False)
14+
@cython.wraparound(False)
15+
def relocate_lost_particles(beam):
16+
''' Memory efficient (and fast) cython function to relocate
17+
particles marked as lost to the end of the beam.u arrays (u = x, y,
18+
z, ...). Returns the number of alive particles n_alive_post after
19+
removal of those marked as lost.
20+
21+
Description of the algorithm:
22+
(1) Starting from the end of the numpy array view beam.alive, find
23+
the index of the last particle in the array which is still
24+
alive. Store its array index in last_alive.
25+
(2) Loop through the alive array from there (continuing in reverse
26+
order). If a particle i is found for which alive[i] == 0, i.e.
27+
it is a lost one, swap its position (and data x, y, z, ...) with
28+
the one located at index last_alive.
29+
(3) Move last_alive by -1. Due to the chosen procedure, the particle
30+
located at the new last_alive index is known to be alive.
31+
(4) Repeat steps (2) and (3) until index i = 0 is reached.
32+
'''
33+
cdef double[::1] x = beam.x
34+
cdef double[::1] y = beam.y
35+
cdef double[::1] z = beam.z
36+
cdef double[::1] xp = beam.xp
37+
cdef double[::1] yp = beam.yp
38+
cdef double[::1] dp = beam.dp
39+
cdef unsigned int[::1] id = beam.id
40+
cdef unsigned int[::1] alive = beam.alive
41+
42+
# Temporary variables for swapping entries.
43+
cdef double t_x, t_xp, t_y, t_yp, t_z, t_dp
44+
cdef unsigned int t_alive, t_id
45+
46+
# Find last_alive index.
47+
cdef int n_alive_pri = alive.shape[0]
48+
cdef int last_alive = n_alive_pri - 1
49+
while not alive[last_alive]:
50+
last_alive -= 1
51+
52+
# Identify particles marked as lost and relocate them.
53+
cdef int n_alive_post = last_alive + 1
54+
cdef int i
55+
for i in xrange(last_alive-1, -1, -1):
56+
if not alive[i]:
57+
# Swap lost particle coords with last_alive.
58+
t_x, t_y, t_z = x[i], y[i], z[i]
59+
t_xp, t_yp, t_dp = xp[i], yp[i], dp[i]
60+
t_id, t_alive = id[i], alive[i]
61+
62+
x[i], y[i], z[i] = x[last_alive], y[last_alive], z[last_alive]
63+
xp[i], yp[i], dp[i] = xp[last_alive], yp[last_alive], dp[last_alive]
64+
id[i], alive[i] = id[last_alive], alive[last_alive]
65+
66+
x[last_alive], y[last_alive], z[last_alive] = t_x, t_y, t_z
67+
xp[last_alive], yp[last_alive], dp[last_alive] = t_xp, t_yp, t_dp
68+
id[last_alive], alive[last_alive] = t_id, t_alive
69+
70+
# Move last_alive pointer and update number of alive
71+
# particles.
72+
last_alive -= 1
73+
n_alive_post -= 1
74+
75+
return n_alive_post

0 commit comments

Comments
 (0)