ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
erf_grid_utils Namespace Reference

Classes

struct  UniformGridMetadata
 
struct  GeometricStretch
 A geometrically stretched vertical grid: dz_k = dz0 * ratio^k. More...
 
struct  InterpolationStencil
 

Functions

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real comparison_tolerance (amrex::Real lhs, amrex::Real rhs) noexcept
 
bool nearly_equal (amrex::Real lhs, amrex::Real rhs)
 
std::string validate_uniform_axis (const amrex::Vector< amrex::Real > &coordinates, int expected_size, const std::string &axis_name, const std::string &field_description, amrex::Real &origin, amrex::Real &spacing)
 
std::string validate_matching_grid (const UniformGridMetadata &reference, const UniformGridMetadata &candidate, const std::string &reference_description, const std::string &candidate_description)
 
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE GeometricStretch geometric_stretch (amrex::Real dz0, amrex::Real z_top, int nz, amrex::Real tol) noexcept
 Solve for the geometric stretch factor that fills a domain of depth z_top with nz layers whose first is dz0 thick. More...
 
GeometricStretch build_geometric_zlevels (amrex::Vector< amrex::Real > &zlevels, amrex::Real dz0, amrex::Real z_top, amrex::Real tol)
 Fill a staggered z-level array with a geometrically stretched grid. More...
 
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE InterpolationStencil uniform_interpolation_stencil (amrex::Real coordinate, amrex::Real origin, amrex::Real spacing, int point_count) noexcept
 

Function Documentation

◆ build_geometric_zlevels()

GeometricStretch erf_grid_utils::build_geometric_zlevels ( amrex::Vector< amrex::Real > &  zlevels,
amrex::Real  dz0,
amrex::Real  z_top,
amrex::Real  tol 
)
inline

Fill a staggered z-level array with a geometrically stretched grid.

zlevels must already be sized to nz+1; its length is what sets nz. The top level is assigned z_top exactly rather than by accumulation, so the array closes on the domain top to the last bit even though the sum that got there is only accurate to tol.

Parameters
[in,out]zlevelsstaggered z-levels, sized nz+1 on entry and filled on exit
[in]dz0requested thickness of the first layer
[in]z_topdepth of the column to be filled
[in]tolconvergence tolerance passed to geometric_stretch
Returns
the stretch that was used, so the caller can report or check it
278 {
279  const int nz = static_cast<int>(zlevels.size()) - 1;
281  "build_geometric_zlevels needs a zlevels array of at least two entries");
282 
283  const GeometricStretch gs = geometric_stretch(dz0, z_top, nz, tol);
284 
285  amrex::Real dz = gs.dz0;
286  zlevels[0] = amrex::Real(0.0);
287  for (int k(1); k<=nz; ++k) {
288  zlevels[k] = zlevels[k-1] + dz;
289  dz *= gs.ratio;
290  }
291  zlevels[nz] = z_top;
292 
293  return gs;
294 }
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
@ dz
Definition: ERF_AdvanceWDM6.cpp:272
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE GeometricStretch geometric_stretch(amrex::Real dz0, amrex::Real z_top, int nz, amrex::Real tol) noexcept
Solve for the geometric stretch factor that fills a domain of depth z_top with nz layers whose first ...
Definition: ERF_GridUtils.H:212

Referenced by init_zlevels().

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

◆ comparison_tolerance()

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real erf_grid_utils::comparison_tolerance ( amrex::Real  lhs,
amrex::Real  rhs 
)
noexcept
31 {
32  amrex::Real scale = amrex::Real(1.0);
33  const amrex::Real lhs_abs = amrex::Math::abs(lhs);
34  const amrex::Real rhs_abs = amrex::Math::abs(rhs);
35  if (lhs_abs > scale) scale = lhs_abs;
36  if (rhs_abs > scale) scale = rhs_abs;
37  // NetCDF coordinate variables are commonly stored as float32 even when
38  // ERF is built with double precision. A few float32 ulps at the
39  // coordinate magnitude are therefore source-file quantization, not grid
40  // nonuniformity. The Real tolerance is retained for native Real data;
41  // the float tolerance is deliberately a small ulp-based allowance rather
42  // than an arbitrary absolute threshold.
43  const amrex::Real real_ulp =
45  const amrex::Real float_ulp =
46  amrex::Real(8.0) * static_cast<amrex::Real>(std::numeric_limits<float>::epsilon()) * scale;
47  return (real_ulp > float_ulp) ? real_ulp : float_ulp;
48 }
real(c_double), parameter epsilon
Definition: ERF_module_model_constants.F90:12

