How to test convergence of temporal discretization of the heat equation? (+MWCE)

Dear community,

how to test convergence of the spatial discretization of the transient heat equation with Legacy FEniCS is well documented, see here and open the downloaded .html file.
In summary, you determine the averaged error E between an assumed analytical solution u_\mathrm{e} and the numerically calculated solution u for an increasing amount of elements n_\mathrm{s}.

I used the same procedure, with the only difference, that I increased the amount of time steps n_\mathrm{t}, to test the convergence of the temporal discretization by using the forward-Euler method (CFL-conditions are satisfied), but didn’t get a linear correlation between time steps and L2-error for double logarithmic scales, see figure below.

It also yields convergence rates r far smaller than 2 (elements with degree = 1 were used). Convergence rates r of two neighbouring amount of time steps i and i-1 were calculated by

r_{i-1/i} = \frac{\mathrm{log} \left ( \frac{E_i}{E_{i-1}} \right )}{\mathrm{log} \left ( \frac{h_{i}}{h_{i-1}} \right ) } \quad \text{with} \ h = \frac{1}{n_\mathrm{t}} \ .
i 1 2 3 4 5 6
time steps n_\mathrm{t} 100 200 300 400 500 600
convergence rate r 0.007151622055142903 0.00408879703330978 0.0028850131814337193 0.0022331434928824093 0.0018228565668401908

That’s why the following questions come to mind…

1. Is this procedure adequate to test convergence of the temporal discretization by using the forward-Euler method?
2. If so, do the results prove a successful temporal discretization?

If you´re interested in the MWCE of an 1-d transient heat equation with Dirichlet boundary conditions, open the spoiler and run the code with FEniCS Legacy.

MWCE
"""
FEniCS tutorial demo program: 1d-Heat equation with Dirichlet conditions.
Test problem is chosen to give an exact solution at all nodes of the mesh.
Temporal discretization scheme: explicit forward-EULER

  u'  = D*Laplace(u) + f  in the unit line
  u_e = u_D = alpha - t * sin(x) / T
  u   = u_D               on the boundary
  u   = u_D(t=0)          at t = 0

  u' = - sin(x) / T
  Laplace(u) =  t * sin(x) / T
  f = - sin(x) / T - D * t * sin(x) / T
"""
import matplotlib.pyplot as plt
import numpy as np
from fenics import *
set_log_level(30)


def solver(D, dt, num_steps, degree, mesh, V, alpha):
    """ Solves variational problem and calc L2-error between solution u and exact analytical solution u_e"""

    # CFL check because of explicit forward EULER
    element_size = mesh.hmin()
    CFL = D * dt / element_size
    if CFL > 1.0:
        sys.exit("CFL-conditions not satisfied (CFL> 1)!")

    # Define DIRICHLET boundary condition
    u_D = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree)

    def boundary(x, on_boundary):
        return on_boundary

    bc = DirichletBC(V, u_D, boundary)

    # Define exact analytical solution
    u_e = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree+1+3)

    # Initialize list to save L2 errornorm between u_e and u
    L2_err_list = []

    # Define initial value
    u_n = interpolate(u_D, V)

    # Define variational problem
    u = TrialFunction(V)
    v = TestFunction(V)
    f_n = Expression('- sin(x[0]) / T - D * t * sin(x[0]) / T', D=D, t=0, T=T, degree=degree)

    # Weak form
    F = u * v * dx + dt * D * dot(grad(u_n), grad(v)) * dx - (u_n + dt * f_n) * v * dx
    a, L = lhs(F), rhs(F)

    # Time-stepping
    u = Function(V)
    t = 0
    for n in range(num_steps):

        # Update to current time
        t += dt
        u_D.t = t
        u_e.t = t

        # Compute solution u
        solve(a == L, u, bc)

        # Compute L2-error and add to list
        L2_err_list += [errornorm(u_e, u, 'L2', degree_rise=3)]

        # Update for next time step
        u_n.assign(u)
        f_n.t = t

    # Calc average L2-error
    L2_err_avg = 1/len(L2_err_list) * sum(L2_err_list)


    return L2_err_avg



