ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
ERF_TerrainMetrics.cpp File Reference
#include <ERF_TerrainMetrics.H>
#include <ERF_Utils.H>
#include <AMReX_ParmParse.H>
#include <ERF_Constants.H>
#include <ERF_Interpolation_1D.H>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <numeric>
#include <utility>
#include <vector>
Include dependency graph for ERF_TerrainMetrics.cpp:

Functions

void validate_flat_terrain (int lev, const MultiFab &z_phys_nd, const Vector< Real > &z_levels_h)
 
void validate_flat_top (int lev, const Geometry &geom, const MultiFab &z_phys_nd)
 
void init_default_zphys (int, const Geometry &geom, MultiFab &z_phys_nd, MultiFab &z_phys_cc, Real z_offset)
 
BoxArray join_boxes_stacked_in_z (const BoxArray &ba)
 
void make_terrain_fitted_coords (int lev, const Geometry &geom, MultiFab &z_phys_nd, Vector< Real > const &z_levels_h, GpuArray< ERF_BC, AMREX_SPACEDIM *2 > &phys_bc_type, FineTerrain fine_terrain, MultiFab const *z_phys_interp)
 
FineTerrain which_fine_terrain ()
 
void init_fine_terrain_grid (int lev, Geometry const &geom, MultiFab &z_phys_nd, Vector< Real > const &z_levels_h, FineTerrain fine_terrain, MultiFab const *z_phys_interp)
 
void init_which_terrain_grid (int lev, Geometry const &geom, MultiFab &z_phys_nd, Vector< Real > const &z_levels_h, FineTerrain fine_terrain, MultiFab const *z_phys_interp)
 
void make_J (const Geometry &geom, MultiFab &z_phys_nd, MultiFab &detJ_cc)
 
void make_areas (const Geometry &geom, MultiFab &z_phys_nd, MultiFab &ax, MultiFab &ay, MultiFab &az)
 
void make_zcc (const Geometry &geom, MultiFab &z_phys_nd, MultiFab &z_phys_cc)
 
Real get_dzmin_terrain (MultiFab &z_phys_nd)
 

Function Documentation

◆ get_dzmin_terrain()

Real get_dzmin_terrain ( MultiFab &  z_phys_nd)

Computation min dz at cell-center

1263 {
1264  auto const& ma_z_nd_arr = z_phys_nd.const_arrays();
1265  GpuTuple<Real> min = ParReduce(TypeList<ReduceOpMin>{},
1266  TypeList<Real>{},
1267  z_phys_nd, IntVect(0),
1268  [=] AMREX_GPU_DEVICE (int box_no, int i, int j, int k) noexcept
1269  -> GpuTuple<Real>
1270  {
1271  amrex::Real dz = Compute_Z_AtWFace(i,j,k+1,ma_z_nd_arr[box_no]) -
1272  Compute_Z_AtWFace(i,j,k ,ma_z_nd_arr[box_no]);
1273  return { dz };
1274  });
1275  Real r = (get<0>(min) + std::numeric_limits<amrex::Real>::epsilon());
1276  ParallelDescriptor::ReduceRealMin(r);
1277  return r;
1278 }
amrex::Real Real
Definition: ERF_ShocInterface.H:19
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real Compute_Z_AtWFace(const int &i, const int &j, const int &k, const amrex::Array4< const amrex::Real > &z_nd)
Definition: ERF_TerrainMetrics.H:740
@ dz
Definition: ERF_AdvanceWDM6.cpp:273
real(c_double), parameter epsilon
Definition: ERF_module_model_constants.F90:12

Referenced by ERF::init_zphys(), ERF::ReadCheckpointFile(), and ERF::remake_zphys().

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

◆ init_default_zphys()

void init_default_zphys ( int  ,
const Geometry &  geom,
MultiFab &  z_phys_nd,
MultiFab &  z_phys_cc,
Real  z_offset 
)

Define a default z_phys so we have it even if a completely regular grid This will be over-written if we use z_levels, or grid stretching, or terrain-fitted grids

118 {
119  const auto& dx = geom.CellSize();
120  Real dz = dx[2];
121  Real z_bot = geom.ProbLo(2) - z_offset;
122 
123  for (MFIter mfi(z_phys_nd,true); mfi.isValid(); ++mfi)
124  {
125  const Box& bx = mfi.growntilebox();
126  const Array4< Real> z_nd_arr = z_phys_nd.array(mfi);
127  ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
128  {
129  z_nd_arr(i,j,k) = z_bot + k * dz;
130  });
131  }
132 
133  for (MFIter mfi(z_phys_cc,true); mfi.isValid(); ++mfi)
134  {
135  const Box& bx = mfi.growntilebox();
136  const Array4< Real> z_cc_arr = z_phys_cc.array(mfi);
137  ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
138  {
139  z_cc_arr(i,j,k) = z_bot + (k + myhalf) * dz;
140  });
141  }
142 }
const Real dx
Definition: ERF_InitCustomPert_ABL.H:44
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);})
constexpr amrex::Real myhalf
Definition: ERF_NumericalConstants.H:41

Referenced by ERF::init_stuff().

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

◆ init_fine_terrain_grid()

void init_fine_terrain_grid ( int  lev,
Geometry const &  geom,
MultiFab &  z_phys_nd,
Vector< Real > const &  z_levels_h,
FineTerrain  fine_terrain,
MultiFab const *  z_phys_interp 
)

Finish the terrain-fitted mesh on a fine level built from the coarse level.

On entry z_phys_nd holds the mesh interpolated from the coarse level in its valid region, with – for Transform – the fine-resolution terrain already written into the k=0 slab. The nodes outside the domain are still bogus_large_value, because neither the interpolation nor make_terrain_fitted_coords()'s setDomainBndry() call gave them a usable value, so we fill them here the same way the BTF branch does.

