5.2.1. Naive implementation

Let us first ignore this complication and implement the equation naively. We will add a flag advection_in_partial_integration to choose between both different weak forms of the advection term:

from pyoomph import *
from pyoomph.expressions import *


class ConvectionDiffusionEquation(Equations):
     def __init__(self,u,D,advection_in_partial_integration,space="C2"):
             super(ConvectionDiffusionEquation, self).__init__()
             self.u=u # advection velocity
             self.D=D # diffusivity
             self.space=space # space of the field c
             self.advection_in_partial_integration=advection_in_partial_integration # Which weak form to use

     def define_fields(self):
             self.define_scalar_field("c",self.space) # The scalar field to advect

     def define_residuals(self):
             c,phi=var_and_test("c")
             # Advection either intergrated by parts or not
             advection=-weak(self.u*c,grad(phi)) if self.advection_in_partial_integration else weak(div(self.u*c),phi)
             # TPZ or MPT time stepping can be of advantage compared to BDF2
             self.add_residual(time_scheme("TPZ",weak(partial_t(c),phi)+advection+weak(self.D*grad(c),grad(phi))))

Depending on the value of advection_in_partial_integration, we either use (5.3) or (5.4) for the weak form. Furthermore, we changed the time stepping from the default "BDF2" to "TPZ", which can be of advantage (cf. Section 3.6.3 for time stepping methods).

As a problem class, we use bump which is swirled around by one period. When the diffusivity is low, we expect the bump to be only slightly smaller in amplitude and only slightly coarser due to diffusion. The problem class hence reads:

class ConvectionDiffusionProblem(Problem):
     def __init__(self):
             super(ConvectionDiffusionProblem, self).__init__()
             self.u=2*pi*vector([-var("coordinate_y"),var("coordinate_x")]) # Circular flow, one rotation at t=1
             self.D=0.001 # diffusivity
             self.L=1 # size of the mesh
             self.N=4 # number of elements of the coarsest mesh in each direction
             self.max_refinement_level=5 # max refinement level
             self.advection_in_partial_integration=False # which weak form to choose

     def define_problem(self):
             self.add_mesh(RectangularQuadMesh(lower_left=-self.L/2,size=self.L,N=self.N))

             eqs=ConvectionDiffusionEquation(self.u,self.D,self.advection_in_partial_integration)
             eqs+=MeshFileOutput() # output

             # use a bump as initial condition
             bump_pos=vector([-self.L/5,0]) # center pos of the bump
             bump_width=0.005*self.L # width of the bump
             bump_amplitude=1 # amplitude of the bump
             xdiff=var("coordinate")-bump_pos # difference between coordinate and bump center
             bump=bump_amplitude*exp(-dot(xdiff,xdiff)/bump_width) # Gaussian bump
             eqs+=InitialCondition(c=bump)

             # Set the boundaries to 0
             for b in ["top","left","right","bottom"]:
                     eqs+=DirichletBC(c=0)@b

             # Errors: We evaluate the jumps in the gradients of c at the element boundaries, i.e. when crossing to the next element
             # this is not only done at the current time step, but also on the previous one
             error_fluxes=[grad(var("c")),evaluate_in_past(grad(var("c")))]
             eqs+=SpatialErrorEstimator(*error_fluxes)

             self.add_equations(eqs@"domain") # adding the equation

The most interesting thing here is that we can also define a SpatialErrorEstimator based on history values. Instead of passing keyword arguments, we can also pass positional arguments to the SpatialErrorEstimator. The error estimator requires gradients, but these can also be evaluated at previous time steps. This ensures that the wake remains finer resolved.

The run code is again simple:

if __name__=="__main__":
     with ConvectionDiffusionProblem() as problem:
             problem.advection_in_partial_integration=True # Can also set it to false
             problem.D=0.0001 # diffusivity
             problem.run(1,outstep=0.01,maxstep=0.0025,spatial_adapt=1,temporal_error=1)

Using outstep=0.01 in the run(), we will get 100 outputs, but due to maxstep=0.0025, we solve at least 4 times per output. spatial_adapt=1 will perform, as usual, one spatial adaption per solve, whereas temporal_error=1 just ensures that the time step gets reduced when it does not converge.

Results at different times are depicted in Fig. 5.4.

Advecting a bump with low diffusivity.

Fig. 5.4 Advecting a bump with low diffusivity.