Coverage for pySDC/tutorial/step_1/A_spatial_problem_setup.py: 100%
32 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1# ---
2# jupyter:
3# jupytext:
4# formats: py:percent
5# kernelspec:
6# display_name: Python 3
7# name: python3
8# ---
10# %% [markdown]
11# # Part A: Spatial problem setup
12#
13# We start as simple as possible: no SDC, no time stepping, just a spatial problem. pySDC calls this a
14# **problem class**: it holds the spatial discretisation of an equation and knows how to evaluate its right-hand
15# side. Here we take `heatNd_unforced`, the heat equation
16#
17# $$
18# u_t = \nu\, \Delta u \quad \text{on } [0, 1], \qquad u(0, t) = u(1, t) = 0,
19# $$
20#
21# discretised with second-order finite differences.
23# %%
24import numpy as np
25import matplotlib.pyplot as plt
27from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_unforced
29prob = heatNd_unforced(
30 nvars=1023, # number of degrees of freedom
31 nu=0.1, # diffusion coefficient
32 freq=4, # frequency of the test function
33 bc='dirichlet-zero', # boundary conditions
34)
35print(f'{prob.nvars[0]} unknowns, mesh width {prob.dx:.2e}, operator of shape {prob.A.shape}')
37# %% [markdown]
38# Parameters go in as keyword arguments. Almost everything in pySDC is configured this way; later, the same
39# arguments are collected in a dictionary and handed over to the controller.
40#
41# ## Data types
42#
43# The values of a solution live in a pySDC **data type**, here `mesh`, which is a NumPy array with some extra
44# meta-information. Every problem class says which data type it uses, as `dtype_u` for the solution and `dtype_f`
45# for the right-hand side, and every data type is created from the problem's `init` attribute:
47# %%
48u = prob.dtype_u(init=prob.init)
49print(type(u).__name__, u.shape, 'init =', prob.init)
51# %% [markdown]
52# :::{note}
53# `init` and `nvars` look the same here, so why two attributes? `init` is whatever the data type needs to set
54# itself up. For a finite-difference mesh that is just the number of unknowns, but for finite elements it is
55# the function space. So: whenever you create a data type, use `init`.
56# :::
57#
58# ## Testing the operator
59#
60# Before building anything on top of this discretisation, we check it. We fill `u` with $\sin(\pi k x)$, whose
61# exact right-hand side is $-\nu (\pi k)^2 \sin(\pi k x)$, and compare that with what `eval_f` computes. We only
62# use inner points: the boundary values are fixed at zero and are not part of the unknowns.
65# %%
66def run_accuracy_check(prob):
67 """
68 Routine to check the error of the Laplacian vs. its FD discretization
70 Args:
71 prob: a problem instance
73 Returns:
74 the error between the analytic Laplacian and the computed one of a given function
75 """
77 # create x values, use only inner points
78 xvalues = np.array([(i + 1) * prob.dx for i in range(prob.nvars[0])])
80 # create a mesh instance and fill it with a sine wave
81 u = prob.dtype_u(init=prob.init)
82 u[:] = np.sin(np.pi * prob.freq[0] * xvalues)
84 # create a mesh instance and fill it with the Laplacian of the sine wave
85 u_lap = prob.dtype_u(init=prob.init)
86 u_lap[:] = -((np.pi * prob.freq[0]) ** 2) * prob.nu * np.sin(np.pi * prob.freq[0] * xvalues)
88 # compare analytic and computed solution using the eval_f routine of the problem class
89 err = abs(prob.eval_f(u, 0) - u_lap)
91 return err
94err = run_accuracy_check(prob)
95print(f'Error of the spatial accuracy test: {err:8.6e}')
97# %% [markdown]
98# `abs` of a `mesh` is its maximum norm, so this is the largest pointwise error. Relative to the size of the
99# right-hand side, about $0.1 \cdot (4\pi)^2 \approx 16$, that is small. Seeing it helps:
101# %% tags=["hide-input"]
102x = np.array([(i + 1) * prob.dx for i in range(prob.nvars[0])])
103u[:] = np.sin(np.pi * prob.freq[0] * x)
104f_exact = -((np.pi * prob.freq[0]) ** 2) * prob.nu * u
105f_disc = prob.eval_f(u, 0)
107fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 3))
108ax1.plot(x, f_exact, lw=3, alpha=0.4, label='exact')
109ax1.plot(x, f_disc, '--', label='eval_f')
110ax1.set_xlabel('$x$')
111ax1.set_title(r'$\nu\,\Delta u$')
112ax1.legend(frameon=False)
113ax2.plot(x, np.abs(f_disc - f_exact))
114ax2.set_xlabel('$x$')
115ax2.set_title('pointwise error')
116fig.tight_layout()
118# %% [markdown]
119# The error has the shape of the solution itself, as expected from the leading term of a second-order
120# finite-difference stencil, $\frac{h^2}{12} u''''$. In [Part B](B_spatial_accuracy_check) we check that it
121# really shrinks like $h^2$.
122#
123# ## Summary
124#
125# - A problem class bundles the discretised equation; parameters are passed as keyword arguments.
126# - Values live in data types such as `mesh`, created from the problem's `init` attribute.
127# - Test your operators before you build a time integrator on top of them.
128#
129# The tests check exactly what we saw above:
131# %%
132assert err <= 2e-04, f"ERROR: the spatial accuracy is higher than expected, got {err}"