561 {
562  AMREX_ALWAYS_ASSERT(lev > 0);
563  AMREX_ALWAYS_ASSERT(fine_terrain != FineTerrain::None);
564 
565  const Box& domain = geom.Domain();
566  int domlo_x = domain.smallEnd(0); int domhi_x = domain.bigEnd(0) + 1;
567  int domlo_y = domain.smallEnd(1); int domhi_y = domain.bigEnd(1) + 1;
568  int domlo_z = domain.smallEnd(2);
569 
570  IntVect ngrowVect = z_phys_nd.nGrowVect();
571 
572  if (fine_terrain == FineTerrain::Transform) {
573  AMREX_ALWAYS_ASSERT(z_phys_interp != nullptr);
574 
575  const BoxArray& ba = z_phys_nd.boxArray();
576 
577  //
578  // The correction is anchored at the surface, so a box that does not reach the
579  // surface would keep the uncorrected interpolated mesh and would not join the
580  // box below it. Say so rather than silently building a tangled mesh.
581  //
582  for (int i = 0; i < ba.size(); i++) {
583  if (ba[i].smallEnd(2) != domlo_z) {
584  Abort("erf.amr_terrain_refinement = transform requires that every box on a fine level reach the surface: increase amr.max_grid_size_z or use erf.amr_terrain_refinement = interpolate");
585  }
586  }
587 
588  //
589  // Decay over the height of the fine grids as a whole and never over the height
590  // of an individual box, so that the mesh does not depend on the decomposition.
591  //
592  int khi_lev = ba.minimalBox().bigEnd(2);
593  Real z_lev_top = z_levels_h[khi_lev];
594  Real z_lev_bot = z_levels_h[domlo_z];
595  AMREX_ALWAYS_ASSERT(z_lev_top > z_lev_bot);
596 
597  Gpu::DeviceVector<Real> z_levels_d(z_levels_h.size());
598  Gpu::copy(Gpu::hostToDevice, z_levels_h.begin(), z_levels_h.end(), z_levels_d.begin());
599  auto const& z_lev = z_levels_d.data();
600 
601  //
602  // Correct the ghost nodes along with the valid ones. On a fine level whose
603  // grids do not cover the domain, the lateral ghost nodes have no valid owner
604  // anywhere, so the FillBoundary below cannot bring them up to date: left
605  // uncorrected they would keep the interpolated mesh above the surface while
606  // their k=0 node already holds the fine terrain, and the first layer of such a
607  // column inverts wherever the fine terrain stands more than a layer thickness
608  // above the interpolated one. Both fields the correction reads are defined on
609  // those nodes -- but only inside the domain, so stop there and leave the nodes
610  // outside it to the clamp below, which is what gives them a mesh at all.
611  //
612  const Box domain_nd = convert(domain, IntVect(1,1,1));
613 
614  for (MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi)
615  {
616  const Box bx = mfi.growntilebox(ngrowVect) & domain_nd;
617 
618  Array4<Real > const& z_arr = z_phys_nd.array(mfi);
619  Array4<Real const> const& z_interp = z_phys_interp->const_array(mfi);
620 
621  ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
622  {
623  if (k > domlo_z && k <= khi_lev) {
624  // How much taller the fine terrain is than the interpolated surface
625  Real delta_terrain = z_arr(i,j,domlo_z) - z_interp(i,j,domlo_z);
626 
627  Real decay = (z_lev_top - z_lev[k]) / (z_lev_top - z_lev_bot);
628  decay = amrex::max(Real(0.0), amrex::min(Real(1.0), decay));
629 
630  z_arr(i,j,k) = z_interp(i,j,k) + decay * delta_terrain;
631  }
632  });
633  } // mfi
634 
635  // Give every ghost node that does have a valid owner that owner's value, rather
636  // than the one this box computed for itself, before the fill below reads them
637  z_phys_nd.FillBoundary(geom.periodicity());
638  }
639 
640  //
641  // Extend the mesh to the nodes outside the lateral domain boundary by copying the
642  // nearest column inside it, which is the value the BTF branch gives those nodes.
643  // Like that branch we clamp in a periodic direction too, even though FillBoundary
644  // below will replace what we write there with the periodic image: that way every
645  // node we read here is one inside the domain, whose value we know is already set.
646  //
647  int imin = domlo_x; int imax = domhi_x;
648  int jmin = domlo_y; int jmax = domhi_y;
649 
650  for (MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi)
651  {
652  Box gbx = mfi.growntilebox(ngrowVect);
653  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
654 
655  // Note that we only write nodes outside the domain and only read nodes inside
656  // it, so no node is both read and written here.
657  ParallelFor(gbx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
658  {
659  int ii = amrex::max(amrex::min(i,imax),imin);
660  int jj = amrex::max(amrex::min(j,jmax),jmin);
661  if (ii != i || jj != j) {
662  z_arr(i,j,k) = z_arr(ii,jj,k);
663  }
664  });
665  } // mfi
666 
667  z_phys_nd.FillBoundary(geom.periodicity());
668 
669  for (MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi)
670  {
671  const Box& bx = mfi.validbox();
672  Box gbx = mfi.growntilebox(ngrowVect);
673 
674  if (bx.smallEnd(2) == domlo_z)
675  {
676  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
677 
678  // Fill lateral boundaries below the bottom surface
679  ParallelFor(makeSlab(gbx,2,domlo_z), [=] AMREX_GPU_DEVICE (int i, int j, int)
680  {
681  z_arr(i,j,domlo_z-1) = two*z_arr(i,j,domlo_z) - z_arr(i,j,domlo_z+1);
682  });
683  }
684  } // mfi
685 } // init_fine_terrain_grid
AMREX_ALWAYS_ASSERT(bx.length()[2]==khi+1)
constexpr amrex::Real two
Definition: ERF_NumericalConstants.H:38
@ Transform
add the fine terrain as a correction that decays with height
@ None
caller has not prepared a fine mesh from the coarse level

Referenced by init_which_terrain_grid().

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

◆ init_which_terrain_grid()

void init_which_terrain_grid ( int  lev,
Geometry const &  geom,
MultiFab &  z_phys_nd,
Vector< Real > const &  z_levels_h,
FineTerrain  fine_terrain,
MultiFab const *  z_phys_interp 
)

Initialize the terrain grid using the selected model (BTF, STF, or Sullivan TF).

