Coverage for pySDC/implementations/problem_classes/Lorenz.py: 94%

54 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 12:08 +0000

1import numpy as np 

2from pySDC.core.problem import Problem, WorkCounter 

3from pySDC.implementations.datatype_classes.mesh import mesh 

4from pySDC.core.errors import ConvergenceError 

5 

6 

7class LorenzAttractor(Problem): 

8 r""" 

9 Lorenz system of three chaotic ODEs, treated fully implicitly with a Newton solver. 

10 

11 The Lorenz attractor is a system of three ordinary differential equations (ODEs) that exhibits some chaotic behaviour. 

12 It is well known for the "Butterfly Effect", because the solution looks like a butterfly (solve to :math:`T_{end} = 100` 

13 or so to see this with these initial conditions) and because of the chaotic nature. 

14 

15 Lorenz developed this system from equations modelling convection in a layer of fluid with the top and bottom surfaces 

16 kept at different temperatures. In the notation used here, the first component of u is proportional to the convective 

17 motion, the second component is proportional to the temperature difference between the surfaces and the third component 

18 is proportional to the distortion of the vertical temperature profile from linearity. 

19 

20 See doi.org/10.1175/1520-0469(1963)020<0130:DNF>2.0.CO;2 for the original publication. 

21 

22 Since the problem is non-linear, we need to use a Newton solver. 

23 

24 The system of ODEs is given by 

25 

26 .. math:: 

27 \frac{d y_1(t)}{dt} = \sigma (y_2 (t) - y_1 (t)), 

28 

29 .. math:: 

30 \frac{d y_2(t)}{dt} = \rho y_1 (t) - y_2 (t) - y_1 (t) y_3 (t), 

31 

32 .. math:: 

33 \frac{d y_3(t)}{dt} = y_1 (t) y_2 (t) - \beta y_3 (t) 

34 

35 with initial condition :math:`(y_1(0), y_2(0), y_3(0))^T = (1, 1, 1)^T` (default) for :math:`t \in [0, 1]`. 

36 The problem parameters for this problem are :math:`\sigma = 10`, :math:`\rho = 28` and :math:`\beta = 8/3`. 

37 Lorenz chose these parameters such that the Reynolds number :math:`\rho` is slightly supercritical 

38 as to provoke instability of steady convection. 

39 

40 Parameters 

41 ---------- 

42 sigma : float, optional 

43 Parameter :math:`\sigma` of the problem. 

44 rho : float, optional 

45 Parameter :math:`\rho` of the problem. 

46 beta : float, optional 

47 Parameter :math:`\beta` of the problem. 

48 u0 : tuple, optional 

49 Initial solution :math:`u_0` of the problem. 

50 newton_tol : float, optional 

51 Tolerance for Newton for termination. 

52 newton_maxiter : int, optional 

53 Maximum number of iterations for Newton's method. 

54 stop_at_nan : bool, optional 

55 Indicates that the Newton solver should raise an error if ``nan`` values arise, instead of only warning. 

56 

57 Attributes 

58 ---------- 

59 work_counter : dict 

60 Counts the iterations/nfev (here for Newton's method and the nfev for the right-hand side). 

61 """ 

62 

63 dtype_u = mesh 

64 dtype_f = mesh 

65 

66 def __init__( 

67 self, sigma=10.0, rho=28.0, beta=8.0 / 3.0, u0=(1, 1, 1), newton_tol=1e-9, newton_maxiter=99, stop_at_nan=True 

68 ): 

69 """Initialization routine""" 

70 

71 nvars = 3 

72 # invoke super init, passing number of dofs, dtype_u and dtype_f 

73 super().__init__(init=(nvars, None, np.dtype('float64'))) 

74 self._makeAttributeAndRegister('nvars', 'stop_at_nan', localVars=locals(), readOnly=True) 

75 self._makeAttributeAndRegister( 

76 'sigma', 'rho', 'beta', 'u0', 'newton_tol', 'newton_maxiter', localVars=locals(), readOnly=False 

77 ) 

78 self.work_counters['newton'] = WorkCounter() 

79 self.work_counters['rhs'] = WorkCounter() 

80 

81 def eval_f(self, u, t): 

82 """ 

83 Routine to evaluate the right-hand side of the problem. 

84 

85 Parameters 

86 ---------- 

87 u : dtype_u 

88 Current values of the numerical solution. 

89 t : float 

90 Current time of the numerical solution is computed. 

91 

92 Returns 

93 ------- 

94 f : dtype_f 

95 The right-hand side of the problem. 

96 """ 

97 # abbreviations 

98 sigma = self.sigma 

99 rho = self.rho 

100 beta = self.beta 

101 

102 f = self.dtype_f(self.init) 

103 

104 f[0] = sigma * (u[1] - u[0]) 

105 f[1] = rho * u[0] - u[1] - u[0] * u[2] 

106 f[2] = u[0] * u[1] - beta * u[2] 

107 

108 self.work_counters['rhs']() 

109 return f 

110 

111 def solve_system(self, rhs, dt, u0, t): 

112 """ 

113 Simple Newton solver for the nonlinear system. 

114 

115 Parameters 

116 ---------- 

117 rhs : dtype_f 

118 Right-hand side for the nonlinear system. 

119 dt : float 

120 Abbrev. for the local stepsize (or any other factor required). 

121 u0 : dtype_u 

122 Initial guess for the iterative solver 

123 t : float 

124 Current time (e.g. for time-dependent BCs). 

125 

126 Returns 

127 ------- 

128 me : dtype_u 

129 The solution as mesh. 

130 """ 

131 

132 # abbreviations 

133 sigma = self.sigma 

134 rho = self.rho 

135 beta = self.beta 

136 

137 # start Newton iterations 

138 u = self.dtype_u(u0) 

139 res = np.inf 

140 for _n in range(0, self.newton_maxiter): 

141 # assemble G such that G(u) = 0 at the solution to the step 

142 G = np.array( 

143 [ 

144 u[0] - dt * sigma * (u[1] - u[0]) - rhs[0], 

145 u[1] - dt * (rho * u[0] - u[1] - u[0] * u[2]) - rhs[1], 

146 u[2] - dt * (u[0] * u[1] - beta * u[2]) - rhs[2], 

147 ] 

148 ) 

149 

150 # compute the residual and determine if we are done 

151 res = np.linalg.norm(G, np.inf) 

152 if res <= self.newton_tol: 

153 break 

154 if np.isnan(res) and self.stop_at_nan: 

155 self.logger.warning('Newton got nan after %i iterations...' % _n) 

156 raise ConvergenceError('Newton got nan after %i iterations, aborting...' % _n) 

157 elif np.isnan(res): 

158 self.logger.warning('Newton got nan after %i iterations...' % _n) 

159 break 

160 

161 # assemble inverse of Jacobian J of G 

162 prefactor = 1.0 / ( 

163 dt**3 * sigma * (u[0] ** 2 + u[0] * u[1] + beta * (-rho + u[2] + 1)) 

164 + dt**2 * (beta * sigma + beta - rho * sigma + sigma + u[0] ** 2 + sigma * u[2]) 

165 + dt * (beta + sigma + 1) 

166 + 1 

167 ) 

168 J_inv = prefactor * np.array( 

169 [ 

170 [ 

171 beta * dt**2 + dt**2 * u[0] ** 2 + beta * dt + dt + 1, 

172 beta * dt**2 * sigma + dt * sigma, 

173 -(dt**2) * sigma * u[0], 

174 ], 

175 [ 

176 beta * dt**2 * rho + dt**2 * (-u[0]) * u[1] - beta * dt**2 * u[2] + dt * rho - dt * u[2], 

177 beta * dt**2 * sigma + beta * dt + dt * sigma + 1, 

178 dt**2 * sigma * (-u[0]) - dt * u[0], 

179 ], 

180 [ 

181 dt**2 * rho * u[0] - dt**2 * u[0] * u[2] + dt**2 * u[1] + dt * u[1], 

182 dt**2 * sigma * u[0] + dt**2 * sigma * u[1] + dt * u[0], 

183 -(dt**2) * rho * sigma + dt**2 * sigma + dt**2 * sigma * u[2] + dt * sigma + dt + 1, 

184 ], 

185 ] 

186 ) 

187 

188 # solve the linear system for the Newton correction J delta = G 

189 delta = J_inv @ G 

190 

191 # update solution 

192 u = u - delta 

193 self.work_counters['newton']() 

194 

195 return u 

196 

197 def u_exact(self, t, u_init=None, t_init=None): 

198 r""" 

199 Routine to return initial conditions or to approximate exact solution using ``SciPy``. 

200 

201 Parameters 

202 ---------- 

203 t : float 

204 Time at which the approximated exact solution is computed. 

205 u_init : pySDC.implementations.problem_classes.Lorenz.dtype_u 

206 Initial conditions for getting the exact solution. 

207 t_init : float 

208 The starting time. 

209 

210 Returns 

211 ------- 

212 me : dtype_u 

213 The approximated exact solution. 

214 """ 

215 

216 me = self.dtype_u(self.init) 

217 

218 if t > 0: 

219 

220 def eval_rhs(t, u): 

221 r""" 

222 Evaluate the right hand side, but switch the arguments for ``SciPy``. 

223 

224 Args: 

225 t (float): Time 

226 u (numpy.ndarray): Solution at time t 

227 

228 Returns: 

229 (numpy.ndarray): Right hand side evaluation 

230 """ 

231 return self.eval_f(u, t) 

232 

233 me[:] = self.generate_scipy_reference_solution(eval_rhs, t, u_init, t_init) 

234 else: 

235 me[:] = self.u0 

236 return me