Recon1d_PLM_WLS.F90

1!> Piecewise Linear Method using Weighted Conservative Least Squares 1D reconstruction
3
4! This file is part of MOM6. See LICENSE.md for the license.
5
6use recon1d_type, only : recon1d, testing
7
8implicit none ; private
9
10public plm_wls, testing
11
12!> PLM reconstruction using Weighted Least Squares constrained to conserve for central cell
13!!
14!! The source for the methods ultimately used by this class are:
15!! - init() *locally defined
16!! - reconstruct() *locally defined
17!! - average() *locally defined
18!! - f() *locally defined
19!! - dfdx() *locally defined
20!! - x() -> recon1d_type.x()
21!! - check_reconstruction() *locally defined
22!! - unit_tests() *locally defined
23!! - destroy() *locally defined
24!! - remap_to_sub_grid() -> recon1d_type.remap_to_sub_grid()
25!! - init_parent() -> init()
26!! - reconstruct_parent() -> reconstruct()
27type, extends (recon1d) :: plm_wls
28
29 real, allocatable :: ul(:) !< Left edge value [A]
30 real, allocatable :: ur(:) !< Right edge value [A]
31 real, allocatable, private :: slp(:) !< Difference across cell, ur - ul [A].
32 !! This is redundant with ul and ur and not used
33 !! in any evaluations, but is needed for testing.
34
35contains
36 !> Implementation of the PLM_WLS initialization
37 procedure :: init => init
38 !> Implementation of the PLM_WLS reconstruction
39 procedure :: reconstruct => reconstruct
40 !> Implementation of the PLM_WLS average over an interval [A]
41 procedure :: average => average
42 !> Implementation of evaluating the PLM_WLS reconstruction at a point [A]
43 procedure :: f => f
44 !> Implementation of the derivative of the PLM_WLS reconstruction at a point [A]
45 procedure :: dfdx => dfdx
46 !> Implementation of deallocation for PLM_WLS
47 procedure :: destroy => destroy
48 !> Implementation of check reconstruction for the PLM_WLS reconstruction
50 !> Implementation of unit tests for the PLM_WLS reconstruction
51 procedure :: unit_tests => unit_tests
52
53 !> Duplicate interface to init()
54 procedure :: init_parent => init
55 !> Duplicate interface to reconstruct()
56 procedure :: reconstruct_parent => reconstruct
57
58end type plm_wls
59
60contains
61
62!> Initialize a 1D PLM reconstruction for n cells
63subroutine init(this, n, h_neglect, check)
64 class(plm_wls), intent(out) :: this !< This reconstruction
65 integer, intent(in) :: n !< Number of cells in this column
66 real, optional, intent(in) :: h_neglect !< A negligibly small width used in cell reconstructions [H]
67 logical, optional, intent(in) :: check !< If true, enable some consistency checking
68
69 this%n = n
70
71 allocate( this%u_mean(n) )
72 allocate( this%ul(n) )
73 allocate( this%ur(n) )
74 allocate( this%slp(n) )
75
76 this%h_neglect = tiny( this%u_mean(1) )
77 if (present(h_neglect)) this%h_neglect = h_neglect
78 this%check = .false.
79 if (present(check)) this%check = check
80
81end subroutine init
82
83!> Calculate a 1D PLM_WLS reconstruction based on h(:) and u(:)
84subroutine reconstruct(this, h, u)
85 class(plm_wls), intent(inout) :: this !< This reconstruction
86 real, intent(in) :: h(*) !< Grid spacing (thickness) [typically H]
87 real, intent(in) :: u(*) !< Cell mean values [A]
88 ! Local variables
89 real :: slp ! The PLM slopes (difference across cell) [A]
90 real :: u_l, u_r, u_c ! Left, right, and center values [A]
91 real :: h_l, h_c, h_r ! Thickness of left, center and right cells [H]
92 real :: h_l0, h_r0 ! Thickness of left and right cells with h_neglect added [H]
93 real :: hx2l, hx2r ! Contributions to denominator, <h x^2> [H3]
94 real :: hxyl, hxyr ! Contributions to numerator, <h x y> [H2 A]
95 integer :: n, km1, k, kp1
96
97 n = this%n
98
99 ! Loop over all cells
100 do k = 1, n
101 km1 = max(1, k-1)
102 kp1 = min(n, k+1)
103 u_l = u(km1)
104 u_c = u(k)
105 u_r = u(kp1)
106
107 h_l = h(km1) * real( k - km1 ) ! This zeroes h_l at k==1
108 h_c = h(k)
109 h_r = h(kp1) * real( kp1 - k ) ! This zeroes h_r at k==n
110
111 ! This is the slope that minimizes the error
112 ! sum_l={-1,1} h(k+l) * [ u(k+l) - u(k) + slp * ( z(k+l) - z(k) ) ]
113 ! i.e. volume weighted least squares
114 h_l0 = h_l + this%h_neglect
115 h_r0 = h_r + this%h_neglect
116 hxyl = ( h_l * ( h_c + h_l ) ) * ( u_c - u_l )
117 hxyr = ( h_r * ( h_c + h_r ) ) * ( u_r - u_c )
118 hx2l = h_l0 * ( h_c + h_l0 )**2
119 hx2r = h_r0 * ( h_c + h_r0 )**2
120 slp = 2. * h_c * ( hxyr + hxyl ) / ( hx2l + hx2r )
121
122 ! Mean value
123 this%u_mean(k) = u_c
124
125 ! Left edge
126 this%ul(k) = u_c - 0.5 * slp
127
128 ! Right edge
129 this%ur(k) = u_c + 0.5 * slp
130
131 ! Store slope
132 this%slp(k) = slp
133 enddo
134
135end subroutine reconstruct
136
137!> Value of PLM_WLS reconstruction at a point in cell k [A]
138real function f(this, k, x)
139 class(plm_wls), intent(in) :: this !< This reconstruction
140 integer, intent(in) :: k !< Cell number
141 real, intent(in) :: x !< Non-dimensional position within element [nondim]
142 real :: du ! Difference across cell [A]
143
144 du = this%ur(k) - this%ul(k)
145
146 ! This expression might be used beyond the element to evaluate
147 ! LS errors. In other PLM implementations x is bounded to the
148 ! element and the expressions are constructed to not exceed
149 ! bounds. There are no such constraints for PLM_WLS.
150 f = this%u_mean(k) + du * ( x - 0.5)
151 !f = this%u_mean(k) + this%slp(k) * ( x - 0.5)
152
153end function f
154
155!> Derivative of PLM_WLS reconstruction at a point in cell k [A]
156real function dfdx(this, k, x)
157 class(plm_wls), intent(in) :: this !< This reconstruction
158 integer, intent(in) :: k !< Cell number
159 real, intent(in) :: x !< Non-dimensional position within element [nondim]
160
161 dfdx = this%ur(k) - this%ul(k)
162
163end function dfdx
164
165!> Average between xa and xb for cell k of a 1D PLM reconstruction [A]
166real function average(this, k, xa, xb)
167 class(plm_wls), intent(in) :: this !< This reconstruction
168 integer, intent(in) :: k !< Cell number
169 real, intent(in) :: xa !< Start of averaging interval on element (0 to 1)
170 real, intent(in) :: xb !< End of averaging interval on element (0 to 1)
171 real :: xmab ! Mid-point between xa and xb (0 to 1)
172 real :: u_a, u_b ! Values at xa and xb [A]
173
174 ! Mid-point between xa and xb
175 xmab = 0.5 * ( xa + xb )
176
177 ! This expression for u_a can overshoot u_r but is good for xmab<<1
178 u_a = this%ul(k) + ( this%ur(k) - this%ul(k) ) * xmab
179 ! This expression for u_b can overshoot u_l but is good for 1-xmab<<1
180 u_b = this%ur(k) + ( this%ul(k) - this%ur(k) ) * ( 1. - xmab )
181
182 ! Since u_a and u_b are both bounded, this will perserve uniformity but will the
183 ! sum be bounded? Emperically it seems to work...
184 average = 0.5 * ( u_a + u_b )
185
186end function average
187
188!> Deallocate the PLM reconstruction
189subroutine destroy(this)
190 class(plm_wls), intent(inout) :: this !< This reconstruction
191
192 deallocate( this%u_mean, this%ul, this%ur )
193
194end subroutine destroy
195
196!> Checks the PLM_WLS reconstruction for consistency
197logical function check_reconstruction(this, h, u)
198 class(plm_wls), intent(in) :: this !< This reconstruction
199 real, intent(in) :: h(*) !< Grid spacing (thickness) [typically H]
200 real, intent(in) :: u(*) !< Cell mean values [A]
201 ! Local variables
202 integer :: k
203 real :: slp ! Cell slope [A]
204 type(plm_wls) :: perturbed !< A perturbed reconstruction
205 real :: u_l, u_r, u_c ! Left, right, and center values [A]
206 real :: h_l, h_c, h_r ! Thickness of left, center and right cells [H]
207 real :: h_l0, h_r0, h_c0 ! Thickness of left, right, center cells with h_neglect added [H]
208 real :: x_l, x_r ! Positions of left and right cells [H]
209 real :: hx2l, hx2r ! Contributions to denominator, <h x^2> [H3]
210 real :: hxyl, hxyr ! Contributions to numerator, <h x y> [H2 A]
211 real :: hy2l, hy2r ! Contributions to error, <h y^2> [H3]
212 real :: y_l, y_r ! Left, right, value differencess [A]
213 real :: b_h, bp_h ! slp / h_c [A H-1]
214 integer :: km1, kp1
215
216 check_reconstruction = .false.
217
218 do k = 1, this%n
219 if ( abs( this%u_mean(k) - u(k) ) > 0. ) check_reconstruction = .true.
220 enddo
221
222 ! Check the cell reconstruction is monotonic within each cell (it should be as a straight line)
223 do k = 1, this%n
224 if ( ( this%u_mean(k) - this%ul(k) ) * ( this%ur(k) - this%u_mean(k) ) < 0. ) check_reconstruction = .true.
225 enddo
226
227 ! Check the cell is a straight line (to within machine precision)
228 do k = 1, this%n
229 if ( abs(2. * this%u_mean(k) - ( this%ul(k) + this%ur(k) )) > epsilon(this%u_mean(1)) * &
230 max(abs(2. * this%u_mean(k)), abs(this%ul(k)), abs(this%ur(k))) ) check_reconstruction = .true.
231 enddo
232
233 ! Create a perturbable reconstruction
234 perturbed = this ! Complete copy of this
235 ! Check the copy is identical
236 do k = 1, this%n
237 if ( abs( perturbed%u_mean(k) - this%u_mean(k) ) > 0. ) check_reconstruction = .true.
238 if ( abs( perturbed%ul(k) - this%ul(k) ) > 0. ) check_reconstruction = .true.
239 if ( abs( perturbed%ur(k) - this%ur(k) ) > 0. ) check_reconstruction = .true.
240 if ( abs( perturbed%slp(k) - this%slp(k) ) > 0. ) check_reconstruction = .true.
241 enddo
242 ! The !DIR$ NOINLINE directive would be needed here to avoid ifort -O2 changing answers
243 ! Now perturb the slope. The local error should not decrease.
244 do k = 1, this%n
245 slp = this%slp(k) * ( 1.0 + 1. * epsilon(slp) )
246 perturbed%slp(k) = slp
247 perturbed%ul(k) = u(k) - 0.5 * slp
248 perturbed%ur(k) = u(k) + 0.5 * slp
249 if ( ls_error(perturbed, k, h, u) < ls_error(this, k, h, u) ) check_reconstruction = .true.
250
251 slp = this%slp(k) * ( 1.0 - 1. * epsilon(slp) )
252 perturbed%slp(k) = slp
253 perturbed%ul(k) = u(k) - 0.5 * slp
254 perturbed%ur(k) = u(k) + 0.5 * slp
255 if ( ls_error(perturbed, k, h, u) < ls_error(this, k, h, u) ) check_reconstruction = .true.
256 enddo
257
258end function check_reconstruction
259
260!> Returns local least squares error for a particular cell
261!!
262!! Note that this is the error relative to the minimum of the loss function so that at the
263!! true solution this function returns zero. See module documentation.
264real function ls_error(this, k, h, u)
265 type(plm_wls), intent(in) :: this !< This reconstruction
266 integer, intent(in) :: k !< Cell number
267 real, intent(in) :: h(*) !< Grid spacing (thickness) [typically H]
268 real, intent(in) :: u(*) !< Cell mean values [A]
269 ! Local variables
270 real :: u_l, u_r, u_c ! Left, right, and center values [A]
271 real :: h_l, h_c, h_r ! Thickness of left, center and right cells [H]
272 real :: h_l0, h_r0, hc0 ! Thickness of left, right, center cells with h_neglect added [H]
273 real :: hx2l, hx2r ! Contributions to denominator, <h x^2> [H3]
274 real :: hxyl, hxyr ! Contributions to numerator, <h x y> [H2 A]
275 real :: slp ! The PLM slopes (difference across cell) [A]
276 integer :: km1, kp1
277
278 km1 = max(1, k-1)
279 kp1 = min(this%n, k+1)
280 u_l = u(km1)
281 u_c = u(k)
282 u_r = u(kp1)
283
284 h_l = h(km1) * real( k - km1 ) ! This zeroes h_l at k==1
285 h_r = h(kp1) * real( kp1 - k ) ! This zeroes h_r at k==n
286 h_c = h(k)
287 hc0 = h_c + this%h_neglect
288
289 h_l0 = h_l + this%h_neglect
290 h_r0 = h_r + this%h_neglect
291 hxyl = ( h_l * ( h_c + h_l ) ) * ( u_c - u_l )
292 hxyr = ( h_r * ( h_c + h_r ) ) * ( u_r - u_c )
293 hx2l = h_l0 * ( h_c + h_l0 )**2
294 hx2r = h_r0 * ( h_c + h_r0 )**2
295 slp = 2. * h_c * ( hxyr + hxyl ) / ( hx2l + hx2r )
296 ls_error = h_c * ( ( hx2l + hx2r ) * ( this%slp(k) - slp ) )**2
297 ls_error = ls_error / ( hc0 * ( hx2l + hx2r ) )
298end function ls_error
299
300!> Runs PLM_WLS reconstruction unit tests and returns True for any fails, False otherwise
301logical function unit_tests(this, verbose, stdout, stderr)
302 class(plm_wls), intent(inout) :: this !< This reconstruction
303 logical, intent(in) :: verbose !< True, if verbose
304 integer, intent(in) :: stdout !< I/O channel for stdout
305 integer, intent(in) :: stderr !< I/O channel for stderr
306 ! Local variables
307 real, allocatable :: ul(:), ur(:), um(:) ! test values [A]
308 real, allocatable :: ull(:), urr(:) ! test values [A]
309 type(testing) :: test ! convenience functions
310 integer :: k
311
312 call test%set( stdout=stdout ) ! Sets the stdout channel in test
313 call test%set( stderr=stderr ) ! Sets the stderr channel in test
314 call test%set( verbose=verbose ) ! Sets the verbosity flag in test
315
316 call this%init(3, h_neglect=1.e-20)
317 call test%test( this%n /= 3, "Setting number of levels")
318 allocate( um(3), ul(3), ur(3), ull(3), urr(3) )
319
320 call this%reconstruct( (/1.,1.,1./), (/-1.,0.,2./) )
321 call test%real_arr(3, this%slp, (/1.,1.5,2./), "(1,1,1)(-1,0,2) slope")
322
323 do k = 1, 3
324 um(k) = ls_error(this, k, (/1.,1.,1./), (/-1.,0.,2./) )
325 enddo
326 call test%real_arr(3, um, (/0.,0.,0./), "(1,1,1)(-1,0,2) LS' rel error")
327
328 call this%reconstruct( (/0.,1.,1./), (/-1.,0.,2./) )
329 call test%real_arr(3, this%slp, (/0.,2.,2./), "(0,1,1)(-1,0,2) slope")
330
331 do k = 1, 3
332 um(k) = ls_error(this, k, (/0.,1.,1./), (/-1.,0.,2./) )
333 enddo
334 call test%real_arr(3, um, (/0.,0.,0./), "(0,1,1)(-1,0,2) LS' rel error")
335
336 call this%reconstruct( (/1.,1.,1./), (/-2.,0.,1./) )
337 call test%real_arr(3, this%slp, (/2.,1.5,1./), "(1,1,1)(-2,0,1) slope")
338
339 call this%reconstruct( (/1.,1.,0./), (/-2.,0.,1./) )
340 call test%real_arr(3, this%slp, (/2.,2.,0./), "(1,1,0)(-2,0,1) slope")
341
342 call this%destroy()
343 call this%init(3) ! Reset to defaults
344
345 ! Straight line data on uniform grid
346 call this%reconstruct( (/2.,2.,2./), (/1.,3.,5./) )
347 call test%real_arr(3, this%u_mean, (/1.,3.,5./), "Straight line data")
348
349 do k = 1, 3
350 ul(k) = this%f(k, 0.)
351 um(k) = this%f(k, 0.5)
352 ur(k) = this%f(k, 1.)
353 enddo
354 call test%real_arr(3, ul, (/0.,2.,4./), "Evaluation on left edge")
355 call test%real_arr(3, um, (/1.,3.,5./), "Evaluation in center")
356 call test%real_arr(3, ur, (/2.,4.,6./), "Evaluation on right edge")
357
358 do k = 1, 3
359 ul(k) = this%dfdx(k, 0.)
360 um(k) = this%dfdx(k, 0.5)
361 ur(k) = this%dfdx(k, 1.)
362 enddo
363 call test%real_arr(3, ul, (/2.,2.,2./), "dfdx on left edge")
364 call test%real_arr(3, um, (/2.,2.,2./), "dfdx in center")
365 call test%real_arr(3, ur, (/2.,2.,2./), "dfdx on right edge")
366
367 do k = 1, 3
368 um(k) = ls_error(this, k, (/2.,2.,2./), (/1.,3.,5./) )
369 enddo
370 call test%real_arr(3, um, (/0.,0.,0./), "Rel error is 0")
371
372 do k = 1, 3
373 um(k) = this%average(k, 0.5, 0.75) ! Average from x=0.5 to 0.75 in each cell
374 enddo
375 call test%real_arr(3, um, (/1.25,3.25,5.25/), "Return interval average")
376
377 call test%real_scalar( this%x(1,0.), 0., 'f-1(1,0)=0')
378 call test%real_scalar( this%x(1,1.), 0.5, 'f-1(1,1)=0.5')
379 call test%real_scalar( this%x(1,3.), 1., 'f-1(1,3)=1')
380 call test%real_scalar( this%x(2,1.), 0., 'f-1(2,1)=0')
381 call test%real_scalar( this%x(2,3.), 0.5, 'f-1(2,3)=0.5')
382 call test%real_scalar( this%x(2,5.), 1., 'f-1(2,5)=1')
383 call test%real_scalar( this%x(3,3.), 0., 'f-1(3,3)=0')
384 call test%real_scalar( this%x(3,5.), 0.5, 'f-1(3,5)=0.5')
385 call test%real_scalar( this%x(3,7.), 1., 'f-1(3,7)=1')
386
387 call this%destroy()
388 deallocate( um, ul, ur, ull, urr )
389
390 allocate( um(4), ul(4), ur(4) )
391 call this%init(4)
392
393 deallocate( um, ul, ur )
394
395 unit_tests = test%summarize("PLM_WLS:unit_tests")
396
397end function unit_tests
398
399!> \namespace recon1d_plm_wls
400!!
401!! This implementation of PLM fits the slope using least squares, but retains conservation
402!! for the central cell by passing through the central value.
403!! Cell-wise reconstructions are NOT limited by neighbours.
404!! Thus, this reconstruction does not yield monotonic profiles needed for the general remapping problem.
405!!
406!! The algorithm solves the least squares problem of fitting a straight line through
407!! the neighboring data. The line is constained to pass through the center cell,
408!! \f$ (x_{k}, y_{k}) \f$, so that the construction is conservative. The more general
409!! function \f$ f(x) = a_{k} + b_{k} x \f$ would not conserve for arbitrary data.
410!!
411!! The unknown parameter \f$ b_{k} \f$ in the line
412!! \f[
413!! f(x) = y_{k} + b_{k} ( x - x_{k} ) / h_{k}
414!! \f]
415!! is fit to neighbors \f$ x_{k-1}, y_{k-1} \f$ and \f$ x_{k+1}, y_{k+1} \f$.
416!!
417!! Denoting \f$ y'_{k+j} = y_{k+j} - y_{k} \f$ and \f$ x'_{k+j} = x_{k+j} - x_{k} \f$
418!! the local error is
419!! \f{align}{
420!! e_{k+j} &= b_k \frac{ x_{k+j} - x_{k} }{ h_{k} } + y_{k} - y_{k+j} \\\\
421!! &= b_k \frac{ x'_{k+j} }{ h_{k} } - y'_{k+j}
422!! \;\; . \f}
423!!
424!! We use volume weighting in the loss
425!! \f[
426!! G(b) = h_{k-1} e_{k-1}^2 + h_{k+1} e_{k+1}^2
427!! \;\; . \f]
428!!
429!! When solving for \f$ b_k \f$, we solve \f$ dG/db = 0 \f$ where
430!! \f{align}{
431!! dG/db &= 2 h_{k-1} e_{k-1} \frac{ de_{k-1} }{db} + 2 h_{k+1} e_{k+1} \frac{ de_{k+1} }{db} \\\\
432!! &= 2 h_{k-1} ( b_k \frac{ x'_{k-1} }{ h_{k} } - \frac{ y'_{k-1} ) x'_{k-1} }{ h_{k} } +
433!! 2 h_{k+1} ( b_k \frac{ x'_{k+1} }{ h_{k} } - \frac{ y'_{k+1} ) x'_{k+1} }{ h_{k} } \\\\
434!! &= 4 b_k \frac{ < h x'^2 > }{ h_{k}^2 } - 4 \frac{ < h x' y' > }{ h_{k} }
435!! \f}
436!! and where \f$ < a > = \frac{1}{2} ( a_{k-1} + a_{k+1} ) \f$.
437!! Thus
438!! \f[
439!! b_k = \frac{ h_{k} < h x' y' > }{ < h x'^2 > } \;\; .
440!! \f]
441!!
442!! When evaluating the loss, \f$ G \f$, some rearrangement is necessary to reduce truncation
443!! errors. Since
444!! \f{align}{
445!! e_{k+j}^2 &= \left( b \frac{ x'_{k+j} }{ h_{k} } - y'_{k+j} \right)^2 \\\\
446!! &= b^2 \frac{ {x'}_{k+j}^2 }{ h_{k}^2 } - 2 b \frac{ x'_{k+j} y'_{k+j} }{ h_{k} } + {y'}_{k+j}^2
447!! \f}
448!! then
449!! \f{align}{
450!! G(b) &= 2 < h e^2 > \\\\
451!! &= 2 b^2 \frac{ < h {x'}^2 > }{ h_{k}^2 } - 4 b \frac{ < h x' y' > }{ h_{k} } + 2 < h {y'}^2 >
452!! \;\; .
453!! \f}
454!!
455!! If we denote the value of b that yields the minimum value as \f$ b^* \f$ then
456!! \f[
457!! G(b^*) = 2 < h {y'}^2 > - \frac{ 2 < h x' y' >^2 }{ < h {x'}^2 > }
458!! \;\; .
459!! \f]
460!!
461!! Let
462!! \f{align}{
463!! G''(b) &= G(b) - G(b^*) \\\\
464!! &= 2 b^2 \frac{ < h {x'}^2 > }{ h_{k}^2 } - 4 b \frac{ < h x' y' > }{ h_{k} }
465!! + 2 \frac{ < h x' y' >^2 }{ < h {x'}^2 > } \\\\
466!! &= 2 \frac{ \left( b < h {x'}^2 > - h_{k} < h x' y' > \right)^2 }{ h_{k} < h {x'}^2 > }
467!! \;\; .
468!! \f}
469!! Minimizing \f$ G''(b) \f$ is equivalent to minimizing \f$ G(b) \f$ for the same data.
470!! \f$ G''(b^*)=0 \f$ so evaluation with the last form, in the vicinity of \f$ b^* \f$, avoids
471!! large cancelling terms.
472
473end module recon1d_plm_wls