Parameters
[in]levLevel in the AMR hierarchy.
[in]geomGeometry defining the domain.
[out]z_phys_ndMultiFab to be filled with nodal physical heights.
[in]z_levels_hReference z-levels for grid stretching.
702 {
703  // User-selected method from inputs file (BTF default)
704  ParmParse pp("erf");
705  int terrain_smoothing = 0;
706  pp.queryAdd("terrain_smoothing", terrain_smoothing);
707 
708  //
709  // The STF and Sullivan TF transformations need the full column from the surface to
710  // the domain top, so on a fine level we build the mesh from the one interpolated
711  // from the coarse level instead. Only a caller that has done that interpolation
712  // (and told us so) may take this path; everyone else still aborts.
713  //
714  if (lev > 0 && terrain_smoothing != 0) {
715  if (fine_terrain == FineTerrain::None) {
716  Abort("terrain_smoothing = " + std::to_string(terrain_smoothing) +
717  " on level > 0 requires setting erf.amr_terrain_refinement (transform or interpolate)");
718  }
719  init_fine_terrain_grid(lev, geom, z_phys_nd, z_levels_h, fine_terrain, z_phys_interp);
720  return;
721  }
722 
723  // Number of ghost cells
724  int ngrow = z_phys_nd.nGrow();
725  IntVect ngrowVect = z_phys_nd.nGrowVect();
726 
727  const Box& domain = geom.Domain();
728  int domlo_x = domain.smallEnd(0); int domhi_x = domain.bigEnd(0) + 1;
729  int domlo_y = domain.smallEnd(1); int domhi_y = domain.bigEnd(1) + 1;
730  int domlo_z = domain.smallEnd(2); int domhi_z = domain.bigEnd(2) + 1;
731 
732  int imin = domlo_x; // if (geom.isPeriodic(0)) imin -= z_phys_nd.nGrowVect()[0];
733  int jmin = domlo_y; // if (geom.isPeriodic(1)) jmin -= z_phys_nd.nGrowVect()[1];
734 
735  int imax = domhi_x; // if (geom.isPeriodic(0)) imax += z_phys_nd.nGrowVect()[0];
736  int jmax = domhi_y; // if (geom.isPeriodic(1)) jmax += z_phys_nd.nGrowVect()[1];
737 
738  int nz = static_cast<int>(z_levels_h.size());
739  Real z_top = z_levels_h[nz-1];
740 
741  // The STF and Sullivan transforms measure heights and terrain from the bottom of the
742  // domain, which is z_levels_h[0] = geometry.prob_lo[2]; the BTF one does so already
743  Real z_bot = z_levels_h[0];
744  Real depth = z_top - z_bot;
745 
746  Gpu::DeviceVector<Real> z_levels_d;
747  z_levels_d.resize(nz);
748  Gpu::copy(Gpu::hostToDevice, z_levels_h.begin(), z_levels_h.end(), z_levels_d.begin());
749 
750  switch(terrain_smoothing) {
751  case 0: // BTF Method
752  {
753  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
754  {
755  // Note that this box is nodal because it is based on z_phys_nd
756  const Box& bx = mfi.validbox();
757 
758  int k0 = bx.smallEnd()[2];
759 
760  // Grown box with corrected ghost cells at top
761  Box gbx = mfi.growntilebox(ngrowVect);
762 
763  if (bx.smallEnd(2) == domlo_z) {
764  gbx.setSmall(2,domlo_z);
765  } else {
766  gbx.growLo(2,-1);
767  }
768 
769  // Note that we don't overwrite the values at the high end of the box
770  // regardless of whether the box reaches the top of the domain or not.
771  // In the case of lev > 0, this ensures that the fine nodes at the top
772  // of a fine box are those that are interpolated from the coarse grid
773  if (bx.bigEnd(2) == domhi_z) {
774  gbx.setBig(2,domhi_z);
775  } else {
776  gbx.growHi(2,-1);
777  if (gbx.bigEnd(2) > domhi_z) {
778  gbx.setBig(2,domhi_z);
779  }
780  }
781 
782  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
783  auto const& z_lev = z_levels_d.data();
784 
785  //
786  // Vertical grid stretching using BTF model from p2163 of Klemp2011
787  // z_levels are only defined from k = dom_lo to dom_hi (nodal)
788  //
789  ParallelFor(gbx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
790  {
791  int ii = amrex::max(amrex::min(i,imax),imin);
792  int jj = amrex::max(amrex::min(j,jmax),jmin);
793 
794  //
795  // Start with flat z_lev set either with uniform cell size or specified z_levels
796  // If k0 = 0 then z_arr at k0 has already been filled from the terrain data
797  // If k0 > 0 then z_arr at k0 has already been filled from interpolation
798  //
799  // A box that does not start at the bottom (e.g. a refined box starting at
800  // k0 = 1) has ghost nodes below the domain, where z_lev is not defined;
801  // extend the levels there by reflection about the bottom
802  Real z = (k >= domlo_z) ? z_lev[k] : two*z_lev[domlo_z] - z_lev[2*domlo_z-k];
803  Real z_sfc = z_arr(ii,jj,k0);
804  Real z_lev_sfc = z_lev[k0];
805 
806  z_arr(i,j,k) = ( (z_sfc - z_lev_sfc) * z_top +
807  (z_top - z_sfc ) * z ) / (z_top - z_lev_sfc);
808  });
809  } // mfi
810 
811  z_phys_nd.FillBoundary(geom.periodicity());
812 
813  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
814  {
815  // Note that this box is nodal because it is based on z_phys_nd
816  const Box& bx = mfi.validbox();
817  Box gbx = mfi.growntilebox(ngrowVect);
818 
819  int k0 = bx.smallEnd()[2];
820 
821  if (k0 == 0)
822  {
823  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
824 
825  // Fill lateral boundaries below the bottom surface
826  ParallelFor(makeSlab(gbx,2,0), [=] AMREX_GPU_DEVICE (int i, int j, int)
827  {
828  z_arr(i,j,-1) = two*z_arr(i,j,0) - z_arr(i,j,1);
829  });
830  }
831  } // mfi
832  break;
833  } // case 0
834 
835  case 1: // STF Method
836  {
837  // Get MultiFab spanning domain with 1 level of ghost cells
838  MultiFab h_mf( z_phys_nd.boxArray(), z_phys_nd.DistributionMap(), 1, ngrow+1);
839  MultiFab h_mf_old(z_phys_nd.boxArray(), z_phys_nd.DistributionMap(), 1, ngrow+1);
840 
841  // Save max height for smoothing
842  Real h_m;
843 
844  // Create 2D MF without allocation
845  MultiFab mf2d;
846  {
847  BoxList bl2d = h_mf.boxArray().boxList();
848  for (auto& b : bl2d) { b.setRange(2,b.smallEnd(2)); }
849  BoxArray ba2d(std::move(bl2d));
850  mf2d = MultiFab(ba2d, h_mf.DistributionMap(), 1, ngrow, MFInfo().SetAlloc(false));
851  }
852 
853  // Get MultiArray4s from the multifabs
854  MultiArray4<Real> const& ma_h_s = h_mf.arrays();
855  MultiArray4<Real> const& ma_h_s_old = h_mf_old.arrays();
856  MultiArray4<Real> const& ma_z_phys = z_phys_nd.arrays();
857 
858  // Bottom boundary
859  int k0 = domlo_z;
860 
861  // Get max value
862  h_m = ParReduce(TypeList<ReduceOpMax>{}, TypeList<Real>{}, mf2d, IntVect(ngrow,ngrow,0),
863  [=] AMREX_GPU_DEVICE (int box_no, int i, int j, int) noexcept
864  -> GpuTuple<Real>
865  {
866  // Get Array4s
867  const auto & h = ma_h_s[box_no];
868  const auto & z_arr = ma_z_phys[box_no];
869 
870  int ii = amrex::max(amrex::min(i,imax),imin);
871  int jj = amrex::max(amrex::min(j,jmax),jmin);
872 
873  // Fill the lateral boundaries
874  z_arr(i,j,k0) = z_arr(ii,jj,k0);
875 
876  // Populate h with the terrain height above the bottom of the domain
877  h(i,j,k0) = z_arr(i,j,k0) - z_bot;
878 
879  // Return height for max
880  return { h(i,j,k0) };
881  });
882  amrex::ParallelDescriptor::ReduceRealMax(h_m);
883 
884  if (h_m < std::numeric_limits<Real>::epsilon()) h_m = Real(1e-16);
885 
886  // Fill ghost cells (neglects domain boundary if not periodic)
887  h_mf.FillBoundary(geom.periodicity());
888 
889  // Make h_mf copy for old values
890  MultiFab::Copy(h_mf_old, h_mf,0,0,1,h_mf_old.nGrow());
891 
892  // Minimum allowed fractional grid spacing
893  Real gamma_m = myhalf;
894  pp.queryAdd("terrain_gamma_m", gamma_m);
895  Real z_H = Real(2.44)*h_m/(1-gamma_m); // Klemp2011 Eqn. 11
896 
897  // Levels at which the minimum-thickness criterion forced us to stop smoothing
898  // early, reported once below: a terrain that needs this at many levels is at the
899  // edge of what STF can represent on this vertical grid.
900  int n_capped = 0;
901 
902  // Populate h_mf at k>0 with h_s, solving in ordered 2D slices
903  for (int k = domlo_z+1; k <= domhi_z; k++) // skip terrain level
904  {
905  auto const& z_lev_h = z_levels_h.data();
906 
907  Real zz = z_lev_h[k] - z_bot;
908  Real zz_minus = z_lev_h[k-1] - z_bot;
909 
910  // Hybrid attenuation profile, Klemp2011 Eqn. 9
911  Real A;
912  Real foo = std::cos((PI/2)*(zz/z_H));
913  if(zz < z_H) { A = foo*foo*foo*foo*foo*foo; } // A controls rate of return to atm
914  else { A = 0; }
915  Real foo_minus = std::cos((PI/2)*(zz_minus/z_H));
916  Real A_minus;
917  if(zz_minus < z_H) { A_minus = foo_minus*foo_minus*foo_minus*foo_minus*foo_minus*foo_minus; } // A controls rate of return to atm
918  else { A_minus = 0; }
919 
920  unsigned maxIter = 50; // M_k in paper
921  unsigned iter = 0;
922  Real threshold = gamma_m;
923  Real diff = bogus_large_value;
924  while (iter < maxIter)
925  {
926 
927  diff = ParReduce(TypeList<ReduceOpMin>{}, TypeList<Real>{}, mf2d, IntVect(ngrow,ngrow,0),
928  [=] AMREX_GPU_DEVICE (int box_no, int i, int j, int) noexcept
929  -> GpuTuple<Real>
930  {
931  const auto & h_s = ma_h_s[box_no];
932  const auto & h_s_old = ma_h_s_old[box_no];
933 
934  Real beta_k = Real(0.2)*std::min(zz/(2*h_m),Real(1.0)); //smoothing coefficient (Eqn. 8)
935 
936  // Clip indices for ghost-cells
937  int ii = amrex::min(amrex::max(i,domlo_x),domhi_x);
938  int jj = amrex::min(amrex::max(j,domlo_y),domhi_y);
939 
940  if (iter == 0) {
941  h_s(i,j,k) = h_s_old(i,j,k-1) + beta_k*(h_s_old(ii+1,jj ,k-1)
942  + h_s_old(ii-1,jj ,k-1)
943  + h_s_old(ii ,jj+1,k-1)
944  + h_s_old(ii ,jj-1,k-1) - 4*h_s_old(ii,jj,k-1));
945  }
946  else {
947  h_s(i,j,k) = h_s_old(i,j,k ) + beta_k*(h_s_old(ii+1,jj ,k )
948  + h_s_old(ii-1,jj ,k )
949  + h_s_old(ii ,jj+1,k )
950  + h_s_old(ii ,jj-1,k ) - 4*h_s_old(ii,jj,k ));
951  }
952 
953  // Minimum vertical grid spacing condition (Klemp2011 Eqn. 7)
954  return { (zz + A * h_s(i,j,k) - (zz_minus + A_minus * h_s(i,j,k-1))) / (zz - zz_minus) };
955 
956  }); //ParReduce
957 
958  ParallelDescriptor::ReduceRealMin(diff);
959 
960  //
961  // Each pass smooths h_s at THIS level while the level below is already
962  // fixed, so over a peak it always thins the layer: the passes have to
963  // stop before Eqn. 7 is violated, not after. Measuring only once a pass
964  // has been committed is not enough -- nothing then stops the pass that
965  // breaks the criterion from being the one we keep, which is how a column
966  // over steep terrain ended up at 0.21 of its nominal layer thickness
967  // against gamma_m = 0.5. Reject that pass and restore what preceded it.
968  //
969  // At iter 0 there is no earlier pass at this level to fall back to, so
970  // carry the level below up unsmoothed instead. That leaves only the
971  // A-decay term, whose worst case is exactly what z_H = 2.44 h_m/(1-gamma_m)
972  // was chosen to bound: max|dA/dz| = 2.439/z_H, so h_m max|dA/dz| dz equals
973  // (1-gamma_m) dz and the thickness ratio lands on gamma_m.
974  //
975  if (diff < threshold) {
976  ++n_capped;
977  if (iter == 0) {
978  for ( MFIter mfi(h_mf, TilingIfNotGPU()); mfi.isValid(); ++mfi )
979  {
980  Box xybx = mfi.growntilebox(ngrow);
981  xybx.setRange(2,0);
982  Array4<Real> const& h_s = h_mf.array(mfi);
983  ParallelFor(xybx, [=] AMREX_GPU_DEVICE (int i, int j, int) noexcept
984  {
985  h_s(i,j,k) = h_s(i,j,k-1);
986  });
987  }
988  MultiFab::Copy(h_mf_old, h_mf,0,0,1,h_mf_old.nGrow());
989  h_mf_old.FillBoundary(geom.periodicity());
990  } else {
991  // h_mf_old still holds the last pass that satisfied the criterion
992  MultiFab::Copy(h_mf, h_mf_old,0,0,1,h_mf.nGrow());
993  }
994  break;
995  }
996 
997  MultiFab::Copy(h_mf_old, h_mf,0,0,1,h_mf_old.nGrow());
998 
999  iter++;
1000 
1001  // Fill ghost points
1002  h_mf_old.FillBoundary(geom.periodicity());
1003 
1004  } // while
1005 
1006  auto const& z_lev_d = z_levels_d.data();
1007 
1008  // Populate z_phys_nd by solving z_arr(i,j,k) = z + A*h_s(i,j,k)
1009  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1010  {
1011  // Grown box with no z range
1012  Box xybx = mfi.growntilebox(ngrow);
1013  xybx.setRange(2,0);
1014 
1015  Array4<Real> const& h_s = h_mf_old.array(mfi);
1016  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
1017 
1018  ParallelFor(xybx, [=] AMREX_GPU_DEVICE (int i, int j, int) {
1019 
1020  // Location of nodes
1021  Real z = z_lev_d[k];
1022 
1023  // STF model from p2164 of Klemp2011 (Eqn. 4)
1024  z_arr(i,j,k) = z + A*h_s(i,j,k);
1025 
1026  // Fill below the bottom surface
1027  if (k == 1) {
1028  z_arr(i,j,k0-1) = two*z_arr(i,j,k0) - z_arr(i,j,k);
1029  }
1030  });
1031  } // mfi
1032  } // k
1033 
1034  if (n_capped > 0) {
1035  amrex::Print() << "NOTE: the smoothed terrain-following grid at level " << lev
1036  << " hit its minimum layer-thickness floor (erf.terrain_gamma_m = "
1037  << gamma_m << ") at " << n_capped << " of "
1038  << (domhi_z - domlo_z) << " levels, so the terrain smoothing was "
1039  << "capped there. Raising erf.terrain_gamma_m, coarsening the "
1040  << "vertical grid or using erf.terrain_smoothing = 0 would give "
1041  << "this terrain more room.\n";
1042  }
1043 
1044  Gpu::streamSynchronize();
1045 
1046  break;
1047  } // case 1
1048 
1049  case 2: // Sullivan TF Method
1050  {
1051  int k0 = 0;
1052 
1053  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1054  {
1055  // Grown box with corrected ghost cells at top
1056  Box gbx = mfi.growntilebox(ngrow);
1057  gbx.setRange(2,domlo_z,domhi_z+1);
1058 
1059  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
1060  auto const& z_lev = z_levels_d.data();
1061 
1062  ParallelFor(gbx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
1063  {
1064  // Vertical grid stretching
1065  Real z = z_lev[k];
1066 
1067  int ii = amrex::max(amrex::min(i,imax),imin);
1068  int jj = amrex::max(amrex::min(j,jmax),jmin);
1069 
1070  // Fill levels using model from Sullivan et. al. 2014
1071  int omega = 3; //Used to adjust how rapidly grid lines level out. omega=1 is BTF!
1072  z_arr(i,j,k) = z + static_cast<Real>(std::pow((one - (z - z_bot)/depth),omega) * (z_arr(ii,jj,k0) - z_bot));
1073 
1074  // Fill lateral boundaries and below the bottom surface
1075  if (k == k0) {
1076  z_arr(i,j,k0 ) = z_arr(ii,jj,k0);
1077  }
1078  if (k == 1) {
1079  z_arr(i,j,k0-1) = two*z_arr(ii,jj,k0) - z_arr(i,j,k);
1080  }
1081  });
1082  } // mfi
1083  break;
1084  } // case 2
1085 
1086  case 3: // Debugging Test Method -- applies Sullivan TF starting at k = 1 so that domain does not change size
1087  {
1088  int k0 = 0;
1089 
1090  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1091  {
1092  // Grown box with corrected ghost cells at top
1093  Box gbx = mfi.growntilebox(ngrow);
1094  gbx.setRange(2,domlo_z,domhi_z+1);
1095 
1096  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
1097  auto const& z_lev = z_levels_d.data();
1098 
1099  ParallelFor(gbx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
1100  {
1101  // Vertical grid stretching
1102  Real z = z_lev[k];
1103 
1104  int ii = amrex::max(amrex::min(i,imax),imin);
1105  int jj = amrex::max(amrex::min(j,jmax),jmin);
1106 
1107  // Fill values outside the lateral boundaries and below the bottom surface (necessary if init_type = "WRFInput")
1108  if (k == k0+1) {
1109  z_arr(i,j,k) = z + (z_arr(ii,jj,k0) - z_bot);
1110  } else {
1111  // Fill levels using model from Sullivan et. al. 2014
1112  int omega = 3; //Used to adjust how rapidly grid lines level out. omega=1 is BTF!
1113  z_arr(i,j,k) = z + static_cast<Real>(std::pow((one - (z - z_bot)/depth),omega) * (z_arr(ii,jj,k0) - z_bot));
1114  }
1115  });
1116  gbx.setBig(2,0);
1117  ParallelFor(gbx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
1118  {
1119  z_arr(i,j,k ) = z_bot;
1120  z_arr(i,j,k-1) = two*z_bot - z_arr(i,j,k+1);
1121  });
1122  } // mfi
1123  break;
1124  } // case 3
1125  } //switch
1126 }
constexpr amrex::Real bogus_large_value
Definition: ERF_Constants.H:19
ParmParse pp("prob")
constexpr amrex::Real one
Definition: ERF_NumericalConstants.H:37
constexpr amrex::Real PI
Definition: ERF_NumericalConstants.H:46
void init_fine_terrain_grid(int lev, Geometry const &geom, MultiFab &z_phys_nd, Vector< Real > const &z_levels_h, FineTerrain fine_terrain, MultiFab const *z_phys_interp)
Definition: ERF_TerrainMetrics.cpp:555
@ omega
Definition: ERF_Morrison.H:57
constexpr int A
Definition: ERF_TwoStreamColumn.H:603

