ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
ERF_NodalReconstruction.H File Reference
#include <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include "AMReX.H"
#include "AMReX_Array4.H"
#include "AMReX_BLassert.H"
#include "AMReX_Box.H"
#include "AMReX_FArrayBox.H"
#include "AMReX_GpuDevice.H"
#include "AMReX_Geometry.H"
#include "AMReX_MultiFab.H"
#include "AMReX_ParallelDescriptor.H"
#include "AMReX_Print.H"
#include "AMReX_REAL.H"
#include "ERF_NumericalConstants.H"
Include dependency graph for ERF_NodalReconstruction.H:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Classes

struct  SolveInfo
 Convergence and diagnostic information for the reconstruction solve. More...
 
class  NodalReconstruction
 

Enumerations

enum class  VariationOperator { FirstDeriv , Laplacian }
 

Functions

void deaverage_line (amrex::Real const *t, int n, amrex::Real *s)
 
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)
 
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)
 
void fill_nodal_level_from_slice (amrex::MultiFab &z_phys_nd, int klev, amrex::FArrayBox const &z_nd_slice)
 

Enumeration Type Documentation

◆ VariationOperator

enum VariationOperator
strong
Enumerator
FirstDeriv 
Laplacian 
26 {
27  FirstDeriv,
28  Laplacian
29 };

Function Documentation

◆ deaverage_line()

void deaverage_line ( amrex::Real const *  t,
int  n,
amrex::Real *  s 
)
inline

Exactly de-average one line.

Given cell values t(0..n-1), produce node values s(0..n) satisfying

1/2 ( s(k) + s(k+1) ) = t(k),    k = 0 .. n-1

exactly. The system is underdetermined by exactly one degree of freedom: the recursion s(k+1) = 2 t(k) - s(k) solves it for any seed, and changing the seed adds the sawtooth (-1)^k, which the averaging operator annihilates. So the seed is free, and it is chosen here in closed form to minimize the first-difference energy sum_k ( s(k+1) - s(k) )^2 along the line, i.e. to leave as little of that sawtooth in the answer as the constraint permits.

Parameters
[in]tcell values, length n
[in]nnumber of cells
[out]snode values, length n+1
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 }
constexpr amrex::Real two
Definition: ERF_NumericalConstants.H:38
constexpr amrex::Real one
Definition: ERF_NumericalConstants.H:37
constexpr amrex::Real zero
Definition: ERF_NumericalConstants.H:36
amrex::Real Real
Definition: ERF_ShocInterface.H:19
@ t
Definition: ERF_WSM6.H:272

Referenced by deaverage_nodal_slice().

Here is the caller graph for this function:

◆ deaverage_nodal_slice()

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 
)
inline

Exactly de-average one horizontal slice: find nodal S with avg4(S) = T to round-off, rather than the closest such field a smoothness prior will allow.

The four-node averaging operator is a tensor product of two one-dimensional two-point averages, A = Ax (x) Ay. Each of those is surjective – the line recursion above always solves – so A is surjective and an exact solution always exists; the system is underdetermined, not inconsistent, with (nx+1)(ny+1) unknowns against nx*ny equations. Separability means it can be solved in two sweeps: first Ay U = T for U (cell-centered in x, nodal in y), then Ax S = U. Both are O(N) and non-iterative.

What cannot be removed is grid-scale structure. The null space is spanned by (-1)^i a(j) + (-1)^j b(i), in which a does not vary with i, so the amplitude of the sawtooth cannot be varied along a line; rough terrain anywhere on a line leaves a signature everywhere on it. Minimizing the first-difference energy line by line, as deaverage_line does, exhausts that freedom – a globally coupled optimization over the whole null space was measured to give no further improvement. The caller is therefore responsible for deciding whether the result is usable as terrain, and for falling back if it is not.

Parameters
[in]w_face_boxbox of the w-face slice
[in]z_w_facew-face heights on w_face_box
[in]z_refdirect interpolation of z_w_face to the nodes, for diagnostics only
Returns
the nodal heights and the diagnostics describing them
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 }
const int nx
Definition: ERF_InitCustomPertVels_CloudChamber.H:14
const int ny
Definition: ERF_InitCustomPertVels_CloudChamber.H:15
void deaverage_line(amrex::Real const *t, int n, amrex::Real *s)
Definition: ERF_NodalReconstruction.H:887
constexpr amrex::Real fourth
Definition: ERF_NumericalConstants.H:42
@ U
Definition: ERF_IndexDefines.H:129
constexpr int S
Definition: ERF_TwoStreamColumn.H:604
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

Referenced by reconstruct_nodal_height_slice().

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

◆ fill_nodal_level_from_slice()

void fill_nodal_level_from_slice ( amrex::MultiFab &  z_phys_nd,
int  klev,
amrex::FArrayBox const &  z_nd_slice 
)
inline

Copy one reconstructed nodal slice into level klev of a nodal MultiFab, filling the lateral ghost nodes by clamping to the nearest valid node.

Parameters
[in,out]z_phys_ndNodal MultiFab of physical heights.
[in]klevVertical node index to fill.
[in]z_nd_sliceNodal slice, indexed at k = 0, spanning the whole domain in (x,y).
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 }
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);})
Here is the call graph for this function:

◆ reconstruct_nodal_height_slice()

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 
)
inline

Reconstruct one horizontal slice of nodal heights from heights that live at w-faces, reporting diagnostics and aborting if the result is unusable as terrain.

Both NetCDF initialization paths need exactly this operation and differ only in how the w-face slice is assembled (geopotential from PH + PHB for wrfinput, HGT_M for metgrid) and in what they do with the answer, so the solve and its checks live here and are called from both.

The solve is serial and covers the whole domain, so it runs on the I/O rank only and the resulting nodal slice is broadcast. Running it redundantly on every rank would cost a full-domain CG solve and several full-domain managed allocations per rank, i.e. a startup stall and a memory spike that grow with the rank count rather than with the decomposition. z_w_face_slice must be host-accessible and must hold the global slice on the I/O rank.

This is collective: every rank must call it.

Parameters
[in]w_face_slice_boxBox covering the domain at the w-face staggering, a single k = 0 slab.
[in]geomGeometry of the level, used for the grid spacing.
[in]z_w_face_sliceHeights on w_face_slice_box.
[in]klevVertical index of the slice; used only in messages.
[in]src_nameName of the source data; used only in messages.
Returns
The nodal heights on surroundingNodes(w_face_slice_box), indexed at k = 0.
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 }
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
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
Definition: ERF_NodalReconstruction.H:130
Here is the call graph for this function: