Coverage for pySDC/implementations/controller_classes/ParaDiag.py: 94%

54 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +0000

1import logging 

2from typing import Any, Dict 

3 

4 

5class ParaDiag: 

6 """ 

7 What ParaDiag is, independently of how the block is spread over processes. 

8 

9 Mixed into a controller, this replaces its iteration and leaves everything around it alone: 

10 

11 class controller_ParaDiag_nonMPI(ParaDiag, controller_nonMPI) 

12 class controller_ParaDiag_MPI(ParaDiag, controller_MPI) 

13 

14 It deliberately has no `__init__` and no controller of its own to inherit from, so that it can 

15 be mixed into either transport's controller without the two having to agree on a constructor 

16 signature. A concrete ParaDiag controller calls `prepare_ParaDiag_params` on the two 

17 dictionaries and then hands them to its controller's initialisation. 

18 

19 What stays with the concrete classes is everything whose implementation depends on where the 

20 other steps are: `apply_matrix`, `prepare_Jacobians`, `compute_all_at_once_residual`, 

21 `update_G_inv` and the block driver. 

22 """ 

23 

24 @staticmethod 

25 def prepare_ParaDiag_params(controller_params: Dict[str, Any], description: Dict[str, Any]) -> None: 

26 """ 

27 Check and complete the parameters ParaDiag needs, in place. 

28 

29 Call this *before* the controller's own initialisation: it only reads and writes the two 

30 dictionaries, and must have run by the time the steps are built. 

31 

32 Args: 

33 controller_params (dict): parameter set for the controller and the steps 

34 description (dict): all the parameters to set up the rest (levels, problems, ...) 

35 """ 

36 from pySDC.implementations.sweeper_classes.ParaDiagSweepers import QDiagonalization 

37 

38 # A list with more than one entry makes `Step` build more than one level. This has to be 

39 # checked here: the controllers build G^-1 from the sweeper parameters before the steps exist. 

40 for key in ['problem_params', 'level_params', 'sweeper_params']: 

41 if any(type(value) is list and len(value) > 1 for value in description.get(key, {}).values()): 

42 raise NotImplementedError('ParaDiag does not support multiple levels') 

43 

44 if QDiagonalization in description['sweeper_class'].__mro__: 

45 description['sweeper_params']['ignore_ic'] = True 

46 description['sweeper_params']['update_f_evals'] = False 

47 else: 

48 logging.getLogger('controller').warning( 

49 f'Warning: Your sweeper class {description["sweeper_class"]} is not derived from {QDiagonalization}. You probably want to use another sweeper class.' 

50 ) 

51 

52 if not controller_params.get('all_to_done', True): 

53 raise NotImplementedError('ParaDiag only implemented with option `all_to_done=True`') 

54 if 'alpha' not in controller_params.keys(): 

55 from pySDC.core.errors import ParameterError 

56 

57 raise ParameterError('Please supply alpha as a parameter to the ParaDiag controller!') 

58 controller_params['average_jacobian'] = controller_params.get('average_jacobian', True) 

59 

60 controller_params['all_to_done'] = True 

61 

62 # ------------------------------------------------------------------ the iteration 

63 

64 def get_stages(self) -> Dict[str, Any]: 

65 """ 

66 ParaDiag has one iteration stage, and no predictor because it has no coarse level. 

67 

68 Returns: 

69 dict: stage name -> the method that runs it 

70 """ 

71 return { 

72 'SPREAD': self.spread, 

73 'IT_CHECK': self.it_check, 

74 'IT_PARADIAG': self.it_ParaDiag, 

75 } 

76 

77 def next_iteration_stage(self, S: Any) -> str: 

78 """ 

79 Args: 

80 S (pySDC.Step.step): The current step 

81 

82 Returns: 

83 str: name of the stage to enter 

84 """ 

85 return 'IT_PARADIAG' 

86 

87 def compute_residual_after_spread(self, S: Any) -> None: 

88 """ 

89 ParaDiag's residual is the one of the composite collocation problem, which `it_ParaDiag` 

90 computes as part of the iteration. The convergence check runs before the first iteration, 

91 so the initial guess needs its residual here. 

92 

93 Args: 

94 S (pySDC.Step.step): The current step 

95 """ 

96 S.levels[0].sweep.compute_residual() 

97 

98 def step_is_active(self, time: float, block_start: float, t0: float, Tend: float, dt: float) -> bool: 

99 """ 

100 ParaDiag diagonalizes across the whole block, so it cannot drop a step out of one. A block 

101 that starts before `Tend` is run whole, past `Tend` if need be. 

102 

103 Args: 

104 time (float): when this step starts 

105 block_start (float): when the first step of this step's block starts 

106 t0 (float): starting time of the run 

107 Tend (float): ending time 

108 dt (float): size of this step, which is the size of every step in the block 

109 

110 Returns: 

111 bool: whether this step takes part 

112 """ 

113 active = super().step_is_active(block_start, block_start=block_start, t0=t0, Tend=Tend, dt=dt) 

114 

115 if active and not super().step_is_active(time, block_start=block_start, t0=t0, Tend=Tend, dt=dt): 

