Skip to content

Commit 70fa713

Browse files
author
Thomas Baumann
committed
Added Lorenz attractor problem
1 parent c2c066a commit 70fa713

File tree

3 files changed

+363
-0
lines changed

3 files changed

+363
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import numpy as np
2+
from pySDC.core.Problem import ptype
3+
from pySDC.implementations.datatype_classes.mesh import mesh
4+
5+
6+
class LorenzAttractor(ptype):
7+
"""
8+
Simple script to run a Lorenz attractor problem.
9+
10+
The Lorenz attractor is a system of three ordinary differential equations that exhibits some chaotic behaviour.
11+
It is well known for the "Butterfly Effect", because the solution looks like a butterfly (solve to Tend = 100) or
12+
so to see this with these initial conditions) and because of the chaotic nature.
13+
14+
Since the problem is non-linear, we need to use a Newton solver.
15+
16+
Problem and initial conditions does not originate from, but was taken from doi.org/10.2140/camcos.2015.10.1
17+
"""
18+
19+
def __init__(self, problem_params):
20+
"""
21+
Initialization routine
22+
23+
Args:
24+
problem_params (dict): custom parameters for the example
25+
"""
26+
27+
# take care of essential parameters in defaults such that they always exist
28+
defaults = {
29+
'sigma': 10.0,
30+
'rho': 28.0,
31+
'beta': 8.0 / 3.0,
32+
'nvars': 3,
33+
'newton_tol': 1e-9,
34+
'newton_maxiter': 99,
35+
}
36+
37+
for key in defaults.keys():
38+
problem_params[key] = problem_params.get(key, defaults[key])
39+
40+
# invoke super init, passing number of dofs, dtype_u and dtype_f
41+
super().__init__(
42+
init=(problem_params['nvars'], None, np.dtype('float64')),
43+
dtype_u=mesh,
44+
dtype_f=mesh,
45+
params=problem_params,
46+
)
47+
48+
def eval_f(self, u, t):
49+
"""
50+
Routine to evaluate the RHS
51+
52+
Args:
53+
u (dtype_u): current values
54+
t (float): current time
55+
56+
Returns:
57+
dtype_f: the RHS
58+
"""
59+
# abbreviations
60+
sigma = self.params.sigma
61+
rho = self.params.rho
62+
beta = self.params.beta
63+
64+
f = self.dtype_f(self.init)
65+
66+
f[0] = sigma * (u[1] - u[0])
67+
f[1] = rho * u[0] - u[1] - u[0] * u[2]
68+
f[2] = u[0] * u[1] - beta * u[2]
69+
return f
70+
71+
def solve_system(self, rhs, dt, u0, t):
72+
"""
73+
Simple Newton solver for the nonlinear system.
74+
Notice that I did not go through the trouble of inverting the Jacobian beforehand. If you have some time on your
75+
hands feel free to do that! In the current implementation it is inverted using `numpy.linalg.solve`, which is a
76+
bit more expensive than simple matrix-vector multiplication.
77+
78+
Args:
79+
rhs (dtype_f): right-hand side for the linear system
80+
dt (float): abbrev. for the local stepsize (or any other factor required)
81+
u0 (dtype_u): initial guess for the iterative solver
82+
t (float): current time (e.g. for time-dependent BCs)
83+
84+
Returns:
85+
dtype_u: solution as mesh
86+
"""
87+
88+
# abbreviations
89+
sigma = self.params.sigma
90+
rho = self.params.rho
91+
beta = self.params.beta
92+
93+
# start Newton iterations
94+
u = self.dtype_u(u0)
95+
res = np.inf
96+
for n in range(0, self.params.newton_maxiter):
97+
98+
# assemble G such that G(u) = 0 at the solution to the step
99+
G = np.array(
100+
[
101+
u[0] - dt * sigma * (u[1] - u[0]) - rhs[0],
102+
u[1] - dt * (rho * u[0] - u[1] - u[0] * u[2]) - rhs[1],
103+
u[2] - dt * (u[0] * u[1] - beta * u[2]) - rhs[2],
104+
]
105+
)
106+
107+
# compute the residual and determine if we are done
108+
res = np.linalg.norm(G, np.inf)
109+
if res <= self.params.newton_tol or np.isnan(res):
110+
break
111+
112+
# assemble Jacobian J of G
113+
J = np.array(
114+
[
115+
[1.0 + dt * sigma, -dt * sigma, 0],
116+
[-dt * (rho - u[2]), 1 + dt, dt * u[0]],
117+
[-dt * u[1], -dt * u[0], 1.0 + dt * beta],
118+
]
119+
)
120+
121+
# solve the linear system for the Newton correction J delta = G
122+
delta = np.linalg.solve(J, G)
123+
124+
# update solution
125+
u = u - delta
126+
127+
return u
128+
129+
def u_exact(self, t, u_init=None, t_init=None):
130+
"""
131+
Routine to return initial conditions or to approximate exact solution using scipy.
132+
133+
Args:
134+
t (float): current time
135+
u_init (pySDC.implementations.problem_classes.Lorenz.dtype_u): initial conditions for getting the exact solution
136+
t_init (float): the starting time
137+
138+
Returns:
139+
dtype_u: exact solution
140+
"""
141+
me = self.dtype_u(self.init)
142+
143+
if t > 0:
144+
from scipy.integrate import solve_ivp
145+
146+
def rhs(t, u):
147+
"""
148+
Evaluate the right hand side, but switch the arguments for scipy.
149+
150+
Args:
151+
t (float): Time
152+
u (numpy.ndarray): Solution at time t
153+
154+
Returns:
155+
(numpy.ndarray): Right hand side evaluation
156+
"""
157+
return self.eval_f(u, t)
158+
159+
tol = 100 * np.finfo(float).eps
160+
u_init = self.u_exact(t=0) if u_init is None else u_init
161+
t_init = 0 if t_init is None else t_init
162+
163+
me[:] = solve_ivp(rhs, (t_init, t), u_init, rtol=tol, atol=tol).y[:, -1]
164+
else:
165+
me[:] = 1.0
166+
return me
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# script to run a Lorenz attractor problem
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
5+
from pySDC.helpers.stats_helper import get_sorted, get_list_of_types
6+
from pySDC.implementations.problem_classes.Lorenz import LorenzAttractor
7+
from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit
8+
from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI
9+
from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity
10+
from pySDC.core.Errors import ProblemError
11+
from pySDC.projects.Resilience.hook import log_data, hook_collection
12+
13+
14+
def run_Lorenz(
15+
custom_description=None,
16+
num_procs=1,
17+
Tend=1.0,
18+
hook_class=log_data,
19+
fault_stuff=None,
20+
custom_controller_params=None,
21+
custom_problem_params=None,
22+
use_MPI=False,
23+
**kwargs,
24+
):
25+
"""
26+
Run a Lorenz attractor problem with default parameters.
27+
28+
Args:
29+
custom_description (dict): Overwrite presets
30+
num_procs (int): Number of steps for MSSDC
31+
Tend (float): Time to integrate to
32+
hook_class (pySDC.Hook): A hook to store data
33+
fault_stuff (dict): A dictionary with information on how to add faults
34+
custom_controller_params (dict): Overwrite presets
35+
custom_problem_params (dict): Overwrite presets
36+
use_MPI (bool): Whether or not to use MPI
37+
38+
Returns:
39+
dict: The stats object
40+
controller: The controller
41+
Tend: The time that was supposed to be integrated to
42+
"""
43+
44+
# initialize level parameters
45+
level_params = dict()
46+
level_params['dt'] = 1e-2
47+
48+
# initialize sweeper parameters
49+
sweeper_params = dict()
50+
sweeper_params['quad_type'] = 'RADAU-RIGHT'
51+
sweeper_params['num_nodes'] = 3
52+
sweeper_params['QI'] = 'IE'
53+
54+
problem_params = {
55+
'newton_tol': 1e-9,
56+
'newton_maxiter': 99,
57+
}
58+
59+
if custom_problem_params is not None:
60+
problem_params = {**problem_params, **custom_problem_params}
61+
62+
# initialize step parameters
63+
step_params = dict()
64+
step_params['maxiter'] = 4
65+
66+
# initialize controller parameters
67+
controller_params = dict()
68+
controller_params['logger_level'] = 30
69+
controller_params['hook_class'] = hook_collection + [hook_class]
70+
controller_params['mssdc_jac'] = False
71+
72+
if custom_controller_params is not None:
73+
controller_params = {**controller_params, **custom_controller_params}
74+
75+
# fill description dictionary for easy step instantiation
76+
description = dict()
77+
description['problem_class'] = LorenzAttractor
78+
description['problem_params'] = problem_params
79+
description['sweeper_class'] = generic_implicit
80+
description['sweeper_params'] = sweeper_params
81+
description['level_params'] = level_params
82+
description['step_params'] = step_params
83+
84+
if custom_description is not None:
85+
for k in custom_description.keys():
86+
if k == 'sweeper_class':
87+
description[k] = custom_description[k]
88+
continue
89+
description[k] = {**description.get(k, {}), **custom_description.get(k, {})}
90+
91+
# set time parameters
92+
t0 = 0.0
93+
94+
# instantiate controller
95+
if use_MPI:
96+
from mpi4py import MPI
97+
from pySDC.implementations.controller_classes.controller_MPI import controller_MPI
98+
99+
comm = kwargs.get('comm', MPI.COMM_WORLD)
100+
controller = controller_MPI(controller_params=controller_params, description=description, comm=comm)
101+
102+
# get initial values on finest level
103+
P = controller.S.levels[0].prob
104+
uinit = P.u_exact(t0)
105+
else:
106+
controller = controller_nonMPI(
107+
num_procs=num_procs, controller_params=controller_params, description=description
108+
)
109+
110+
# get initial values on finest level
111+
P = controller.MS[0].levels[0].prob
112+
uinit = P.u_exact(t0)
113+
114+
# insert faults
115+
if fault_stuff is not None:
116+
raise NotImplementedError
117+
118+
# call main function to get things done...
119+
try:
120+
uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend)
121+
except ProblemError:
122+
stats = controller.hooks.return_stats()
123+
124+
return stats, controller, Tend
125+
126+
127+
def plot_solution(stats):
128+
"""
129+
Plot the solution in 3D.
130+
131+
Args:
132+
stats (dict): The stats object of the run
133+
134+
Returns:
135+
None
136+
"""
137+
fig = plt.figure()
138+
ax = fig.add_subplot(projection='3d')
139+
140+
u = get_sorted(stats, type='u')
141+
ax.plot([me[1][0] for me in u], [me[1][1] for me in u], [me[1][2] for me in u])
142+
ax.set_xlabel('x')
143+
ax.set_ylabel('y')
144+
ax.set_zlabel('z')
145+
plt.show()
146+
147+
148+
def check_solution(stats, controller, thresh=5e-4):
149+
"""
150+
Check if the global error solution wrt. a scipy reference solution is tolerable.
151+
152+
Args:
153+
stats (dict): The stats object of the run
154+
controller (pySDC.Controller.controller): The controller
155+
thresh (float): Threshold for accepting the accuracy
156+
157+
Returns:
158+
None
159+
"""
160+
u = get_sorted(stats, type='u')
161+
u_exact = controller.MS[0].levels[0].prob.u_exact(t=u[-1][0])
162+
error = np.linalg.norm(u[-1][1] - u_exact)
163+
164+
assert error < thresh, f"Error too large, got e={error:.2e}"
165+
166+
167+
def main(plotting=True):
168+
"""
169+
Make a test run and see if the accuracy checks out.
170+
171+
Args:
172+
plotting (bool): Plot the solution or not
173+
174+
Returns:
175+
None
176+
"""
177+
custom_description = {}
178+
custom_description['convergence_controllers'] = {Adaptivity: {'e_tol': 1e-5}}
179+
custom_controller_params = {'logger_level': 30}
180+
stats, controller, _ = run_Lorenz(
181+
custom_description=custom_description, custom_controller_params=custom_controller_params, Tend=10
182+
)
183+
check_solution(stats, controller, 5e-4)
184+
if plotting:
185+
plot_solution(stats)
186+
187+
188+
if __name__ == "__main__":
189+
main()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import pytest
2+
3+
4+
@pytest.mark.base
5+
def test_main():
6+
from pySDC.projects.Resilience.Lorentz import main
7+
8+
main(False)

0 commit comments

Comments
 (0)