Coverage for pySDC/implementations/problem_classes/PenningTrap_3D.py: 92%

120 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 14:51 +0000

1import numpy as np 

2from numba import jit 

3 

4from pySDC.core.errors import ProblemError 

5from pySDC.core.problem import Problem, WorkCounter 

6from pySDC.implementations.datatype_classes.particles import particles, fields, acceleration 

7 

8 

9# noinspection PyUnusedLocal 

10class penningtrap(Problem): 

11 r""" 

12 This class implements a standard Penning trap problem on the time interval :math:`[0, t_{end}]` 

13 fully investigated in [1]_. The equations are given by the following equation of motion 

14 

15 .. math:: 

16 \frac{dv}{dt}=f(x,v)=\alpha[E(x,t)+v\times B(x,t)], 

17 

18 .. math:: 

19 \frac{dx}{dt}=v 

20 

21 with the particles :math:`x, v\in \mathbb{R}^{3}`. For the penning trap problem, the other parameters are given by 

22 the constant magnetic field :math:`B=\frac{\omega_{B}}{\alpha}\cdot \hat{e_{z}}\in \mathbb{R}^{3}` 

23 along the :math:`z`-axis with the particle's charge-to-mass ratio :math:`\alpha=\frac{q}{m}` so that 

24 

25 .. math:: 

26 v\times B=\frac{\omega_{B}}{\alpha}\left( 

27 \begin{matrix} 

28 0 & 1 & 0\\ 

29 -1 & 0 & 0\\ 

30 0 & 0 & 0 

31 \end{matrix} 

32 \right)v. 

33 

34 The electric field :math:`E(x_{i})=E_{ext}(x_{i})+E_{int}(x_{i})\in \mathbb{R}^{3}` where 

35 

36 .. math:: 

37 E_{ext}(x_{i})=-\epsilon\frac{\omega_{E}^{2}}{\alpha}\left( 

38 \begin{matrix} 

39 1 & 0 & 0\\ 

40 0 & 1 & 0\\ 

41 0 & 0 & -2 

42 \end{matrix} 

43 \right)x_{i} 

44 

45 and the inter-particle Coulomb interaction 

46 

47 .. math:: 

48 E_{int}(x_{i})=\sum_{k=1, k\neq i}^{N_{particles}}Q_{k}\frac{x_{i}-x_{k}}{(|x_{i}-x_{k}|^{2}+\lambda^{2})^{3/2}} 

49 

50 with the smoothing parameter :math:`\lambda>0`. 

51 The exact solution also given for the single particle penning trap more detailed [1]_, [2]_. 

52 For to solve nonlinear equation of system, Boris trick is used (see [2]_). 

53 

54 Parameters 

55 ---------- 

56 omega_B : float 

57 Amplitude of magnetic field. 

58 omega_E : float 

59 Amplitude of electric field. 

60 u0 : np.1darray 

61 Initial condition for position, and for velocity. 

62 q : np.1darray 

63 Particle's charge. 

64 m : np.1darray 

65 Mass. 

66 nparts : int 

67 The number of particles. 

68 sig : float 

69 The smoothing parameter :math:`\lambda>0`. 

70 

71 Attributes 

72 ---------- 

73 work_counter : dict 

74 Counts the calls of the right-hand side, and calls of the Boris solver. 

75 

76 References 

77 ---------- 

78 .. [1] F. Penning. Die Glimmentladung bei niedrigem Druck zwischen koaxialen Zylindern in einem axialen Magnetfeld. 

79 Physica. Vol. 3 (1936). 

80 .. [2] Mathias Winkel, Robert Speck and Daniel Ruprecht. A high-order Boris integrator. 

81 Journal of Computational Physics (2015). 

82 """ 

83 

84 dtype_u = particles 

85 dtype_f = fields 

86 

87 def __init__(self, omega_B, omega_E, u0, nparts, sig): 

88 # invoke super init, passing nparts, dtype_u and dtype_f 

89 super().__init__(((3, nparts), None, np.dtype('float64'))) 

90 self._makeAttributeAndRegister('nparts', localVars=locals(), readOnly=True) 

91 self._makeAttributeAndRegister('omega_B', 'omega_E', 'u0', 'sig', localVars=locals()) 

92 self.work_counters['Boris_solver'] = WorkCounter() 

93 self.work_counters['rhs'] = WorkCounter() 

94 

95 @staticmethod 

96 @jit(nopython=True, nogil=True) 

97 def fast_interactions(N, pos, sig, q): 

98 r""" 

99 Computes the fast interactions. 

100 

101 Parameters 

102 ---------- 

103 N : int 

104 Number of particles. 

105 pos : np.2darray 

106 Position. 

107 sig : float 

108 The smoothing parameter :math:`\lambda > 0`. 

109 q : np.1darray 

110 Particle's charge. 

111 

112 Returns 

113 ------- 

114 Efield : np.2darray 

115 The internal E field for each particle. 

116 """ 

