Coverage for pySDC/implementations/sweeper_classes/boris_2nd_order.py: 99%
118 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 14:51 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 14:51 +0000
1import numpy as np
3from pySDC.core.sweeper import Sweeper
6class boris_2nd_order(Sweeper):
7 """
8 Custom sweeper class, implements Sweeper.py
10 Second-order sweeper using velocity-Verlet with Boris scheme as base integrator
12 Attributes:
13 S: node-to-node collocation matrix (first order)
14 SQ: node-to-node collocation matrix (second order)
15 ST: node-to-node trapezoidal matrix
16 Sx: node-to-node Euler half-step for position update
17 """
19 def __init__(self, params):
20 """
21 Initialization routine for the custom sweeper
23 Args:
24 params: parameters for the sweeper
25 """
27 # call parent's initialization routine
29 if "QI" not in params:
30 params["QI"] = "IE"
31 if "QE" not in params:
32 params["QE"] = "EE"
34 super(boris_2nd_order, self).__init__(params)
36 # S- and SQ-matrices (derived from Q) and Sx- and ST-matrices for the integrator
37 [
38 self.S,
39 self.ST,
40 self.SQ,
41 self.Sx,
42 self.QQ,
43 self.QI,
44 self.QT,
45 self.Qx,
46 self.Q,
47 ] = self.__get_Qd()
49 self.qQ = np.dot(self.coll.weights, self.coll.Qmat[1:, 1:])
51 def __get_Qd(self):
52 """
53 Get integration matrices for 2nd-order SDC
55 Returns:
56 S: node-to-node collocation matrix (first order)
57 SQ: node-to-node collocation matrix (second order)
58 ST: node-to-node trapezoidal matrix
59 Sx: node-to-node Euler half-step for position update
60 """
62 # set implicit and explicit Euler matrices (default, but can be changed)
63 QI = self.get_Qdelta_implicit(qd_type=self.params.QI)
64 QE = self.get_Qdelta_explicit(qd_type=self.params.QE)
66 # trapezoidal rule
67 QT = 1 / 2 * (QI + QE)
69 # Qx as in the paper
70 Qx = np.dot(QE, QT) + 1 / 2 * QE * QE
72 Sx = np.zeros(np.shape(self.coll.Qmat))
73 ST = np.zeros(np.shape(self.coll.Qmat))
74 S = np.zeros(np.shape(self.coll.Qmat))
76 # fill-in node-to-node matrices
77 Sx[0, :] = Qx[0, :]
78 ST[0, :] = QT[0, :]
79 S[0, :] = self.coll.Qmat[0, :]
80 for m in range(self.coll.num_nodes):
81 Sx[m + 1, :] = Qx[m + 1, :] - Qx[m, :]
82 ST[m + 1, :] = QT[m + 1, :] - QT[m, :]
83 S[m + 1, :] = self.coll.Qmat[m + 1, :] - self.coll.Qmat[m, :]
84 # SQ via dot-product, could also be done via QQ
85 SQ = np.dot(S, self.coll.Qmat)
87 # QQ-matrix via product of Q
88 QQ = np.dot(self.coll.Qmat, self.coll.Qmat)
90 return [S, ST, SQ, Sx, QQ, QI, QT, Qx, self.coll.Qmat]
92 def update_nodes(self):
93 """
94 Update the u- and f-values at the collocation nodes -> corresponds to a single sweep over all nodes
96 Returns:
97 None
98 """
100 # get current level and problem description
101 L = self.level
102 P = L.prob
104 # only if the level has been touched before
105 assert L.status.unlocked
107 # get number of collocation nodes for easier access
108 M = self.coll.num_nodes
110 # initialize integral terms with zeros, will add stuff later
111 integral = [P.dtype_u(P.init, val=0.0) for l in range(M)]
113 # gather all terms which are known already (e.g. from the previous iteration)
114 # this corresponds to SF(u^k) - SdF(u^k) + tau (note: have integrals in pos and vel!)
115 for m in range(M):
116 for j in range(M + 1):
117 # build RHS from f-terms (containing the E field) and the B field
118 f = P.build_f(L.f[j], L.u[j], L.time + L.dt * self.coll.nodes[j - 1])
119 # add SQF(u^k) - SxF(u^k) for the position
120 integral[m].pos += L.dt * (L.dt * (self.SQ[m + 1, j] - self.Sx[m + 1, j]) * f)
121 # add SF(u^k) - STF(u^k) for the velocity
122 integral[m].vel += L.dt * (self.S[m + 1, j] - self.ST[m + 1, j]) * f
123 # add tau if associated
124 if L.tau[m] is not None:
125 integral[m] += L.tau[m]
126 # tau is 0-to-node, need to change it to node-to-node here
127 if m > 0:
128 integral[m] -= L.tau[m - 1]
130 # do the sweep
131 for m in range(0, M):
132 # build rhs, consisting of the known values from above and new values from previous nodes (at k+1)
133 tmp = P.dtype_u(integral[m])
134 for j in range(m + 1):
135 # build RHS from f-terms (containing the E field) and the B field
136 f = P.build_f(L.f[j], L.u[j], L.time + L.dt * self.coll.nodes[j - 1])
137 # add SxF(u^{k+1})
138 tmp.pos += L.dt * (L.dt * self.Sx[m + 1, j] * f)
139 # add pos at previous node + dt*v0
140 tmp.pos += L.u[m].pos + L.dt * self.coll.delta_m[m] * L.u[0].vel
141 # set new position, is explicit
142 L.u[m + 1].pos = tmp.pos
144 # get E field with new positions and compute mean
145 L.f[m + 1] = P.eval_f(L.u[m + 1], L.time + L.dt * self.coll.nodes[m])
147 ck = tmp.vel
149 # do the boris scheme
150 L.u[m + 1].vel = P.boris_solver(ck, L.dt * np.diag(self.QI)[m + 1], L.f[m], L.f[m + 1], L.u[m])
152 # indicate presence of new values at this level
153 L.status.updated = True
155 return None
157 def integrate(self):
158 """
159 Integrates the right-hand side
161 Returns:
162 list of dtype_u: containing the integral as values
163 """
165 # get current level and problem description
166 L = self.level
167 P = L.prob
169 # create new instance of dtype_u, initialize values with 0
170 p = []
172 for m in range(1, self.coll.num_nodes + 1):
173 p.append(P.dtype_u(P.init, val=0.0))
175 # integrate RHS over all collocation nodes, RHS is here only f(x,v)!
176 for j in range(1, self.coll.num_nodes + 1):
177 f = P.build_f(L.f[j], L.u[j], L.time + L.dt * self.coll.nodes[j - 1])
178 p[-1].pos += L.dt * (L.dt * self.QQ[m, j] * f) + L.dt * self.coll.Qmat[m, j] * L.u[0].vel
179 p[-1].vel += L.dt * self.coll.Qmat[m, j] * f
181 return p
183 def compute_end_point(self):
184 """
185 Compute u at the right point of the interval
187 The value uend computed here is a full evaluation of the Picard formulation (always!)
189 Returns:
190 None
191 """
193 # get current level and problem description
194 L = self.level
195 P = L.prob
197 # start with u0 and add integral over the full interval (using coll.weights)
198 L.uend = P.dtype_u(L.u[0])
199 for m in range(self.coll.num_nodes):
200 f = P.build_f(L.f[m + 1], L.u[m + 1], L.time + L.dt * self.coll.nodes[m])
201 L.uend.pos += L.dt * (L.dt * self.qQ[m] * f) + L.dt * self.coll.weights[m] * L.u[0].vel
202 L.uend.vel += L.dt * self.coll.weights[m] * f
203 # add up tau correction of the full interval (last entry)
204 if L.tau[-1] is not None:
205 L.uend += L.tau[-1]
207 return None
209 def get_sweeper_mats(self):
210 """
211 Returns the matrices Q, QQ, Qx, QT which define the sweeper.
212 """
214 Q = self.Q[1:, 1:]
215 QQ = self.QQ[1:, 1:]
216 Qx = self.Qx[1:, 1:]
217 QT = self.QT[1:, 1:]
219 return Q, QQ, Qx, QT
221 def get_scalar_problems_sweeper_mats(self, lambdas=None):
222 """
223 This function returns the corresponding matrices of an SDC sweep matrix formulation
225 Args:
226 lambdas (numpy.narray): the first entry in lambdas is k-spring constant and the second is mu friction.
227 """
229 Q, QQ, Qx, QT = self.get_sweeper_mats()
231 if lambdas is None:
232 pass
233 # should use lambdas from attached problem and make sure it is scalar SDC
234 raise NotImplementedError("At the moment, the values for the lambda have to be provided")
235 else:
236 k = lambdas[0]
237 mu = lambdas[1]
239 nnodes = self.coll.num_nodes
240 dt = self.level.dt
242 F = np.block(
243 [
244 [-k * np.eye(nnodes), -mu * np.eye(nnodes)],
245 [-k * np.eye(nnodes), -mu * np.eye(nnodes)],
246 ]
247 )
249 C_coll = np.block([[np.eye(nnodes), dt * Q], [np.zeros([nnodes, nnodes]), np.eye(nnodes)]])
250 Q_coll = np.block(
251 [
252 [dt**2 * QQ, np.zeros([nnodes, nnodes])],
253 [np.zeros([nnodes, nnodes]), dt * Q],
254 ]
255 )
256 Q_vv = np.block(
257 [
258 [dt**2 * Qx, np.zeros([nnodes, nnodes])],
259 [np.zeros([nnodes, nnodes]), dt * QT],
260 ]
261 )
262 M_vv = np.eye(2 * nnodes) - np.dot(Q_vv, F)
264 return C_coll, Q_coll, Q_vv, M_vv, F
266 def get_scalar_problems_manysweep_mats(self, nsweeps, lambdas=None):
267 """
268 For a scalar problem, K sweeps of SDC can be written in matrix form.
270 Args:
271 nsweeps (int): number of sweeps
272 lambdas (numpy.ndarray): the first entry in lambdas is k-spring constant and the second is mu friction.
273 """
274 nnodes = self.coll.num_nodes
276 C_coll, Q_coll, Q_vv, M_vv, F = self.get_scalar_problems_sweeper_mats(lambdas=lambdas)
278 K_sdc = np.dot(np.linalg.inv(M_vv), Q_coll - Q_vv) @ F
280 Keig, Kvec = np.linalg.eig(K_sdc)
282 Kp_sdc = np.linalg.matrix_power(K_sdc, nsweeps)
283 Kinv_sdc = np.linalg.inv(np.eye(2 * nnodes) - K_sdc)
285 Kdot_sdc = np.dot(np.eye(2 * nnodes) - Kp_sdc, Kinv_sdc)
286 MC = np.dot(np.linalg.inv(M_vv), C_coll)
288 Mat_sweep = Kp_sdc + np.dot(Kdot_sdc, MC)
290 return Mat_sweep, np.max(np.abs(Keig))
292 def get_scalar_problems_picardsweep_mats(self, nsweeps, lambdas=None):
293 """
294 For a scalar problem, K sweeps of SDC can be written in matrix form.
296 Args:
297 nsweeps (int): number of sweeps
298 lambdas (numpy.ndarray): the first entry in lambdas is k-spring constant and the second is mu friction.
299 """
300 nnodes = self.coll.num_nodes
302 C_coll, Q_coll, Q_vv, M_vv, F = self.get_scalar_problems_sweeper_mats(lambdas=lambdas)
304 K_sdc = np.dot(Q_coll, F)
306 Keig, Kvec = np.linalg.eig(K_sdc)
308 Kp_sdc = np.linalg.matrix_power(K_sdc, nsweeps)
309 Kinv_sdc = np.linalg.inv(np.eye(2 * nnodes) - K_sdc)
311 Kdot_sdc = np.dot(np.eye(2 * nnodes) - Kp_sdc, Kinv_sdc)
313 Mat_sweep = Kp_sdc + np.dot(Kdot_sdc, C_coll)
315 return Mat_sweep, np.max(np.abs(Keig))