Referenced by make_terrain_fitted_coords().

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

◆ join_boxes_stacked_in_z()

BoxArray join_boxes_stacked_in_z ( const BoxArray &  ba)

The cells of a (cell-centered) BoxArray as boxes that are never stacked on each other.

Wherever a box of ba sits on the top face of another box of ba, the run of cells in z that the two belong to becomes a single box. Boxes stacked with different lateral extents are first cut at each other's lateral edges; the joined pieces are then merged in x and y and chopped to no more than the largest lateral size of the boxes they came from. Returns ba itself when no two boxes of ba are stacked.

155 {
156  AMREX_ALWAYS_ASSERT(ba.ixType().cellCentered());
157 
158  const int nboxes = static_cast<int>(ba.size());
159 
160  // Gather the boxes into stacks: link every box to the boxes that sit on its top face
161  std::vector<int> stack_of(nboxes);
162  std::iota(stack_of.begin(), stack_of.end(), 0);
163  auto find_stack = [&stack_of] (int i) {
164  while (stack_of[i] != i) {
165  stack_of[i] = stack_of[stack_of[i]];
166  i = stack_of[i];
167  }
168  return i;
169  };
170 
171  bool any_stacked = false;
172  for (int i = 0; i < nboxes; ++i) {
173  Box above(ba[i]);
174  above.setRange(2, ba[i].bigEnd(2)+1);
175  for (const auto& isect : ba.intersections(above)) {
176  stack_of[find_stack(isect.first)] = find_stack(i);
177  any_stacked = true;
178  }
179  }
180  if (!any_stacked) { return ba; }
181 
182  // std::map keeps the order, and so the joined boxes, the same on every rank
183  std::map<int, std::vector<Box>> stacks;
184  for (int i = 0; i < nboxes; ++i) {
185  stacks[find_stack(i)].push_back(ba[i]);
186  }
187 
188  BoxList bl_joined;
189  for (const auto& stack : stacks)
190  {
191  const std::vector<Box>& boxes = stack.second;
192  if (boxes.size() == 1) {
193  bl_joined.push_back(boxes[0]);
194  continue;
195  }
196 
197  // Cut the stack at every lateral edge of its boxes
198  std::array<std::vector<int>,2> cuts;
199  IntVect max_size(1);
200  for (const auto& b : boxes) {
201  for (int idim = 0; idim < 2; ++idim) {
202  cuts[idim].push_back(b.smallEnd(idim));
203  cuts[idim].push_back(b.bigEnd(idim)+1);
204  max_size[idim] = std::max(max_size[idim], b.length(idim));
205  }
206  }
207  for (auto& c : cuts) {
208  std::sort(c.begin(), c.end());
209  c.erase(std::unique(c.begin(), c.end()), c.end());
210  }
211  auto cut_index = [&cuts] (int idim, int coord) {
212  return static_cast<int>(std::lower_bound(cuts[idim].begin(), cuts[idim].end(), coord)
213  - cuts[idim].begin());
214  };
215 
216  // The z extents of the boxes over each piece, keyed by the piece's cut indices
217  std::map<std::pair<int,int>, std::vector<std::pair<int,int>>> zruns_of_piece;
218  for (const auto& b : boxes) {
219  for (int ix = cut_index(0, b.smallEnd(0)); ix < cut_index(0, b.bigEnd(0)+1); ++ix) {
220  for (int iy = cut_index(1, b.smallEnd(1)); iy < cut_index(1, b.bigEnd(1)+1); ++iy) {
221  zruns_of_piece[std::make_pair(ix,iy)].emplace_back(b.smallEnd(2), b.bigEnd(2));
222  }
223  }
224  }
225 
226  // Over each piece, join the extents that touch into one box per run of cells
227  BoxList bl_stack;
228  for (auto& piece : zruns_of_piece) {
229  const int ix = piece.first.first;
230  const int iy = piece.first.second;
231  auto& zruns = piece.second;
232  std::sort(zruns.begin(), zruns.end());
233  std::size_t n_first = 0;
234  for (std::size_t n = 1; n <= zruns.size(); ++n) {
235  if (n == zruns.size() || zruns[n].first != zruns[n-1].second+1) {
236  bl_stack.push_back(Box(IntVect(cuts[0][ix], cuts[1][iy], zruns[n_first].first),
237  IntVect(cuts[0][ix+1]-1, cuts[1][iy+1]-1, zruns[n-1].second)));
238  n_first = n;
239  }
240  }
241  }
242 
243  // Merging in x and y joins pieces with the same z extent, so it cannot stack them again
244  bl_stack.simplify();
245  max_size[2] = bl_stack.minimalBox().length(2);
246  bl_stack.maxSize(max_size);
247  bl_joined.join(bl_stack);
248  }
249 
250  return BoxArray(std::move(bl_joined));
251 }

