Coverage for pySDC/implementations/hooks/AllenCahn_monitor.py: 100%

60 statements  

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

1import numpy as np 

2 

3from pySDC.core.hooks import Hooks 

4 

5 

6class AllenCahnMonitor(Hooks): 

7 r""" 

8 Track the shrinking circle (or sphere) of an Allen-Cahn run. 

9 

10 Under mean curvature flow a blob of initial radius :math:`R_0` obeys 

11 :math:`R(t)^2 = R_0^2 - 2 (d - 1) t`, so comparing the measured radius against that is the 

12 standard diagnostic for these problems. This hook records ``computed_radius``, 

13 ``exact_radius``, ``computed_volume`` and ``exact_volume`` at :math:`t = 0` and after every 

14 step, plus ``interface_width`` where that is well defined. 

15 

16 The volume is the number of cells in the high phase times the cell volume. Counting is used 

17 rather than integrating the field, even though pySDC's phase fields run from :math:`0` to 

18 :math:`1` and so integrate straight to a volume: the integral also picks up the diffuse 

19 interface, which biases it by :math:`O(\varepsilon)` no matter how fine the mesh, whereas 

20 counting is consistent and its :math:`O(\Delta x)` bias refines away. 

21 

22 Attributes 

23 ---------- 

24 phase_thresh : float 

25 Cells above this count towards the high phase. Taken from the problem when it says, and 

26 :math:`0.5` -- the midpoint of the two wells -- otherwise. 

27 """ 

28 

29 default_phase_thresh = 0.5 

30 

31 def __init__(self): 

32 """Start with no initial radius or dimension; both are filled in by :meth:`pre_run`.""" 

33 super().__init__() 

34 

35 self.init_radius = None 

36 self.ndim = None 

37 self.phase_thresh = self.default_phase_thresh 

38 

39 @staticmethod 

40 def get_real_space(L, u): 

41 """Undo the transform if the problem carries its solution in spectral space.""" 

42 return L.prob.fft.backward(u) if getattr(L.prob, 'spectral', False) else u[:] 

43 

44 def get_volume(self, L, u): 

45 """Volume of the high phase, summed over the space communicator if there is one.""" 

46 count = float(np.count_nonzero(self.get_real_space(L, u) > self.phase_thresh)) 

47 

48 comm = getattr(L.prob, 'comm', None) 

49 if comm is not None: 

50 from mpi4py import MPI 

51 

52 count = comm.allreduce(sendobj=count, op=MPI.SUM) 

53 

54 return count * L.prob.dx**self.ndim 

55 

56 def radius_from_volume(self, vol): 

57 """Radius of the ball of this volume.""" 

58 if self.ndim == 2: 

59 return np.sqrt(vol / np.pi) 

60 elif self.ndim == 3: 

61 return (vol / (np.pi * 4.0 / 3.0)) ** (1.0 / 3.0) 

62 raise NotImplementedError(f'Can only monitor 2D and 3D problems, got {self.ndim}D') 

63 

64 def exact_radius_squared(self, t): 

65 r"""Mean curvature flow shrinks the blob as :math:`R(t)^2 = R_0^2 - 2 (d - 1) t`.""" 

66 return max(self.init_radius**2 - 2.0 * (self.ndim - 1) * t, 0) 

67 

68 def exact_radius(self, t): 

69 """Exact radius at time ``t`` under mean curvature flow, clipped at zero once the blob has vanished.""" 

70 return np.sqrt(self.exact_radius_squared(t)) 

71 

72 def exact_volume(self, t): 

73 """Exact area (2D) or volume (3D) of the blob at time ``t``.""" 

74 r2 = self.exact_radius_squared(t) 

75 return np.pi * r2 if self.ndim == 2 else np.pi * 4.0 / 3.0 * r2**1.5 

76 

77 def measures_interface_width(self, L): 

78 """Only a 2D field held whole on this rank can be cut across to measure the interface.""" 

79 comm = getattr(L.prob, 'comm', None) 

80 return self.ndim == 2 and (comm is None or comm.Get_size() == 1) 

81 

82 def get_interface_width(self, L, u): 

83 """Width of the transition, in units of epsilon, along a cut through the middle.""" 

84 n = L.prob.init[0][0] 

85 rows1 = np.where(u[n // 2, : n // 2] > 0.005) 

86 rows2 = np.where(u[n // 2, : n // 2] < 0.995) 

87 

88 return (rows2[0][-1] - rows1[0][0]) * L.prob.dx / L.prob.eps 

89 

90 def get_diagnostics(self, L, u, t): 

91 """Everything worth recording about ``u``, as a dict of stats entries.""" 

92 vol = self.get_volume(L, u) 

93 

94 diagnostics = { 

95 'computed_radius': self.radius_from_volume(vol), 

96 'exact_radius': self.exact_radius(t), 

97 'computed_volume': vol, 

98 'exact_volume': self.exact_volume(t), 

99 } 

100 

101 if self.measures_interface_width(L): 

102 diagnostics['interface_width'] = self.get_interface_width(L, self.get_real_space(L, u)) 

103 

104 return diagnostics 

105 

106 def record(self, step, L, t, diagnostics): 

107 """ 

108 Add each diagnostic to the stats, with its key as ``type``, on level ``-1``. 

109 

110 Args: 

111 step (pySDC.Step.step): the current step 

112 L (pySDC.Level.level): the finest level 

113 t (float): the time the diagnostics refer to 

114 diagnostics (dict): stats entries as returned by :meth:`get_diagnostics` 

115 """ 

116 for key, value in diagnostics.items(): 

117 self.add_to_stats( 

118 process=step.status.slot, 

119 time=t, 

120 level=-1, 

121 iter=step.status.iter, 

122 sweep=L.status.sweep, 

123 type=key, 

124 value=value, 

125 ) 

126 

127 def pre_run(self, step, level_number): 

128 """ 

129 Read the initial radius, phase threshold and dimension off the problem, and record the diagnostics of the 

130 initial condition if the run starts at :math:`t = 0`. 

131 

132 Args: 

133 step (pySDC.Step.step): the current step 

134 level_number (int): the current level number 

135 """ 

136 super().pre_run(step, level_number) 

137 L = step.levels[0] 

138 

139 self.init_radius = L.prob.radius 

140 self.phase_thresh = getattr(L.prob, 'phase_thresh', self.default_phase_thresh) 

141 self.ndim = len(self.get_real_space(L, L.u[0]).shape) 

142 

143 if L.time == 0.0: 

144 self.record(step, L, L.time, self.get_diagnostics(L, L.u[0], 0.0)) 

145 

146 def post_step(self, step, level_number): 

147 """ 

148 Record the diagnostics of the solution at the end of the step, at time ``L.time + L.dt``. 

149 

150 Args: 

151 step (pySDC.Step.step): the current step 

152 level_number (int): the current level number 

153 """ 

154 super().post_step(step, level_number) 

155 L = step.levels[0] 

156 

157 self.record(step, L, L.time + L.dt, self.get_diagnostics(L, L.uend, L.time + L.dt))