116 self.logger.warning( 

117 'Warning: This controller will solve past your desired end time until the end of its block!' 

118 ) 

119 

120 return active 

121 

122 def prepare_convergence_check(self, *args: Any, **kwargs: Any) -> None: 

123 """ 

124 The residual is already current -- `it_ParaDiag` computed it, and recomputing it the way a 

125 sweep-based algorithm does would need initial conditions that have not been communicated 

126 yet. The end point is not, because nothing sent it anywhere, so compute it here: `it_check` 

127 is what publishes `uend` when a step is done. 

128 

129 Takes whatever its controller passes -- a block of steps or a communicator -- and needs 

130 none of it, because the steps to do this for are the ones this controller owns. 

131 """ 

132 for S in self.steps: 

133 S.levels[0].sweep.compute_end_point() 

134 

135 # ------------------------------------------------------------------ alpha and the transform 

136 

137 @staticmethod 

138 def resolve_alpha(alpha: Any, k: int = 0) -> float: 

139 """ 

140 Read the alpha for iteration `k` out of whatever the user supplied. 

141 

142 `alpha` may be a single number, a sequence indexed by iteration (the last entry is reused once 

143 it runs out), or a callable taking the iteration index. Making it iteration dependent lets the 

144 outer iteration start with a well-conditioned alpha and tighten it later. 

145 

146 Static because the steps need an alpha before the controller has parameters to read it from. 

147 

148 Args: 

149 alpha: the alpha parameter as supplied by the user 

150 k (int): iteration index 

151 

152 Returns: 

153 float: alpha to use for this iteration 

154 """ 

155 if callable(alpha): 

156 return float(alpha(k)) 

157 if hasattr(alpha, '__len__'): 

158 return float(alpha[min(k, len(alpha) - 1)]) 

159 return float(alpha) 

160 

161 def get_alpha(self, k: int = 0) -> float: 

162 """ 

163 Get the ParaDiag alpha parameter for iteration `k`. 

164 

165 Args: 

166 k (int): iteration index 

167 

168 Returns: 

169 float: alpha to use for this iteration 

170 """ 

171 return self.resolve_alpha(self.params.alpha, k) 

172 

173 def get_FFT_matrices(self, k: int = 0) -> Any: 

174 """ 

175 Get the weighted FFT and iFFT matrices for iteration `k`, rebuilding them only when alpha 

176 actually changes. 

177 

178 Args: 

179 k (int): iteration index 

180 

181 Returns: 

182 tuple: the forward and backward weighted FFT matrices 

183 """ 

184 alpha = self.get_alpha(k) 

185 if getattr(self, '_cached_alpha', None) != alpha: 

186 from pySDC.helpers.ParaDiagHelper import get_weighted_FFT_matrix, get_weighted_iFFT_matrix 

187 

188 self._FFT_matrix = get_weighted_FFT_matrix(self.n_steps, alpha) 

189 self._iFFT_matrix = get_weighted_iFFT_matrix(self.n_steps, alpha) 

190 self._cached_alpha = alpha 

191 return self._FFT_matrix, self._iFFT_matrix 

192 

193 def FFT_in_time(self, quantity: Any, k: int = 0) -> None: 

194 """ 

195 Compute weighted forward FFT in time. The weighting is determined by the alpha parameter in ParaDiag 

196 

197 Note: The implementation via matrix-vector multiplication may be inefficient and less stable compared to an FFT 

198 with transposes! 

199 

200 Args: 

201 quantity (str): the level attribute to transform 

202 k (int): iteration index, for an iteration dependent alpha 

203 """ 

204 self.apply_matrix(self.get_FFT_matrices(k)[0], quantity) 

205 

206 def iFFT_in_time(self, quantity: Any, k: int = 0) -> None: 

207 """ 

208 Compute weighted backward FFT in time. The weighting is determined by the alpha parameter in ParaDiag 

209 

210 Args: 

211 quantity (str): the level attribute to transform 

212 k (int): iteration index, for an iteration dependent alpha 

213 """ 

214 self.apply_matrix(self.get_FFT_matrices(k)[1], quantity) 

215 

216 # ------------------------------------------------------------------ what the transport supplies 

217 

218 def apply_matrix(self, mat: Any, quantity: str) -> None: 

219 """ 

220 Apply a square matrix across the steps, in place. 

221 

222 How this is done depends entirely on where the other steps are, so the concrete controllers 

223 implement it. 

224 

225 Args: 

226 mat: square matrix with as many rows as there are steps 

227 quantity (str): 'residual' or 'increment', the level attribute to transform 

228 """ 

229 raise NotImplementedError('ParaDiag controllers have to implement apply_matrix') 

230 

231 def update_G_inv(self, k: int = 0) -> None: 

232 """ 

233 Rebuild G^-1 on the local step(s) when alpha changes with the iteration. 

234 

235 G^-1 depends on alpha, so an iteration dependent alpha means the sweeper's diagonalization has 

236 to be recomputed. Subclasses implement this because only they know which steps they own. 

237 

238 Args: 

239 k (int): iteration index 

240 """ 

241 raise NotImplementedError('ParaDiag controllers have to implement update_G_inv')