Recon1d_PLM_hybgen.F90
1! This file is part of MOM6, the Modular Ocean Model version 6.
2! See the LICENSE file for licensing information.
3! SPDX-License-Identifier: Apache-2.0
4
5!> Piecewise Linear Method 1D reconstruction ported from "hybgen" module in Hycom.
6!!
7!! This implementation of PLM follows Colella and Woodward, 1984, with cells resorting to PCM for
8!! extrema including first and last cells in column. The cell-wise reconstructions are limited so
9!! that the edge values (which are also the extrema in a cell) are bounded by the neighbors. The
10!! limiter yields monotonicity for the CFL<1 transport problem where parts of a cell can only move
11!! to a neighboring cell, but does not yield monotonic profiles for the general remapping problem.
12!! The first and last cells are always limited to PCM.
13!!
14!! The mom_hybgen_remap.hybgen_plm_coefs() function calculates PLM coefficients numerically
15!! equiavalent to the recon1d_plm_hybgen module (this implementation).
16module recon1d_plm_hybgen
17
18use recon1d_type, only : recon1d, testing
19
20implicit none ; private
21
22public plm_hybgen, testing
23
24!> PLM reconstruction following "hybgen".
25!!
26!! This implementation is a refactor of hybgen_plm_coefs() from mom_hybgen_remap.
27!!
28!! The source for the methods ultimately used by this class are:
29!! - init() *locally defined
30!! - reconstruct() *locally defined
31!! - average() *locally defined
32!! - f() *locally defined
33!! - dfdx() *locally defined
34!! - x() -> recon1d_plm_cw.x()
35!! - check_reconstruction() *locally defined
36!! - unit_tests() *locally defined
37!! - destroy() *locally defined
38!! - remap_to_sub_grid() -> recon1d_type.remap_to_sub_grid()
39!! - init_parent() -> init()
40!! - reconstruct_parent() -> reconstruct()
41type, extends (recon1d) :: plm_hybgen
42
43 real, allocatable :: ul(:) !< Left edge value [A]
44 real, allocatable :: ur(:) !< Right edge value [A]
45 real, allocatable :: slp(:) !< Right minus left edge values [A]
46
47contains
48 !> Implementation of the PLM_hybgen initialization
50 !> Implementation of the PLM_hybgen reconstruction
51 procedure :: reconstruct => reconstruct
52 !> Implementation of the PLM_hybgen average over an interval [A]
54 !> Implementation of evaluating the PLM_hybgen reconstruction at a point [A]
56 !> Implementation of the derivative of the PLM_hybgen reconstruction at a point [A]
58 !> Implementation of deallocation for PLM_hybgen
60 !> Implementation of check reconstruction for the PLM_hybgen reconstruction
61 procedure :: check_reconstruction => check_reconstruction
62 !> Implementation of unit tests for the PLM_hybgen reconstruction
63 procedure :: unit_tests => unit_tests
64
65 !> Duplicate interface to init()
66 procedure :: init_parent => init
67 !> Duplicate interface to reconstruct()
68 procedure :: reconstruct_parent => reconstruct
69
70end type plm_hybgen
71
72contains
73
74!> Initialize a 1D PLM reconstruction for n cells
75subroutine init(this, n, h_neglect, check)
76 class(plm_hybgen), intent(out) :: this !< This reconstruction
77 integer, intent(in) :: n !< Number of cells in this column
78 real, optional, intent(in) :: h_neglect !< A negligibly small width used in cell reconstructions [H]
79 logical, optional, intent(in) :: check !< If true, enable some consistency checking
80
81 this%n = n
82
83 allocate( this%u_mean(n) )
84 allocate( this%ul(n) )
85 allocate( this%ur(n) )
86 allocate( this%slp(n) )
87
88 this%h_neglect = tiny( this%u_mean(1) )
89 if (present(h_neglect)) this%h_neglect = h_neglect
90 this%check = .false.
91 if (present(check)) this%check = check
92
93end subroutine init
94
95!> Calculate a 1D PLM reconstructions based on h(:) and u(:)
96subroutine reconstruct(this, h, u)
97 class(plm_hybgen), intent(inout) :: this !< This reconstruction
98 real, intent(in) :: h(*) !< Grid spacing (thickness) [typically H]
99 real, intent(in) :: u(*) !< Cell mean values [A]
100 ! Local variables
101 real :: slp ! The PLM slopes (difference across cell) [A]
102 real :: sigma_l, sigma_c, sigma_r ! Left, central and right slope estimates as
103 ! differences across the cell [A]
104 real :: u_min, u_max ! Minimum and maximum value across cell [A]
105 real :: u_l, u_r, u_c ! Left, right, and center values [A]
106 real :: h_l, h_c, h_r ! Thickness of left, center and right cells [H]
107 real :: h_c0 ! Thickness of center with h_neglect added [H]
108 integer :: k, n
109
110 n = this%n
111
112 ! Loop over all cells
113 do k = 1, n
114 this%u_mean(k) = u(k)
115 enddo
116
117 ! Boundary cells use PCM
118 this%ul(1) = u(1)
119 this%ur(1) = u(1)
120 this%slp(1) = 0.
121
122 ! Loop over interior cells
123 do k = 2, n-1
124 u_l = u(k-1)
125 u_c = u(k)
126 u_r = u(k+1)
127
128 ! Side differences
129 sigma_r = u_r - u_c
130 sigma_l = u_c - u_l
131
132 h_l = h(k-1)
133 h_c = h(k)
134 h_r = h(k+1)
135 ! Avoids division by zero
136 h_c0 = h_c + this%h_neglect
137
138 ! This is the second order slope given by equation 1.7 of
139 ! Piecewise Parabolic Method, Colella and Woodward (1984),
140 ! http://dx.doi.org/10.1016/0021-991(84)90143-8.
141 ! For uniform resolution it simplifies to ( u_r - u_l )/2 .
142 sigma_c = ( h_c / ( h_c0 + ( h_l + h_r ) ) ) * ( &
143 ( 2.*h_l + h_c ) / ( h_r + h_c0 ) * sigma_r &
144 + ( 2.*h_r + h_c ) / ( h_l + h_c0 ) * sigma_l )
145 if (h_c <= this%h_neglect) then
146 sigma_c = 0.
147 else
148 sigma_c = ( h_c / ( h_c + 0.5 * ( h_l + h_r ) ) ) * ( u_r - u_l )
149 endif
150
151 ! Limit slope so that reconstructions are bounded by neighbors
152 u_min = min( u_l, u_c, u_r )
153 u_max = max( u_l, u_c, u_r )
154
155 if ( (sigma_l * sigma_r) > 0.0 ) then
156 ! This limits the slope so that the edge values are bounded by the two cell averages spanning the edge
157 slp = sign( min( abs(sigma_c), 2.*min( u_c - u_min, u_max - u_c ) ), sigma_c )
158! slp = sign( min( abs(sigma_c), 2. * abs(u_c - u_l), 2. * abs(u_r - u_c) ), sigma_c )
159 else
160 ! Extrema in the mean values require a PCM reconstruction
161 slp = 0.0
162 endif
163 this%slp(k) = slp
164
165 ! Left edge
166 u_min = min( u_c, u_l )
167 u_max = max( u_c, u_l )
168 u_l = u_c - 0.5 * slp
169 this%ul(k) = max( min( u_l, u_max), u_min )
170 this%ul(k) = u_l
171
172 ! Right edge
173 u_min = min( u_c, u_r )
174 u_max = max( u_c, u_r )
175 u_r = u_c + 0.5 * slp
176 this%ur(k) = max( min( u_r, u_max), u_min )
177 this%ur(k) = u_r
178 enddo
179
180 ! Boundary cells use PCM
181 this%ul(n) = u(n)
182 this%ur(n) = u(n)
183 this%slp(n) = 0.
184
185end subroutine reconstruct
186
187!> Value of PLM_hybgen reconstruction at a point in cell k [A]
188real function f(this, k, x)
189 class(plm_hybgen), intent(in) :: this !< This reconstruction
190 integer, intent(in) :: k !< Cell number
191 real, intent(in) :: x !< Non-dimensional position within element [nondim]
192 real :: xc ! Bounded version of x [nondim]
193 real :: du ! Difference across cell [A]
194 real :: u_a, u_b ! Two estimate of f [A]
195
196 du = this%ur(k) - this%ul(k)
197 xc = max( 0., min( 1., x ) )
198
199 ! This expression for u_a can overshoot u_r but is good for x<<1
200 u_a = this%ul(k) + du * xc
201 ! This expression for u_b can overshoot u_l but is good for 1-x<<1
202 u_b = this%ur(k) + du * ( xc - 1. )
203
204 ! Since u_a and u_b are both bounded, this will perserve uniformity
205 f = 0.5 * ( u_a + u_b )
206
207end function f
208
209!> Derivative of PLM_hybgen reconstruction at a point in cell k [A]
210real function dfdx(this, k, x)
211 class(plm_hybgen), intent(in) :: this !< This reconstruction
212 integer, intent(in) :: k !< Cell number
213 real, intent(in) :: x !< Non-dimensional position within element [nondim]
214
215 dfdx = this%ur(k) - this%ul(k)
216
217end function dfdx
218
219!> Average between xa and xb for cell k of a 1D PLM reconstruction [A]
220real function average(this, k, xa, xb)
221 class(plm_hybgen), intent(in) :: this !< This reconstruction
222 integer, intent(in) :: k !< Cell number
223 real, intent(in) :: xa !< Start of averaging interval on element (0 to 1)
224 real, intent(in) :: xb !< End of averaging interval on element (0 to 1)
225 real :: xmab ! Mid-point between xa and xb (0 to 1)
226! real :: u_a, u_b ! Values at xa and xb [A]
227
228 ! This form is not guaranteed to be bounded by {ul,ur}
229! u_a = this%ul(k) * ( 1. - xa ) + this%ur(k) * xa
230! u_b = this%ul(k) * ( 1. - xb ) + this%ur(k) * xb
231! average = 0.5 * ( u_a + u_b )
232
233 ! Mid-point between xa and xb
234 xmab = 0.5 * ( xa + xb )
235
236 ! The following expression is exact at xmab=0 and xmab=1,
237 ! i.e. gives the numerically correct values.
238 ! It is not obvious that the expression is monotonic but according to
239 ! https://math.stackexchange.com/questions/907329/accurate-floating-point-linear-interpolation
240 ! it will be for the default rounding behavior. Otherwise is it
241 ! then possible this expression can be outside the range of ul and ur?
242! average = this%ul(k) * ( 1. - xmab ) + this%ur(k) * xmab
243 ! Emperically it fails the uniform value test
244
245 ! The following is more complicated but seems to ensure being within bounds.
246 ! This expression for u_a can overshoot u_r but is good for xmab<<1
247! u_a = this%ul(k) + ( this%ur(k) - this%ul(k) ) * xmab
248 ! This expression for u_b can overshoot u_l but is good for 1-xmab<<1
249! u_b = this%ur(k) + ( this%ul(k) - this%ur(k) ) * ( 1. - xmab )
250 ! Replace xmab with -1 for xmab<0.5, 1 for xmab>=0.5
251! xmab = sign(1., xmab-0.5)
252 ! Select either u_a or u_b, depending whether mid-point of xa, xb is smaller/larger than 0.5
253! average = xmab * u_b + ( 1. - xmab ) * u_a
254
255 ! Since u_a and u_b are both bounded, this will perserve uniformity but will the
256 ! sum be bounded? Emperically it seems to work...
257! average = 0.5 * ( u_a + u_b )
258
259 ! This expression is equivalent to integrating the polynomial form of the PLM reconstruction
260 average = this%ul(k) + xmab * this%slp(k)
261
262end function average
263
264!> Deallocate the PLM reconstruction
265subroutine destroy(this)
266 class(plm_hybgen), intent(inout) :: this !< This reconstruction
267
268 deallocate( this%u_mean, this%ul, this%ur )
269
270end subroutine destroy
271
272!> Checks the PLM_hybgen reconstruction for consistency
273logical function check_reconstruction(this, h, u)
274 class(plm_hybgen), intent(in) :: this !< This reconstruction
275 real, intent(in) :: h(*) !< Grid spacing (thickness) [typically H]
276 real, intent(in) :: u(*) !< Cell mean values [A]
277 ! Local variables
278 integer :: k
279
280 check_reconstruction = .false.
281
282 do k = 1, this%n
283 if ( abs( this%u_mean(k) - u(k) ) > 0. ) check_reconstruction = .true.
284 enddo
285
286 ! Check the cell reconstruction is monotonic within each cell (it should be as a straight line)
287 do k = 1, this%n
288 if ( ( this%u_mean(k) - this%ul(k) ) * ( this%ur(k) - this%u_mean(k) ) < 0. ) check_reconstruction = .true.
289 enddo
290
291 ! Check the cell is a straight line (to within machine precision)
292 do k = 1, this%n
293 if ( abs(2. * this%u_mean(k) - ( this%ul(k) + this%ur(k) )) > epsilon(this%u_mean(1)) * &
294 max(abs(2. * this%u_mean(k)), abs(this%ul(k)), abs(this%ur(k))) ) check_reconstruction = .true.
295 enddo
296
297! The following test fails MOM_remapping:test_recon_consistency with Intel/2023.2.0 on gaea at iter=84
298! ! Check bounding of right edges, w.r.t. the cell means
299! do K = 1, this%n-1
300! if ( ( this%ur(k) - this%u_mean(k) ) * ( this%u_mean(k+1) - this%ur(k) ) < 0. ) check_reconstruction = .true.
301! enddo
302
303! The following test fails MOM_remapping:test_recon_consistency with Intel/2023.2.0 on gaea at iter=161
304! ! Check bounding of left edges, w.r.t. the cell means
305! do K = 2, this%n
306! if ( ( this%u_mean(k) - this%ul(k) ) * ( this%ul(k) - this%u_mean(k-1) ) < 0. ) check_reconstruction = .true.
307! enddo
308
309 ! PLM is not globally monotonic so the following are expected to fail
310
311! ! Check bounding of right edges, w.r.t. this cell mean and the next cell left edge
312! do K = 1, this%n-1
313! if ( ( this%ur(k) - this%u_mean(k) ) * ( this%ul(k+1) - this%ur(k) ) < 0. ) check_reconstruction = .true.
314! enddo
315
316! ! Check bounding of left edges, w.r.t. this cell mean and the previous cell right edge
317! do K = 2, this%n
318! if ( ( this%u_mean(k) - this%ul(k) ) * ( this%ul(k) - this%ur(k-1) ) < 0. ) check_reconstruction = .true.
319! enddo
320
321end function check_reconstruction
322
323!> Runs PLM reconstruction unit tests and returns True for any fails, False otherwise
324logical function unit_tests(this, verbose, stdout, stderr)
325 class(plm_hybgen), intent(inout) :: this !< This reconstruction
326 logical, intent(in) :: verbose !< True, if verbose
327 integer, intent(in) :: stdout !< I/O channel for stdout
328 integer, intent(in) :: stderr !< I/O channel for stderr
329 ! Local variables
330 real, allocatable :: ul(:), ur(:), um(:) ! test values [A]
331 real, allocatable :: ull(:), urr(:) ! test values [A]
332 type(testing) :: test ! convenience functions
333 integer :: k
334
335 call test%set( stdout=stdout ) ! Sets the stdout channel in test
336 call test%set( stderr=stderr ) ! Sets the stderr channel in test
337 call test%set( verbose=verbose ) ! Sets the verbosity flag in test
338
339 call this%init(3)
340 call test%test( this%n /= 3, 'Setting number of levels')
341 allocate( um(3), ul(3), ur(3), ull(3), urr(3) )
342
343 call this%reconstruct( (/2.,2.,2./), (/1.,3.,5./) )
344 call test%real_arr(3, this%u_mean, (/1.,3.,5./), 'Setting cell values')
345
346 do k = 1, 3
347 ul(k) = this%f(k, 0.)
348 um(k) = this%f(k, 0.5)
349 ur(k) = this%f(k, 1.)
350 enddo
351 call test%real_arr(3, ul, (/1.,2.,5./), 'Evaluation on left edge')
352 call test%real_arr(3, um, (/1.,3.,5./), 'Evaluation in center')
353 call test%real_arr(3, ur, (/1.,4.,5./), 'Evaluation on right edge')
354
355 do k = 1, 3
356 ul(k) = this%dfdx(k, 0.)
357 um(k) = this%dfdx(k, 0.5)
358 ur(k) = this%dfdx(k, 1.)
359 enddo
360 call test%real_arr(3, ul, (/0.,2.,0./), 'dfdx on left edge')
361 call test%real_arr(3, um, (/0.,2.,0./), 'dfdx in center')
362 call test%real_arr(3, ur, (/0.,2.,0./), 'dfdx on right edge')
363
364 call test%real_scalar( this%x(1,0.), 0., 'f-1(1,0)=0')
365 call test%real_scalar( this%x(1,1.), 0.5, 'f-1(1,1)=0.5')
366 call test%real_scalar( this%x(1,3.), 1., 'f-1(1,3)=1')
367 call test%real_scalar( this%x(2,1.), 0., 'f-1(2,1)=0')
368 call test%real_scalar( this%x(2,3.), 0.5, 'f-1(2,3)=0.5')
369 call test%real_scalar( this%x(2,5.), 1., 'f-1(2,5)=1')
370 call test%real_scalar( this%x(3,3.), 0., 'f-1(3,3)=0')
371 call test%real_scalar( this%x(3,5.), 0.5, 'f-1(3,5)=0.5')
372 call test%real_scalar( this%x(3,7.), 1., 'f-1(3,7)=1')
373
374 do k = 1, 3
375 um(k) = this%average(k, 0.5, 0.75) ! Average from x=0.25 to 0.75 in each cell
376 enddo
377 call test%real_arr(3, um, (/1.,3.25,5./), 'Return interval average')
378
379 call this%destroy()
380 deallocate( um, ul, ur, ull, urr )
381
382 allocate( um(4), ul(4), ur(4) )
383 call this%init(4)
384
385 ! These values lead to non-monotonic reconstuctions which are
386 ! valid for transport problems but not always appropriate for
387 ! remapping to arbitrary resolution grids.
388 ! The O(h^2) slopes are -, 2, 2, - and the limited
389 ! slopes are 0, 1, 1, 0 so the everywhere the reconstructions
390 ! are bounded by neighbors but ur(2) and ul(3) are out-of-order.
391 call this%reconstruct( (/1.,1.,1.,1./), (/0.,3.,4.,7./) )
392 do k = 1, 4
393 ul(k) = this%f(k, 0.)
394 ur(k) = this%f(k, 1.)
395 enddo
396 call test%real_arr(4, ul, (/0.,2.,3.,7./), 'Evaluation on left edge')
397 call test%real_arr(4, ur, (/0.,4.,5.,7./), 'Evaluation on right edge')
398
399 deallocate( um, ul, ur )
400
401 unit_tests = test%summarize('PLM_hybgen:unit_tests')
402
403end function unit_tests
404
405!> \namespace recon1d_plm_hybgen
406!!
407
408end module recon1d_plm_hybgen