ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
ERF_NodalReconstruction.H
Go to the documentation of this file.
1 #ifndef ERF_NODAL_RECONSTRUCTION_H_
2 #define ERF_NODAL_RECONSTRUCTION_H_
3 
4 #include <algorithm>
5 #include <cmath>
6 #include <limits>
7 #include <string>
8 #include <utility>
9 #include <vector>
10 
11 #include "AMReX.H"
12 #include "AMReX_Array4.H"
13 #include "AMReX_BLassert.H"
14 #include "AMReX_Box.H"
15 #include "AMReX_FArrayBox.H"
16 #include "AMReX_GpuDevice.H"
17 #include "AMReX_Geometry.H"
18 #include "AMReX_MultiFab.H"
19 #include "AMReX_ParallelDescriptor.H"
20 #include "AMReX_Print.H"
21 #include "AMReX_REAL.H"
22 
23 #include "ERF_NumericalConstants.H"
24 
26 {
27  FirstDeriv,
28  Laplacian
29 };
30 
31 /**
32  * @brief Convergence and diagnostic information for the reconstruction solve.
33  */
34 struct SolveInfo {
35  int iterations = 0; //!< total CG iterations, summed over attempts
36  amrex::Real final_residual = zero; //!< CG residual of the accepted solve
37  bool converged = false;
38  int refinements = 0; //!< times the regularization had to be raised
39  amrex::Real regularization = zero; //!< accepted value of mu
40 
41  // Diagnostics on the reconstructed field
42  amrex::Real deviation = zero; //!< max |S - S_ref|
43  amrex::Real deviation_cap = zero; //!< bound imposed on the above
44  amrex::Real max_average_error = zero; //!< max |avg4(S) - T|
45  amrex::Real interp_average_error = zero; //!< max |avg4(S_ref) - T|, for comparison
46  amrex::Real min_value = zero; //!< min over nodes of S
47  amrex::Real max_value = zero; //!< max over nodes of S
48 };
49 
50 /**
51  * Reconstruct a nodal field S from data T that lives at w-faces, such that the
52  * four-node average of S matches T as closely as a *bounded*, smooth nodal
53  * field can.
54  *
55  * ---------------------------------------------------------------------------
56  * Why we do not simply invert the averaging operator
57  * ---------------------------------------------------------------------------
58  * The four-node averaging operator A,
59  *
60  * (A S)(i,j) = 1/4 [ S(i,j) + S(i+1,j) + S(i,j+1) + S(i+1,j+1) ] ,
61  *
62  * has symbol cos(kx/2) cos(ky/2), which vanishes at the grid Nyquist mode.
63  * Inverting A exactly -- e.g. with the back substitution
64  *
65  * S(i+1,j+1) = 4 T(i,j) - S(i,j) - S(i+1,j) - S(i,j+1)
66  *
67  * -- therefore amplifies grid-scale content of T without bound. The Green's
68  * function of that recursion is 4 / [(1+x)(1+y)], a doubly alternating
69  * cumulative sum, so a checkerboard component of T of amplitude a produces a
70  * nodal response of amplitude a*i*j. On a 128^2 grid an O(10 m) grid-scale
71  * roughness in the WRF terrain thus yields nodal heights that are kilometers
72  * away from the WRF terrain, including physically impossible negative
73  * elevations (ERF issue #3709). Adding a free homogeneous solution cannot
74  * repair this: the null space of A is spanned by (-1)^i f(j) + (-1)^j g(i),
75  * which cannot represent the (-1)^(i+j) i j growth of the particular solution.
76  * The failure is silent, because checking that avg4(S) reproduces T is then
77  * satisfied by construction.
78  *
79  * ---------------------------------------------------------------------------
80  * What we do instead
81  * ---------------------------------------------------------------------------
82  * Let S_ref be the direct interpolation of T to the nodes (makeReference) and
83  * R = T - A S_ref the part of T that interpolation fails to reproduce. We
84  * solve for a *correction* E to the interpolant,
85  *
86  * min_E || A E - R ||^2 + lambda || V E ||^2 + mu || E ||^2 , S = S_ref + E,
87  *
88  * where V is a roughness operator (first derivative or Laplacian). The normal
89  * equations
90  *
91  * ( A^T A + lambda V^T V + mu I ) E = A^T R
92  *
93  * are symmetric positive definite for any mu > 0, so E exists, is unique, and
94  * -- unlike the exact inverse -- is bounded. Specifically, writing
95  * ( A^T A + lambda V^T V + mu I ) E = A^T R and pairing with E gives
96  *
97  * || A E ||^2 + lambda || V E ||^2 + mu || E ||^2 <= || A E || || R || ,
98  *
99  * and since 2 sqrt(mu) ||A E|| ||E|| <= ||A E||^2 + mu ||E||^2, we get the
100  * a priori bound
101  *
102  * || E || <= || R || / ( 2 sqrt(mu) ) .
103  *
104  * The correction is therefore controlled by how badly interpolation already
105  * fails, never by the conditioning of A.
106  *
107  * ---------------------------------------------------------------------------
108  * Choosing mu
109  * ---------------------------------------------------------------------------
110  * A single fixed mu cannot serve both smooth and rough terrain: small mu
111  * recovers well-resolved features almost exactly but lets rough fields drift
112  * far from the interpolant, and large mu does the reverse. We therefore start
113  * from a small mu and raise it until the correction satisfies the pointwise cap
114  *
115  * max |E| <= min( 2 max|R| , 1/4 (max T - min T) ) ,
116  *
117  * i.e. the nodes may not move away from the direct interpolation by more than
118  * twice the interpolation misfit, nor by more than a quarter of the relief of
119  * the data. Because ||E|| -> 0 as mu -> infinity the loop always terminates,
120  * and on failure we fall back to the interpolant itself. Smooth terrain keeps
121  * the smallest mu and is reproduced to well below the interpolation error;
122  * rough terrain is pulled back toward the interpolant instead of blowing up.
123  *
124  * The averaging error max |avg4(S) - T| returned in SolveInfo is now a genuine
125  * diagnostic -- it is no longer satisfied by construction -- and is reported
126  * alongside the same quantity for the plain interpolant and the range of S, so
127  * callers can sanity check the result.
128  */
130 {
131 public:
132  using Real = amrex::Real;
133 
134  /**
135  * @brief Construct a nodal reconstruction object for one horizontal slice.
136  *
137  * The operator that is inverted is the purely horizontal four-node average,
138  * so the only thing that matters about the input is that it carries the
139  * w-face staggering; whether the heights come from the wrfinput geopotential
140  * at a z-face or from the metgrid surface field HGT_M is irrelevant here.
141  *
142  * @param w_face_slice_box Box specifying the data region: w-face staggered,
143  * a single slab at k = 0.
144  * @param geom Geometry used for grid spacing and anisotropy weights.
145  */
146  explicit NodalReconstruction (amrex::Box const& w_face_slice_box,
147  amrex::Geometry const& geom)
148  : m_w_face_slice_box(w_face_slice_box),
149  m_ilo(w_face_slice_box.smallEnd(0)),
150  m_ihi(w_face_slice_box.bigEnd(0)),
151  m_jlo(w_face_slice_box.smallEnd(1)),
152  m_jhi(w_face_slice_box.bigEnd(1))
153  {
154  // These are amrex::Abort rather than throw: an uncaught exception under
155  // MPI is std::terminate, which tells the user nothing about what failed.
156  if (!w_face_slice_box.ok()) {
157  amrex::Abort("NodalReconstruction: w_face_slice_box is empty; the input "
158  "heights were probably never read.");
159  }
160  if (w_face_slice_box.length(2) != 1) {
161  amrex::Abort("NodalReconstruction: w_face_slice_box must contain exactly one z cell.");
162  }
163  if (w_face_slice_box.smallEnd(2) != 0) {
164  amrex::Abort("NodalReconstruction: w_face_slice_box must be a slab at k = 0; "
165  "all fabs are indexed at k = 0.");
166  }
167 
168  m_nodal_box = amrex::surroundingNodes(w_face_slice_box);
169  m_nodal_box.setSmall(2, 0);
170  m_nodal_box.setBig(2, 0);
171 
172  // Scratch for the operators applied once per CG iteration, sized once
173  // here rather than allocated inside every matrix-vector product.
174  m_x_edge_box = m_nodal_box; m_x_edge_box.growHi(0,-1);
175  m_y_edge_box = m_nodal_box; m_y_edge_box.growHi(1,-1);
177  m_interior_nodal_box.grow(0,-1);
178  m_interior_nodal_box.grow(1,-1);
179 
180  Real const dx = geom.CellSizeArray()[0];
181  Real const dy = geom.CellSizeArray()[1];
182  if (!(dx > zero) || !(dy > zero)) {
183  amrex::Abort("NodalReconstruction: cell sizes must be positive.");
184  }
185 
186  // Non-dimensional, O(1) stencil weights that respect grid anisotropy.
187  // They are unity on an isotropic grid, so the smoothing weight has the
188  // same meaning regardless of the mesh spacing.
189  Real const h = std::min(dx,dy);
190  m_gx = h / dx;
191  m_gy = h / dy;
192  m_gxx = m_gx * m_gx;
193  m_gyy = m_gy * m_gy;
194  }
195 
196  [[nodiscard]] amrex::Box const& nodalBox () const noexcept {
197  return m_nodal_box;
198  }
199 
200  /**
201  * Direct interpolation of the w-face data to the nodes: the average of the
202  * (up to four) w-faces that touch each node, with edge and corner nodes
203  * taking the average of the w-faces that exist. This is the field the
204  * solve corrects, and the field it falls back to.
205  */
206  [[nodiscard]] amrex::FArrayBox makeReference (amrex::FArrayBox const& T) const
207  {
208  /**
209  * @brief Local storage for the reference nodal field.
210  */
211  amrex::FArrayBox R(m_nodal_box,1,amrex::The_Managed_Arena());
212  auto const T_arr = T.const_array();
213  auto const R_arr = R.array();
214 
215  for (int j=m_jlo; j<=m_jhi+1; ++j) {
216  int const jm = std::max(m_jlo, std::min(j-1, m_jhi));
217  int const jp = std::max(m_jlo, std::min(j , m_jhi));
218  for (int i=m_ilo; i<=m_ihi+1; ++i) {
219  int const im = std::max(m_ilo, std::min(i-1, m_ihi));
220  int const ip = std::max(m_ilo, std::min(i , m_ihi));
221  R_arr(i,j,0) = fourth * ( T_arr(im,jm,0) + T_arr(ip,jm,0)
222  + T_arr(im,jp,0) + T_arr(ip,jp,0) );
223  }
224  }
225  return R;
226  }
227 
228  /**
229  * Solve the regularized least-squares problem described above.
230  *
231  * \param T w-face data to be matched
232  * \param reference the interpolant to correct (see makeReference)
233  * \param var_op roughness operator used as the smoothness prior
234  * \param relative_tolerance CG stopping tolerance
235  * \param smoothing lambda; weight of the roughness penalty
236  * \param min_regularization smallest mu tried; raised by factors of four
237  * until the correction respects its cap
238  * \param max_iterations CG iteration cap per attempt
239  */
240  [[nodiscard]] std::pair<amrex::FArrayBox,SolveInfo>
241  solve (amrex::FArrayBox const& T,
242  amrex::FArrayBox const& reference,
243  VariationOperator var_op,
244  Real relative_tolerance = Real(1.e-10),
245  Real smoothing = Real(1.e-2),
246  Real min_regularization = Real(2.5e-4),
247  int max_iterations = 1000)
248  {
249  if (!(smoothing > zero)) {
250  amrex::Abort("NodalReconstruction: smoothing weight must be positive.");
251  }
252  if (!(min_regularization > zero)) {
253  // With mu == 0 the operator can be singular: the roughness penalty
254  // alone does not control every mode (the Laplacian penalty, for
255  // instance, is blind to bilinear and discrete-harmonic fields).
256  amrex::Abort("NodalReconstruction: regularization must be positive.");
257  }
258 
259  m_var_op = var_op;
260  m_lambda = smoothing;
261 
262  m_w_face_scratch.resize(m_w_face_slice_box ,1,amrex::The_Managed_Arena());
263  m_nd_scratch.resize(m_nodal_box,1,amrex::The_Managed_Arena());
264  m_difx.resize(m_x_edge_box,1,amrex::The_Managed_Arena());
265  m_dify.resize(m_y_edge_box,1,amrex::The_Managed_Arena());
266  if (m_interior_nodal_box.ok()) {
267  m_lap.resize(m_interior_nodal_box,1,amrex::The_Managed_Arena());
268  }
269 
270  SolveInfo info;
271 
272  // Misfit of the plain interpolation, and the cap it earns the solve.
273  /**
274  * @brief Local storage for the interpolation misfit.
275  */
276  amrex::FArrayBox misfit(m_w_face_slice_box,1,amrex::The_Managed_Arena());
277  Real misfit_max = zero;
278  Real t_min = std::numeric_limits<Real>::max();
279  Real t_max = -std::numeric_limits<Real>::max();
280  {
281  applyA(reference,misfit);
282  auto const T_arr = T.const_array();
283  auto const m_arr = misfit.array();
284  for (int j=m_jlo; j<=m_jhi; ++j) {
285  for (int i=m_ilo; i<=m_ihi; ++i) {
286  m_arr(i,j,0) = T_arr(i,j,0) - m_arr(i,j,0);
287  misfit_max = std::max(misfit_max, std::abs(m_arr(i,j,0)));
288  t_min = std::min(t_min, T_arr(i,j,0));
289  t_max = std::max(t_max, T_arr(i,j,0));
290  }
291  }
292  }
293  info.interp_average_error = misfit_max;
294  info.deviation_cap = std::min(deviation_factor * misfit_max,
295  relief_factor * (t_max - t_min));
296 
297  // rhs = A^T (T - A S_ref); the correction solves M E = rhs from E = 0.
298  /**
299  * @brief Local storage for the right-hand side of the normal equations.
300  */
301  amrex::FArrayBox rhs(m_nodal_box,1,amrex::The_Managed_Arena());
302  applyAT(misfit,rhs);
303 
304  /**
305  * @brief Local storage for the nodal correction.
306  */
307  amrex::FArrayBox E(m_nodal_box,1,amrex::The_Managed_Arena());
308  /**
309  * @brief Local storage for the reconstructed nodal field.
310  */
311  amrex::FArrayBox S(m_nodal_box,1,amrex::The_Managed_Arena());
312 
313  m_mu = min_regularization;
314  bool accepted = false;
315  for (int attempt=0; attempt<max_attempts; ++attempt) {
316  SolveInfo attempt_info = conjugateGradient(rhs,E,relative_tolerance,max_iterations);
317  info.iterations += attempt_info.iterations;
318  info.final_residual = attempt_info.final_residual;
319  info.converged = attempt_info.converged;
320 
321  info.deviation = maxAbs(E);
322  if (info.deviation <= info.deviation_cap) { accepted = true; break; }
323 
324  m_mu *= mu_growth;
325  ++info.refinements;
326  }
327 
328  if (accepted) {
329  auto const S_arr = S.array();
330  auto const r_arr = reference.const_array();
331  auto const E_arr = E.const_array();
332  for (int j=m_jlo; j<=m_jhi+1; ++j) {
333  for (int i=m_ilo; i<=m_ihi+1; ++i) {
334  S_arr(i,j,0) = r_arr(i,j,0) + E_arr(i,j,0);
335  }
336  }
337  } else {
338  // Should not happen -- ||E|| -> 0 as mu -> infinity -- but if it
339  // does, the interpolant is a safe, bounded answer.
340  S.copy<amrex::RunOn::Host>(reference,0,0,1);
341  info.deviation = zero;
342  }
343  info.regularization = m_mu;
344 
345  computeDiagnostics(T,S,info);
346  return {std::move(S),info};
347  }
348 
349  //! max_ij | avg4(S) - T |
350  [[nodiscard]] Real maxAverageError (amrex::FArrayBox const& T,
351  amrex::FArrayBox const& S) const
352  {
353  auto const T_arr = T.const_array();
354  auto const S_arr = S.const_array();
355  Real err = zero;
356  for (int j=m_jlo; j<=m_jhi; ++j) {
357  for (int i=m_ilo; i<=m_ihi; ++i) {
358  Real const avg = fourth * ( S_arr(i ,j ,0) + S_arr(i+1,j ,0)
359  + S_arr(i ,j+1,0) + S_arr(i+1,j+1,0) );
360  err = std::max(err, std::abs(avg - T_arr(i,j,0)));
361  }
362  }
363  return err;
364  }
365 
366  [[nodiscard]] Real totalSquaredVariation (amrex::FArrayBox const& S) const
367  {
368  auto const S_arr = S.const_array();
369  Real value = zero;
370 
371  for (int j=m_jlo; j<=m_jhi+1; ++j) {
372  for (int i=m_ilo; i<=m_ihi; ++i) {
373  Real const d = S_arr(i+1,j,0) - S_arr(i,j,0);
374  value += d*d;
375  }
376  }
377  for (int j=m_jlo; j<=m_jhi; ++j) {
378  for (int i=m_ilo; i<=m_ihi+1; ++i) {
379  Real const d = S_arr(i,j+1,0) - S_arr(i,j,0);
380  value += d*d;
381  }
382  }
383  return value;
384  }
385 
386 private:
387  //! The correction may not move a node further from the direct interpolation
388  //! than this multiple of the interpolation misfit ...
389  static constexpr amrex::Real deviation_factor = amrex::Real(2.0);
390  //! ... nor than this fraction of the relief of the data.
391  static constexpr amrex::Real relief_factor = amrex::Real(0.25);
392  //! Factor by which mu is raised when the cap is violated, and the number of
393  //! times we are willing to do so.
394  static constexpr amrex::Real mu_growth = amrex::Real(4.0);
395  static constexpr int max_attempts = 24;
396 
397  amrex::Box m_w_face_slice_box;
398  amrex::Box m_nodal_box;
399  amrex::Box m_x_edge_box;
400  amrex::Box m_y_edge_box;
402  int m_ilo=0;
403  int m_ihi=-1;
404  int m_jlo=0;
405  int m_jhi=-1;
413 
414  mutable amrex::FArrayBox m_w_face_scratch;
415  mutable amrex::FArrayBox m_nd_scratch;
416  //! Scratch for the roughness operators, which are applied once per CG
417  //! iteration; sized in solve() so the iteration itself never allocates.
418  mutable amrex::FArrayBox m_difx;
419  mutable amrex::FArrayBox m_dify;
420  mutable amrex::FArrayBox m_lap;
421 
422  //! (A S)(i,j) = 1/4 [ S(i,j) + S(i+1,j) + S(i,j+1) + S(i+1,j+1) ]
423  /**
424  * @brief Apply the four-node averaging operator A.
425  * @param[in] S Nodal field to be averaged.
426  * @param[out] T Resulting w-face field.
427  */
428  void applyA (amrex::FArrayBox const& S,
429  amrex::FArrayBox& T) const
430  {
431  auto const S_arr = S.const_array();
432  auto const T_arr = T.array();
433  for (int j=m_jlo; j<=m_jhi; ++j) {
434  for (int i=m_ilo; i<=m_ihi; ++i) {
435  T_arr(i,j,0) = fourth * ( S_arr(i ,j ,0) + S_arr(i+1,j ,0)
436  + S_arr(i ,j+1,0) + S_arr(i+1,j+1,0) );
437  }
438  }
439  }
440 
441  //! Adjoint of applyA: scatter each w-face value to its four nodes.
442  /**
443  * @brief Apply the adjoint of the averaging operator A.
444  * @param[in] T W-face field to be scattered.
445  * @param[out] R Resulting nodal field.
446  */
447  void applyAT (amrex::FArrayBox const& T,
448  amrex::FArrayBox& R) const
449  {
450  R.setVal<amrex::RunOn::Host>(zero);
451  auto const T_arr = T.const_array();
452  auto const R_arr = R.array();
453  for (int j=m_jlo; j<=m_jhi; ++j) {
454  for (int i=m_ilo; i<=m_ihi; ++i) {
455  Real const q = fourth * T_arr(i,j,0);
456  R_arr(i ,j ,0) += q;
457  R_arr(i+1,j ,0) += q;
458  R_arr(i ,j+1,0) += q;
459  R_arr(i+1,j+1,0) += q;
460  }
461  }
462  }
463 
464  /**
465  * @brief Compute the first derivatives of the nodal field.
466  * @param[in] S Nodal field.
467  * @param[out] difx Resulting x-derivatives.
468  * @param[out] dify Resulting y-derivatives.
469  */
470  void applyD (amrex::FArrayBox const& S,
471  amrex::FArrayBox& difx,
472  amrex::FArrayBox& dify) const
473  {
474  difx.setVal<amrex::RunOn::Host>(zero);
475  dify.setVal<amrex::RunOn::Host>(zero);
476 
477  auto const S_arr = S.const_array();
478  auto const difx_arr = difx.array();
479  auto const dify_arr = dify.array();
480 
481  for (int j = m_jlo; j <= m_jhi + 1; ++j) {
482  for (int i = m_ilo; i <= m_ihi; ++i) {
483  difx_arr(i, j, 0) = m_gx * ( S_arr(i+1, j, 0) - S_arr(i, j, 0) );
484  }
485  }
486 
487  for (int j = m_jlo; j <= m_jhi; ++j) {
488  for (int i = m_ilo; i <= m_ihi + 1; ++i) {
489  dify_arr(i, j, 0) = m_gy * ( S_arr(i, j+1, 0) - S_arr(i, j, 0) );
490  }
491  }
492  }
493 
494  /**
495  * @brief Apply the adjoint of the derivative operator.
496  * @param[in] difx X-derivatives.
497  * @param[in] dify Y-derivatives.
498  * @param[out] R Resulting nodal field.
499  */
500  void applyDT (amrex::FArrayBox const& difx,
501  amrex::FArrayBox const& dify,
502  amrex::FArrayBox& R) const
503  {
504  R.setVal<amrex::RunOn::Host>(zero);
505 
506  auto const qx_arr = difx.const_array();
507  auto const qy_arr = dify.const_array();
508  auto const R_arr = R.array();
509 
510  for (int j = m_jlo; j <= m_jhi + 1; ++j) {
511  for (int i = m_ilo; i <= m_ihi; ++i) {
512  R_arr(i , j, 0) -= m_gx * qx_arr(i, j, 0);
513  R_arr(i+1, j, 0) += m_gx * qx_arr(i, j, 0);
514  }
515  }
516 
517  for (int j = m_jlo; j <= m_jhi; ++j) {
518  for (int i = m_ilo; i <= m_ihi + 1; ++i) {
519  R_arr(i, j , 0) -= m_gy * qy_arr(i, j, 0);
520  R_arr(i, j+1, 0) += m_gy * qy_arr(i, j, 0);
521  }
522  }
523  }
524 
525  /**
526  * @brief Apply the product of the derivative operator and its adjoint.
527  * @param[in] S Nodal field.
528  * @param[out] R Resulting nodal field.
529  */
530  void applyDTD (amrex::FArrayBox const& S,
531  amrex::FArrayBox& R) const
532  {
533  // m_difx/m_dify are sized once in solve(); this routine runs once per CG
534  // iteration, so allocating here would allocate O(iterations) times.
535  applyD(S, m_difx, m_dify);
536  applyDT(m_difx, m_dify, R);
537  }
538 
539  /**
540  * @brief Apply the Laplacian operator to a nodal field.
541  * @param[in] S Nodal field.
542  * @param[out] lap Resulting Laplacian field.
543  */
544  void applyL (amrex::FArrayBox const& S,
545  amrex::FArrayBox& lap) const
546  {
547  lap.setVal<amrex::RunOn::Host>(zero);
548 
549  auto const S_arr = S.const_array();
550  auto const lap_arr = lap.array();
551 
552  for (int j = m_jlo + 1; j <= m_jhi; ++j) {
553  for (int i = m_ilo + 1; i <= m_ihi; ++i) {
554  lap_arr(i, j, 0) = m_gxx * ( S_arr(i+1, j , 0)
555  - two * S_arr(i , j , 0)
556  + S_arr(i-1, j , 0) )
557  + m_gyy * ( S_arr(i , j+1, 0)
558  - two * S_arr(i , j , 0)
559  + S_arr(i , j-1, 0) );
560  }
561  }
562  }
563 
564  /**
565  * @brief Apply the adjoint of the Laplacian operator.
566  * @param[in] lap Laplacian field.
567  * @param[out] R Resulting nodal field.
568  */
569  void applyLT (amrex::FArrayBox const& lap,
570  amrex::FArrayBox& R) const
571  {
572  R.setVal<amrex::RunOn::Host>(zero);
573 
574  auto const q_arr = lap.const_array();
575  auto const R_arr = R.array();
576 
577  for (int j = m_jlo + 1; j <= m_jhi; ++j) {
578  for (int i = m_ilo + 1; i <= m_ihi; ++i) {
579  const amrex::Real val = q_arr(i, j, 0);
580  R_arr(i-1, j , 0) += m_gxx * val;
581  R_arr(i+1, j , 0) += m_gxx * val;
582  R_arr(i , j-1, 0) += m_gyy * val;
583  R_arr(i , j+1, 0) += m_gyy * val;
584  R_arr(i , j , 0) -= two * (m_gxx + m_gyy) * val;
585  }
586  }
587  }
588 
589  /**
590  * @brief Apply the product of the Laplacian operator and its adjoint.
591  * @param[in] S Nodal field.
592  * @param[out] R Resulting nodal field.
593  */
594  void applyLTL (amrex::FArrayBox const& S,
595  amrex::FArrayBox& R) const
596  {
597  if (!m_interior_nodal_box.ok()) {
598  // Fewer than two cells in a direction: there is no interior node to
599  // evaluate the Laplacian at. mu keeps the operator definite.
600  R.setVal<amrex::RunOn::Host>(zero);
601  return;
602  }
603  // m_lap is sized once in solve(); see the note in applyDTD.
604  applyL(S, m_lap);
605  applyLT(m_lap, R);
606  }
607 
608  /**
609  * @brief Apply the selected variation operator (first derivative or Laplacian).
610  * @param[in] S Nodal field.
611  * @param[out] R Resulting nodal field.
612  */
613  void applyVariationOperator (amrex::FArrayBox const& S,
614  amrex::FArrayBox& R) const
615  {
616  switch (m_var_op) {
618  applyDTD(S, R);
619  break;
620 
622  applyLTL(S, R);
623  break;
624 
625  default:
626  amrex::Abort("NodalReconstruction: unsupported variation operator.");
627  }
628  }
629 
630  //! R = ( A^T A + lambda V^T V + mu I ) S
631  /**
632  * @brief Apply the full system operator M = A^T A + lambda V^T V + mu I.
633  * @param[in] S Nodal field.
634  * @param[out] R Resulting nodal field.
635  */
636  void applyM (amrex::FArrayBox const& S,
637  amrex::FArrayBox& R) const
638  {
642 
643  auto const R_arr = R.array();
644  auto const v_arr = m_nd_scratch.const_array();
645  auto const S_arr = S.const_array();
646  for (int j=m_jlo; j<=m_jhi+1; ++j) {
647  for (int i=m_ilo; i<=m_ihi+1; ++i) {
648  R_arr(i,j,0) += m_lambda * v_arr(i,j,0) + m_mu * S_arr(i,j,0);
649  }
650  }
651  }
652 
653  /**
654  * Diagonal of M, assembled by accumulating the square of every stencil
655  * coefficient that touches a node. Used as a Jacobi preconditioner.
656  */
657  void computeDiagonal (amrex::FArrayBox& diag) const
658  {
659  diag.setVal<amrex::RunOn::Host>(m_mu);
660  auto const d_arr = diag.array();
661 
662  // A^T A: each incident cell contributes (1/4)^2
663  Real const qa = fourth * fourth;
664  for (int j=m_jlo; j<=m_jhi; ++j) {
665  for (int i=m_ilo; i<=m_ihi; ++i) {
666  d_arr(i ,j ,0) += qa;
667  d_arr(i+1,j ,0) += qa;
668  d_arr(i ,j+1,0) += qa;
669  d_arr(i+1,j+1,0) += qa;
670  }
671  }
672 
673  // lambda V^T V
675  Real const qx = m_lambda * m_gx * m_gx;
676  Real const qy = m_lambda * m_gy * m_gy;
677  for (int j=m_jlo; j<=m_jhi+1; ++j) {
678  for (int i=m_ilo; i<=m_ihi; ++i) {
679  d_arr(i ,j,0) += qx;
680  d_arr(i+1,j,0) += qx;
681  }
682  }
683  for (int j=m_jlo; j<=m_jhi; ++j) {
684  for (int i=m_ilo; i<=m_ihi+1; ++i) {
685  d_arr(i,j ,0) += qy;
686  d_arr(i,j+1,0) += qy;
687  }
688  }
689  } else {
690  Real const qx = m_lambda * m_gxx * m_gxx;
691  Real const qy = m_lambda * m_gyy * m_gyy;
692  Real const qc = m_lambda * four * (m_gxx + m_gyy) * (m_gxx + m_gyy);
693  for (int j=m_jlo+1; j<=m_jhi; ++j) {
694  for (int i=m_ilo+1; i<=m_ihi; ++i) {
695  d_arr(i-1,j ,0) += qx;
696  d_arr(i+1,j ,0) += qx;
697  d_arr(i ,j-1,0) += qy;
698  d_arr(i ,j+1,0) += qy;
699  d_arr(i ,j ,0) += qc;
700  }
701  }
702  }
703  }
704 
705  /**
706  * @brief Compute diagnostic metrics for the reconstructed nodal field.
707  * @param[in] T W-face data.
708  * @param[in] S Reconstructed nodal field.
709  * @param[in,out] info Structure to store diagnostics.
710  */
711  void computeDiagnostics (amrex::FArrayBox const& T,
712  amrex::FArrayBox const& S,
713  SolveInfo& info) const
714  {
715  auto const S_arr = S.const_array();
716  Real smin = std::numeric_limits<Real>::max();
717  Real smax = -std::numeric_limits<Real>::max();
718  for (int j=m_jlo; j<=m_jhi+1; ++j) {
719  for (int i=m_ilo; i<=m_ihi+1; ++i) {
720  smin = std::min(smin, S_arr(i,j,0));
721  smax = std::max(smax, S_arr(i,j,0));
722  }
723  }
724  info.min_value = smin;
725  info.max_value = smax;
727  }
728 
729  [[nodiscard]] Real maxAbs (amrex::FArrayBox const& F) const
730  {
731  auto const F_arr = F.const_array();
732  Real v = zero;
733  for (int j=m_jlo; j<=m_jhi+1; ++j) {
734  for (int i=m_ilo; i<=m_ihi+1; ++i) {
735  v = std::max(v, std::abs(F_arr(i,j,0)));
736  }
737  }
738  return v;
739  }
740 
741  //! Jacobi-preconditioned conjugate gradient on the SPD operator M.
742  //! x is both the initial guess and the result; it is reset to zero here
743  //! because every attempt restarts the correction from the interpolant.
744  [[nodiscard]] SolveInfo conjugateGradient (amrex::FArrayBox const& rhs,
745  amrex::FArrayBox& x,
746  Real relative_tolerance,
747  int max_iterations) const
748  {
749  /**
750  * @brief Local storage for the operator diagonal.
751  */
752  amrex::FArrayBox diag(m_nodal_box,1,amrex::The_Managed_Arena());
753  computeDiagonal(diag);
754 
755  /**
756  * @brief Local storage for the residual.
757  */
758  amrex::FArrayBox r (m_nodal_box,1,amrex::The_Managed_Arena());
759  /**
760  * @brief Local storage for the preconditioned residual.
761  */
762  amrex::FArrayBox z (m_nodal_box,1,amrex::The_Managed_Arena());
763  /**
764  * @brief Local storage for the search direction.
765  */
766  amrex::FArrayBox p (m_nodal_box,1,amrex::The_Managed_Arena());
767  /**
768  * @brief Local storage for the product of the operator and the search direction.
769  */
770  amrex::FArrayBox Mp(m_nodal_box,1,amrex::The_Managed_Arena());
771 
772  x.setVal<amrex::RunOn::Host>(zero);
773  r.copy<amrex::RunOn::Host>(rhs,0,0,1); // r = rhs - M*0
774  applyJacobi(diag,r,z);
775  p.copy<amrex::RunOn::Host>(z,0,0,1);
776 
777  Real rz = dot(r,z);
778  Real const initial = std::sqrt(dot(r,r));
779  Real const tol = relative_tolerance * std::max({initial,one});
780 
781  SolveInfo info;
782  info.final_residual = initial;
783  if (initial <= tol) {
784  info.converged = true;
785  return info;
786  }
787 
788  for (int it=0; it<max_iterations; ++it) {
789  applyM(p,Mp);
790  Real const pMp = dot(p,Mp);
791  if (!(pMp > zero) || !std::isfinite(pMp)) {
792  amrex::Abort("NodalReconstruction: CG curvature failure.");
793  }
794 
795  Real const alpha = rz/pMp;
796  {
797  auto const x_arr = x.array();
798  auto const r_arr = r.array();
799  auto const p_arr = p.const_array();
800  auto const Mp_arr = Mp.const_array();
801  for (int j=m_jlo; j<=m_jhi+1; ++j) {
802  for (int i=m_ilo; i<=m_ihi+1; ++i) {
803  x_arr(i,j,0) += alpha * p_arr(i,j,0);
804  r_arr(i,j,0) -= alpha * Mp_arr(i,j,0);
805  }
806  }
807  }
808 
809  info.iterations = it+1;
810  info.final_residual = std::sqrt(dot(r,r));
811  if (info.final_residual <= tol) {
812  info.converged = true;
813  return info;
814  }
815 
816  applyJacobi(diag,r,z);
817  Real const rz_new = dot(r,z);
818  Real const beta = rz_new/rz;
819  {
820  auto const p_arr = p.array();
821  auto const z_arr = z.const_array();
822  for (int j=m_jlo; j<=m_jhi+1; ++j) {
823  for (int i=m_ilo; i<=m_ihi+1; ++i) {
824  p_arr(i,j,0) = z_arr(i,j,0) + beta * p_arr(i,j,0);
825  }
826  }
827  }
828  rz = rz_new;
829  }
830  return info;
831  }
832 
833  /**
834  * @brief Apply the Jacobi preconditioner.
835  * @param[in] diag Operator diagonal.
836  * @param[in] r Residual field.
837  * @param[out] z Preconditioned residual field.
838  */
839  void applyJacobi (amrex::FArrayBox const& diag,
840  amrex::FArrayBox const& r,
841  amrex::FArrayBox& z) const
842  {
843  auto const d_arr = diag.const_array();
844  auto const r_arr = r.const_array();
845  auto const z_arr = z.array();
846  for (int j=m_jlo; j<=m_jhi+1; ++j) {
847  for (int i=m_ilo; i<=m_ihi+1; ++i) {
848  z_arr(i,j,0) = r_arr(i,j,0) / d_arr(i,j,0);
849  }
850  }
851  }
852 
853  [[nodiscard]] Real dot (amrex::FArrayBox const& a,
854  amrex::FArrayBox const& b) const
855  {
856  auto const a_arr = a.const_array();
857  auto const b_arr = b.const_array();
858  Real v = zero;
859  for (int j=m_jlo; j<=m_jhi+1; ++j) {
860  for (int i=m_ilo; i<=m_ihi+1; ++i) {
861  v += a_arr(i,j,0) * b_arr(i,j,0);
862  }
863  }
864  return v;
865  }
866 };
867 
868 /**
869  * Exactly de-average one line.
870  *
871  * Given cell values t(0..n-1), produce node values s(0..n) satisfying
872  *
873  * 1/2 ( s(k) + s(k+1) ) = t(k), k = 0 .. n-1
874  *
875  * exactly. The system is underdetermined by exactly one degree of freedom: the
876  * recursion s(k+1) = 2 t(k) - s(k) solves it for any seed, and changing the seed
877  * adds the sawtooth (-1)^k, which the averaging operator annihilates. So the
878  * seed is free, and it is chosen here in closed form to minimize the
879  * first-difference energy sum_k ( s(k+1) - s(k) )^2 along the line, i.e. to leave
880  * as little of that sawtooth in the answer as the constraint permits.
881  *
882  * \param[in] t cell values, length n
883  * \param[in] n number of cells
884  * \param[out] s node values, length n+1
885  */
886 inline void
888 {
889  using amrex::Real;
890 
891  // Particular solution, seeded at zero.
892  s[0] = zero;
893  for (int k(0); k<n; ++k) { s[k+1] = two*t[k] - s[k]; }
894 
895  // With s(k) = (-1)^k s0 + p(k) we have s(k+1) - s(k) = -2 (-1)^k s0 + dp(k),
896  // and minimizing the sum of squares of that over s0 gives the closed form
897  // s0 = sum_k (-1)^k dp(k) / (2n).
898  Real acc(zero), sgn(one);
899  for (int k(0); k<n; ++k) { acc += sgn * (s[k+1] - s[k]); sgn = -sgn; }
900  Real const s0 = acc / (two * static_cast<Real>(n));
901 
902  sgn = one;
903  for (int k(0); k<=n; ++k) { s[k] += sgn * s0; sgn = -sgn; }
904 }
905 
906 /**
907  * Exactly de-average one horizontal slice: find nodal S with avg4(S) = T to
908  * round-off, rather than the closest such field a smoothness prior will allow.
909  *
910  * The four-node averaging operator is a tensor product of two one-dimensional
911  * two-point averages, A = Ax (x) Ay. Each of those is surjective -- the line
912  * recursion above always solves -- so A is surjective and an exact solution
913  * always exists; the system is underdetermined, not inconsistent, with
914  * (nx+1)(ny+1) unknowns against nx*ny equations. Separability means it can be
915  * solved in two sweeps: first Ay U = T for U (cell-centered in x, nodal in y),
916  * then Ax S = U. Both are O(N) and non-iterative.
917  *
918  * What cannot be removed is grid-scale structure. The null space is spanned by
919  * (-1)^i a(j) + (-1)^j b(i), in which a does not vary with i, so the amplitude of
920  * the sawtooth cannot be varied along a line; rough terrain anywhere on a line
921  * leaves a signature everywhere on it. Minimizing the first-difference energy
922  * line by line, as deaverage_line does, exhausts that freedom -- a globally
923  * coupled optimization over the whole null space was measured to give no further
924  * improvement. The caller is therefore responsible for deciding whether the
925  * result is usable as terrain, and for falling back if it is not.
926  *
927  * \param[in] w_face_box box of the w-face slice
928  * \param[in] z_w_face w-face heights on \p w_face_box
929  * \param[in] z_ref direct interpolation of \p z_w_face to the nodes, for diagnostics only
930  * \return the nodal heights and the diagnostics describing them
931  */
932 inline std::pair<amrex::FArrayBox,SolveInfo>
933 deaverage_nodal_slice (amrex::Box const& w_face_box,
934  amrex::FArrayBox const& z_w_face,
935  amrex::FArrayBox const& z_ref)
936 {
937  using amrex::Real;
938 
939  amrex::Box nd_box = amrex::surroundingNodes(w_face_box);
940  nd_box.setSmall(2,0);
941  nd_box.setBig(2,0);
942 
943  amrex::FArrayBox S(nd_box, 1, amrex::The_Managed_Arena());
944 
945  int const ilo = w_face_box.smallEnd(0), ihi = w_face_box.bigEnd(0);
946  int const jlo = w_face_box.smallEnd(1), jhi = w_face_box.bigEnd(1);
947  int const nx = ihi - ilo + 1;
948  int const ny = jhi - jlo + 1;
949 
950  auto const T_arr = z_w_face.const_array();
951  auto const R_arr = z_ref.const_array();
952  auto const S_arr = S.array();
953 
954  // U is the x-average of S, so it is cell-centered in x and nodal in y.
955  std::vector<Real> U(static_cast<std::size_t>(nx) * static_cast<std::size_t>(ny+1));
956  std::vector<Real> tline(static_cast<std::size_t>(std::max(nx,ny)));
957  std::vector<Real> sline(static_cast<std::size_t>(std::max(nx,ny)+1));
958 
959  // Sweep 1: Ay U = T, one line in y per x-cell.
960  for (int i(0); i<nx; ++i) {
961  for (int j(0); j<ny; ++j) { tline[static_cast<std::size_t>(j)] = T_arr(ilo+i,jlo+j,0); }
962  deaverage_line(tline.data(), ny, sline.data());
963  for (int j(0); j<=ny; ++j) {
964  U[static_cast<std::size_t>(i)*static_cast<std::size_t>(ny+1) +
965  static_cast<std::size_t>(j)] = sline[static_cast<std::size_t>(j)];
966  }
967  }
968 
969  // Sweep 2: Ax S = U, one line in x per y-node.
970  for (int j(0); j<=ny; ++j) {
971  for (int i(0); i<nx; ++i) {
972  tline[static_cast<std::size_t>(i)] =
973  U[static_cast<std::size_t>(i)*static_cast<std::size_t>(ny+1) +
974  static_cast<std::size_t>(j)];
975  }
976  deaverage_line(tline.data(), nx, sline.data());
977  for (int i(0); i<=nx; ++i) { S_arr(ilo+i,jlo+j,0) = sline[static_cast<std::size_t>(i)]; }
978  }
979 
980  SolveInfo info;
981  info.converged = true;
982  info.iterations = 0;
983  info.refinements = 0;
984  info.regularization = zero;
985  info.deviation_cap = zero;
986  info.min_value = std::numeric_limits<Real>::max();
987  info.max_value = -std::numeric_limits<Real>::max();
988 
989  for (int j(jlo); j<=jhi+1; ++j) {
990  for (int i(ilo); i<=ihi+1; ++i) {
991  Real const v = S_arr(i,j,0);
992  info.min_value = std::min(info.min_value, v);
993  info.max_value = std::max(info.max_value, v);
994  info.deviation = std::max(info.deviation, std::abs(v - R_arr(i,j,0)));
995  }
996  }
997  for (int j(jlo); j<=jhi; ++j) {
998  for (int i(ilo); i<=ihi; ++i) {
999  Real const a4 = fourth * ( S_arr(i,j,0) + S_arr(i+1,j,0)
1000  + S_arr(i,j+1,0) + S_arr(i+1,j+1,0) );
1001  Real const r4 = fourth * ( R_arr(i,j,0) + R_arr(i+1,j,0)
1002  + R_arr(i,j+1,0) + R_arr(i+1,j+1,0) );
1003  info.max_average_error = std::max(info.max_average_error,
1004  std::abs(a4 - T_arr(i,j,0)));
1005  info.interp_average_error = std::max(info.interp_average_error,
1006  std::abs(r4 - T_arr(i,j,0)));
1007  }
1008  }
1009  info.final_residual = info.max_average_error;
1010 
1011  return {std::move(S), info};
1012 }
1013 
1014 /**
1015  * Reconstruct one horizontal slice of nodal heights from heights that live at
1016  * w-faces, reporting diagnostics and aborting if the result is unusable as
1017  * terrain.
1018  *
1019  * Both NetCDF initialization paths need exactly this operation and differ only
1020  * in how the w-face slice is assembled (geopotential from PH + PHB for
1021  * wrfinput, HGT_M for metgrid) and in what they do with the answer, so the
1022  * solve and its checks live here and are called from both.
1023  *
1024  * The solve is serial and covers the whole domain, so it runs on the I/O rank
1025  * only and the resulting nodal slice is broadcast. Running it redundantly on
1026  * every rank would cost a full-domain CG solve and several full-domain managed
1027  * allocations *per rank*, i.e. a startup stall and a memory spike that grow with
1028  * the rank count rather than with the decomposition. \p z_w_face_slice must be
1029  * host-accessible and must hold the *global* slice on the I/O rank.
1030  *
1031  * This is collective: every rank must call it.
1032  *
1033  * @param[in] w_face_slice_box Box covering the domain at the w-face staggering, a single k = 0 slab.
1034  * @param[in] geom Geometry of the level, used for the grid spacing.
1035  * @param[in] z_w_face_slice Heights on \p w_face_slice_box.
1036  * @param[in] klev Vertical index of the slice; used only in messages.
1037  * @param[in] src_name Name of the source data; used only in messages.
1038  * @return The nodal heights on surroundingNodes(w_face_slice_box), indexed at k = 0.
1039  */
1040 inline amrex::FArrayBox
1041 reconstruct_nodal_height_slice (amrex::Box const& w_face_slice_box,
1042  amrex::Geometry const& geom,
1043  amrex::FArrayBox const& z_w_face_slice,
1044  int klev,
1045  std::string const& src_name)
1046 {
1047  // Fail here, with a message naming the data, rather than deep inside the
1048  // solver: an empty box means the source heights were never read, and the
1049  // degenerate loop bounds it produces would otherwise read out of bounds.
1050  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(w_face_slice_box.ok() && w_face_slice_box.length(2) == 1,
1051  "reconstruct_nodal_height_slice: the input slice is empty "
1052  "or spans more than one z level -- check that the source "
1053  "heights were read successfully.");
1054 
1055  // The slice is typically filled by a device kernel, and everything below
1056  // reads it from the host.
1057  amrex::Gpu::streamSynchronize();
1058 
1059  amrex::Box nd_box = amrex::surroundingNodes(w_face_slice_box);
1060  nd_box.setSmall(2, 0);
1061  nd_box.setBig(2, 0);
1062 
1063  // Broadcast target; also the return value, so it must be device-readable.
1064  amrex::FArrayBox z_nd_slice(nd_box, 1, amrex::The_Managed_Arena());
1065 
1066  int const ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
1067 
1068  // Non-zero if the reconstruction is unusable; decided on the I/O rank and
1069  // broadcast so that every rank aborts together rather than one rank calling
1070  // MPI_Abort out from under the others.
1071  int bad_reconstruction = 0;
1072 
1073  if (amrex::ParallelDescriptor::IOProcessor())
1074  {
1075  // Range of the source data, used to decide whether a reconstruction is
1076  // usable as terrain. Nodes may legitimately over/undershoot the w-face
1077  // values where the terrain is under-resolved, but only by a fraction of the
1078  // relief of the layer itself.
1079  amrex::Real src_min = std::numeric_limits<amrex::Real>::max();
1080  amrex::Real src_max = -std::numeric_limits<amrex::Real>::max();
1081  {
1082  auto const src_arr = z_w_face_slice.const_array();
1083  amrex::LoopOnCpu(w_face_slice_box, [=,&src_min,&src_max] (int i, int j, int /*k*/) noexcept
1084  {
1085  src_min = std::min(src_min, src_arr(i,j,0));
1086  src_max = std::max(src_max, src_arr(i,j,0));
1087  });
1088  }
1089  amrex::Real const relief = std::max(src_max - src_min, amrex::Real(1.0));
1090  amrex::Real const slack = std::max(amrex::Real(0.5) * relief, amrex::Real(10.0));
1091 
1092  auto in_range = [=] (SolveInfo const& si) noexcept -> bool
1093  {
1094  return std::isfinite(si.min_value) && std::isfinite(si.max_value) &&
1095  (si.min_value >= src_min - slack) && (si.max_value <= src_max + slack);
1096  };
1097 
1098  NodalReconstruction NR_solver(w_face_slice_box, geom);
1099  amrex::FArrayBox z_ref = NR_solver.makeReference(z_w_face_slice);
1100 
1101  // Invert the four-node averaging operator exactly. The operator is
1102  // surjective, so this is always possible; what it costs is grid-scale
1103  // structure in the nodal field, because the sawtooth it cannot control is
1104  // the operator's own null space. See deaverage_nodal_slice.
1105  //
1106  // Exactness is what the callers actually need. Each w-face height is the
1107  // average of the four nodes around it, and each cell-centered height is in
1108  // turn the average of the two w-faces bracketing it, so avg4(S) = T makes
1109  // both of them -- and hence what the vertical remap of the atmospheric state
1110  // searches against, and what the lateral pressure-gradient metric
1111  // differences -- agree with the source to round-off rather than to within a
1112  // regularization-dependent error.
1113  std::pair<amrex::FArrayBox,SolveInfo> result = deaverage_nodal_slice(w_face_slice_box,
1114  z_w_face_slice, z_ref);
1115  SolveInfo info = result.second;
1116  bool const exact_usable = in_range(info);
1117 
1118  if (exact_usable) {
1119  amrex::Print() << "Nodal reconstruction at k = " << klev << ": exact de-averaging"
1120  << "\n nodal heights in [" << info.min_value << ", " << info.max_value
1121  << "] m vs " << src_name << " in [" << src_min << ", " << src_max << "] m"
1122  << "\n max |avg4(nodal) - " << src_name << "| = " << info.max_average_error
1123  << " m (direct interpolation gives " << info.interp_average_error << " m)"
1124  << "\n max deviation from direct interpolation = " << info.deviation
1125  << " m" << std::endl;
1126  } else {
1127  // Rough enough terrain can force an exact solution whose nodal heights are
1128  // nowhere near the source. Fall back to the regularized least-squares fit,
1129  // which stays bounded but no longer reproduces the source exactly, and say so.
1130  amrex::Print() << "WARNING: exact de-averaging at k = " << klev
1131  << " gives nodal heights in [" << info.min_value << ", " << info.max_value
1132  << "] m, outside the range of the " << src_name << " ["
1133  << src_min << ", " << src_max << "] m.\n"
1134  << " Falling back to the regularized least-squares fit, which "
1135  "does NOT reproduce the " << src_name << " exactly; the w-face "
1136  "heights will differ from the source by the error reported below.\n";
1137 
1138  amrex::Real const tol = amrex::Real(1.e-10);
1139  result = NR_solver.solve(z_w_face_slice, z_ref, VariationOperator::FirstDeriv, tol);
1140  info = result.second;
1141 
1142  if (!info.converged) {
1143  amrex::Print() << "WARNING: Nodal reconstruction did not converge at k = " << klev
1144  << "; residual is: " << info.final_residual
1145  << " and requested tolerance was: " << tol << "\n";
1146  }
1147 
1148  amrex::Print() << "Nodal reconstruction at k = " << klev << ": "
1149  << info.iterations << " CG iterations, " << info.refinements
1150  << " refinements, regularization " << info.regularization
1151  << "\n nodal heights in [" << info.min_value << ", " << info.max_value
1152  << "] m vs " << src_name << " in [" << src_min << ", " << src_max << "] m"
1153  << "\n max |avg4(nodal) - " << src_name << "| = " << info.max_average_error
1154  << " m (direct interpolation gives " << info.interp_average_error << " m)"
1155  << "\n max deviation from direct interpolation = " << info.deviation
1156  << " m (cap " << info.deviation_cap << " m)" << std::endl;
1157 
1158  if (!in_range(info)) { bad_reconstruction = 1; }
1159  }
1160 
1161  z_nd_slice.copy<amrex::RunOn::Host>(result.first, nd_box, 0, nd_box, 0, 1);
1162  }
1163 
1164  amrex::ParallelDescriptor::Bcast(&bad_reconstruction, 1, ioproc);
1165  if (bad_reconstruction) {
1166  amrex::Abort("Nodal reconstruction produced heights far outside the range of the "
1167  + src_name + "; the reconstruction is not usable as terrain.");
1168  }
1169 
1170  amrex::ParallelDescriptor::Bcast(z_nd_slice.dataPtr(), z_nd_slice.size(), ioproc);
1171 
1172  return z_nd_slice;
1173 }
1174 
1175 /**
1176  * Copy one reconstructed nodal slice into level \p klev of a nodal MultiFab,
1177  * filling the lateral ghost nodes by clamping to the nearest valid node.
1178  *
1179  * @param[in,out] z_phys_nd Nodal MultiFab of physical heights.
1180  * @param[in] klev Vertical node index to fill.
1181  * @param[in] z_nd_slice Nodal slice, indexed at k = 0, spanning the whole domain in (x,y).
1182  */
1183 inline void
1184 fill_nodal_level_from_slice (amrex::MultiFab& z_phys_nd,
1185  int klev,
1186  amrex::FArrayBox const& z_nd_slice)
1187 {
1188  amrex::Box const& nd_box = z_nd_slice.box();
1189  int const ilo = nd_box.smallEnd(0); int const ihi = nd_box.bigEnd(0);
1190  int const jlo = nd_box.smallEnd(1); int const jhi = nd_box.bigEnd(1);
1191 
1192  auto const z_slice_arr = z_nd_slice.const_array();
1193 
1194  for ( amrex::MFIter mfi(z_phys_nd); mfi.isValid(); ++mfi ) {
1195  amrex::Box gbx = mfi.growntilebox();
1196 
1197  // Boxes that do not reach this level get their surface data from the
1198  // ParallelCopy in make_terrain_fitted_coords
1199  if (klev < gbx.smallEnd(2) || klev > gbx.bigEnd(2)) { continue; }
1200 
1201  amrex::Box sbx = amrex::makeSlab(gbx, 2, klev);
1202  auto const z_arr = z_phys_nd.array(mfi);
1203  amrex::ParallelFor(sbx, [=] AMREX_GPU_DEVICE (int i, int j, int k) noexcept
1204  {
1205  int const ii = amrex::max(amrex::min(i,ihi),ilo);
1206  int const jj = amrex::max(amrex::min(j,jhi),jlo);
1207  z_arr(i,j,k) = z_slice_arr(ii,jj,0);
1208  });
1209  }
1210 
1211  // The kernels above read z_nd_slice, which every caller destroys as soon as
1212  // this returns. Freeing it while the launch is still queued would hand its
1213  // bytes back to the arena for reuse under a running kernel.
1214  amrex::Gpu::streamSynchronize();
1215 }
1216 
1217 #endif
const int nx
Definition: ERF_InitCustomPertVels_CloudChamber.H:14
const int ny
Definition: ERF_InitCustomPertVels_CloudChamber.H:15
const Real dy
Definition: ERF_InitCustomPert_ABL.H:45
const Real dx
Definition: ERF_InitCustomPert_ABL.H:44
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
ParallelFor(fab_box, [=] AMREX_GPU_DEVICE(int i, int j, int k) { qrcuten_arr(i, j, k)=Real(0);qscuten_arr(i, j, k)=Real(0);qicuten_arr(i, j, k)=Real(0);})
void deaverage_line(amrex::Real const *t, int n, amrex::Real *s)
Definition: ERF_NodalReconstruction.H:887
VariationOperator
Definition: ERF_NodalReconstruction.H:26
std::pair< amrex::FArrayBox, SolveInfo > deaverage_nodal_slice(amrex::Box const &w_face_box, amrex::FArrayBox const &z_w_face, amrex::FArrayBox const &z_ref)
Definition: ERF_NodalReconstruction.H:933
amrex::FArrayBox reconstruct_nodal_height_slice(amrex::Box const &w_face_slice_box, amrex::Geometry const &geom, amrex::FArrayBox const &z_w_face_slice, int klev, std::string const &src_name)
Definition: ERF_NodalReconstruction.H:1041
void fill_nodal_level_from_slice(amrex::MultiFab &z_phys_nd, int klev, amrex::FArrayBox const &z_nd_slice)
Definition: ERF_NodalReconstruction.H:1184
Dimensionless numeric literals and pure mathematical constants.
constexpr amrex::Real four
Definition: ERF_NumericalConstants.H:40
constexpr amrex::Real two
Definition: ERF_NumericalConstants.H:38
constexpr amrex::Real one
Definition: ERF_NumericalConstants.H:37
constexpr amrex::Real fourth
Definition: ERF_NumericalConstants.H:42
constexpr amrex::Real zero
Definition: ERF_NumericalConstants.H:36
amrex::Real Real
Definition: ERF_ShocInterface.H:19
Definition: ERF_NodalReconstruction.H:130
amrex::Real m_gyy
Definition: ERF_NodalReconstruction.H:409
amrex::Box m_x_edge_box
Definition: ERF_NodalReconstruction.H:399
void applyLTL(amrex::FArrayBox const &S, amrex::FArrayBox &R) const
Apply the product of the Laplacian operator and its adjoint.
Definition: ERF_NodalReconstruction.H:594
void applyDT(amrex::FArrayBox const &difx, amrex::FArrayBox const &dify, amrex::FArrayBox &R) const
Apply the adjoint of the derivative operator.
Definition: ERF_NodalReconstruction.H:500
SolveInfo conjugateGradient(amrex::FArrayBox const &rhs, amrex::FArrayBox &x, Real relative_tolerance, int max_iterations) const
Definition: ERF_NodalReconstruction.H:744
amrex::Real m_lambda
Definition: ERF_NodalReconstruction.H:410
int m_ihi
Definition: ERF_NodalReconstruction.H:403
void applyAT(amrex::FArrayBox const &T, amrex::FArrayBox &R) const
Adjoint of applyA: scatter each w-face value to its four nodes.
Definition: ERF_NodalReconstruction.H:447
Real maxAverageError(amrex::FArrayBox const &T, amrex::FArrayBox const &S) const
max_ij | avg4(S) - T |
Definition: ERF_NodalReconstruction.H:350
amrex::Box const & nodalBox() const noexcept
Definition: ERF_NodalReconstruction.H:196
VariationOperator m_var_op
Definition: ERF_NodalReconstruction.H:412
void applyJacobi(amrex::FArrayBox const &diag, amrex::FArrayBox const &r, amrex::FArrayBox &z) const
Apply the Jacobi preconditioner.
Definition: ERF_NodalReconstruction.H:839
int m_ilo
Definition: ERF_NodalReconstruction.H:402
void computeDiagonal(amrex::FArrayBox &diag) const
Definition: ERF_NodalReconstruction.H:657
Real totalSquaredVariation(amrex::FArrayBox const &S) const
Definition: ERF_NodalReconstruction.H:366
amrex::FArrayBox makeReference(amrex::FArrayBox const &T) const
Definition: ERF_NodalReconstruction.H:206
void applyM(amrex::FArrayBox const &S, amrex::FArrayBox &R) const
R = ( A^T A + lambda V^T V + mu I ) S.
Definition: ERF_NodalReconstruction.H:636
void applyA(amrex::FArrayBox const &S, amrex::FArrayBox &T) const
(A S)(i,j) = 1/4 [ S(i,j) + S(i+1,j) + S(i,j+1) + S(i+1,j+1) ]
Definition: ERF_NodalReconstruction.H:428
NodalReconstruction(amrex::Box const &w_face_slice_box, amrex::Geometry const &geom)
Construct a nodal reconstruction object for one horizontal slice.
Definition: ERF_NodalReconstruction.H:146
static constexpr amrex::Real relief_factor
... nor than this fraction of the relief of the data.
Definition: ERF_NodalReconstruction.H:391
void applyD(amrex::FArrayBox const &S, amrex::FArrayBox &difx, amrex::FArrayBox &dify) const
Compute the first derivatives of the nodal field.
Definition: ERF_NodalReconstruction.H:470
amrex::Real m_gxx
Definition: ERF_NodalReconstruction.H:408
amrex::FArrayBox m_nd_scratch
Definition: ERF_NodalReconstruction.H:415
amrex::FArrayBox m_w_face_scratch
Definition: ERF_NodalReconstruction.H:414
amrex::Box m_y_edge_box
Definition: ERF_NodalReconstruction.H:400
Real maxAbs(amrex::FArrayBox const &F) const
Definition: ERF_NodalReconstruction.H:729
void applyLT(amrex::FArrayBox const &lap, amrex::FArrayBox &R) const
Apply the adjoint of the Laplacian operator.
Definition: ERF_NodalReconstruction.H:569
static constexpr amrex::Real mu_growth
Definition: ERF_NodalReconstruction.H:394
amrex::Box m_nodal_box
Definition: ERF_NodalReconstruction.H:398
std::pair< amrex::FArrayBox, SolveInfo > solve(amrex::FArrayBox const &T, amrex::FArrayBox const &reference, VariationOperator var_op, Real relative_tolerance=Real(1.e-10), Real smoothing=Real(1.e-2), Real min_regularization=Real(2.5e-4), int max_iterations=1000)
Definition: ERF_NodalReconstruction.H:241
amrex::Box m_interior_nodal_box
Definition: ERF_NodalReconstruction.H:401
amrex::FArrayBox m_difx
Definition: ERF_NodalReconstruction.H:418
amrex::Real m_mu
Definition: ERF_NodalReconstruction.H:411
int m_jlo
Definition: ERF_NodalReconstruction.H:404
static constexpr amrex::Real deviation_factor
Definition: ERF_NodalReconstruction.H:389
void applyVariationOperator(amrex::FArrayBox const &S, amrex::FArrayBox &R) const
Apply the selected variation operator (first derivative or Laplacian).
Definition: ERF_NodalReconstruction.H:613
void computeDiagnostics(amrex::FArrayBox const &T, amrex::FArrayBox const &S, SolveInfo &info) const
Compute diagnostic metrics for the reconstructed nodal field.
Definition: ERF_NodalReconstruction.H:711
amrex::Real m_gx
Definition: ERF_NodalReconstruction.H:406
amrex::Box m_w_face_slice_box
Definition: ERF_NodalReconstruction.H:397
Real dot(amrex::FArrayBox const &a, amrex::FArrayBox const &b) const
Definition: ERF_NodalReconstruction.H:853
amrex::Real m_gy
Definition: ERF_NodalReconstruction.H:407
void applyL(amrex::FArrayBox const &S, amrex::FArrayBox &lap) const
Apply the Laplacian operator to a nodal field.
Definition: ERF_NodalReconstruction.H:544
amrex::Real Real
Definition: ERF_NodalReconstruction.H:132
amrex::FArrayBox m_dify
Definition: ERF_NodalReconstruction.H:419
int m_jhi
Definition: ERF_NodalReconstruction.H:405
void applyDTD(amrex::FArrayBox const &S, amrex::FArrayBox &R) const
Apply the product of the derivative operator and its adjoint.
Definition: ERF_NodalReconstruction.H:530
static constexpr int max_attempts
Definition: ERF_NodalReconstruction.H:395
amrex::FArrayBox m_lap
Definition: ERF_NodalReconstruction.H:420
@ qc
Definition: ERF_SatAdj.H:42
@ R
Definition: ERF_IndexDefines.H:133
@ U
Definition: ERF_IndexDefines.H:129
@ T
Definition: ERF_IndexDefines.H:131
@ beta
Definition: ERF_SLM.H:152
@ alpha
Definition: ERF_SLM.H:151
constexpr int S
Definition: ERF_TwoStreamColumn.H:604
@ qa
Definition: ERF_AdvanceWDM6.cpp:276
@ q
Definition: ERF_WSM6.H:273
@ t
Definition: ERF_WSM6.H:272
@ p
Definition: ERF_WSM6.H:280
real(c_double), parameter a4
Definition: ERF_module_model_constants.F90:97
Convergence and diagnostic information for the reconstruction solve.
Definition: ERF_NodalReconstruction.H:34
amrex::Real final_residual
CG residual of the accepted solve.
Definition: ERF_NodalReconstruction.H:36
int iterations
total CG iterations, summed over attempts
Definition: ERF_NodalReconstruction.H:35
amrex::Real max_average_error
max |avg4(S) - T|
Definition: ERF_NodalReconstruction.H:44
amrex::Real max_value
max over nodes of S
Definition: ERF_NodalReconstruction.H:47
amrex::Real min_value
min over nodes of S
Definition: ERF_NodalReconstruction.H:46
bool converged
Definition: ERF_NodalReconstruction.H:37
int refinements
times the regularization had to be raised
Definition: ERF_NodalReconstruction.H:38
amrex::Real deviation
max |S - S_ref|
Definition: ERF_NodalReconstruction.H:42
amrex::Real deviation_cap
bound imposed on the above
Definition: ERF_NodalReconstruction.H:43
amrex::Real interp_average_error
max |avg4(S_ref) - T|, for comparison
Definition: ERF_NodalReconstruction.H:45
amrex::Real regularization
accepted value of mu
Definition: ERF_NodalReconstruction.H:39