Referenced by SurfaceLayer::define_pblh_columns().

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

◆ make_areas()

void make_areas ( const Geometry &  geom,
MultiFab &  z_phys_nd,
MultiFab &  ax,
MultiFab &  ay,
MultiFab &  az 
)

Computation of area fractions on faces

1175 {
1176  const auto* dx = geom.CellSize();
1177  Real dzInv = one/dx[2];
1178 
1179  // Domain valid box (z_nd is nodal)
1180  const Box& domain = geom.Domain();
1181  int domlo_z = domain.smallEnd(2);
1182 
1183  // The z-faces are always full when using terrain-fitted coordinates
1184  az.setVal(1.0);
1185 
1186  //
1187  // x-areas
1188  //
1189 #ifdef _OPENMP
1190 #pragma omp parallel if (Gpu::notInLaunchRegion())
1191 #endif
1192  for ( MFIter mfi(ax, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1193  {
1194  Box gbx = mfi.growntilebox(ax.nGrow());
1195  if (gbx.smallEnd(2) < domlo_z) {
1196  gbx.setSmall(2,domlo_z);
1197  }
1198 
1199  Array4<Real const> z_nd = z_phys_nd.const_array(mfi);
1200  Array4<Real > ax_arr = ax.array(mfi);
1201  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
1202  ax_arr(i, j, k) = Real(.5) * dzInv * (
1203  z_nd(i,j,k+1) + z_nd(i,j+1,k+1) - z_nd(i,j,k) - z_nd(i,j+1,k));
1204  });
1205  }
1206 
1207  //
1208  // y-areas
1209  //
1210 #ifdef _OPENMP
1211 #pragma omp parallel if (Gpu::notInLaunchRegion())
1212 #endif
1213  for ( MFIter mfi(ay, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1214  {
1215  Box gbx = mfi.growntilebox(ay.nGrow());
1216  if (gbx.smallEnd(2) < domlo_z) {
1217  gbx.setSmall(2,domlo_z);
1218  }
1219 
1220  Array4<Real const> z_nd = z_phys_nd.const_array(mfi);
1221  Array4<Real > ay_arr = ay.array(mfi);
1222  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
1223  ay_arr(i, j, k) = Real(.5) * dzInv * (
1224  z_nd(i,j,k+1) + z_nd(i+1,j,k+1) - z_nd(i,j,k) - z_nd(i+1,j,k));
1225  });
1226  }
1227 
1228  ax.FillBoundary(geom.periodicity());
1229  ay.FillBoundary(geom.periodicity());
1230  az.FillBoundary(geom.periodicity());
1231 }