117 Efield = np.zeros((3, N)) 

118 contrib = np.zeros(3) 

119 

120 for i in range(N): 

121 contrib[:] = 0 

122 

123 for j in range(N): 

124 dist2 = ( 

125 (pos[0, i] - pos[0, j]) ** 2 + (pos[1, i] - pos[1, j]) ** 2 + (pos[2, i] - pos[2, j]) ** 2 + sig**2 

126 ) 

127 contrib += q[j] * (pos[:, i] - pos[:, j]) / dist2**1.5 

128 

129 Efield[:, i] += contrib[:] 

130 

131 return Efield 

132 

133 def get_interactions(self, part): 

134 r""" 

135 Routine to compute the particle-particle interaction, assuming :math:`q = 1` for all particles. 

136 

137 Parameters 

138 ---------- 

139 part : dtype_u 

140 The particles. 

141 

142 Returns 

143 ------- 

144 Efield : np.ndarray 

145 The internal E field for each particle. 

146 """ 

147 

148 N = self.nparts 

149 

150 Efield = self.fast_interactions(N, part.pos, self.sig, part.q) 

151 

152 return Efield 

153 

154 def eval_f(self, part, t): 

155 """ 

156 Routine to compute the E and B fields (named f for consistency with the original PEPC version). 

157 

158 Parameters 

159 ---------- 

160 part : dtype_u 

161 The particles. 

162 t : float 

163 Current time of the particles (not used here). 

164 

165 Returns 

166 ------- 

167 f : dtype_f 

168 Fields for the particles (internal and external), i.e., the right-hand side of the problem. 

169 """ 

170 

171 N = self.nparts 

172 

173 self.work_counters['rhs']() 

174 try: 

175 _unused = penningtrap.Harmonic_oscillator 

176 Emat = np.diag([0, 0, -1]) 

177 except AttributeError: 

178 Emat = np.diag([1, 1, -2]) 

179 

180 f = self.dtype_f(self.init) 

181 

182 f.elec[:] = self.get_interactions(part) 

183 

184 for n in range(N): 

185 f.elec[:, n] += self.omega_E**2 / (part.q[n] / part.m[n]) * np.dot(Emat, part.pos[:, n]) 

186 f.magn[:, n] = self.omega_B * np.array([0, 0, 1]) 

187 

188 return f 

189 

190 # TODO : Warning, this should be moved to u_exact(t=0) ! 

191 def u_init(self): 

192 """ 

193 Routine to compute the starting values for the particles. 

194 

195 Returns 

196 ------- 

197 u : dtype_u 

198 Particle set filled with initial data. 

199 """ 

200 

201 u0 = self.u0 

202 N = self.nparts 

203 

204 u = self.dtype_u(self.init) 

205 

206 if u0[2][0] != 1 or u0[3][0] != 1: 

207 raise ProblemError('so far only q = m = 1 is implemented') 

208 

209 # set first particle to u0 

210 u.pos[0, 0] = u0[0][0] 

211 u.pos[1, 0] = u0[0][1] 

212 u.pos[2, 0] = u0[0][2] 

213 u.vel[0, 0] = u0[1][0] 

214 u.vel[1, 0] = u0[1][1] 

215 u.vel[2, 0] = u0[1][2] 

216 

217 u.q[0] = u0[2][0] 

218 u.m[0] = u0[3][0] 

219 

220 # initialize random seed 

221 np.random.seed(N) 

222 

223 comx = u.pos[0, 0] 

224 comy = u.pos[1, 0] 

225 comz = u.pos[2, 0] 

226 

227 for n in range(1, N): 

228 # draw 3 random variables in [-1,1] to shift positions 

229 r = np.random.random_sample(3) - 1 

230 u.pos[0, n] = r[0] + u0[0][0] 

231 u.pos[1, n] = r[1] + u0[0][1] 

232 u.pos[2, n] = r[2] + u0[0][2] 

233 

234 # draw 3 random variables in [-5,5] to shift velocities 

235 r = np.random.random_sample(3) - 5 

236 u.vel[0, n] = r[0] + u0[1][0] 

237 u.vel[1, n] = r[1] + u0[1][1] 

238 u.vel[2, n] = r[2] + u0[1][2] 

239 

240 u.q[n] = u0[2][0] 

241 u.m[n] = u0[3][0] 

242 

243 # gather positions to check center 

244 comx += u.pos[0, n] 

245 comy += u.pos[1, n] 

246 comz += u.pos[2, n] 

247 

248 # print('Center of positions:',comx/N,comy/N,comz/N) 

249 

250 return u 

251 

252 def u_exact(self, t): 

253 r""" 

254 Routine to compute the exact trajectory at time :math:`t` (only for single-particle setup). 

255 

256 Parameters 

257 ---------- 

258 t : float 

259 Current time of the exact trajectory. 

260 

261 Returns 

262 ------- 

263 u : dtype_u 

264 Particle type containing the exact position and velocity. 

265 """ 

