ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
NodalReconstruction Class Reference

#include <ERF_NodalReconstruction.H>

Collaboration diagram for NodalReconstruction:

Public Types

using Real = amrex::Real
 

Public Member Functions

 NodalReconstruction (amrex::Box const &cc_slice_box, amrex::Geometry const &geom)
 Construct a nodal reconstruction object for one horizontal slice. More...
 
amrex::Box const & nodalBox () const noexcept
 
amrex::FArrayBox makeReference (amrex::FArrayBox const &T) const
 
std::pair< amrex::FArrayBox, SolveInfosolve (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)
 
Real maxAverageError (amrex::FArrayBox const &T, amrex::FArrayBox const &S) const
 max_ij | avg4(S) - T | More...
 
Real totalSquaredVariation (amrex::FArrayBox const &S) const
 

Private Member Functions

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) ] More...
 
void applyAT (amrex::FArrayBox const &T, amrex::FArrayBox &R) const
 Adjoint of applyA: scatter each cell value to its four nodes. More...
 
void applyD (amrex::FArrayBox const &S, amrex::FArrayBox &difx, amrex::FArrayBox &dify) const
 Compute the first derivatives of the nodal field. More...
 
void applyDT (amrex::FArrayBox const &difx, amrex::FArrayBox const &dify, amrex::FArrayBox &R) const
 Apply the adjoint of the derivative operator. More...
 
void applyDTD (amrex::FArrayBox const &S, amrex::FArrayBox &R) const
 Apply the product of the derivative operator and its adjoint. More...
 
void applyL (amrex::FArrayBox const &S, amrex::FArrayBox &lap) const
 Apply the Laplacian operator to a nodal field. More...
 
void applyLT (amrex::FArrayBox const &lap, amrex::FArrayBox &R) const
 Apply the adjoint of the Laplacian operator. More...
 
void applyLTL (amrex::FArrayBox const &S, amrex::FArrayBox &R) const
 Apply the product of the Laplacian operator and its adjoint. More...
 
void applyVariationOperator (amrex::FArrayBox const &S, amrex::FArrayBox &R) const
 Apply the selected variation operator (first derivative or Laplacian). More...
 
void applyM (amrex::FArrayBox const &S, amrex::FArrayBox &R) const
 R = ( A^T A + lambda V^T V + mu I ) S. More...
 
void computeDiagonal (amrex::FArrayBox &diag) const
 
void computeDiagnostics (amrex::FArrayBox const &T, amrex::FArrayBox const &S, SolveInfo &info) const
 Compute diagnostic metrics for the reconstructed nodal field. More...
 
Real maxAbs (amrex::FArrayBox const &F) const
 
SolveInfo conjugateGradient (amrex::FArrayBox const &rhs, amrex::FArrayBox &x, Real relative_tolerance, int max_iterations) const
 
void applyJacobi (amrex::FArrayBox const &diag, amrex::FArrayBox const &r, amrex::FArrayBox &z) const
 Apply the Jacobi preconditioner. More...
 
Real dot (amrex::FArrayBox const &a, amrex::FArrayBox const &b) const
 

Private Attributes

amrex::Box m_cc_slice_box
 
amrex::Box m_nodal_box
 
amrex::Box m_x_edge_box
 
amrex::Box m_y_edge_box
 
amrex::Box m_interior_nodal_box
 
int m_ilo =0
 
int m_ihi =-1
 
int m_jlo =0
 
int m_jhi =-1
 
amrex::Real m_gx = one
 
amrex::Real m_gy = one
 
amrex::Real m_gxx = one
 
amrex::Real m_gyy = one
 
amrex::Real m_lambda = zero
 
amrex::Real m_mu = zero
 
VariationOperator m_var_op = VariationOperator::FirstDeriv
 
amrex::FArrayBox m_cc_scratch
 
amrex::FArrayBox m_nd_scratch
 
amrex::FArrayBox m_difx
 
amrex::FArrayBox m_dify
 
amrex::FArrayBox m_lap
 

Static Private Attributes

static constexpr amrex::Real deviation_factor = amrex::Real(2.0)
 
static constexpr amrex::Real relief_factor = amrex::Real(0.25)
 ... nor than this fraction of the relief of the data. More...
 
static constexpr amrex::Real mu_growth = amrex::Real(4.0)
 
static constexpr int max_attempts = 24
 

Detailed Description

Reconstruct a nodal field S from data T that lives at cell centers in (x,y), such that the four-node average of S matches T as closely as a bounded, smooth nodal field can.


Why we do not simply invert the averaging operator

The four-node averaging operator A,

(A S)(i,j) = 1/4 [ S(i,j) + S(i+1,j) + S(i,j+1) + S(i+1,j+1) ] ,

has symbol cos(kx/2) cos(ky/2), which vanishes at the grid Nyquist mode. Inverting A exactly – e.g. with the back substitution

S(i+1,j+1) = 4 T(i,j) - S(i,j) - S(i+1,j) - S(i,j+1)

– therefore amplifies grid-scale content of T without bound. The Green's function of that recursion is 4 / [(1+x)(1+y)], a doubly alternating cumulative sum, so a checkerboard component of T of amplitude a produces a nodal response of amplitude a*i*j. On a 128^2 grid an O(10 m) grid-scale roughness in the WRF terrain thus yields nodal heights that are kilometers away from the WRF terrain, including physically impossible negative elevations (ERF issue #3709). Adding a free homogeneous solution cannot repair this: the null space of A is spanned by (-1)^i f(j) + (-1)^j g(i), which cannot represent the (-1)^(i+j) i j growth of the particular solution. The failure is silent, because checking that avg4(S) reproduces T is then satisfied by construction.


What we do instead

Let S_ref be the direct interpolation of T to the nodes (makeReference) and R = T - A S_ref the part of T that interpolation fails to reproduce. We solve for a correction E to the interpolant,

min_E  || A E - R ||^2 + lambda || V E ||^2 + mu || E ||^2 ,   S = S_ref + E,

where V is a roughness operator (first derivative or Laplacian). The normal equations

( A^T A + lambda V^T V + mu I ) E = A^T R

are symmetric positive definite for any mu > 0, so E exists, is unique, and – unlike the exact inverse – is bounded. Specifically, writing ( A^T A + lambda V^T V + mu I ) E = A^T R and pairing with E gives

|| A E ||^2 + lambda || V E ||^2 + mu || E ||^2 <= || A E || || R || ,

and since 2 sqrt(mu) ||A E|| ||E|| <= ||A E||^2 + mu ||E||^2, we get the a priori bound

|| E || <= || R || / ( 2 sqrt(mu) ) .

The correction is therefore controlled by how badly interpolation already fails, never by the conditioning of A.


Choosing mu

A single fixed mu cannot serve both smooth and rough terrain: small mu recovers well-resolved features almost exactly but lets rough fields drift far from the interpolant, and large mu does the reverse. We therefore start from a small mu and raise it until the correction satisfies the pointwise cap

max |E| <= min( 2 max|R| , 1/4 (max T - min T) ) ,

i.e. the nodes may not move away from the direct interpolation by more than twice the interpolation misfit, nor by more than a quarter of the relief of the data. Because ||E|| -> 0 as mu -> infinity the loop always terminates, and on failure we fall back to the interpolant itself. Smooth terrain keeps the smallest mu and is reproduced to well below the interpolation error; rough terrain is pulled back toward the interpolant instead of blowing up.

The averaging error max |avg4(S) - T| returned in SolveInfo is now a genuine diagnostic – it is no longer satisfied by construction – and is reported alongside the same quantity for the plain interpolant and the range of S, so callers can sanity check the result.

Member Typedef Documentation

◆ Real

using NodalReconstruction::Real = amrex::Real

Constructor & Destructor Documentation

◆ NodalReconstruction()

NodalReconstruction::NodalReconstruction ( amrex::Box const &  cc_slice_box,
amrex::Geometry const &  geom 
)
inlineexplicit

