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