def run_spatial_convergence_analysis(nx, num_steps, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []

    # Iterate through nx
    for i in range(len(nx)):

        # Create mesh and define function space
        mesh = IntervalMesh(nx[i], 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / num_steps[0]

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg = solver(D, dt, num_steps[0], degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list += [L2_err_avg]


    return L2_err_avg_list



def run_temporal_convergence_analysis(nx, num_steps, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []

    # Iterate through num steps
    for i in range(len(num_steps)):

        # Create mesh and define function space
        mesh = IntervalMesh(nx[0], 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / num_steps[i]

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg = solver(D, dt, num_steps[i], degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list += [L2_err_avg]


    return L2_err_avg_list



def compute__convergence_rate(L2_error_avg_list, nx, num_steps, time_or_space):

    # Initialize h
    h = []

    # Calc h
    if time_or_space == 'time':
        for i in range(len(num_steps)):
            h += [1 / num_steps[i]]

    elif time_or_space == 'space':
        for i in range(len(nx)):
            h += [1 / nx[i]]

    # Initialize rate
    rate = [[] for _ in range(len(h) - 1)]

    # Calc rate
    for m in range(len(h) - 1):
        rate[m] += [np.log(L2_error_avg_list[m + 1] / L2_error_avg_list[m]) / np.log(h[m + 1] / h[m])]

    # Print rate
    if time_or_space == 'time':
        print('rate(temporal)) = ', rate)

    elif time_or_space == 'space':
        print('rate(spatial))  = ', rate)



def plot_result_of_convergence_analysis(nx, num_steps, L2_err_avg_list, degree, time_or_space):

    fig, ax = plt.subplots()
    plt.yscale("log")
    plt.xscale("log")
    ax.set_ylabel("avg(L2-errornorm)")

    if time_or_space == 'time':
        fig.suptitle(f'P{degree}-element (nx = {nx})', fontsize=15)
        ax.set_xlabel('n_t')
        ax.plot(num_steps, L2_err_avg_list)

    elif time_or_space == 'space':
        fig.suptitle(f'P{degree}-element (num steps = {num_steps})', fontsize=15)
        ax.set_xlabel("n_x")
        ax.plot(nx, L2_err_avg_list)



# Parameters
D = 0.005                             # diffusion coefficient
T = 1.0                               # final time
num_steps = [100,200,300,400,500,600] # number of time steps
nx = [10,20,30,40,50,60]              # elements per mesh
degree = 1                            # element degree
alpha = 1.0                           # parameter for exact analytical solution

# Run convergence analysis
L2_err_avg_list_spatial  = run_spatial_convergence_analysis (nx, num_steps, D, degree, alpha) # spatial
L2_err_avg_list_temporal = run_temporal_convergence_analysis(nx, num_steps, D, degree, alpha) # temporal

# Compute convergence rate
compute__convergence_rate(L2_err_avg_list_spatial , nx, num_steps, 'space') # spatial
compute__convergence_rate(L2_err_avg_list_temporal, nx, num_steps, 'time')  # temporal

# Plot result of convergence analysis
plot_result_of_convergence_analysis(nx   , num_steps[0], L2_err_avg_list_spatial , degree, 'space') # spatial
plot_result_of_convergence_analysis(nx[0], num_steps   , L2_err_avg_list_temporal, degree, 'time')  # temporal
plt.show()

To test temporal converge you need to have a sufficiently fine grid such that the spatial discretisation error is eliminated. I.e choose Nx=150 and then consider convergence rates.

1 Like

Thanks for pointing this out, but unfortunately errors went through the roof.

Convergence rates:

i 1 2 3 4 5 6
time steps n_\mathrm{t} 100 200 300 400 500 600
convergence rate r -139.1508862022706 -64.64019875670901 104.04267820715933 363.4811039739587 711.8468482280044

I tried different sets of n_\mathrm{t} and n_\mathrm{s} (called it nx in the plot), but never achieved a linear correlation for spatial and temporal discretization with double logarithmic scale at the same time.

updated MWCE
"""
FEniCS tutorial demo program: 1d-Heat equation with Dirichlet conditions.
Test problem is chosen to give an exact solution at all nodes of the mesh.
Temporal discretization scheme: explicit forward-EULER

  u'  = D*Laplace(u) + f  in the unit line
  u_e = u_D = alpha - t * sin(x) / T
  u   = u_D               on the boundary
  u   = u_D(t=0)          at t = 0

  u' = - sin(x) / T
  Laplace(u) =  t * sin(x) / T
  f = - sin(x) / T - D * t * sin(x) / T
"""
import matplotlib.pyplot as plt
import numpy as np
from fenics import *
set_log_level(30)


def solver(D, dt, nt, degree, mesh, V, alpha):
    """ Solves variational problem and calc L2-error between solution u and exact analytical solution u_e"""

    # CFL check because of explicit forward EULER
    element_size = mesh.hmin()
    CFL = D * dt / element_size
    if CFL > 1.0:
        sys.exit("CFL-conditions not satisfied (CFL> 1)!")

    # Define DIRICHLET boundary condition
    u_D = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree)

    def boundary(x, on_boundary):
        return on_boundary

    bc = DirichletBC(V, u_D, boundary)

    # Define exact analytical solution
    u_e = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree+1+3)

    # Initialize list to save L2 errornorm between u_e and u
    L2_err_list = []

    # Define initial value
    u_n = interpolate(u_D, V)

    # Define variational problem
    u = TrialFunction(V)
    v = TestFunction(V)
    f_n = Expression('- sin(x[0]) / T - D * t * sin(x[0]) / T', D=D, t=0, T=T, degree=degree)

    # Weak form
    F = u * v * dx + dt * D * dot(grad(u_n), grad(v)) * dx - (u_n + dt * f_n) * v * dx
    a, L = lhs(F), rhs(F)

    # Time-stepping
    u = Function(V)
    t = 0
    for n in range(nt):

        # Update to current time
        t += dt
        u_D.t = t
        u_e.t = t

        # Compute solution u
        solve(a == L, u, bc)

        # Compute L2-error and add to list
        L2_err_list += [errornorm(u_e, u, 'L2', degree_rise=3)]

        # Update for next time step
        u_n.assign(u)
        f_n.t = t

    # Calc average L2-error
    L2_err_avg = 1/len(L2_err_list) * sum(L2_err_list)


    return L2_err_avg



def run_spatial_convergence_analysis(ns, nt, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []

    # Iterate through ns
    for i in range(len(ns)):

        # Create mesh and define function space
        mesh = IntervalMesh(ns[i], 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / nt

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg = solver(D, dt, nt, degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list += [L2_err_avg]


    return L2_err_avg_list



def run_temporal_convergence_analysis(ns, nt, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []

    # Iterate through num steps
    for i in range(len(nt)):

        # Create mesh and define function space
        mesh = IntervalMesh(ns, 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / nt[i]

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg = solver(D, dt, nt[i], degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list += [L2_err_avg]


    return L2_err_avg_list



def compute__convergence_rate(L2_error_avg_list, ns, nt, time_or_space):

    # Initialize h
    h = []

    # Calc h
    if time_or_space == 'time':
        for i in range(len(nt)):
            h += [1 / nt[i]]

    elif time_or_space == 'space':
        for i in range(len(ns)):
            h += [1 / ns[i]]

    # Initialize rate
    rate = [[] for _ in range(len(h) - 1)]

    # Calc rate
    for m in range(len(h) - 1):
        rate[m] += [np.log(L2_error_avg_list[m + 1] / L2_error_avg_list[m]) / np.log(h[m + 1] / h[m])]

    # Print rate
    if time_or_space == 'time':
        print('rate(temporal)) = ', rate)

    elif time_or_space == 'space':
        print('rate(spatial))  = ', rate)



def plot_result_of_convergence_analysis(ns, nt, L2_err_avg_list, degree, time_or_space):

    fig, ax = plt.subplots()
    plt.yscale("log")
    plt.xscale("log")
    ax.set_ylabel("avg(L2-errornorm)")

    if time_or_space == 'time':
        fig.suptitle(f'P{degree}-element (nx = {ns})', fontsize=15)
        ax.set_xlabel('n_t')
        ax.plot(nt, L2_err_avg_list)

    elif time_or_space == 'space':
        fig.suptitle(f'P{degree}-element (n_t = {nt})', fontsize=15)
        ax.set_xlabel("n_s")
        ax.plot(ns, L2_err_avg_list)



# Parameters
D = 0.005                           # diffusion coefficient
T = 1.0                             # final time
nt = [100, 200, 300, 400, 500, 600] # number of time steps
ns = [10, 20, 30, 40, 50, 150]      # elements per mesh
degree = 1                          # element degree
alpha = 1.0                         # parameter for exact analytical solution

# Run convergence analysis
L2_err_avg_list_spatial  = run_spatial_convergence_analysis (ns    , nt[0], D, degree, alpha) # spatial
L2_err_avg_list_temporal = run_temporal_convergence_analysis(ns[-1], nt   , D, degree, alpha) # temporal

# Compute convergence rate
compute__convergence_rate(L2_err_avg_list_spatial , ns, nt, 'space') # spatial
compute__convergence_rate(L2_err_avg_list_temporal, ns, nt, 'time')  # temporal

# Plot result of convergence analysis
plot_result_of_convergence_analysis(ns    , nt[0], L2_err_avg_list_spatial , degree, 'space') # spatial
plot_result_of_convergence_analysis(ns[-1], nt   , L2_err_avg_list_temporal, degree, 'time')  # temporal
plt.show()

That means that your problem is no longer stable, which is not suprising, as CFL is u \Delta t/ \Delta x, and your \Delta x is significantly smaller. You likely have this issue due to the fact that you choose to use a forward Euler, see for instance; The 1D diffusion equation

I would use a backward Euler method or Crank Nicholson: The 1D diffusion equation

2 Likes

Because of the discontinuity between my boundary conditions and u(t=0) of my actual project, I would like to use the forward Euler method for the first couple of hundred time steps to prevent oscillation of the solution u. Then, I will use the Crank-Nicolson method.

To test temporal convergence of the forward Euler method, I implemented your tips, like choosing a fine mesh (ns=1000) and reducing the final time T significantly ( T=10^{-7}) to satisfy the CFL conditions, see figure below.

The temporal discretization errors (right figure) are also smaller than the spatial ones (left figure), so I assume the spatial discretization error is eliminated, right?

To my eye, there is convergence of the temporal discretization, but not in a linear way for double logarithmic axis scaling.

So, is linear convergence of the temporal discretization for double logarithmic axis scaling a necessary condition or is any form of convergence sufficient?

updated MWCE
"""
FEniCS tutorial demo program: 1d-Heat equation with Dirichlet conditions.
Test problem is chosen to give an exact solution at all nodes of the mesh.
Temporal discretization scheme: explicit forward-EULER

  u'  = D*Laplace(u) + f  in the unit line
  u_e = u_D = alpha - t * sin(x) / T
  u   = u_D               on the boundary
  u   = u_D(t=0)          at t = 0

  u' = - sin(x) / T
  Laplace(u) =  t * sin(x) / T
  f = - sin(x) / T - D * t * sin(x) / T
"""
import matplotlib.pyplot as plt
import numpy as np
from fenics import *
set_log_level(30)


def solver(D, dt, nt, degree, mesh, V, alpha):
    """ Solves variational problem and calc L2-error between solution u and exact analytical solution u_e"""

    # CFL check because of explicit forward EULER
    element_size = mesh.hmin()
    CFL = element_size**2/(2*D*dt)

    # Define DIRICHLET boundary condition
    u_D = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree)

    def boundary(x, on_boundary):
        return on_boundary

    bc = DirichletBC(V, u_D, boundary)

    # Define exact analytical solution
    u_e = Expression('alpha - t*sin(x[0])/T', alpha=alpha, t=0, T=T, degree=degree+1+3)

    # Initialize list to save L2 errornorm between u_e and u
    L2_err_list = []

    # Define initial value
    u_n = interpolate(u_D, V)

    # Define variational problem
    u = TrialFunction(V)
    v = TestFunction(V)
    f_n = Expression('- sin(x[0]) / T - D * t * sin(x[0]) / T', D=D, t=0, T=T, degree=degree)

    # Weak form
    F = u * v * dx + dt * D * dot(grad(u_n), grad(v)) * dx - (u_n + dt * f_n) * v * dx
    a, L = lhs(F), rhs(F)

    # Time-stepping
    u = Function(V)
    t = 0
    for n in range(nt):

        # Update to current time
        t += dt
        u_D.t = t
        u_e.t = t

        # Compute solution u
        solve(a == L, u, bc)

        # Compute L2-error and add to list
        L2_err_list += [errornorm(u_e, u, 'L2', degree_rise=3)]

        # Update for next time step
        u_n.assign(u)
        f_n.t = t

    # Calc average L2-error
    L2_err_avg = 1/len(L2_err_list) * sum(L2_err_list)


    return L2_err_avg, CFL



def run_spatial_convergence_analysis(ns, nt, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []
    CFL_list = []

    # Iterate through ns
    for i in range(len(ns)):

        # Create mesh and define function space
        mesh = IntervalMesh(ns[i], 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / nt

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg, CFL = solver(D, dt, nt, degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list.append(L2_err_avg)
        CFL_list.append(CFL)


    return L2_err_avg_list, CFL_list



def run_temporal_convergence_analysis(ns, nt, D, degree, alpha):

    # Initialize list to save average L2-errornorms
    L2_err_avg_list = []
    CFL_list = []

    # Iterate through num steps
    for i in range(len(nt)):

        # Create mesh and define function space
        mesh = IntervalMesh(ns, 0, 1)
        V = FunctionSpace(mesh, 'P', degree)

        # Calc time step dt
        dt = T / nt[i]

        # Calc average L2-errornorm between u_e and u by running solver
        L2_err_avg, CFL = solver(D, dt, nt[i], degree, mesh, V, alpha)

        # Add average L2-errornorm to list
        L2_err_avg_list.append(L2_err_avg)
        CFL_list.append(CFL)


    return L2_err_avg_list, CFL_list



def compute__convergence_rate(L2_error_avg_list, ns, nt, time_or_space):

    # Initialize h
    h = []

    # Calc h
    if time_or_space == 'time':
        for i in range(len(nt)):
            h += [1 / nt[i]]

    elif time_or_space == 'space':
        for i in range(len(ns)):
            h += [1 / ns[i]]

    # Initialize rate
    rate = [[] for _ in range(len(h) - 1)]

    # Calc rate
    for m in range(len(h) - 1):
        rate[m] += [np.log(L2_error_avg_list[m + 1] / L2_error_avg_list[m]) / np.log(h[m + 1] / h[m])]

    # Print rate
    if time_or_space == 'time':
        print('rate(temporal)) = ', rate)

    elif time_or_space == 'space':
        print('rate(spatial))  = ', rate)



def plot_result_of_convergence_analysis(ns, nt, L2_err_avg_list, degree, time_or_space):

    fig, ax = plt.subplots()
    plt.yscale("log")
    plt.xscale("log")
    ax.set_ylabel("avg(L2-errornorm)")

    if time_or_space == 'time':
        fig.suptitle(f'P{degree}-element (n_s = {ns})', fontsize=15)
        ax.set_xlabel('n_t')
        ax.plot(nt, L2_err_avg_list)

    elif time_or_space == 'space':
        fig.suptitle(f'P{degree}-element (n_t = {nt})', fontsize=15)
        ax.set_xlabel("n_s")
        ax.plot(ns, L2_err_avg_list)



def plot_CFL(ns, nt, CFL_list, degree, time_or_space):

    fig, ax = plt.subplots()
    ax.set_ylabel("CFL")
    plt.yscale("log")
    plt.xscale("log")

    if time_or_space == 'time':
        fig.suptitle(f'dx²/2D*dt = CFL ≥ 1   (n_s = {ns})', fontsize=15)
        ax.set_xlabel('n_t')
        ax.scatter(nt, CFL_list)

    elif time_or_space == 'space':
        fig.suptitle(f'dx²/2D*dt = CFL ≥ 1   (n_t = {nt})', fontsize=15)
        ax.set_xlabel("n_s")
        ax.scatter(ns, CFL_list)



# Parameters
D = 1.0     # diffusion coefficient
T = 10**-7  # final time
nt = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000] # number of time steps
ns = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000] # elements per mesh
degree = 1  # element degree
alpha = 1.0 # parameter for exact analytical solution

# Run convergence analysis
L2_err_avg_list_spatial, CFL_list_spatial   = run_spatial_convergence_analysis (ns    , nt[0], D, degree, alpha) # spatial
L2_err_avg_list_temporal, CFL_list_temporal = run_temporal_convergence_analysis(ns[-1], nt   , D, degree, alpha) # temporal

# Compute convergence rate
compute__convergence_rate(L2_err_avg_list_spatial , ns, nt, 'space') # spatial
compute__convergence_rate(L2_err_avg_list_temporal, ns, nt, 'time')  # temporal

# Plot result of convergence analysis
plot_result_of_convergence_analysis(ns    , nt[0], L2_err_avg_list_spatial , degree, 'space') # spatial
plot_result_of_convergence_analysis(ns[-1], nt   , L2_err_avg_list_temporal, degree, 'time')  # temporal

# Plot CFL
#plot_CFL(ns    , nt[0], CFL_list_spatial , degree, 'space') # spatial
plot_CFL(ns[-1], nt   , CFL_list_temporal, degree, 'time')  # temporal
plt.show()

So that left graph shows convergence as you refine the spatial grid and keep the number of timesteps fixed to 100, and the right graph shows convergence as you refine in time and keep the spatial grid fixed to 1000?

Then your graphs are showing that the error goes down with spatial resolution, but remains fixed for temporal refinement, which simply says that your error is still spatially dominant. That is not so surprising given a mesh size of 1E-3 and a timestep size of 1E-10.

Explicit time stepping for parabolic problems (such as the heat problems, 2nd order in space 1st order in time) are generally quite tricky. As you’ve shown in your ‘CFL’ condition expression (a term typically reserved for (advective)transport-based problems, more generally we simply talk about the ‘stability region’ of the time stepper), the condition scales quadratically with Dx and only linearly with Dt. That quickly becomes quite a painful condition on Dt.

As Dokken mentioned, you probably want to use an implicit stepper. You say “I want to use forward Euler method <…> to prevent oscillation of the solution”. But it is the backwards Euler method that is extremely dissipative and will suppress oscillations.

If you’re really dead-set on Explicit Euler, then:

  • Use a time-step that is ‘just enough’ to get you into the stability region, and not to 10^3 per your ‘CFL’ graph.
  • maybe you could increase the polynomial order in space. That will push down the spatial error at a much higher rate, potentially getting you to the point where temporal error dominates. Note: increasing P will affect the stability condition, but only as a constant factor, not as a change of the exponent.

P.s. in your initial post you mention an expected rate of convergence r of 2 of Explicit Euler, but this is actually merely 1.

Pps. One usually writes the stability condition the other way around, so 2Ddt/dx^2 < 1.

2 Likes