Construct a nodal reconstruction object for one horizontal slice.

The operator that is inverted is the purely horizontal four-node average, so the only thing that matters about the input is that it is cell-centered in (x,y); whether the data happens to sit on a z-face (wrfinput geopotential) or at a cell center (metgrid HGT_M) is irrelevant here.

Parameters
cc_slice_boxBox specifying the data region: cell-centered in (x,y), a single slab at k = 0.
geomGeometry used for grid spacing and anisotropy weights.
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  }
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::Real Real
Definition: ERF_ShocInterface.H:19
amrex::Real m_gyy
Definition: ERF_NodalReconstruction.H:408
amrex::Box m_x_edge_box
Definition: ERF_NodalReconstruction.H:398
int m_ihi
Definition: ERF_NodalReconstruction.H:402
int m_ilo
Definition: ERF_NodalReconstruction.H:401
amrex::Real m_gxx
Definition: ERF_NodalReconstruction.H:407
amrex::Box m_y_edge_box
Definition: ERF_NodalReconstruction.H:399
amrex::Box m_nodal_box
Definition: ERF_NodalReconstruction.H:397
amrex::Box m_interior_nodal_box
Definition: ERF_NodalReconstruction.H:400
int m_jlo
Definition: ERF_NodalReconstruction.H:403
amrex::Real m_gx
Definition: ERF_NodalReconstruction.H:405
amrex::Box m_cc_slice_box
Definition: ERF_NodalReconstruction.H:396
amrex::Real m_gy
Definition: ERF_NodalReconstruction.H:406
int m_jhi
Definition: ERF_NodalReconstruction.H:404

Member Function Documentation

◆ applyA()

void NodalReconstruction::applyA ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  T 
) const
inlineprivate

(A S)(i,j) = 1/4 [ S(i,j) + S(i+1,j) + S(i,j+1) + S(i+1,j+1) ]

Apply the four-node averaging operator A.

Parameters
[in]SNodal field to be averaged.
[out]TResulting cell-centered field.
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  }
constexpr amrex::Real fourth
Definition: ERF_Constants.H:14
@ T
Definition: ERF_IndexDefines.H:128

Referenced by applyM(), and solve().

Here is the caller graph for this function:

◆ applyAT()

void NodalReconstruction::applyAT ( amrex::FArrayBox const &  T,
amrex::FArrayBox &  R 
) const
inlineprivate

Adjoint of applyA: scatter each cell value to its four nodes.

Apply the adjoint of the averaging operator A.

Parameters
[in]TCell-centered field to be scattered.
[out]RResulting nodal field.
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  }
@ R
Definition: ERF_IndexDefines.H:130
@ q
Definition: ERF_WSM6.H:184

Referenced by applyM(), and solve().

Here is the caller graph for this function:

◆ applyD()

void NodalReconstruction::applyD ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  difx,
amrex::FArrayBox &  dify 
) const
inlineprivate

Compute the first derivatives of the nodal field.

Parameters
[in]SNodal field.
[out]difxResulting x-derivatives.
[out]difyResulting y-derivatives.
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  }

Referenced by applyDTD().

Here is the caller graph for this function:

◆ applyDT()

void NodalReconstruction::applyDT ( amrex::FArrayBox const &  difx,
amrex::FArrayBox const &  dify,
amrex::FArrayBox &  R 
) const
inlineprivate

Apply the adjoint of the derivative operator.

Parameters
[in]difxX-derivatives.
[in]difyY-derivatives.
[out]RResulting nodal field.
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  }

Referenced by applyDTD().

Here is the caller graph for this function:

◆ applyDTD()

void NodalReconstruction::applyDTD ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  R 
) const
inlineprivate

Apply the product of the derivative operator and its adjoint.

Parameters
[in]SNodal field.
[out]RResulting nodal field.
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  }
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
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::FArrayBox m_difx
Definition: ERF_NodalReconstruction.H:417
amrex::FArrayBox m_dify
Definition: ERF_NodalReconstruction.H:418