Referenced by ERF::update_terrain_arrays().

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

◆ make_J()

void make_J ( const Geometry &  geom,
MultiFab &  z_phys_nd,
MultiFab &  detJ_cc 
)

Computation of detJ at cell-center

1135 {
1136  const auto *dx = geom.CellSize();
1137  Real dzInv = one/dx[2];
1138 
1139  // Domain valid box (z_nd is nodal)
1140  const Box& domain = geom.Domain();
1141  int domlo_z = domain.smallEnd(2);
1142 
1143  // Number of ghost cells
1144  int ngrow= detJ_cc.nGrow();
1145 
1146 #ifdef _OPENMP
1147 #pragma omp parallel if (Gpu::notInLaunchRegion())
1148 #endif
1149  for ( MFIter mfi(detJ_cc, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1150  {
1151  Box gbx = mfi.growntilebox(ngrow);
1152  if (gbx.smallEnd(2) < domlo_z) {
1153  gbx.setSmall(2,domlo_z);
1154  }
1155  Array4<Real const> z_nd = z_phys_nd.const_array(mfi);
1156  Array4<Real > detJ = detJ_cc.array(mfi);
1157  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
1158  detJ(i, j, k) = Real(.25) * dzInv * (
1159  z_nd(i,j,k+1) + z_nd(i+1,j,k+1) + z_nd(i,j+1,k+1) + z_nd(i+1,j+1,k+1)
1160  -z_nd(i,j,k ) - z_nd(i+1,j,k ) - z_nd(i,j+1,k ) - z_nd(i+1,j+1,k ) );
1161  });
1162  }
1163  detJ_cc.FillBoundary(geom.periodicity());
1164 }

Referenced by ERF::update_terrain_arrays().

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

◆ make_terrain_fitted_coords()

void make_terrain_fitted_coords ( int  lev,
const Geometry &  geom,
MultiFab &  z_phys_nd,
Vector< Real > const &  z_levels_h,
GpuArray< ERF_BC, AMREX_SPACEDIM *2 > &  phys_bc_type,
FineTerrain  fine_terrain,
MultiFab const *  z_phys_interp 
)

Computation of the terrain grid from BTF, STF, or Sullivan TF model