266 

267 # some abbreviations 

268 wE = self.omega_E 

269 wB = self.omega_B 

270 N = self.nparts 

271 u0 = self.u0 

272 

273 if N != 1: 

274 raise ProblemError('u_exact is only valid for a single particle') 

275 

276 u = self.dtype_u(((3, 1), self.init[1], self.init[2])) 

277 

278 wbar = np.sqrt(2) * wE 

279 

280 # position and velocity in z direction is easy to compute 

281 u.pos[2, 0] = u0[0][2] * np.cos(wbar * t) + u0[1][2] / wbar * np.sin(wbar * t) 

282 u.vel[2, 0] = -u0[0][2] * wbar * np.sin(wbar * t) + u0[1][2] * np.cos(wbar * t) 

283 

284 # define temp. variables to compute complex position 

285 Op = 1 / 2 * (wB + np.sqrt(wB**2 - 4 * wE**2)) 

286 Om = 1 / 2 * (wB - np.sqrt(wB**2 - 4 * wE**2)) 

287 Rm = (Op * u0[0][0] + u0[1][1]) / (Op - Om) 

288 Rp = u0[0][0] - Rm 

289 Im = (Op * u0[0][1] - u0[1][0]) / (Op - Om) 

290 Ip = u0[0][1] - Im 

291 

292 # compute position in complex notation 

293 w = (Rp + Ip * 1j) * np.exp(-Op * t * 1j) + (Rm + Im * 1j) * np.exp(-Om * t * 1j) 

294 # compute velocity as time derivative of the position 

295 dw = -1j * Op * (Rp + Ip * 1j) * np.exp(-Op * t * 1j) - 1j * Om * (Rm + Im * 1j) * np.exp(-Om * t * 1j) 

296 

297 # get the appropriate real and imaginary parts 

298 u.pos[0, 0] = w.real 

299 u.vel[0, 0] = dw.real 

300 u.pos[1, 0] = w.imag 

301 u.vel[1, 0] = dw.imag 

302 

303 return u 

304 

305 def build_f(self, f, part, t): 

306 """ 

307 Helper function to assemble the correct right-hand side out of B and E field. 

308 

309 Parameters 

310 ---------- 

311 f : dtype_f 

312 The field values. 

313 part : dtype_u 

314 The current particles data. 

315 t : float 

316 The current time. 

317 

318 Returns 

319 ------- 

320 rhs : acceleration 

321 Correct right-hand side of type acceleration. 

322 """ 

323 

324 if not isinstance(part, particles): 

325 raise ProblemError('something is wrong during build_f, got %s' % type(part)) 

326 

327 N = self.nparts 

328 

329 rhs = acceleration(self.init) 

330 for n in range(N): 

331 rhs[:, n] = part.q[n] / part.m[n] * (f.elec[:, n] + np.cross(part.vel[:, n], f.magn[:, n])) 

332 

333 return rhs 

334 

335 # noinspection PyTypeChecker 

336 def boris_solver(self, c, dt, old_fields, new_fields, old_parts): 

337 r""" 

338 The actual Boris solver for static (!) B fields, extended by the c-term. 

339 

340 Parameters 

341 ---------- 

342 c : dtype_u 

343 The c term gathering the known values from the previous iteration. 

344 dt : float 

345 The (probably scaled) time step size. 

346 old_fields : dtype_f 

347 The field values at the previous node :math:`m`. 

348 new_fields : dtype_f 

349 The field values at the current node :math:`m+1`. 

350 old_parts : dtype_u 

351 The particles at the previous node :math:`m`. 

352 

353 Returns 

354 ------- 

355 vel : particles 

356 The velocities at the :math:`(m+1)`-th node. 

357 """ 

358 

359 N = self.nparts 

360 vel = particles.velocity(self.init) 

361 self.work_counters['Boris_solver']() 

362 Emean = 0.5 * (old_fields.elec + new_fields.elec) 

363 for n in range(N): 

364 a = old_parts.q[n] / old_parts.m[n] 

365 

366 c[:, n] += dt / 2 * a * np.cross(old_parts.vel[:, n], old_fields.magn[:, n] - new_fields.magn[:, n]) 

367 

368 # pre-velocity, separated by the electric forces (and the c term) 

369 vm = old_parts.vel[:, n] + dt / 2 * a * Emean[:, n] + c[:, n] / 2 

370 # rotation 

371 t = dt / 2 * a * new_fields.magn[:, n] 

372 s = 2 * t / (1 + np.linalg.norm(t, 2) ** 2) 

373 vp = vm + np.cross(vm + np.cross(vm, t), s) 

374 # post-velocity 

375 vel[:, n] = vp + dt / 2 * a * Emean[:, n] + c[:, n] / 2 

376 

377 return vel