Referenced by applyVariationOperator().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ applyJacobi()

void NodalReconstruction::applyJacobi ( amrex::FArrayBox const &  diag,
amrex::FArrayBox const &  r,
amrex::FArrayBox &  z 
) const
inlineprivate

Apply the Jacobi preconditioner.

Parameters
[in]diagOperator diagonal.
[in]rResidual field.
[out]zPreconditioned residual field.
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  }

Referenced by conjugateGradient().

Here is the caller graph for this function:

◆ applyL()

void NodalReconstruction::applyL ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  lap 
) const
inlineprivate

Apply the Laplacian operator to a nodal field.

Parameters
[in]SNodal field.
[out]lapResulting Laplacian field.
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  }
constexpr amrex::Real two
Definition: ERF_Constants.H:10

Referenced by applyLTL().

Here is the caller graph for this function:

◆ applyLT()

void NodalReconstruction::applyLT ( amrex::FArrayBox const &  lap,
amrex::FArrayBox &  R 
) const
inlineprivate

Apply the adjoint of the Laplacian operator.

Parameters
[in]lapLaplacian field.
[out]RResulting nodal field.
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  }

Referenced by applyLTL().

Here is the caller graph for this function:

◆ applyLTL()

void NodalReconstruction::applyLTL ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  R 
) const
inlineprivate

Apply the product of the Laplacian operator and its adjoint.

Parameters
[in]SNodal field.
[out]RResulting nodal field.
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  }
void applyLT(amrex::FArrayBox const &lap, amrex::FArrayBox &R) const
Apply the adjoint of the Laplacian operator.
Definition: ERF_NodalReconstruction.H:568
void applyL(amrex::FArrayBox const &S, amrex::FArrayBox &lap) const
Apply the Laplacian operator to a nodal field.
Definition: ERF_NodalReconstruction.H:543
amrex::FArrayBox m_lap
Definition: ERF_NodalReconstruction.H:419

Referenced by applyVariationOperator().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ applyM()

void NodalReconstruction::applyM ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  R 
) const
inlineprivate

R = ( A^T A + lambda V^T V + mu I ) S.

Apply the full system operator M = A^T A + lambda V^T V + mu I.

Parameters
[in]SNodal field.
[out]RResulting nodal field.
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  }
amrex::Real m_lambda
Definition: ERF_NodalReconstruction.H:409
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
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
amrex::FArrayBox m_nd_scratch
Definition: ERF_NodalReconstruction.H:414
amrex::FArrayBox m_cc_scratch
Definition: ERF_NodalReconstruction.H:413
amrex::Real m_mu
Definition: ERF_NodalReconstruction.H:410
void applyVariationOperator(amrex::FArrayBox const &S, amrex::FArrayBox &R) const
Apply the selected variation operator (first derivative or Laplacian).
Definition: ERF_NodalReconstruction.H:612

Referenced by conjugateGradient().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ applyVariationOperator()

void NodalReconstruction::applyVariationOperator ( amrex::FArrayBox const &  S,
amrex::FArrayBox &  R 
) const
inlineprivate

Apply the selected variation operator (first derivative or Laplacian).

Parameters
[in]SNodal field.
[out]RResulting nodal field.
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  }
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
VariationOperator m_var_op
Definition: ERF_NodalReconstruction.H:411
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

Referenced by applyM().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ computeDiagnostics()

void NodalReconstruction::computeDiagnostics ( amrex::FArrayBox const &  T,
amrex::FArrayBox const &  S,
SolveInfo info 
) const
inlineprivate

Compute diagnostic metrics for the reconstructed nodal field.

Parameters
[in]TCell-centered data.
[in]SReconstructed nodal field.
[in,out]infoStructure to store diagnostics.
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  }
Real maxAverageError(amrex::FArrayBox const &T, amrex::FArrayBox const &S) const
max_ij | avg4(S) - T |
Definition: ERF_NodalReconstruction.H:349
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

Referenced by solve().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ computeDiagonal()

void NodalReconstruction::computeDiagonal ( amrex::FArrayBox &  diag) const
inlineprivate

Diagonal of M, assembled by accumulating the square of every stencil coefficient that touches a node. Used as a Jacobi preconditioner.

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  }
constexpr amrex::Real four
Definition: ERF_Constants.H:12
@ qc
Definition: ERF_SatAdj.H:41
@ qa
Definition: ERF_AdvanceWDM6.cpp:274

Referenced by conjugateGradient().

Here is the caller graph for this function:

◆ conjugateGradient()

SolveInfo NodalReconstruction::conjugateGradient ( amrex::FArrayBox const &  rhs,
amrex::FArrayBox &  x,
Real  relative_tolerance,
int  max_iterations 
) const
inlineprivate

Jacobi-preconditioned conjugate gradient on the SPD operator M. x is both the initial guess and the result; it is reset to zero here because every attempt restarts the correction from the interpolant.

Local storage for the operator diagonal.

Local storage for the residual.

Local storage for the preconditioned residual.

Local storage for the search direction.

Local storage for the product of the operator and the search direction.

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  }
constexpr amrex::Real one
Definition: ERF_Constants.H:9
amrex::Real beta
Definition: ERF_InitCustomPert_DataAssimilation_ISV.H:10
void applyJacobi(amrex::FArrayBox const &diag, amrex::FArrayBox const &r, amrex::FArrayBox &z) const
Apply the Jacobi preconditioner.
Definition: ERF_NodalReconstruction.H:838
void computeDiagonal(amrex::FArrayBox &diag) const
Definition: ERF_NodalReconstruction.H:656
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
Real dot(amrex::FArrayBox const &a, amrex::FArrayBox const &b) const
Definition: ERF_NodalReconstruction.H:852
@ 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

Referenced by solve().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ dot()

Real NodalReconstruction::dot ( amrex::FArrayBox const &  a,
amrex::FArrayBox const &  b 
) const
inlineprivate
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  }

Referenced by conjugateGradient().

Here is the caller graph for this function:

◆ makeReference()

amrex::FArrayBox NodalReconstruction::makeReference ( amrex::FArrayBox const &  T) const
inline

Direct interpolation of the cell-centered data to the nodes: the average of the (up to four) cells that touch each node, with edge and corner nodes taking the average of the cells that exist. This is the field the solve corrects, and the field it falls back to.

Local storage for the reference nodal field.

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  }

Referenced by reconstruct_nodal_height_slice().

Here is the caller graph for this function:

◆ maxAbs()

Real NodalReconstruction::maxAbs ( amrex::FArrayBox const &  F) const
inlineprivate
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  }

Referenced by solve().

Here is the caller graph for this function:

◆ maxAverageError()

Real NodalReconstruction::maxAverageError ( amrex::FArrayBox const &  T,
amrex::FArrayBox const &  S 
) const
inline

max_ij | avg4(S) - T |

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  }

Referenced by computeDiagnostics().

Here is the caller graph for this function:

◆ nodalBox()

amrex::Box const& NodalReconstruction::nodalBox ( ) const
inlinenoexcept
195  {
196  return m_nodal_box;
197  }

◆ solve()

std::pair<amrex::FArrayBox,SolveInfo> NodalReconstruction::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 
)
inline

Solve the regularized least-squares problem described above.

Parameters
Tcell-centered data to be matched
referencethe interpolant to correct (see makeReference)
var_oproughness operator used as the smoothness prior
relative_toleranceCG stopping tolerance
smoothinglambda; weight of the roughness penalty
min_regularizationsmallest mu tried; raised by factors of four until the correction respects its cap
max_iterationsCG iteration cap per attempt

Local storage for the interpolation misfit.

Local storage for the right-hand side of the normal equations.

Local storage for the nodal correction.

Local storage for the reconstructed nodal field.

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  }
SolveInfo conjugateGradient(amrex::FArrayBox const &rhs, amrex::FArrayBox &x, Real relative_tolerance, int max_iterations) const
Definition: ERF_NodalReconstruction.H:743
static constexpr amrex::Real relief_factor
... nor than this fraction of the relief of the data.
Definition: ERF_NodalReconstruction.H:390
Real maxAbs(amrex::FArrayBox const &F) const
Definition: ERF_NodalReconstruction.H:728
static constexpr amrex::Real mu_growth
Definition: ERF_NodalReconstruction.H:393
static constexpr amrex::Real deviation_factor
Definition: ERF_NodalReconstruction.H:388
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
static constexpr int max_attempts
Definition: ERF_NodalReconstruction.H:394
int iterations
total CG iterations, summed over attempts
Definition: ERF_NodalReconstruction.H:34
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

Referenced by reconstruct_nodal_height_slice().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ totalSquaredVariation()

Real NodalReconstruction::totalSquaredVariation ( amrex::FArrayBox const &  S) const
inline
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  }

Member Data Documentation

◆ deviation_factor

constexpr amrex::Real NodalReconstruction::deviation_factor = amrex::Real(2.0)
staticconstexprprivate

The correction may not move a node further from the direct interpolation than this multiple of the interpolation misfit ...

Referenced by solve().

◆ m_cc_scratch

amrex::FArrayBox NodalReconstruction::m_cc_scratch
mutableprivate

Referenced by applyM(), and solve().

◆ m_cc_slice_box

amrex::Box NodalReconstruction::m_cc_slice_box
private

Referenced by solve().

◆ m_difx

amrex::FArrayBox NodalReconstruction::m_difx
mutableprivate

Scratch for the roughness operators, which are applied once per CG iteration; sized in solve() so the iteration itself never allocates.

Referenced by applyDTD(), and solve().

◆ m_dify

amrex::FArrayBox NodalReconstruction::m_dify
mutableprivate

Referenced by applyDTD(), and solve().

◆ m_gx

amrex::Real NodalReconstruction::m_gx = one
private

◆ m_gxx

amrex::Real NodalReconstruction::m_gxx = one
private

◆ m_gy

amrex::Real NodalReconstruction::m_gy = one
private

◆ m_gyy

amrex::Real NodalReconstruction::m_gyy = one
private

◆ m_ihi

◆ m_ilo

◆ m_interior_nodal_box

amrex::Box NodalReconstruction::m_interior_nodal_box
private

◆ m_jhi

◆ m_jlo

◆ m_lambda

amrex::Real NodalReconstruction::m_lambda = zero
private

Referenced by applyM(), computeDiagonal(), and solve().

◆ m_lap

amrex::FArrayBox NodalReconstruction::m_lap
mutableprivate

Referenced by applyLTL(), and solve().

◆ m_mu

amrex::Real NodalReconstruction::m_mu = zero
private

Referenced by applyM(), computeDiagonal(), and solve().

◆ m_nd_scratch

amrex::FArrayBox NodalReconstruction::m_nd_scratch
mutableprivate

Referenced by applyM(), and solve().

◆ m_nodal_box

amrex::Box NodalReconstruction::m_nodal_box
private

◆ m_var_op

VariationOperator NodalReconstruction::m_var_op = VariationOperator::FirstDeriv
private

◆ m_x_edge_box

amrex::Box NodalReconstruction::m_x_edge_box
private

Referenced by NodalReconstruction(), and solve().

◆ m_y_edge_box

amrex::Box NodalReconstruction::m_y_edge_box
private

Referenced by NodalReconstruction(), and solve().

◆ max_attempts

constexpr int NodalReconstruction::max_attempts = 24
staticconstexprprivate

Referenced by solve().

◆ mu_growth

constexpr amrex::Real NodalReconstruction::mu_growth = amrex::Real(4.0)
staticconstexprprivate

Factor by which mu is raised when the cap is violated, and the number of times we are willing to do so.

Referenced by solve().

◆ relief_factor

constexpr amrex::Real NodalReconstruction::relief_factor = amrex::Real(0.25)
staticconstexprprivate

... nor than this fraction of the relief of the data.

Referenced by solve().


The documentation for this class was generated from the following file: