Coverage for pySDC/implementations/problem_classes/HeatFiredrake.py: 98%
63 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 12:08 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 12:08 +0000
1from pySDC.core.problem import Problem, WorkCounter
2from pySDC.implementations.datatype_classes.firedrake_mesh import firedrake_mesh, IMEX_firedrake_mesh
3import firedrake as fd
4import numpy as np
5from mpi4py import MPI
8class Heat1DForcedFiredrake(Problem):
9 r"""
10 Forced 1D heat equation with Dirichlet BCs and Firedrake finite elements, IMEX with diffusion implicit.
12 Example implementing the forced one-dimensional heat equation with Dirichlet boundary conditions
14 .. math::
15 \frac{d u}{d t} = \nu \frac{d^2 u}{d x^2} + f
17 for :math:`x \in \Omega:=[0,1]`, where the forcing term :math:`f` is defined by
19 .. math::
20 f(x, t) = -\sin(\pi x) (\sin(t) - \nu \pi^2 \cos(t)).
22 For initial conditions with constant c
24 .. math::
25 u(x, 0) = \sin(\pi x) + c,
27 the exact solution is given by
29 .. math::
30 u(x, t) = \sin(\pi x)\cos(t) + c.
32 Here, the problem is discretized with finite elements using firedrake. Hence, the problem
33 is reformulated to the *weak formulation*
35 .. math:
36 \int_\Omega u_t v\,dx = - \nu \int_\Omega \nabla u \nabla v\,dx + \int_\Omega f v\,dx.
38 We invert the Laplacian implicitly and treat the forcing term explicitly.
39 The solvers for the arising variational problems are cached for multiple collocation nodes and step sizes.
41 Parameters
42 ----------
43 nvars : int, optional
44 Spatial resolution, i.e., numbers of degrees of freedom in space.
45 nu : float, optional
46 Diffusion coefficient :math:`\nu`.
47 c: float, optional
48 Constant for the Dirichlet boundary condition :math: `c`
49 LHS_cache_size : int, optional
50 Cache size for variational problem solvers
51 comm : MPI communicator, optional
52 Supply an MPI communicator for spatial parallelism
53 """
55 dtype_u = firedrake_mesh
56 dtype_f = IMEX_firedrake_mesh
58 def __init__(self, n=30, nu=0.1, c=0.0, order=4, LHS_cache_size=12, mesh=None, comm=None):
59 """
60 Initialization
62 Args:
63 n (int): Number of degrees of freedom
64 nu (float): Diffusion parameter
65 c (float): Boundary condition constant
66 order (int): Order of finite elements
67 LHS_cache_size (int): Size of the cache for solvers
68 mesh (Firedrake mesh, optional): Give a custom mesh, for instance from a hierarchy
69 comm (mpi4pi.Intracomm, optional): MPI communicator for spatial parallelism
70 """
71 comm = MPI.COMM_WORLD if comm is None else comm
73 # prepare Firedrake mesh and function space
74 self.mesh = fd.UnitIntervalMesh(n, comm=comm) if mesh is None else mesh
75 self.V = fd.FunctionSpace(self.mesh, "CG", order)
77 # prepare pySDC problem class infrastructure by passing the function space to super init
78 super().__init__(self.V)
79 self._makeAttributeAndRegister(
80 'n', 'nu', 'c', 'order', 'LHS_cache_size', 'comm', localVars=locals(), readOnly=True
81 )
83 # prepare caches and IO variables for solvers
84 self.solvers = {}
85 self.tmp_in = fd.Function(self.V)
86 self.tmp_out = fd.Function(self.V)
88 # prepare work counters
89 self.work_counters['solver_setup'] = WorkCounter()
90 self.work_counters['solves'] = WorkCounter()
91 self.work_counters['rhs'] = WorkCounter()
93 def eval_f(self, u, t):
94 """
95 Evaluate the right hand side.
96 The forcing term is simply interpolated to the grid.
97 The Laplacian is evaluated via a variational problem, where the mass matrix is inverted and homogeneous boundary conditions are applied.
98 Note that we cache the solver to obtain much better performance.
100 Parameters
101 ----------
102 u : dtype_u
103 Solution at which to evaluate
104 t : float
105 Time at which to evaluate
107 Returns
108 -------
109 f : dtype_f
110 The evaluated right hand side
111 """
112 # construct and cache a solver for the implicit part of the right hand side evaluation
113 if not hasattr(self, '_solv_eval_f_implicit'):
114 v = fd.TestFunction(self.V)
115 u_trial = fd.TrialFunction(self.V)
117 a = u_trial * v * fd.dx
118 L_impl = -fd.inner(self.nu * fd.nabla_grad(self.tmp_in), fd.nabla_grad(v)) * fd.dx
120 bcs = [fd.bcs.DirichletBC(self.V, fd.Constant(0), area) for area in [1, 2]]
122 prob = fd.LinearVariationalProblem(a, L_impl, self.tmp_out, bcs=bcs)
123 self._solv_eval_f_implicit = fd.LinearVariationalSolver(prob)
125 # copy the solution we want to evaluate at into the input buffer
126 self.tmp_in.assign(u.functionspace)
128 # perform the solve using the cached solver
129 self._solv_eval_f_implicit.solve()
131 me = self.dtype_f(self.init)
133 # copy the result of the solver from the output buffer to the variable this function returns
134 me.impl.assign(self.tmp_out)
136 # evaluate explicit part.
137 # Because it does not depend on the current solution, we can simply interpolate the expression
138 x = fd.SpatialCoordinate(self.mesh)
139 me.expl.interpolate(-(np.sin(t) - self.nu * np.pi**2 * np.cos(t)) * fd.sin(np.pi * x[0]))
141 self.work_counters['rhs']()
143 return me
145 def solve_system(self, rhs, factor, *args, **kwargs):
146 r"""
147 Linear solver for :math:`(M - factor nu * Lap) u = rhs`.
149 Parameters
150 ----------
151 rhs : dtype_f
152 Right-hand side for the nonlinear system.
153 factor : float
154 Abbrev. for the node-to-node stepsize (or any other factor required).
155 *args, **kwargs
156 Take the initial guess ``u0`` and the current time ``t`` of the generic interface, neither is used here.
158 Returns
159 -------
160 u : dtype_u
161 Solution.
162 """
164 # construct and cache a solver for the current factor (preconditioner entry times step size)
165 if factor not in self.solvers.keys():
167 # check if we need to evict something from the cache
168 if len(self.solvers) >= self.LHS_cache_size:
169 self.solvers.pop(list(self.solvers.keys())[0])
171 u = fd.TrialFunction(self.V)
172 v = fd.TestFunction(self.V)
174 a = u * v * fd.dx + fd.Constant(factor) * fd.inner(self.nu * fd.nabla_grad(u), fd.nabla_grad(v)) * fd.dx
175 L = fd.inner(self.tmp_in, v) * fd.dx
177 bcs = [fd.bcs.DirichletBC(self.V, fd.Constant(self.c), area) for area in [1, 2]]
179 prob = fd.LinearVariationalProblem(a, L, self.tmp_out, bcs=bcs)
180 self.solvers[factor] = fd.LinearVariationalSolver(prob)
182 self.work_counters['solver_setup']()
184 # copy solver rhs to the input buffer. Copying also to the output buffer uses it as initial guess
185 self.tmp_in.assign(rhs.functionspace)
186 self.tmp_out.assign(rhs.functionspace)
188 # call the cached solver
189 self.solvers[factor].solve()
191 # copy from output buffer to return variable
192 me = self.dtype_u(self.init)
193 me.assign(self.tmp_out)
195 self.work_counters['solves']()
196 return me
198 @fd.utils.cached_property
199 def x(self):
200 """
201 Spatial coordinate of the mesh, cached after the first access.
203 Returns
204 -------
205 x : ufl.SpatialCoordinate
206 Symbolic spatial coordinate, used to interpolate expressions on the mesh.
207 """
208 return fd.SpatialCoordinate(self.mesh)
210 def u_exact(self, t):
211 r"""
212 Routine to compute the exact solution at time :math:`t`.
214 Parameters
215 ----------
216 t : float
217 Time of the exact solution.
219 Returns
220 -------
221 me : dtype_u
222 Exact solution.
223 """
224 me = self.u_init
225 me.interpolate(np.cos(t) * fd.sin(np.pi * self.x[0]) + self.c)
226 return me