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 "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_Constants.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

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)
 
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 
25 {
26  FirstDeriv,
27  Laplacian
28 };

Function Documentation

◆ 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).
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 }
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 &  cc_slice_box,
amrex::Geometry const &  geom,
amrex::FArrayBox const &  z_cc_slice,
int  klev,
std::string const &  src_name 
)
inline

Reconstruct one horizontal slice of nodal heights from heights that live at cell centers in (x,y), 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 cell-centered 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_cc_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]cc_slice_boxBox covering the domain, cell-centered in (x,y) and a single k = 0 slab.
[in]geomGeometry of the level, used for the grid spacing.
[in]z_cc_sliceHeights on cc_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(cc_slice_box), indexed at k = 0.
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 }
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
amrex::Real Real
Definition: ERF_ShocInterface.H:19
Definition: ERF_NodalReconstruction.H:129
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
Here is the call graph for this function: