Skip to content

Commit 14894d7

Browse files
committed
Added playground for heat equation with time-dependent BCs
1 parent 3a9d54d commit 14894d7

3 files changed

Lines changed: 299 additions & 0 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import numpy as np
2+
3+
from pySDC.core.problem import Problem
4+
from pySDC.implementations.datatype_classes.mesh import mesh
5+
from pySDC.implementations.problem_classes.generic_spectral import GenericSpectralLinear
6+
7+
8+
class GenericSpectralLinearTimeDepBCs(GenericSpectralLinear):
9+
def solve_system(self, rhs, dt, u0=None, t=0, *args, **kwargs):
10+
"""
11+
Do an implicit Euler step to solve M u_t + Lu = rhs, with M the mass matrix and L the linear operator as setup by
12+
``GenericSpectralLinear.setup_L`` and ``GenericSpectralLinear.setup_M``.
13+
14+
The implicit Euler step is (M - dt L) u = M rhs. Note that M need not be invertible as long as (M + dt*L) is.
15+
This means solving with dt=0 to mimic explicit methods does not work for all problems, in particular simple DAEs.
16+
17+
Note that by putting M rhs on the right hand side, this function can only solve algebraic conditions equal to
18+
zero. If you want something else, it should be easy to overload this function.
19+
"""
20+
21+
self.heterogeneous_setup()
22+
23+
if self.spectral_space:
24+
rhs_hat = rhs.copy()
25+
if u0 is not None:
26+
u0_hat = u0.copy().flatten()
27+
else:
28+
u0_hat = None
29+
else:
30+
rhs_hat = self.spectral.transform(rhs)
31+
if u0 is not None:
32+
u0_hat = self.spectral.transform(u0).flatten()
33+
else:
34+
u0_hat = None
35+
36+
# apply inverse right preconditioner to initial guess
37+
if u0_hat is not None and 'direct' not in self.solver_type:
38+
if not hasattr(self, '_Pr_inv'):
39+
self._PR_inv = self.linalg.splu(self.Pr.astype(complex)).solve
40+
u0_hat[...] = self._PR_inv(u0_hat)
41+
42+
rhs_hat = (self.M @ rhs_hat.flatten()).reshape(rhs_hat.shape)
43+
rhs_hat = self.spectral.put_BCs_in_rhs_hat(rhs_hat)
44+
self.put_time_dep_BCs_in_rhs(
45+
rhs_hat, t
46+
) # this line is the difference between this and the generic implementation
47+
rhs_hat = self.Pl @ rhs_hat.flatten()
48+
49+
if dt not in self.cached_factorizations.keys():
50+
if self.heterogeneous:
51+
M = self.M_CPU
52+
L = self.L_CPU
53+
Pl = self.Pl_CPU
54+
Pr = self.Pr_CPU
55+
else:
56+
M = self.M
57+
L = self.L
58+
Pl = self.Pl
59+
Pr = self.Pr
60+
61+
A = M + dt * L
62+
A = Pl @ self.spectral.put_BCs_in_matrix(A) @ Pr
63+
64+
if dt not in self.cached_factorizations.keys():
65+
if len(self.cached_factorizations) >= self.max_cached_factorizations:
66+
self.cached_factorizations.pop(list(self.cached_factorizations.keys())[0])
67+
self.logger.debug(f'Evicted matrix factorization for {dt=:.6f} from cache')
68+
69+
solver = self.spectral.linalg.factorized(A)
70+
71+
self.cached_factorizations[dt] = solver
72+
self.logger.debug(f'Cached matrix factorization for {dt=:.6f}')
73+
self.work_counters['factorizations']()
74+
75+
_sol_hat = self.cached_factorizations[dt](rhs_hat)
76+
self.work_counters[self.solver_type]()
77+
self.logger.debug(f'Used cached matrix factorization for {dt=:.6f}')
78+
79+
sol_hat = self.spectral.u_init_forward
80+
sol_hat[...] = (self.Pr @ _sol_hat).reshape(sol_hat.shape)
81+
82+
if self.spectral_space:
83+
return sol_hat
84+
else:
85+
sol = self.spectral.u_init
86+
sol[:] = self.spectral.itransform(sol_hat).real
87+
88+
if self.spectral.debug:
89+
self.spectral.check_BCs(sol)
90+
91+
return sol
92+
93+
94+
class Heat1DTimeDependentBCs(GenericSpectralLinearTimeDepBCs):
95+
"""
96+
1D Heat equation with time-dependent Dirichlet Boundary conditions discretized on (-1, 1) using an ultraspherical spectral method.
97+
"""
98+
99+
dtype_u = mesh
100+
dtype_f = mesh
101+
102+
def __init__(self, nvars=128, a=1, b=2, f=1, nu=1e-2, ft=np.pi, **kwargs):
103+
"""
104+
Constructor. `kwargs` are forwarded to parent class constructor.
105+
106+
Args:
107+
nvars (int): Resolution
108+
a (float): Left BC value at t=0
109+
b (float): Right BC value at t=0
110+
f (int): Frequency of the solution
111+
nu (float): Diffusion parameter
112+
ft (int): frequency of the BCs in time
113+
"""
114+
self._makeAttributeAndRegister('nvars', 'a', 'b', 'f', 'nu', 'ft', localVars=locals(), readOnly=True)
115+
116+
bases = [{'base': 'ultraspherical', 'N': nvars}]
117+
components = ['u']
118+
119+
GenericSpectralLinear.__init__(self, bases, components, real_spectral_coefficients=True, **kwargs)
120+
121+
self.x = self.get_grid()[0]
122+
123+
I = self.get_Id()
124+
Dxx = self.get_differentiation_matrix(axes=(0,), p=2)
125+
126+
S2 = self.get_basis_change_matrix(p_in=2, p_out=0)
127+
U2 = self.get_basis_change_matrix(p_in=0, p_out=2)
128+
129+
self.Dxx = S2 @ Dxx
130+
131+
L_lhs = {
132+
'u': {'u': -nu * Dxx},
133+
}
134+
self.setup_L(L_lhs)
135+
136+
M_lhs = {'u': {'u': U2 @ I}}
137+
self.setup_M(M_lhs)
138+
139+
self.add_BC(component='u', equation='u', axis=0, x=-1, v=a, kind="Dirichlet", line=-1)
140+
self.add_BC(component='u', equation='u', axis=0, x=1, v=b, kind="Dirichlet", line=-2)
141+
self.setup_BCs()
142+
143+
def eval_f(self, u, *args, **kwargs):
144+
f = self.f_init
145+
iu = self.index('u')
146+
147+
if self.spectral_space:
148+
u_hat = u.copy()
149+
else:
150+
u_hat = self.transform(u)
151+
152+
u_hat[iu] = (self.nu * (self.Dxx @ u_hat[iu].flatten())).reshape(u_hat[iu].shape)
153+
154+
if self.spectral_space:
155+
me = u_hat
156+
else:
157+
me = self.itransform(u_hat).real
158+
159+
f[iu][...] = me[iu]
160+
return f
161+
162+
def u_exact(self, t=0):
163+
"""
164+
Get initial conditions
165+
166+
Args:
167+
t (float): When you want the exact solution
168+
169+
Returns:
170+
Heat1DUltraspherical.dtype_u: Exact solution
171+
"""
172+
assert t == 0
173+
174+
xp = self.xp
175+
iu = self.index('u')
176+
u = self.spectral.u_init_physical
177+
178+
u[iu] = (
179+
xp.sin(np.pi * self.x) * xp.exp(-self.nu * (self.f * np.pi) ** 2 * t)
180+
+ (self.b - self.a) / 2 * self.x
181+
+ (self.b + self.a) / 2
182+
)
183+
184+
if self.spectral_space:
185+
u_hat = self.spectral.u_init_forward
186+
u_hat[...] = self.transform(u)
187+
u = u_hat
188+
189+
# apply BCs
190+
u = self.solve_system(u, 1e-9, u, t)
191+
192+
return u
193+
194+
def put_time_dep_BCs_in_rhs(self, rhs_hat, t):
195+
"""
196+
Put the time dependent BCs in the right hand side.
197+
198+
In this simple 1D case the BCs are simply in the last two lines of the problem, so we can put there whatever we want.
199+
Note that in 2D you essentially do the same, but you need to unflatten the RHS, put the BCs in the last lines, and then reflatten.
200+
"""
201+
rhs_hat[0, -1] = self.a * self.xp.cos(t * self.ft)
202+
rhs_hat[0, -2] = self.b * self.xp.cos(t * self.ft)
203+
return rhs_hat
204+
205+
def get_fig(self):
206+
import matplotlib.pyplot as plt
207+
208+
fig, ax = plt.subplots()
209+
return fig
210+
211+
def plot(self, u, t, fig):
212+
if self.spectral_space:
213+
u = self.itransform(u)
214+
ax = fig.get_axes()[0]
215+
ax.cla()
216+
ax.plot(self.x, u[0])
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
from pySDC.helpers.stats_helper import get_sorted, get_list_of_types
4+
5+
from pySDC.playgrounds.time_dep_BCs.heat_eq_time_dep_BCs import Heat1DTimeDependentBCs
6+
from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit
7+
from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI
8+
from pySDC.implementations.hooks.plotting import PlotPostStep
9+
10+
11+
def run_heat(dt=1e-1, Tend=4):
12+
level_params = {}
13+
level_params['dt'] = dt
14+
15+
sweeper_params = {}
16+
sweeper_params['quad_type'] = 'RADAU-RIGHT'
17+
sweeper_params['num_nodes'] = 3
18+
sweeper_params['QI'] = 'LU'
19+
20+
problem_params = {}
21+
22+
step_params = {}
23+
step_params['maxiter'] = 4
24+
25+
controller_params = {}
26+
controller_params['logger_level'] = 15
27+
controller_params['hook_class'] = PlotPostStep
28+
29+
description = {}
30+
description['problem_class'] = Heat1DTimeDependentBCs
31+
description['problem_params'] = problem_params
32+
description['sweeper_class'] = generic_implicit
33+
description['sweeper_params'] = sweeper_params
34+
description['level_params'] = level_params
35+
description['step_params'] = step_params
36+
37+
t0 = 0.0
38+
39+
controller = controller_nonMPI(num_procs=1, controller_params=controller_params, description=description)
40+
41+
P = controller.MS[0].levels[0].prob
42+
uinit = P.u_exact(t0)
43+
44+
uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend)
45+
46+
return stats, controller
47+
48+
49+
if __name__ == '__main__':
50+
run_heat()
51+
plt.show()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pytest
2+
3+
4+
@pytest.mark.parametrize('a', [1, 3.14])
5+
@pytest.mark.parametrize('b', [-1, -3.14])
6+
@pytest.mark.parametrize('t', [1, 3.14])
7+
def test_time_dep_heat_eq(a, b, t):
8+
from pySDC.playgrounds.time_dep_BCs.heat_eq_time_dep_BCs import Heat1DTimeDependentBCs
9+
import numpy as np
10+
11+
problem = Heat1DTimeDependentBCs(a=a, b=b, ft=1)
12+
u0 = problem.u_exact(0)
13+
14+
u = problem.solve_system(u0, t, u0, t)
15+
16+
if not problem.spectral_space:
17+
u = problem.transform(u)
18+
19+
expect_boundary = np.empty((2, 2))
20+
expect_boundary = problem.put_time_dep_BCs_in_rhs(expect_boundary, t)
21+
22+
right_boundary = u.sum()
23+
expect_right_boundary = expect_boundary[0, -2]
24+
assert np.isclose(
25+
right_boundary, expect_right_boundary
26+
), f'Got {right_boundary} at right boundary but expected {expect_right_boundary} at {t=}'
27+
28+
left_boundary = u[0, ::2].sum() - u[0, 1::2].sum()
29+
expect_left_boundary = expect_boundary[0, -1]
30+
assert np.isclose(
31+
left_boundary, expect_left_boundary
32+
), f'Got {left_boundary} at left boundary but expected {expect_left_boundary} at {t=}'

0 commit comments

Comments
 (0)