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

53 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-15 06:23 +0000

1import logging 

2from typing import Any, Dict 

3 

4import numpy as np 

5 

6 

7class ParaDiag: 

8 """ 

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

10 

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

12 

13 class controller_ParaDiag_nonMPI(ParaDiag, controller_nonMPI) 

14 class controller_ParaDiag_MPI(ParaDiag, controller_MPI) 

15 

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

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

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

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

20 

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

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

23 `update_G_inv` and the block driver. 

24 """ 

25 

26 @staticmethod 

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

28 """ 

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

30 

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

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

33 

34 Args: 

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

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

37 """ 

38 from pySDC.implementations.sweeper_classes.ParaDiagSweepers import QDiagonalization 

39 

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

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

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

43 else: 

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

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

46 ) 

47 

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

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

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

51 from pySDC.core.errors import ParameterError 

52 

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

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

55 

56 controller_params['all_to_done'] = True 

57 

58 # ------------------------------------------------------------------ the iteration 

59 

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

61 """ 

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

63 

64 Returns: 

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

66 """ 

67 return { 

68 'SPREAD': self.spread, 

69 'IT_CHECK': self.it_check, 

70 'IT_PARADIAG': self.it_ParaDiag, 

71 } 

72 

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

74 """ 

75 Args: 

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

77 

78 Returns: 

79 str: name of the stage to enter 

80 """ 

81 return 'IT_PARADIAG' 

82 

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

84 """ 

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

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

87 so the initial guess needs its residual here. 

88 

89 Args: 

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

91 """ 

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

93 

94 def step_is_active(self, time: float, block_start: float, Tend: float) -> bool: 

95 """ 

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

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

98 

99 Args: 

100 time (float): when this step starts 

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

102 Tend (float): ending time 

103 

104 Returns: 

105 bool: whether this step takes part 

106 """ 

107 active = block_start < Tend - 10 * np.finfo(float).eps 

108 

109 if active and time >= Tend - 10 * np.finfo(float).eps: 

110 self.logger.warning( 

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

112 ) 

113 

114 return active 

115 

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

117 """ 

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

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

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

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

122 

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

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

125 """ 

126 for S in self.steps: 

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

128 

129 # ------------------------------------------------------------------ alpha and the transform 

130 

131 @staticmethod 

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

133 """ 

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

135 

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

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

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

139 

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

141 

142 Args: 

143 alpha: the alpha parameter as supplied by the user 

144 k (int): iteration index 

145 

146 Returns: 

147 float: alpha to use for this iteration 

148 """ 

149 if callable(alpha): 

150 return float(alpha(k)) 

151 if hasattr(alpha, '__len__'): 

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

153 return float(alpha) 

154 

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

156 """ 

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

158 

159 Args: 

160 k (int): iteration index 

161 

162 Returns: 

163 float: alpha to use for this iteration 

164 """ 

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

166 

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

168 """ 

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

170 actually changes. 

171 

172 Args: 

173 k (int): iteration index 

174 

175 Returns: 

176 tuple: the forward and backward weighted FFT matrices 

177 """ 

178 alpha = self.get_alpha(k) 

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

180 from pySDC.helpers.ParaDiagHelper import get_weighted_FFT_matrix, get_weighted_iFFT_matrix 

181 

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

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

184 self._cached_alpha = alpha 

185 return self._FFT_matrix, self._iFFT_matrix 

186 

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

188 """ 

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

190 

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

192 with transposes! 

193 

194 Args: 

195 quantity (str): the level attribute to transform 

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

197 """ 

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

199 

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

201 """ 

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

203 

204 Args: 

205 quantity (str): the level attribute to transform 

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

207 """ 

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

209 

210 # ------------------------------------------------------------------ what the transport supplies 

211 

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

213 """ 

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

215 

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

217 implement it. 

218 

219 Args: 

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

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

222 """ 

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

224 

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

226 """ 

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

228 

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

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

231 

232 Args: 

233 k (int): iteration index 

234 """ 

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