Referenced by nearly_equal(), uniform_interpolation_stencil(), and validate_uniform_axis().

Here is the caller graph for this function:

◆ geometric_stretch()

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE GeometricStretch erf_grid_utils::geometric_stretch ( amrex::Real  dz0,
amrex::Real  z_top,
int  nz,
amrex::Real  tol 
)
noexcept

Solve for the geometric stretch factor that fills a domain of depth z_top with nz layers whose first is dz0 thick.

The layer thicknesses are dz0 * ratio^k, so the column depth is the geometric sum

S(ratio) = dz0 * (ratio^nz - 1) / (ratio - 1),

and the factor wanted is the root of S(ratio) = z_top, found by Newton iteration from ratio = 1.03. The derivative is analytic, so the iteration converges in a handful of passes for any physically sensible (dz0, z_top, nz).

This is the natural construction for an idealized case, where dz0 and z_top are what the user knows a priori and the vertical distribution between them is free. It is not the right construction when a file already carries a vertical distribution worth keeping – see wrf_mean_layer_thickness in ERF_InitFromWRFInput.cpp – because one constant factor cannot represent a stretch ratio that varies with height.

A dz0 at or above the uniform spacing z_top/nz cannot be stretched to fit, since the thicknesses only grow: that case returns the uniform grid with uniform = true, rather than a factor below one that would thin the layers aloft. A single layer is the same situation for a different reason – the sum of one layer is dz0 whatever the factor is, so S has no root and its derivative is identically zero – and is answered the same way.

Parameters
[in]dz0requested thickness of the first layer
[in]z_topdepth of the column to be filled
[in]nznumber of layers
[in]tolconvergence tolerance on the column depth, in the same units as z_top
Returns
the stretch factor, the first-layer thickness used, and whether it converged
216 {
217  GeometricStretch gs;
218 
219  const amrex::Real Nz = static_cast<amrex::Real>(nz);
220  if (nz < 1 || !(z_top > amrex::Real(0.0)) || !(dz0 > amrex::Real(0.0))) {
221  return gs;
222  }
223 
224  // Default to the uniform grid, which is also the answer whenever the requested first
225  // layer is already at least as thick as the uniform spacing, and the only grid there
226  // is when the column holds a single layer.
227  if (nz == 1 || dz0 >= z_top/Nz) {
228  gs.dz0 = z_top/Nz;
229  gs.ratio = amrex::Real(1.0);
230  gs.residual = amrex::Real(0.0);
231  gs.uniform = true;
232  gs.converged = true;
233  return gs;
234  }
235 
236  amrex::Real SFact = amrex::Real(1.03);
237  amrex::Real F = dz0 * ( (std::pow(SFact,Nz) - amrex::Real(1.0)) / (SFact - amrex::Real(1.0)) ) - z_top;
238 
239  const int max_iter = 50;
240  int iter = 0;
241  while (amrex::Math::abs(F) > tol && iter < max_iter) {
242  const amrex::Real dFdSF = dz0 * ( Nz * std::pow(SFact,Nz-amrex::Real(1.0)) * (SFact - amrex::Real(1.0))
243  - std::pow(SFact,Nz) + amrex::Real(1.0) ) /
244  std::pow(SFact-amrex::Real(1.0),amrex::Real(2.0));
245  SFact -= F/dFdSF;
246  const amrex::Real SFact_min = amrex::Real(1.0) + tol;
247  SFact = (SFact > SFact_min) ? SFact : SFact_min;
248  F = dz0 * ( (std::pow(SFact,Nz) - amrex::Real(1.0)) / (SFact - amrex::Real(1.0)) ) - z_top;
249  ++iter;
250  }
251 
252  gs.dz0 = dz0;
253  gs.ratio = SFact;
254  gs.residual = F;
255  gs.uniform = false;
256  gs.converged = (amrex::Math::abs(F) <= tol);
257  return gs;
258 }

