Coverage for pySDC/implementations/sweeper_classes/delta_form_MPI.py: 92%

61 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 20:28 +0000

1r""" 

2Node-parallel delta-form sweeper. 

3 

4Kept in its own module because importing it requires ``mpi4py``, which 

5:mod:`~pySDC.implementations.sweeper_classes.delta_form` does not. 

6 

7The MPI sweeper assigns one collocation node per rank and therefore uses only the **diagonal** of 

8:math:`Q^\Delta`. The delta form collapses accordingly: with 

9:math:`\varepsilon_r = u_0 + \tau_r + \Delta t (Q f^k)_r - u^k_r` for the rank's own node, 

10 

11.. math:: 

12 \delta_r = \varepsilon_r + \Delta t Q^\Delta_{rr}\,\big(f(u^k_r + \delta_r) - f(u^k_r)\big), 

13 

14with no accumulation over other nodes. The node-local piece is identical to the serial case, so 

15:meth:`DeltaFormMixin._solve_correction` is reused unchanged and all three strategies 

16(``solve_system_delta``, ``linear_implicit``, substitution fallback) work here too. 

17 

18""" 

19 

20from mpi4py import MPI 

21 

22from pySDC.implementations.sweeper_classes.delta_form import DeltaFormMixin 

23from pySDC.implementations.sweeper_classes.generic_implicit_MPI import generic_implicit_MPI 

24 

25 

26class delta_implicit_MPI(DeltaFormMixin, generic_implicit_MPI): 

27 """Delta-form counterpart of :class:`generic_implicit_MPI`. One collocation node per rank.""" 

28 

29 def _set_work_scale(self, values): 

30 """ 

31 Agree the divisor across the node communicator. 

32 

33 Each rank holds one collocation node, so left alone every rank would scale its corrections by 

34 its own node's residual and quantise differently from a serial run doing the same work. The 

35 scale is conceptually the residual's magnitude, which is a property of the whole sweep, so it 

36 is reduced. One scalar per sweep. 

37 

38 Parameters 

39 ---------- 

40 values : list 

41 This rank's residual, in backend units. 

42 """ 

43 super()._set_work_scale(values) 

44 if self._work_dtype is not None and self._scales_corrections(): 

45 self._work_scale = self.comm.allreduce(self._work_scale, op=MPI.MAX) 

46 

47 def _residual_nodes(self): 

48 r""" 

49 Compute :math:`\varepsilon_r` at this rank's node, in backend precision. 

50 

51 Returned as a one-element list, so the multi-level machinery -- which is written for a list 

52 of nodes -- reads the same here as it does serially. A residual handed down by a transfer is 

53 preferred over rebuilding one, exactly as in the serial sweeper. 

54 

55 Returns 

56 ------- 

57 list 

58 This rank's collocation residual, as a single-element list. 

59 """ 

60 if self.eps_in is not None: 

61 return self.eps_in 

62 

63 lvl = self.level 

64 eps = self.integrate() 

65 eps += lvl.u[0] 

66 eps -= lvl.u[self.rank + 1] 

67 if lvl.tau[self.rank] is not None: 

68 eps += lvl.tau[self.rank] 

69 return [eps] 

70 

71 def _sweep_nodes(self): 

72 """ 

73 Perform one delta-form sweep for this rank's collocation node. 

74 

75 Returns 

76 ------- 

77 None 

78 """ 

79 lvl = self.level 

80 prob = lvl.prob 

81 assert lvl.status.unlocked 

82 self._delta_setup() 

83 

84 rank = self.rank 

85 t_node = lvl.time + lvl.dt * self.coll.nodes[rank] 

86 alpha = lvl.dt * self.QI[rank + 1, rank + 1] 

87 

88 u_old = prob.dtype_u(lvl.u[rank + 1]) 

89 f_old = prob.dtype_f(lvl.f[rank + 1]) 

90 

91 residual = self._residual_nodes() 

92 self._set_work_scale(residual) 

93 delta = self._solve_correction(self._to_work(prob, residual[0]), alpha, u_old, f_old, t_node) 

94 

95 lvl.u[rank + 1] = u_old + self._to_backend(prob, self._to_work(prob, delta)) 

96 lvl.f[rank + 1] = prob.eval_f(lvl.u[rank + 1], t_node) 

97 

98 # The sweep itself needs no increment -- QI is diagonal here, so nothing accumulates across 

99 # nodes. A level that inherited its residual does need one, to advance that residual, so it 

100 # is evaluated exactly when there is something to advance. 

101 if getattr(self, 'eps_in', None) is not None: 

102 self._f_increment(prob, lvl.f[rank + 1], f_old, u_old, delta, t_node) 

103 

104 lvl.status.updated = True 

105 return None 

106 

107 def advance_residual(self, eps, deltas, dfs): 

108 r""" 

109 Advance this rank's residual by an update. 

110 

111 The :math:`Q\,\Delta f` term couples all collocation nodes, so it is a reduction here 

112 rather than a sum, in the same shape :meth:`generic_implicit_MPI.integrate` uses. 

113 

114 Parameters 

115 ---------- 

116 eps : list 

117 This rank's residual, as a single-element list. 

118 deltas : list 

119 The update applied to this rank's nodal value. 

120 dfs : list 

121 The resulting right-hand side increment at this rank's node. 

122 

123 Returns 

124 ------- 

125 list 

126 The advanced residual. 

127 """ 

128 lvl = self.level 

129 integral = lvl.prob.dtype_u(lvl.prob.init, val=0.0) 

130 for m in range(self.coll.num_nodes): 

131 recvBuf = integral if m == self.rank else None 

132 self.comm.Reduce(lvl.dt * self.coll.Qmat[m + 1, self.rank + 1] * dfs[0], recvBuf, root=m, op=MPI.SUM) 

133 return [self._to_work(lvl.prob, eps[0] - deltas[0] + integral)] 

134 

135 def compute_residual(self, stage=''): 

136 """ 

137 Report the tracked residual, reduced over the node communicator. 

138 

139 The serial version takes a maximum over the nodes it holds; here each rank holds one, so the 

140 same quantity is an ``allreduce``. Shaped after 

141 :meth:`generic_implicit_MPI.compute_residual`. 

142 

143 Parameters 

144 ---------- 

145 stage : str 

146 The stage of the step this level belongs to. 

147 

148 Returns 

149 ------- 

150 None 

151 """ 

152 if self.eps_in is None: 

153 return generic_implicit_MPI.compute_residual(self, stage=stage) 

154 

155 lvl = self.level 

156 if stage in self.params.skip_residual_computation: 

157 lvl.status.residual = 0.0 if lvl.status.residual is None else lvl.status.residual 

158 return None 

159 

160 norm = abs(self.eps_in[0]) 

161 kind = lvl.params.residual_type 

162 if kind.endswith('rel'): 

163 norm = norm / abs(lvl.u[0]) 

164 if kind.startswith('full'): 

165 lvl.status.residual = self.comm.allreduce(norm, op=MPI.MAX) 

166 elif kind.startswith('last'): 

167 lvl.status.residual = self.comm.bcast(norm, root=self.comm.size - 1) 

168 else: 

169 raise NotImplementedError(f'residual type "{kind}" not implemented!') 

170 lvl.status.updated = False 

171 return None