331 {
332  const Box& domain = geom.Domain();
333 
334  int domlo_z = domain.smallEnd(2);
335  int domhi_z = domain.bigEnd(2) + 1;
336 
337  // Just in case ...
338  z_phys_nd.setDomainBndry(bogus_large_value,0,1,geom);
339 
340  // ****************************************************************************
341 
342  if (lev == 0) {
343  BoxArray ba(z_phys_nd.boxArray());
344  bool all_boxes_touch_bottom = true;
345  for (int i = 0; i < ba.size(); i++) {
346  if (ba[i].smallEnd(2) != domlo_z) {
347  all_boxes_touch_bottom = false;
348  }
349  }
350 
351  if (all_boxes_touch_bottom) {
352  init_which_terrain_grid(lev, geom, z_phys_nd, z_levels_h, fine_terrain, z_phys_interp);
353  } else {
354 
355  BoxArray ba_new(domain);
356  ChopGrids2D(ba_new, domain, ParallelDescriptor::NProcs());
357 
358  DistributionMapping dm_new(ba_new);
359  ba_new.surroundingNodes();
360 
361  MultiFab z_phys_nd_new(ba_new, dm_new, 1, z_phys_nd.nGrowVect());
362 
363  z_phys_nd_new.ParallelCopy(z_phys_nd,0,0,1,z_phys_nd.nGrowVect(),z_phys_nd.nGrowVect());
364 
365  init_which_terrain_grid(lev, geom, z_phys_nd_new, z_levels_h, fine_terrain, z_phys_interp);
366 
367  z_phys_nd.ParallelCopy(z_phys_nd_new,0,0,1,z_phys_nd.nGrowVect(),z_phys_nd.nGrowVect());
368  }
369  } else { // lev > 0
370  //
371  // BTF maps each column of a box between the box's lowest node and the domain top.
372  // A box that sits on another box of this level must continue the columns of the box
373  // below it: its own lowest node holds only the mesh interpolated from the coarse
374  // level, so starting from there would make the mesh above it follow the coarse
375  // terrain and depend on where the grids are split in z. As on level 0 we therefore
376  // build the mesh on boxes that hold whole columns and copy it back. The bottom box
377  // of a column, such as a refined patch aloft, still starts from its own lowest node.
378  //
379  ParmParse pp("erf");
380  int terrain_smoothing = 0;
381  pp.query("terrain_smoothing", terrain_smoothing);
382 
383  const ColumnGrids* cg = (terrain_smoothing == 0)
384  ? &column_grids(lev, geom, amrex::convert(z_phys_nd.boxArray(), IntVect::TheCellVector()))
385  : nullptr;
386 
387  if (cg == nullptr || !cg->stacked) {
388  init_which_terrain_grid(lev, geom, z_phys_nd, z_levels_h, fine_terrain, z_phys_interp);
389  } else {
390  const IntVect& ng = z_phys_nd.nGrowVect();
391  MultiFab z_phys_nd_col(cg->ba_col_nd, cg->dm_col, 1, ng);
392 
393  z_phys_nd_col.ParallelCopy(z_phys_nd,0,0,1,ng,ng);
394 
395  init_which_terrain_grid(lev, geom, z_phys_nd_col, z_levels_h, fine_terrain, z_phys_interp);
396 
397  // Copy the ghost nodes too, then give every node the value of a box that holds it
398  // as valid, rather than one a neighbouring box computed for its ghost node
399  z_phys_nd.ParallelCopy(z_phys_nd_col,0,0,1,ng,ng);
400  z_phys_nd.ParallelCopy(z_phys_nd_col,0,0,1,IntVect(0),ng);
401  }
402  }
403 
404  //
405  // A node on the high side of a periodic direction is the same node as its image on the
406  // low side, and a box that holds one as valid is not touched by FillBoundary -- so give
407  // both copies one value, or the faces on either side of the node see different metrics
408  // and the mass flux through them differs. make_terrain_surface_periodic already does
409  // this for a surface built by init_terrain_surface (and that is what the immersed
410  // boundary reads), but not for one that comes from wrfinput, met_em, a restart or the
411  // interpolation from a coarser level; this covers all of them, and the whole column.
412  // The copy kept is that of the lower-numbered box, which for the usual grids (and
413  // always within one box) is the low side, as in make_terrain_surface_periodic.
414  //
415  // Only the lateral periodicity: in a domain that is also periodic in z, the top node of
416  // a column is the image of the bottom one, but its height is not -- syncing them would
417  // give the top node the bottom's height and the top cell a negative thickness.
418  //
419  if (geom.isPeriodic(0) || geom.isPeriodic(1)) {
420  const Box& dom = geom.Domain();
421  const Periodicity lateral_period(IntVect(AMREX_D_DECL(geom.isPeriodic(0) ? dom.length(0) : 0,
422  geom.isPeriodic(1) ? dom.length(1) : 0,
423  0)));
424  z_phys_nd.OverrideSync(lateral_period);
425  }
426 
427  //
428  // Fill ghost layers and corners (including periodic) -- no matter what level
429  //
430  z_phys_nd.FillBoundary(geom.periodicity());
431 
432  if (phys_bc_type[Orientation(0,Orientation::low )] == ERF_BC::symmetry ||
433  phys_bc_type[Orientation(0,Orientation::high)] == ERF_BC::symmetry ||
434  phys_bc_type[Orientation(1,Orientation::low )] == ERF_BC::symmetry ||
435  phys_bc_type[Orientation(1,Orientation::high)] == ERF_BC::symmetry) {
436 
437  const auto& dom_lo = lbound(convert(domain,IntVect(1,1,1)));
438  const auto& dom_hi = ubound(convert(domain,IntVect(1,1,1)));
439 
440  //
441  // Mirror the whole ghost region, not just its first layer: the base state carries
442  // ComputeGhostCells+1 layers and the state stencils reach several cells past the
443  // boundary, so mirroring only the first would leave the rest of the ghost region
444  // holding whatever the terrain construction extrapolated into it.
445  //
446  const IntVect& ngv = z_phys_nd.nGrowVect();
447 
448  for (MFIter mfi(z_phys_nd,true); mfi.isValid(); ++mfi) {
449  //
450  // Whether this tile reaches a domain boundary is a question about its valid
451  // nodes, so ask it of the tilebox. The grown tilebox begins a ghost width short
452  // of the boundary and so never equals it -- asking it of the grown box is what
453  // kept this mirroring from ever running. The grown box is still what we sweep,
454  // so that the ghost rows in the other two directions are covered as well.
455  //
456  const Box& vbx = mfi.tilebox();
457  const Box& bx = mfi.growntilebox();
458  const Array4< Real> z_nd_arr = z_phys_nd.array(mfi);
459  if (phys_bc_type[Orientation(0,Orientation::low)] == ERF_BC::symmetry && vbx.smallEnd(0) == dom_lo.x) {
460  const int ng = ngv[0];
461  ParallelFor(makeSlab(bx,0,1), [=] AMREX_GPU_DEVICE (int , int j, int k)
462  {
463  for (int n = 1; n <= ng; ++n) {
464  z_nd_arr(dom_lo.x-n,j,k) = z_nd_arr(dom_lo.x+n,j,k);
465  }
466  });
467  }
468  if (phys_bc_type[Orientation(0,Orientation::high)] == ERF_BC::symmetry && vbx.bigEnd(0) == dom_hi.x) {
469  const int ng = ngv[0];
470  ParallelFor(makeSlab(bx,0,1), [=] AMREX_GPU_DEVICE (int , int j, int k)
471  {
472  for (int n = 1; n <= ng; ++n) {
473  z_nd_arr(dom_hi.x+n,j,k) = z_nd_arr(dom_hi.x-n,j,k);
474  }
475  });
476  }
477  if (phys_bc_type[Orientation(1,Orientation::low)] == ERF_BC::symmetry && vbx.smallEnd(1) == dom_lo.y) {
478  const int ng = ngv[1];
479  ParallelFor(makeSlab(bx,1,1), [=] AMREX_GPU_DEVICE (int i, int , int k)
480  {
481  for (int n = 1; n <= ng; ++n) {
482  z_nd_arr(i,dom_lo.y-n,k) = z_nd_arr(i,dom_lo.y+n,k);
483  }
484  });
485  }
486  if (phys_bc_type[Orientation(1,Orientation::high)] == ERF_BC::symmetry && vbx.bigEnd(1) == dom_hi.y) {
487  const int ng = ngv[1];
488  ParallelFor(makeSlab(bx,1,1), [=] AMREX_GPU_DEVICE (int i, int , int k)
489  {
490  for (int n = 1; n <= ng; ++n) {
491  z_nd_arr(i,dom_hi.y+n,k) = z_nd_arr(i,dom_hi.y-n,k);
492  }
493  });
494  }
495  }
496  }
497 
498  //********************************************************************************
499  // Populate domain boundary cells in z-direction
500  //********************************************************************************
501  int ngrow = z_phys_nd.nGrow();
502 
503  for ( MFIter mfi(z_phys_nd, TilingIfNotGPU()); mfi.isValid(); ++mfi )
504  {
505  // Only set values above top of domain if this box reaches that far
506  Box nd_bx = mfi.tilebox();
507 
508  // Note that domhi_z is already nodal in the z-direction
509  if (nd_bx.bigEnd(2) >= domhi_z) {
510  // Grown box with no z range
511  Box bx_zhi = mfi.growntilebox(ngrow);
512  bx_zhi.setSmall(2,domhi_z+1);
513  Array4<Real> const& z_arr = z_phys_nd.array(mfi);
514 
515  // Extrapolate top layer
516  ParallelFor(bx_zhi, [=] AMREX_GPU_DEVICE (int i, int j, int k) {
517  z_arr(i,j,k) = z_arr(i,j,domhi_z)
518  + (k-domhi_z) * (z_arr(i,j,domhi_z) - z_arr(i,j,domhi_z-1));
519  });
520  }
521  }
522 } // make_terrain_fitted_coords
void ChopGrids2D(BoxArray &ba, const Box &domain, int target_size)
Definition: ERF_ChopGrids.cpp:35
void init_which_terrain_grid(int lev, Geometry const &geom, MultiFab &z_phys_nd, Vector< Real > const &z_levels_h, FineTerrain fine_terrain, MultiFab const *z_phys_interp)
Definition: ERF_TerrainMetrics.cpp:696
@ ng
Definition: ERF_Morrison.H:51
real(c_double), private cg
Definition: ERF_module_mp_morr_two_moment.F90:203

