Coverage for pySDC/helpers/problem_helper.py: 97%

102 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 14:51 +0000

1import numpy as np 

2from scipy.special import factorial 

3 

4 

5def get_steps(derivative, order, stencil_type): 

6 """ 

7 Get the offsets for the FD stencil. 

8 

9 Args: 

10 derivative (int): Order of the derivative 

11 order (int): Order of accuracy 

12 stencil_type (str): Type of the stencil 

13 steps (list): Provide specific steps, overrides `stencil_type` 

14 

15 Returns: 

16 int: The number of elements in the stencil 

17 numpy.ndarray: The offsets for the stencil 

18 """ 

19 if stencil_type == 'center': 

20 n = order + derivative - (derivative + 1) % 2 // 1 

21 steps = np.arange(n) - n // 2 

22 elif stencil_type == 'forward': 

23 n = order + derivative 

24 steps = np.arange(n) 

25 elif stencil_type == 'backward': 

26 n = order + derivative 

27 steps = -np.arange(n) 

28 elif stencil_type == 'upwind': 

29 n = order + derivative 

30 

31 if n <= 3: 

32 n, steps = get_steps(derivative, order, 'backward') 

33 else: 

34 steps = np.append(-np.arange(n - 1)[::-1], [1]) 

35 else: 

36 raise ValueError( 

37 f'Stencil must be of type "center", "forward", "backward" or "upwind", not {stencil_type}. If you want something else you can also give specific steps.' 

38 ) 

39 return n, steps 

40 

41 

42def get_finite_difference_stencil(derivative, order=None, stencil_type=None, steps=None): 

43 """ 

44 Derive general finite difference stencils from Taylor expansions 

45 

46 Args: 

47 derivative (int): Order of the derivative 

48 order (int): Order of accuracy 

49 stencil_type (str): Type of the stencil 

50 steps (list): Provide specific steps, overrides `stencil_type` 

51 

52 Returns: 

53 numpy.ndarray: The weights of the stencil 

54 numpy.ndarray: The offsets for the stencil 

55 """ 

56 

57 if steps is not None: 

58 n = len(steps) 

59 else: 

60 n, steps = get_steps(derivative, order, stencil_type) 

61 

62 # make a matrix that contains the Taylor coefficients 

63 A = np.zeros((n, n)) 

64 idx = np.arange(n) 

65 inv_facs = 1.0 / factorial(idx) 

66 for i in range(0, n): 

67 A[i, :] = steps ** idx[i] * inv_facs[i] 

68 

69 # make a right hand side vector that is zero everywhere except at the position of the desired derivative 

70 sol = np.zeros(n) 

71 sol[derivative] = 1.0 

72 

73 # solve the linear system for the finite difference coefficients 

74 coeff = np.linalg.solve(A, sol) 

75 

76 # sort coefficients and steps 

77 coeff = coeff[np.argsort(steps)] 

78 steps = np.sort(steps) 

79 

80 return coeff, steps 

81 

82 

83def get_finite_difference_matrix( 

84 derivative, 

85 order, 

86 stencil_type=None, 

87 steps=None, 

88 dx=None, 

89 size=None, 

90 dim=None, 

91 bc=None, 

92 cupy=False, 

93 bc_params=None, 

94): 

95 """ 

96 Build FD matrix from stencils, with boundary conditions. 

97 Keep in mind that the boundary conditions may require further modification of the right hand side. 

98 

99 Args: 

100 derivative (int): Order of the spatial derivative 

101 order (int): Order of accuracy 

102 stencil_type (str): Type of stencil 

103 steps (list): Provide specific steps, overrides `stencil_type` 

104 dx (float): Mesh width 

105 size (int): Number of degrees of freedom per dimension 

106 dim (int): Number of dimensions 

107 bc (str): Boundary conditions for both sides 

108 cupy (bool): Construct a GPU ready matrix if yes 

109 

110 Returns: 

111 Sparse matrix: Finite difference matrix 

112 numpy.ndarray: Vector containing information about the boundary conditions 

113 """ 

114 if cupy: 

115 import cupyx.scipy.sparse as sp 

116 else: 

117 import scipy.sparse as sp 

118 

119 # get stencil 

120 coeff, steps = get_finite_difference_stencil( 

121 derivative=derivative, order=order, stencil_type=stencil_type, steps=steps 

122 ) 

123 

124 if type(bc) is not tuple: 

125 assert type(bc) == str, 'Please pass BCs as string or tuple of strings' 

126 bc = (bc, bc) 

127 bc_params = bc_params if bc_params is not None else {} 

128 if type(bc_params) is not list: 

129 bc_params = [bc_params, bc_params] 

130 

131 b = np.zeros(size**dim) 

132 

133 if bc[0] == 'periodic': 

134 assert bc[1] == 'periodic' 

135 A_1d = 0 * sp.eye(size, format='csc') 

136 for i in steps: 

137 A_1d += coeff[i] * sp.eye(size, k=steps[i]) 

138 if steps[i] > 0: 

139 A_1d += coeff[i] * sp.eye(size, k=-size + steps[i]) 

140 if steps[i] < 0: 

141 A_1d += coeff[i] * sp.eye(size, k=size + steps[i]) 

142 else: 

143 A_1d = sp.diags(coeff, steps, shape=(size, size), format='lil') 

144 

