|
| 1 | +1D Vlasov-Poisson Equation |
| 2 | +=========================== |
| 3 | + |
| 4 | +This tutorial was contributed by `Colin Cotter |
| 5 | +<mailto:[email protected]>`__ and Werner Bauer. |
| 6 | + |
| 7 | +A plasma is a continuum of moving particles with nonunique velocity |
| 8 | +at each point in space. In :math:`d` dimensions, the plasma is |
| 9 | +described by a density :math:`f(x,v,t)` where :math:`x\in \mathbb{R}^d` |
| 10 | +are the physical coordinates and :math:`v \in \mathbb{R}^d` are velocity |
| 11 | +coordinates. Hence, in :math:`d` dimensions, a :math:`2d` |
| 12 | +dimensional mesh is required. To deal with this curse of |
| 13 | +dimensionality, particle-in-cell methods are usually used. However, |
| 14 | +in 1 dimension, it is tractable to simulate the plasma on a 2 |
| 15 | +dimensional mesh. |
| 16 | + |
| 17 | +The Vlasov equation models the (collisionless) conservation of plasma |
| 18 | +particles, according to |
| 19 | + |
| 20 | +.. math:: |
| 21 | + f_t + \nabla_{\vec{x}} \cdot (\vec{v}f) + \nabla_{\vec{v}} \cdot (\vec{a}f) = 0, |
| 22 | +
|
| 23 | +where |
| 24 | + |
| 25 | +.. math:: |
| 26 | + \nabla_{\vec{x}} = (\partial_{x_1},\ldots, \partial_{x_d}), \quad |
| 27 | + \nabla_{\vec{v}} = (\partial_{v_1},\ldots, \partial_{v_d}). |
| 28 | +
|
| 29 | +To close the system, we need a formula for the acceleration :math:`\vec{a}`. |
| 30 | +In the (single species) Vlasov-Poisson model, the acceleration is |
| 31 | +determined by the electrostatic force, |
| 32 | + |
| 33 | +.. math:: |
| 34 | + \vec{a} = -\frac{1}{m}\nabla\phi, |
| 35 | +
|
| 36 | +where :math:`m` |
| 37 | +is the mass per plasma particle, and :math:`\phi` is the electrostatic |
| 38 | +potential determined by the Poisson equation, |
| 39 | + |
| 40 | +.. math:: |
| 41 | + -\nabla^2\phi = q_0\int_{\mathbb{R}^d} f(\vec{x},\vec{v},t)\,\mathrm{d} v, |
| 42 | +
|
| 43 | +where :math:`q_0` is the electric charge per plasma particle. |
| 44 | + |
| 45 | +In this demo we specialise to :math:`d=1`, and the equations become |
| 46 | + |
| 47 | +.. math:: |
| 48 | + f_t + (fv)_x + (-f\phi_x/m)_v = 0, \quad |
| 49 | + -\phi_{xx} = q_0\int f(x,v,t)\,\mathrm{d} v, |
| 50 | +
|
| 51 | +with coordinates :math:`(x,v)\in \mathbb{R}^2`. From now on we will |
| 52 | +relabel these coordinates :math:`(x,v)\mapsto (x_1,x_2)`, obtaining |
| 53 | +the equivalent form, |
| 54 | + |
| 55 | +.. math:: |
| 56 | + f_t + \nabla\cdot(\vec{u}f) = 0, \quad \vec{u} = (v,-\phi_x/m), \quad |
| 57 | + -\phi_{x_1x_1} = q_0\int f(x_1,x_2,t)\,\mathrm{d} x_2, |
| 58 | +
|
| 59 | +where :math:`\nabla=(\partial_{x_1},\partial_{x_2})`. From now on we will |
| 60 | +choose units such that :math:`q_0,m` are absorbed into the definition of |
| 61 | +:math:`f`. |
| 62 | + |
| 63 | +To proceed, we need to develop variational formulations of these |
| 64 | +equations. |
| 65 | + |
| 66 | +For the density we will use a discontinuous Galerkin formulation, |
| 67 | +and the continuity equation becomes |
| 68 | + |
| 69 | +.. math:: |
| 70 | +
|
| 71 | + \int_\Omega \! q \frac{\partial f}{\partial t} \, \mathrm{d} x |
| 72 | + &= \int_\Omega \! f \nabla \cdot (q \vec{u}) \, \mathrm{d} x\\ |
| 73 | + &\quad- \int_{\Gamma_\mathrm{int}} \! \widetilde{f}(q_+ \vec{u} \cdot \vec{n}_+ |
| 74 | + + q_- \vec{u} \cdot \vec{n}_-) \, \mathrm{d} S\\ |
| 75 | + &\quad- \int_{\Gamma_I} q f_\mathrm{in} \vec{u} \cdot |
| 76 | + \vec{n} \, \mathrm{d} s\\ |
| 77 | + &\quad- \int_{\Gamma_O} q f \vec{u} \cdot |
| 78 | + \vec{n} \, \mathrm{d} s |
| 79 | + \qquad \forall q \in V, |
| 80 | +
|
| 81 | +where :math:`\Omega` is the computational domain in :math:`(x,v)` |
| 82 | +space, :math:`V` is the discontinuous finite element space, |
| 83 | +:math:`\Gamma_\mathrm{int}` is the set of interior cell edges, |
| 84 | +:math:`\Gamma_I` is the inlet part of |
| 85 | +exterior boundary where :math:`\vec{u}\cdot\vec{n}<0`, |
| 86 | +:math:`\Gamma_O` is the outlet part of |
| 87 | +exterior boundary where :math:`\vec{u}\cdot\vec{n}>0`, :math:`n` is |
| 88 | +the normal to each edge, :math:`\tilde{f}` is the upwind value of |
| 89 | +:math:`f`, and :math:`f_{\mathrm{in}}` is the inflow boundary value |
| 90 | +for :math:`f`. See the Discontinuous Galerkin advection |
| 91 | +:doc:`demo<DG_advection.py>` for more details. The unapproximated |
| 92 | +problem should have :math:`-\infty < x_2 < \infty`, i.e. unbounded velocities, but we approximate |
| 93 | +the problem by solving in the domain :math:`\Omega=I_1\times [-H/2, H/2]`, |
| 94 | +where :math:`I` is some chosen interval in the spatial dimension. |
| 95 | + |
| 96 | +For the Poisson equation, we will use a regular Galerkin formulation. |
| 97 | +The difficulty in the formulation is the integral over :math:`x_2`. We |
| 98 | +deal with this by considering a space :math:`\bar{W}` which is restricted |
| 99 | +to functions that are constant in the vertical. Multiplying by a |
| 100 | +test function :math:`\psi\in \bar{W}` and integrating by parts gives |
| 101 | + |
| 102 | +.. math:: |
| 103 | +
|
| 104 | + \int \psi_{x_1}\phi_{x_1}\, \mathrm{d} x_1 |
| 105 | + = \int \int f(x_1,x_2,t) \psi\, \mathrm{d} x_1\,\mathrm{d} x_2, \quad |
| 106 | + \forall \psi \in \bar{W}. |
| 107 | +
|
| 108 | +Since the left hand side integrand is independent of :math:`v=x_2`, we |
| 109 | +can integrate over :math:`x_2` and divide by :math:`H`, to obtain |
| 110 | + |
| 111 | +.. math:: |
| 112 | +
|
| 113 | + \int_\Omega \psi_{x_1}\phi_{x_1}/H\, \mathrm{d} x |
| 114 | + = \int f \psi\, \mathrm{d} x, \quad |
| 115 | + \forall \psi \in \bar{W}, |
| 116 | +
|
| 117 | +which is now in a form which we can implement easily in Firedrake. One |
| 118 | +final issue is that this problem only has a solution up to an additive |
| 119 | +constant, so we further restrict :math:`\phi \in \mathring{\bar{W}}`, |
| 120 | +where |
| 121 | + |
| 122 | +.. math:: |
| 123 | + \mathring{\bar{W}} = \{ w\in \bar{W}: \bar{w}=0\}, |
| 124 | +
|
| 125 | +where here the bar indicates a spatial average, |
| 126 | + |
| 127 | +.. math:: |
| 128 | +
|
| 129 | + \bar{w} = \frac{\int_{\Omega} w\, \mathrm{d} x}{\int_{\Omega} 1 \mathrm{d} x}. |
| 130 | + |
| 131 | +Then we seek the solution of |
| 132 | + |
| 133 | +.. math:: |
| 134 | +
|
| 135 | + \int_\Omega \psi_{x_1}\phi_{x_1}\,\mathrm{d} x |
| 136 | + = \int H(f-\bar{f}) \psi\, \mathrm{d} x, \quad |
| 137 | + \forall \psi \in \mathring{\bar{W}}. |
| 138 | +
|
| 139 | +To discretise in time, we will use an SSPRK3 time discretisation, similar to the DG advection :doc:`demo<DG_advection.py>`. At |
| 140 | +each Runge-Kutta stage, we must solve for the electrostatic potential, |
| 141 | +and then use it to compute :math:`\vec{u}`, in order to compute |
| 142 | +:math:`\partial f/\partial t`. |
| 143 | + |
| 144 | +As usual, to implement this problem, we start by importing the |
| 145 | +Firedrake namespace. :: |
| 146 | + |
| 147 | + from firedrake import * |
| 148 | + |
| 149 | +We build the mesh by constructing a 1D mesh, which will be extruded in |
| 150 | +the vertical. Here we will use periodic boundary conditions in the |
| 151 | +:math:`x_1` direction, :: |
| 152 | + |
| 153 | + ncells = 50 |
| 154 | + L = 8*pi |
| 155 | + base_mesh = PeriodicIntervalMesh(ncells, L) |
| 156 | + |
| 157 | +The mesh is then extruded upwards in the "velocity" direction. :: |
| 158 | + |
| 159 | + H = 10.0 |
| 160 | + nlayers = 50 |
| 161 | + mesh = ExtrudedMesh(base_mesh, layers=nlayers, layer_height=H/nlayers) |
| 162 | + |
| 163 | +We want to have :math:`v=0` in the middle of the domain, so that we |
| 164 | +can have negative and positive velocities. This requires to edit the |
| 165 | +coordinate field. :: |
| 166 | + |
| 167 | + x, v = SpatialCoordinate(mesh) |
| 168 | + mesh.coordinates.interpolate(as_vector([x, v-H/2])) |
| 169 | + |
| 170 | +Now we build a discontinuous finite element space for the density, :: |
| 171 | + |
| 172 | + V = FunctionSpace(mesh, 'DQ', 1) |
| 173 | + |
| 174 | +and a continuous finite element space for the electostatic potential. |
| 175 | +The space is continuous in the horizontal and constant in the vertical, |
| 176 | +specified through the ``vfamily``. :: |
| 177 | + |
| 178 | + Wbar = FunctionSpace(mesh, 'CG', 1, vfamily='R', vdegree=0) |
| 179 | + |
| 180 | +We create a :class:`~.Function` to store the solution at the current |
| 181 | +time, and then set its initial condition, |
| 182 | + |
| 183 | +.. math:: |
| 184 | +
|
| 185 | + f(x,v,0) = \frac{1}{\sqrt{2\pi}}v^2\exp(-v^2/2)(1+ A\cos(kx)), |
| 186 | + \quad A=0.05, \quad k=0.5. |
| 187 | +
|
| 188 | +:: |
| 189 | + |
| 190 | + fn = Function(V, name="density") |
| 191 | + A = Constant(0.05) |
| 192 | + k = Constant(0.5) |
| 193 | + fn.interpolate(v**2*exp(-v**2/2)*(1 + A*cos(k*x))/(2*pi)**0.5) |
| 194 | + |
| 195 | +We will need the (conserved) average :math:`\bar{f}` for the Poisson |
| 196 | +equation. :: |
| 197 | + |
| 198 | + One = Function(V).assign(1.0) |
| 199 | + fbar = assemble(fn*dx)/assemble(One*dx) |
| 200 | + |
| 201 | +We create a :class:`~.Function` to store the electrostatic potential. :: |
| 202 | + |
| 203 | + phi = Function(Wbar, name="potential") |
| 204 | + |
| 205 | +The next task is to create the solver for the electrostatic potential, which |
| 206 | +will be called every timestep. |
| 207 | + |
| 208 | +We create a :class:`~.Function` to store the intermediate densities at each |
| 209 | +Runge-Kutta stage. The right hand side of the Poisson equation will be |
| 210 | +evaluated using this :class:`~.Function` to obtain the potential at each |
| 211 | +stage. Defining this beforehand will enable us to reuse the solver. :: |
| 212 | + |
| 213 | + fstar = Function(V) |
| 214 | + |
| 215 | +Now we express the Poisson equation in UFL. :: |
| 216 | + |
| 217 | + psi = TestFunction(Wbar) |
| 218 | + dphi = TrialFunction(Wbar) |
| 219 | + phi_eqn = dphi.dx(0)*psi.dx(0)*dx - H*(fstar-fbar)*psi*dx |
| 220 | + |
| 221 | +To deal mathematically with the null space of the potential, we expressed the |
| 222 | +problem in :math:`\mathring{\bar{W}}`. Programmatically we will express the |
| 223 | +problem in :math:`\bar{W}` and deal with the null space by defining a basis |
| 224 | +for the space of globally constant functions, which we will later pass to PETSc |
| 225 | +so the solver can remove it from the solution. :: |
| 226 | + |
| 227 | + nullspace = VectorSpaceBasis(constant=True, comm=COMM_WORLD) |
| 228 | + |
| 229 | +However, the null space also means that the assembled matrix of the |
| 230 | +Poisson problem will be singular, which will prevent us from using a |
| 231 | +direct solver. To deal with this, we will precondition the Poisson problem |
| 232 | +with a version shifted by :math:`\int_{\Omega}\phi\psi\mathrm{d}x`. The |
| 233 | +shifted problem is well-posed on :math:`\bar{W}`, so the assembled matrix |
| 234 | +will be non-singular and so solvable with direct methods. :: |
| 235 | + |
| 236 | + shift_eqn = dphi.dx(0)*psi.dx(0)*dx + dphi*psi*dx |
| 237 | + |
| 238 | +We use these to define a :class:`~.LinearVariationalProblem`. :: |
| 239 | + |
| 240 | + phi_problem = LinearVariationalProblem(lhs(phi_eqn), rhs(phi_eqn), |
| 241 | + phi, aP=shift_eqn) |
| 242 | + |
| 243 | +Now we build the :class:`~.LinearVariationalSolver`. The problem |
| 244 | +is preconditioned by the shifted operator which is solved using a direct |
| 245 | +solver, and we pass the nullspace of globally constant functions to |
| 246 | +the solver. :: |
| 247 | + |
| 248 | + params = { |
| 249 | + 'ksp_type': 'gmres', |
| 250 | + 'pc_type': 'lu', |
| 251 | + 'ksp_rtol': 1.0e-8, |
| 252 | + } |
| 253 | + phi_solver = LinearVariationalSolver(phi_problem, |
| 254 | + nullspace=nullspace, |
| 255 | + solver_parameters=params) |
| 256 | + |
| 257 | +Now we move onto the solver to compute :math:`\partial f/\partial t`. We |
| 258 | +define a symbolic :math:`\Delta t` which we will update later. :: |
| 259 | + |
| 260 | + dtc = Constant(0) |
| 261 | + |
| 262 | +At each stage, the solver will take in the intermediate solution ``fstar`` and |
| 263 | +return the stage increment :math:`\Delta t\partial f/\partial t` in ``df_out``. :: |
| 264 | + |
| 265 | + df_out = Function(V) |
| 266 | + |
| 267 | +Now we express the equation in UFL, starting with the left hand side |
| 268 | +bilinear form :: |
| 269 | + |
| 270 | + q = TestFunction(V) |
| 271 | + u = as_vector([v, -phi.dx(0)]) |
| 272 | + n = FacetNormal(mesh) |
| 273 | + un = 0.5*(dot(u, n) + abs(dot(u, n))) |
| 274 | + df = TrialFunction(V) |
| 275 | + df_a = q*df*dx |
| 276 | + |
| 277 | +The problem is defined on an extruded mesh, so the interior facets are |
| 278 | +separated into horizontal and vertical ones. :: |
| 279 | + |
| 280 | + dS = dS_h + dS_v |
| 281 | + |
| 282 | +Now we build the right hand side linear form. A conditional operator |
| 283 | +is used to deal with the inflow and outflow parts of the exterior |
| 284 | +boundary. Due to the periodic boundary conditions in :math:`x_1`, the only exterior boundary is at the top and bottom of the domain, with measure `ds_tb`. :: |
| 285 | + |
| 286 | + df_L = dtc*(div(u*q)*fstar*dx |
| 287 | + - (q('+') - q('-'))*(un('+')*fstar('+') - un('-')*fstar('-'))*dS |
| 288 | + - conditional(dot(u, n) > 0, q*dot(u, n)*fstar, 0.)*ds_tb |
| 289 | + ) |
| 290 | + |
| 291 | +We then use this to build a solver. :: |
| 292 | + |
| 293 | + df_problem = LinearVariationalProblem(df_a, df_L, df_out) |
| 294 | + df_solver = LinearVariationalSolver(df_problem) |
| 295 | + |
| 296 | +We are getting close to the time loop. We set up some timestepping |
| 297 | +parameters. :: |
| 298 | + |
| 299 | + T = 50.0 # maximum timestep |
| 300 | + t = 0. # model time |
| 301 | + ndump = 100 # frequency of file dumps |
| 302 | + dumpn = 0 # dump counter |
| 303 | + nsteps = 5000 |
| 304 | + dt = T/nsteps |
| 305 | + dtc.assign(dt) |
| 306 | + |
| 307 | +We set up some :class:`~.Function`\s to store Runge-Kutta stage variables. :: |
| 308 | + |
| 309 | + f1 = Function(V) |
| 310 | + f2 = Function(V) |
| 311 | + |
| 312 | +We set up a ``VTKFile`` object to write output every ``ndump`` |
| 313 | +timesteps. :: |
| 314 | + |
| 315 | + outfile = VTKFile("vlasov.pvd") |
| 316 | + |
| 317 | +We want to output the initial condition, so need to solve for the electrostatic |
| 318 | +potential that corresponds to the initial density. :: |
| 319 | + |
| 320 | + fstar.assign(fn) |
| 321 | + phi_solver.solve() |
| 322 | + outfile.write(fn, phi) |
| 323 | + phi.assign(.0) |
| 324 | + |
| 325 | +Now we start the timeloop using a lovely progress bar. Note that |
| 326 | +we have 5000 timesteps so this may take a few minutes to run:: |
| 327 | + |
| 328 | + for step in ProgressBar("Timestep").iter(range(nsteps)): |
| 329 | + |
| 330 | +Each Runge-Kutta stage involves solving for :math:`\phi` before solving |
| 331 | +for :math:`\partial f/\partial t`. Here is the first stage. :: |
| 332 | + |
| 333 | + # |
| 334 | + fstar.assign(fn) |
| 335 | + phi_solver.solve() |
| 336 | + df_solver.solve() |
| 337 | + f1.assign(fn + df_out) |
| 338 | + |
| 339 | +The second stage. :: |
| 340 | + |
| 341 | + # |
| 342 | + fstar.assign(f1) |
| 343 | + phi_solver.solve() |
| 344 | + df_solver.solve() |
| 345 | + f2.assign(3*fn/4 + (f1 + df_out)/4) |
| 346 | + |
| 347 | +The third stage. :: |
| 348 | + |
| 349 | + # |
| 350 | + fstar.assign(f2) |
| 351 | + phi_solver.solve() |
| 352 | + df_solver.solve() |
| 353 | + fn.assign(fn/3 + 2*(f2 + df_out)/3) |
| 354 | + t += dt |
| 355 | + |
| 356 | +Finally we output to the VTK file if it is time to do that. :: |
| 357 | + |
| 358 | + # |
| 359 | + dumpn += 1 |
| 360 | + if dumpn % ndump == 0: |
| 361 | + dumpn = 0 |
| 362 | + outfile.write(fn, phi) |
| 363 | + |
| 364 | +Images of the solution at shown below. |
| 365 | + |
| 366 | +.. figure:: vlasov_0s_LR.png |
| 367 | + :align: center |
| 368 | + |
| 369 | + Solution at :math:`t = 0.` |
| 370 | + |
| 371 | +.. figure:: vlasov_15_LR.png |
| 372 | + :align: center |
| 373 | + |
| 374 | + Solution at :math:`t = 15.` |
| 375 | + |
| 376 | +We also present solutions at double the resolution, by doubling the number |
| 377 | +of horizontal cells and the number of layers, halving the timestep (by doubling the number of steps), and doubling ``nsteps``. |
| 378 | + |
| 379 | +.. figure:: vlasov_0s_HR.png |
| 380 | + :align: center |
| 381 | + |
| 382 | + Solution at :math:`t = 0.` |
| 383 | + |
| 384 | +.. figure:: vlasov_15_HR.png |
| 385 | + :align: center |
| 386 | + |
| 387 | + Solution at :math:`t = 15.` |
| 388 | + |
| 389 | + |
| 390 | +A Python script version of this demo can be found :demo:`here <vp1d.py>`. |
0 commit comments