Skip to content

Commit 16e086e

Browse files
colinjcotterdhamJHopeCollinsconnorjward
authored
Vlasov demo (#4320)
* Vlasov demo --------- Co-authored-by: David Ham <[email protected]> Co-authored-by: Josh Hope-Collins <[email protected]> Co-authored-by: Connor Ward <[email protected]>
1 parent 80b6edd commit 16e086e

File tree

8 files changed

+393
-1
lines changed

8 files changed

+393
-1
lines changed
61.9 KB
Loading
60.8 KB
Loading
229 KB
Loading
219 KB
Loading
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
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>`.

docs/source/advanced_tut.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ element systems.
2424
Rayleigh-Benard convection.<demos/rayleigh-benard.py>
2525
Netgen support.<demos/netgen_mesh.py>
2626
Full-waveform inversion: spatial and wave sources parallelism.<demos/full_waveform_inversion.py>
27+
1D Vlasov-Poisson equation using vertical independent function spaces.<demos/vp1d.py>
2728
Degree-independent multigrid convergence using patch relaxation.<demos/poisson_mg_patches.py>
2829
Monolithic multigrid with Vanka relaxation for Stokes.<demos/stokes_vanka_patches.py>
2930
Vertex/edge star multigrid relaxation for H(div).<demos/hdiv_riesz_star.py>

0 commit comments

Comments
 (0)