Referenced by ERF::init_zphys(), and ERF::remake_zphys().

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

◆ make_zcc()

void make_zcc ( const Geometry &  geom,
MultiFab &  z_phys_nd,
MultiFab &  z_phys_cc 
)

Computation of z_phys at cell-center

1240 {
1241 #ifdef _OPENMP
1242 #pragma omp parallel if (Gpu::notInLaunchRegion())
1243 #endif
1244  for ( MFIter mfi(z_phys_cc, TilingIfNotGPU()); mfi.isValid(); ++mfi )
1245  {
1246  Box gbx = mfi.growntilebox();
1247  Array4<Real const> z_nd = z_phys_nd.const_array(mfi);
1248  Array4<Real > z_cc = z_phys_cc.array(mfi);
1249  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
1250  z_cc(i, j, k) = Real(.125) * ( z_nd(i,j,k ) + z_nd(i+1,j,k ) + z_nd(i,j+1,k ) + z_nd(i+1,j+1,k )
1251  +z_nd(i,j,k+1) + z_nd(i+1,j,k+1) + z_nd(i,j+1,k+1) + z_nd(i+1,j+1,k+1) );
1252  });
1253  }
1254  z_phys_cc.FillBoundary(geom.periodicity());
1255 }

Referenced by ERF::post_timestep(), and ERF::update_terrain_arrays().

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

◆ validate_flat_terrain()

void validate_flat_terrain ( int  lev,
const MultiFab &  z_phys_nd,
const Vector< Real > &  z_levels_h 
)
21 {
22  // The comparison is against heights that have been round-tripped through z_phys_nd, so the
23  // threshold has to track the working precision: a hard-wired 1e-10 is far below single-
24  // precision round-off at domain-top magnitudes and would abort on a genuinely flat mesh.
25  // Keep the original (double-precision) threshold, floored at a few hundred ulps.
26  const Real scale = std::max({Real(1.0), std::abs(z_levels_h.front()), std::abs(z_levels_h.back())});
27  const Real rel_tol = std::max(Real(1.0e-10), Real(100.0) * std::numeric_limits<Real>::epsilon());
28  const Real tolerance = rel_tol * scale;
29 
30  MultiFab expected(z_phys_nd.boxArray(), z_phys_nd.DistributionMap(), 1, 0);
31  Gpu::DeviceVector<Real> z_levels_d(z_levels_h.size());
32  Gpu::copy(Gpu::hostToDevice, z_levels_h.begin(), z_levels_h.end(), z_levels_d.begin());
33  const Real* z_levels = z_levels_d.data();
34 
35  for (MFIter mfi(expected); mfi.isValid(); ++mfi) {
36  const Box& bx = mfi.validbox();
37  auto const& expected_arr = expected.array(mfi);
38  ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) {
39  expected_arr(i,j,k) = z_levels[k];
40  });
41  }
42 
43  MultiFab error(expected.boxArray(), expected.DistributionMap(), 1, 0);
44  MultiFab::Copy(error, expected, 0, 0, 1, 0);
45  MultiFab::Subtract(error, z_phys_nd, 0, 0, 1, 0);
46  const Real max_error = error.norminf(0, 0, false);
47 
48  if (max_error > tolerance) {
49  Abort("erf.flat_terrain = true, but z_phys_nd is not horizontally "
50  "flat or does not match the configured vertical spacing at "
51  "level " + std::to_string(lev) + ".");
52  }
53 }

Referenced by ERF::init_zphys().

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

◆ validate_flat_top()

void validate_flat_top ( int  lev,
const Geometry &  geom,
const MultiFab &  z_phys_nd 
)

Abort unless the top of the domain is flat, i.e. z_phys_nd is the same at every node of the top slab. The top boundary is a horizontal plane: erf_slow_rhs_pre sets Omega = rho w on the top face, the anelastic projection converts it with OmegaFromW, and the two only agree when the terrain slopes vanish there. Every mesh builder pins the top node to z_top, so this guards against a mesh (custom, read from a file or a checkpoint) that does not.

Parameters
[in]levLevel index.
[in]geomGeometry information.
[in]z_phys_ndNodal physical height field.
70 {
71  const int k_top = geom.Domain().bigEnd(2) + 1;
72 
73  ReduceOps<ReduceOpMin, ReduceOpMax> reduce_op;
74  ReduceData<Real, Real> reduce_data(reduce_op);
75  using ReduceTuple = typename decltype(reduce_data)::Type;
76 
77  for (MFIter mfi(z_phys_nd); mfi.isValid(); ++mfi) {
78  const Box& bx = mfi.validbox();
79  if (bx.bigEnd(2) < k_top) { continue; }
80  auto const& z_arr = z_phys_nd.const_array(mfi);
81  reduce_op.eval(makeSlab(bx,2,k_top), reduce_data,
82  [=] AMREX_GPU_DEVICE (int i, int j, int k) -> ReduceTuple {
83  return {z_arr(i,j,k), z_arr(i,j,k)};
84  });
85  }
86 
87  ReduceTuple hv = reduce_data.value(reduce_op);
88  Real z_top_min = amrex::get<0>(hv);
89  Real z_top_max = amrex::get<1>(hv);
90  ParallelDescriptor::ReduceRealMin(z_top_min);
91  ParallelDescriptor::ReduceRealMax(z_top_max);
92 
93  // No box at this level reaches the top of the domain
94  if (z_top_min > z_top_max) { return; }
95 
96  // Same precision-aware tolerance as validate_flat_terrain
97  const Real scale = std::max({Real(1.0), std::abs(z_top_min), std::abs(z_top_max)});
98  const Real rel_tol = std::max(Real(1.0e-10), Real(100.0) * std::numeric_limits<Real>::epsilon());
99 
100  if (z_top_max - z_top_min > rel_tol * scale) {
101  Print() << "Top of the domain at level " << lev << " ranges from "
102  << z_top_min << " to " << z_top_max << std::endl;
103  Abort("The top of the domain must be flat: z_phys_nd varies along the top boundary at level "
104  + std::to_string(lev) + ".");
105  }
106 }

Referenced by ERF::update_terrain_arrays().

Here is the caller graph for this function:

◆ which_fine_terrain()

FineTerrain which_fine_terrain ( )

Translate erf.amr_terrain_refinement into a FineTerrain.

529 {
530  ParmParse pp("erf");
531  std::string amr_terrain_refinement = "interpolate";
532  pp.query("amr_terrain_refinement", amr_terrain_refinement);
533 
534  if (amr_terrain_refinement == "interpolate") {
536  } else if (amr_terrain_refinement == "transform") {
537  return FineTerrain::Transform;
538  }
539 
540  Abort("erf.amr_terrain_refinement = " + amr_terrain_refinement +
541  " is not recognized; it must be \"interpolate\" or \"transform\"");
543 }
@ Interpolate
use the coordinates interpolated from the coarse level as-is

Referenced by ERF::init_zphys(), and ERF::remake_zphys().

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