Referenced by build_geometric_zlevels().

Here is the caller graph for this function:

◆ nearly_equal()

bool erf_grid_utils::nearly_equal ( amrex::Real  lhs,
amrex::Real  rhs 
)
inline
52 {
53  return std::abs(lhs - rhs) <= comparison_tolerance(lhs, rhs);
54 }
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real comparison_tolerance(amrex::Real lhs, amrex::Real rhs) noexcept
Definition: ERF_GridUtils.H:30

Referenced by validate_matching_grid().

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

◆ uniform_interpolation_stencil()

AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE InterpolationStencil erf_grid_utils::uniform_interpolation_stencil ( amrex::Real  coordinate,
amrex::Real  origin,
amrex::Real  spacing,
int  point_count 
)
noexcept
309 {
310  InterpolationStencil stencil;
311  if (point_count < 2 || spacing <= amrex::Real(0.0)) {
312  return stencil;
313  }
314 
315  const amrex::Real upper = origin + static_cast<amrex::Real>(point_count - 1) * spacing;
316  const amrex::Real lower_tolerance = comparison_tolerance(coordinate, origin);
317  const amrex::Real upper_tolerance = comparison_tolerance(coordinate, upper);
318 
319  if (coordinate < origin - lower_tolerance || coordinate > upper + upper_tolerance) {
320  return stencil;
321  }
322 
323  const amrex::Real bounded_coordinate =
324  (coordinate <= origin + lower_tolerance) ? origin :
325  ((coordinate >= upper - upper_tolerance) ? upper : coordinate);
326  const amrex::Real fractional_index = (bounded_coordinate - origin) / spacing;
327  int lower = static_cast<int>(amrex::Math::floor(fractional_index));
328  lower = (lower < point_count - 1) ? lower : point_count - 2;
329 
330  amrex::Real weight = fractional_index - static_cast<amrex::Real>(lower);
331  weight = (weight < amrex::Real(0.0)) ? amrex::Real(0.0) :
332  ((weight > amrex::Real(1.0)) ? amrex::Real(1.0) : weight);
333 
334  stencil.lower = lower;
335  stencil.weight = weight;
336  stencil.inside = true;
337  return stencil;
338 }

Referenced by ForestDrag::define_drag_field().

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

◆ validate_matching_grid()

std::string erf_grid_utils::validate_matching_grid ( const UniformGridMetadata &  reference,
const UniformGridMetadata &  candidate,
const std::string &  reference_description,
const std::string &  candidate_description 
)
inline
133 {
134  if (candidate.nx != reference.nx || candidate.ny != reference.ny) {
135  return candidate_description + " dimensions (" + std::to_string(candidate.nx) +
136  " x " + std::to_string(candidate.ny) + ") do not match " +
137  reference_description + " dimensions (" + std::to_string(reference.nx) +
138  " x " + std::to_string(reference.ny) + ")";
139  }
140 
141  struct MetadataValue {
142  const char* name;
143  amrex::Real reference_value;
144  amrex::Real candidate_value;
145  };
146  const MetadataValue values[] = {
147  {"x spacing", reference.dx, candidate.dx},
148  {"y spacing", reference.dy, candidate.dy},
149  {"x origin", reference.xmin, candidate.xmin},
150  {"y origin", reference.ymin, candidate.ymin}
151  };
152  for (const auto& value : values) {
153  if (!nearly_equal(value.reference_value, value.candidate_value)) {
154  std::ostringstream message;
155  message << candidate_description << " " << value.name << " ("
156  << value.candidate_value << ") does not match "
157  << reference_description << " (" << value.reference_value << ")";
158  return message.str();
159  }
160  }
161 
162  return {};
163 }
std::string name
Definition: ERF_Plotfile2DCatalog.cpp:105
bool nearly_equal(amrex::Real lhs, amrex::Real rhs)
Definition: ERF_GridUtils.H:51

