|
| 1 | +Solving the Wave Equation with Irksome and an explicit Nystrom method |
| 2 | +===================================================================== |
| 3 | + |
| 4 | +Let's start with the simple wave equation on :math:`\Omega = [0,1] |
| 5 | +\times [0,1]`, with boundary :math:`\Gamma`: |
| 6 | + |
| 7 | +.. math:: |
| 8 | +
|
| 9 | + u_{tt} - \Delta u &= 0 |
| 10 | +
|
| 11 | + u & = 0 \quad \textrm{on}\ \Gamma |
| 12 | +
|
| 13 | +At each time :math:`t`, the solution |
| 14 | +to this equation will be some function :math:`u\in V`, for a suitable function |
| 15 | +space :math:`V`. |
| 16 | + |
| 17 | +We transform this into weak form by multiplying by an arbitrary test function |
| 18 | +:math:`v\in V` and integrating over :math:`\Omega`. We know have the |
| 19 | +variational problem of finding :math:`u:[0,T]\rightarrow V` such |
| 20 | +that |
| 21 | + |
| 22 | +.. math:: |
| 23 | +
|
| 24 | + (u_{tt}, v) + (\nabla u, \nabla v) = 0 |
| 25 | +
|
| 26 | +As usual, we need to import firedrake:: |
| 27 | + |
| 28 | + from firedrake import * |
| 29 | + |
| 30 | +We will also need to import certain items from irksome:: |
| 31 | + |
| 32 | + from irksome import Dt, MeshConstant, StageDerivativeNystromTimeStepper, ClassicNystrom4Tableau |
| 33 | + |
| 34 | +Here, we will use the "classic" Nystrom method, a 4-stage explicit time-stepper:: |
| 35 | + |
| 36 | + nystrom_tableau = ClassicNystrom4Tableau() |
| 37 | + |
| 38 | +Now we define the mesh and piecewise linear approximating space in |
| 39 | +standard Firedrake fashion:: |
| 40 | + |
| 41 | + N = 32 |
| 42 | + |
| 43 | + msh = UnitSquareMesh(N, N) |
| 44 | + V = FunctionSpace(msh, "CG", 2) |
| 45 | + |
| 46 | +We define variables to store the time step and current time value, noting that an explicit scheme requires a small timestep for stability:: |
| 47 | + |
| 48 | + dt = Constant(0.2 / N) |
| 49 | + t = Constant(0.0) |
| 50 | + |
| 51 | +We define the initial condition for the fully discrete problem, which |
| 52 | +will get overwritten at each time step. For a second-order problem, |
| 53 | +we need an initial condition for the solution and its time derivative |
| 54 | +(in this case, taken to be zero):: |
| 55 | + |
| 56 | + x, y = SpatialCoordinate(msh) |
| 57 | + uinit = sin(pi * x) * cos(pi * y) |
| 58 | + u = Function(V) |
| 59 | + u.interpolate(uinit) |
| 60 | + ut = Function(V) |
| 61 | + |
| 62 | +Now, we will define the semidiscrete variational problem using |
| 63 | +standard UFL notation, augmented by the ``Dt`` operator from Irksome. |
| 64 | +Here, the optional second argument indicates the number of derivatives, |
| 65 | +(defaulting to 1):: |
| 66 | + |
| 67 | + v = TestFunction(V) |
| 68 | + F = inner(Dt(u, 2), v)*dx + inner(grad(u), grad(v))*dx |
| 69 | + bc = DirichletBC(V, 0, "on_boundary") |
| 70 | + |
| 71 | +We're using an explicit scheme, so we only need to solve mass matrices in the update system. So, some good solver parameters here are to use a fieldsplit (so we only invert the diagonal blocks of the stage-coupled system) with incomplete Cholesky as a preconditioner for the mass matrices on the diagonal blocks:: |
| 72 | + |
| 73 | + mass_params = {"snes_type": "ksponly", |
| 74 | + "mat_type": "aij", |
| 75 | + "ksp_type": "preonly", |
| 76 | + "pc_type": "fieldsplit", |
| 77 | + "pc_fieldsplit_type": "multiplicative"} |
| 78 | + |
| 79 | + per_field = {"ksp_type": "cg", |
| 80 | + "pc_type": "icc"} |
| 81 | + |
| 82 | + for s in range(nystrom_tableau.num_stages): |
| 83 | + mass_params["fieldsplit_%s" % (s,)] = per_field |
| 84 | + |
| 85 | +Most of Irksome's magic happens in the |
| 86 | +:class:`.StageDerivativeNystromTimeStepper`. It takes our semidiscrete |
| 87 | +form `F` and the tableau and produces the variational form for |
| 88 | +computing the stage unknowns. Then, it sets up a variational problem to be |
| 89 | +solved for the stages at each time step. Here, we use `dDAE` style boundary conditions, which impose boundary conditions on the stages to match those of :math:`u_t` on the boundary, consistent with the underlying partitioned scheme:: |
| 90 | + |
| 91 | + stepper = StageDerivativeNystromTimeStepper( |
| 92 | + F, nystrom_tableau, t, dt, u, ut, bcs=bc, bc_type="dDAE", |
| 93 | + solver_parameters=mass_params) |
| 94 | + |
| 95 | +The system energy is an important quantity for the wave equation. It |
| 96 | +won't be conserved with our choice of time-stepping method, but serves as a good diagnostic:: |
| 97 | + |
| 98 | + E = 0.5 * (inner(ut, ut) * dx + inner(grad(u), grad(u)) * dx) |
| 99 | + print(f"Initial energy: {assemble(E)}") |
| 100 | + |
| 101 | +Then, we can loop over time steps, much like with 1st order systems:: |
| 102 | + |
| 103 | + while (float(t) < 1.0): |
| 104 | + if (float(t) + float(dt) > 1.0): |
| 105 | + dt.assign(1.0 - float(t)) |
| 106 | + stepper.advance() |
| 107 | + t.assign(float(t) + float(dt)) |
| 108 | + print(f"Time: {float(t)}, Energy: {assemble(E)}") |
| 109 | + |
0 commit comments