145 # Default parameters for Dirichlet and Neumann BCs 

146 bc_params_defaults = { 

147 'val': 0.0, 

148 'neumann_bc_order': order, 

149 'reduce': False, 

150 } 

151 

152 # Loop over each side (0 for left, 1 for right) 

153 for iS in [0, 1]: 

154 # -- check Boundary condition types 

155 assert "neumann" in bc[iS] or "dirichlet" in bc[iS], f"unknown BC type : {bc[iS]}" 

156 

157 # -- boundary condition parameters 

158 bc_params[iS] = {**bc_params_defaults, **bc_params[iS]} 

159 par = bc_params[iS].copy() 

160 

161 # -- extract parameters and raise an error if additionals 

162 val = par.pop('val') 

163 reduce = par.pop('reduce') 

164 neumann_bc_order = par.pop('neumann_bc_order') 

165 assert len(par) == 0, f"unused BCs parameters : {par}" 

166 

167 # -- half stencil width 

168 sWidth = -min(steps) if iS == 0 else max(steps) 

169 

170 # -- loop over lines of A that have to be modified 

171 for i in range(sWidth): 

172 # -- index of the line 

173 iLine = i if iS == 0 else -i - 1 

174 # -- slice for coefficients used in the A matrix 

175 sCoeff = slice(1, None) if iS == 0 else slice(None, -1) 

176 # -- index of coefficient used in the b vector 

177 iCoeff = 0 if iS == 0 else -1 

178 

179 if reduce: 

180 # -- reduce order close to boundary 

181 b_coeff, b_steps = get_finite_difference_stencil( 

182 derivative=derivative, 

183 order=2 * (i + 1), 

184 stencil_type='center', 

185 ) 

186 else: 

187 # -- shift stencil close to boundary 

188 b_steps = ( 

189 np.arange(-(i + 1), order + derivative - (i + 1)) 

190 if iS == 0 

191 else np.arange(-(order + derivative) + (i + 2), (i + 2)) 

192 ) 

193 

194 b_coeff, b_steps = get_finite_difference_stencil(derivative=derivative, steps=b_steps) 

195 

196 # -- column slice where to put coefficients in the A matrix 

197 colSlice = slice(None, len(b_coeff) - 1) if iS == 0 else slice(-len(b_coeff) + 1, None) 

198 

199 # -- modify A 

200 A_1d[iLine, :] = 0 

201 A_1d[iLine, colSlice] = b_coeff[sCoeff] 

202 

203 if "dirichlet" in bc[iS]: 

204 # -- modify b 

205 b[iLine] = val * b_coeff[iCoeff] 

206 

207 elif "neumann" in bc[iS]: 

208 nOrder = neumann_bc_order 

209 

210 # -- generate the first derivative stencil 

211 n_coeff, n_steps = get_finite_difference_stencil( 

212 derivative=1, order=nOrder, stencil_type="forward" if iS == 0 else "backward" 

213 ) 

214 

215 # -- column slice where to put coefficients in the A matrix 

216 colSlice = slice(None, len(n_coeff) - 1) if iS == 0 else slice(-len(n_coeff) + 1, None) 

217 

218 # -- additional modification to A 

219 A_1d[iLine, colSlice] -= b_coeff[iCoeff] / n_coeff[iCoeff] * n_coeff[sCoeff] 

220 

221 # -- modify B 

222 b[iLine] = val * b_coeff[iCoeff] / n_coeff[iCoeff] * dx 

223 

224 # TODO: extend the BCs to higher dimensions 

225 A_1d = A_1d.tocsc() 

226 if dim == 1: 

227 A = A_1d 

228 elif dim == 2: 

229 A = sp.kron(A_1d, sp.eye(size)) + sp.kron(sp.eye(size), A_1d) 

230 elif dim == 3: 

231 A = ( 

232 sp.kron(A_1d, sp.eye(size**2)) 

233 + sp.kron(sp.eye(size**2), A_1d) 

234 + sp.kron(sp.kron(sp.eye(size), A_1d), sp.eye(size)) 

235 ) 

236 else: 

237 raise NotImplementedError(f'Dimension {dim} not implemented.') 

238 

239 A /= dx**derivative 

240 b /= dx**derivative 

241 

242 return A, b 

243 

244 

245def get_1d_grid(size, bc, left_boundary=0.0, right_boundary=1.0): 

246 """ 

247 Generate a grid in one dimension and obtain mesh spacing for finite difference discretization. 

248 

249 Args: 

250 size (int): Number of degrees of freedom per dimension 

251 bc (str): Boundary conditions for both sides 

252 left_boundary (float): x value at the left boundary 

253 right_boundary (float): x value at the right boundary 

254 

255 Returns: 

256 float: mesh spacing 

257 numpy.ndarray: 1d mesh 

258 """ 

259 L = right_boundary - left_boundary 

260 if bc == 'periodic': 

261 dx = L / size 

262 xvalues = np.array([left_boundary + dx * i for i in range(size)]) 

263 elif "dirichlet" in bc or "neumann" in bc: 

264 dx = L / (size + 1) 

265 xvalues = np.array([left_boundary + dx * (i + 1) for i in range(size)]) 

266 else: 

267 raise NotImplementedError(f'Boundary conditions \"{bc}\" not implemented.') 

268 

269 return dx, xvalues