Referenced by ForestDrag::ForestDrag().

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

◆ validate_uniform_axis()

std::string erf_grid_utils::validate_uniform_axis ( const amrex::Vector< amrex::Real > &  coordinates,
int  expected_size,
const std::string &  axis_name,
const std::string &  field_description,
amrex::Real &  origin,
amrex::Real &  spacing 
)
inline
63 {
64  if (expected_size < 2) {
65  return field_description + " requires at least two " + axis_name +
66  " coordinates for bilinear interpolation";
67  }
68  if (coordinates.size() != static_cast<amrex::Long>(expected_size)) {
69  return field_description + " has " + axis_name + " coordinate length " +
70  std::to_string(coordinates.size()) + ", expected " +
71  std::to_string(expected_size);
72  }
73 
74  origin = coordinates.front();
75  // Derive the spacing from the two endpoints rather than from the first
76  // interval alone. The returned origin/spacing pair is what
77  // uniform_interpolation_stencil uses to reconstruct the axis as
78  // origin + index*spacing, so a first interval that is rounded slightly
79  // short or long would otherwise accumulate that error over the whole axis
80  // -- for a float32 axis with many points the reconstructed far end can
81  // drift by (point_count-1) ulps, which is large enough to shift the
82  // stencil by a cell near the upper edge. Averaging over the full extent
83  // pins both endpoints instead. A non-finite last coordinate propagates
84  // into spacing and is caught by the isfinite check below.
85  spacing = (coordinates[expected_size-1] - coordinates.front()) /
86  static_cast<amrex::Real>(expected_size - 1);
87  if (!std::isfinite(origin) || !std::isfinite(spacing) || spacing <= amrex::Real(0.0)) {
88  return field_description + " must have finite, strictly increasing " +
89  axis_name + " coordinates with nonzero spacing";
90  }
91 
92  for (int index = 1; index < expected_size; ++index) {
93  const amrex::Real coordinate = coordinates[index];
94  if (!std::isfinite(coordinate) || coordinate <= coordinates[index-1]) {
95  return field_description + " must have finite, strictly increasing " +
96  axis_name + " coordinates";
97  }
98  // Compare each interval against the mean spacing instead of
99  // accumulating the first spacing over the whole axis. This avoids
100  // rejecting a valid float32 axis because each stored coordinate was
101  // rounded independently, while still rejecting materially nonuniform
102  // grids.
103  //
104  // The tolerance is the *larger* of the two allowances, not the
105  // smaller: the ulp term covers source-file quantization, which scales
106  // with the coordinate magnitude, while the fraction-of-spacing term
107  // covers grids whose coordinates are small enough that a few ulps are
108  // negligible. Taking the minimum cancels the ulp allowance in exactly
109  // the case it exists for -- a fine grid at a large offset. Concretely,
110  // at a UTM easting near 5e5 the float32 ulp is 0.03125 m, so a 0.4 m
111  // raster stores intervals that deviate from the nominal spacing by up
112  // to 0.025 m: more than 5% of the spacing, yet far below the ulp
113  // allowance. Integer-metre spacings are unaffected either way, since
114  // integers are exactly representable in float32 at UTM magnitudes.
115  const amrex::Real local_spacing = coordinate - coordinates[index-1];
116  const amrex::Real spacing_tolerance = std::max(
117  amrex::Real(0.05) * std::abs(spacing),
118  comparison_tolerance(coordinate, coordinates[index-1]));
119  if (std::abs(local_spacing - spacing) > spacing_tolerance) {
120  return field_description + " has nonuniform " + axis_name +
121  " coordinates, which are not supported";
122  }
123  }
124 
125  return {};
126 }
Here is the call graph for this function: