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

60 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 20:28 +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 super().__init__() 

33 

34 self.init_radius = None 

35 self.ndim = None 

36 self.phase_thresh = self.default_phase_thresh 

37 

38 @staticmethod 

39 def get_real_space(L, u): 

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

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

42 

43 def get_volume(self, L, u): 

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

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

46 

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

48 if comm is not None: 

49 from mpi4py import MPI 

50 

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

52 

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

54 

55 def radius_from_volume(self, vol): 

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

57 if self.ndim == 2: 

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

59 elif self.ndim == 3: 

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

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

62 

63 def exact_radius_squared(self, t): 

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

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

66 

67 def exact_radius(self, t): 

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

69 

70 def exact_volume(self, t): 

71 r2 = self.exact_radius_squared(t) 

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

73 

74 def measures_interface_width(self, L): 

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

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

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

78 

79 def get_interface_width(self, L, u): 

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

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

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

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

84 

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

86 

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

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

89 vol = self.get_volume(L, u) 

90 

91 diagnostics = { 

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

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

94 'computed_volume': vol, 

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

96 } 

97 

98 if self.measures_interface_width(L): 

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

100 

101 return diagnostics 

102 

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

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

105 self.add_to_stats( 

106 process=step.status.slot, 

107 time=t, 

108 level=-1, 

109 iter=step.status.iter, 

110 sweep=L.status.sweep, 

111 type=key, 

112 value=value, 

113 ) 

114 

115 def pre_run(self, step, level_number): 

116 super().pre_run(step, level_number) 

117 L = step.levels[0] 

118 

119 self.init_radius = L.prob.radius 

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

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

122 

123 if L.time == 0.0: 

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

125 

126 def post_step(self, step, level_number): 

127 super().post_step(step, level_number) 

128 L = step.levels[0] 

129 

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