|
17 | 17 |
|
18 | 18 | from ..general import pmath as pm |
19 | 19 | from ..field_maps.field_map import FieldMapSliceWise |
| 20 | +from ..gpu.pypic import make_PyPIC |
20 | 21 | from pypic_factory import create_mesh |
21 | 22 | from spacecharge import TransverseGaussianSpaceCharge |
22 | 23 |
|
@@ -120,6 +121,220 @@ def track(self, beam, pypic_state=None): |
120 | 121 | beam.dp += force_fields[2] * kick_factor/beam.gamma |
121 | 122 |
|
122 | 123 |
|
| 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 | + |
123 | 338 | class FrozenGaussianSpaceCharge25D(FieldMapSliceWise): |
124 | 339 | '''Transverse slice-by-slice (2.5D) frozen space charge assuming |
125 | 340 | a static transverse Gaussian distribution of a fixed RMS size. |
|
0 commit comments