ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
ERF_GridUtils.H
Go to the documentation of this file.
1 #ifndef ERF_GRID_UTILS_H_
2 #define ERF_GRID_UTILS_H_
3 
4 #include <algorithm>
5 #include <cmath>
6 #include <limits>
7 #include <sstream>
8 #include <string>
9 
10 #include <AMReX.H>
11 #include <AMReX_GpuQualifiers.H>
12 #include <AMReX_Math.H>
13 #include <AMReX_REAL.H>
14 #include <AMReX_Vector.H>
15 
16 namespace erf_grid_utils {
17 
19 {
20  int nx{0};
21  int ny{0};
26 };
27 
28 AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
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 }
49 
50 inline bool
52 {
53  return std::abs(lhs - rhs) <= comparison_tolerance(lhs, rhs);
54 }
55 
56 inline std::string
57 validate_uniform_axis (const amrex::Vector<amrex::Real>& coordinates,
58  int expected_size,
59  const std::string& axis_name,
60  const std::string& field_description,
61  amrex::Real& origin,
62  amrex::Real& spacing)
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 }
127 
128 inline std::string
130  const UniformGridMetadata& candidate,
131  const std::string& reference_description,
132  const std::string& candidate_description)
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 }
164 
165 /**
166  * \brief A geometrically stretched vertical grid: dz_k = dz0 * ratio^k.
167  *
168  * The three numbers that define the grid, plus whether the solve that produced them
169  * actually converged, so a caller can fail loudly instead of using a bad factor.
170  */
172 {
173  amrex::Real dz0{amrex::Real(0.0)}; //!< thickness of the first layer, as actually used
174  amrex::Real ratio{amrex::Real(1.0)}; //!< layer-to-layer stretch factor
175  amrex::Real residual{amrex::Real(0.0)}; //!< sum of the layers minus z_top, at exit
176  bool uniform{false}; //!< true when the requested dz0 could not be honored
177  bool converged{false}; //!< true when |residual| <= tol
178 };
179 
180 /**
181  * \brief Solve for the geometric stretch factor that fills a domain of depth z_top with
182  * nz layers whose first is dz0 thick.
183  *
184  * The layer thicknesses are dz0 * ratio^k, so the column depth is the geometric sum
185  *
186  * S(ratio) = dz0 * (ratio^nz - 1) / (ratio - 1),
187  *
188  * and the factor wanted is the root of S(ratio) = z_top, found by Newton iteration from
189  * ratio = 1.03. The derivative is analytic, so the iteration converges in a handful of
190  * passes for any physically sensible (dz0, z_top, nz).
191  *
192  * This is the natural construction for an **idealized** case, where dz0 and z_top are what
193  * the user knows a priori and the vertical distribution between them is free. It is not
194  * the right construction when a file already carries a vertical distribution worth keeping
195  * -- see wrf_mean_layer_thickness in ERF_InitFromWRFInput.cpp -- because one constant factor
196  * cannot represent a stretch ratio that varies with height.
197  *
198  * A dz0 at or above the uniform spacing z_top/nz cannot be stretched to fit, since the
199  * thicknesses only grow: that case returns the uniform grid with uniform = true, rather
200  * than a factor below one that would thin the layers aloft. A single layer is the same
201  * situation for a different reason -- the sum of one layer is dz0 whatever the factor is,
202  * so S has no root and its derivative is identically zero -- and is answered the same way.
203  *
204  * @param[in] dz0 requested thickness of the first layer
205  * @param[in] z_top depth of the column to be filled
206  * @param[in] nz number of layers
207  * @param[in] tol convergence tolerance on the column depth, in the same units as z_top
208  * @return the stretch factor, the first-layer thickness used, and whether it converged
209  */
210 AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
211 GeometricStretch
213  amrex::Real z_top,
214  int nz,
215  amrex::Real tol) noexcept
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 }
259 
260 /**
261  * \brief Fill a staggered z-level array with a geometrically stretched grid.
262  *
263  * zlevels must already be sized to nz+1; its length is what sets nz. The top level is
264  * assigned z_top exactly rather than by accumulation, so the array closes on the domain
265  * top to the last bit even though the sum that got there is only accurate to tol.
266  *
267  * @param[in,out] zlevels staggered z-levels, sized nz+1 on entry and filled on exit
268  * @param[in] dz0 requested thickness of the first layer
269  * @param[in] z_top depth of the column to be filled
270  * @param[in] tol convergence tolerance passed to geometric_stretch
271  * @return the stretch that was used, so the caller can report or check it
272  */
273 inline GeometricStretch
274 build_geometric_zlevels (amrex::Vector<amrex::Real>& zlevels,
275  amrex::Real dz0,
276  amrex::Real z_top,
277  amrex::Real tol)
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 }
295 
297 {
298  int lower{0};
300  bool inside{false};
301 };
302 
303 AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
304 InterpolationStencil
306  amrex::Real origin,
307  amrex::Real spacing,
308  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 }
339 
340 } // namespace erf_grid_utils
341 
342 #endif
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
std::string name
Definition: ERF_Plotfile2DCatalog.cpp:105
amrex::Real Real
Definition: ERF_ShocInterface.H:19
@ dz
Definition: ERF_AdvanceWDM6.cpp:272
Definition: ERF_GridUtils.H:16
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)
Definition: ERF_GridUtils.H:57
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.
Definition: ERF_GridUtils.H:274
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
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE InterpolationStencil uniform_interpolation_stencil(amrex::Real coordinate, amrex::Real origin, amrex::Real spacing, int point_count) noexcept
Definition: ERF_GridUtils.H:305
bool nearly_equal(amrex::Real lhs, amrex::Real rhs)
Definition: ERF_GridUtils.H:51
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real comparison_tolerance(amrex::Real lhs, amrex::Real rhs) noexcept
Definition: ERF_GridUtils.H:30
std::string validate_matching_grid(const UniformGridMetadata &reference, const UniformGridMetadata &candidate, const std::string &reference_description, const std::string &candidate_description)
Definition: ERF_GridUtils.H:129
real(c_double), parameter epsilon
Definition: ERF_module_model_constants.F90:12
A geometrically stretched vertical grid: dz_k = dz0 * ratio^k.
Definition: ERF_GridUtils.H:172
amrex::Real ratio
layer-to-layer stretch factor
Definition: ERF_GridUtils.H:174
amrex::Real residual
sum of the layers minus z_top, at exit
Definition: ERF_GridUtils.H:175
bool converged
true when |residual| <= tol
Definition: ERF_GridUtils.H:177
bool uniform
true when the requested dz0 could not be honored
Definition: ERF_GridUtils.H:176
amrex::Real dz0
thickness of the first layer, as actually used
Definition: ERF_GridUtils.H:173
Definition: ERF_GridUtils.H:297
int lower
Definition: ERF_GridUtils.H:298
bool inside
Definition: ERF_GridUtils.H:300
amrex::Real weight
Definition: ERF_GridUtils.H:299
Definition: ERF_GridUtils.H:19
amrex::Real dy
Definition: ERF_GridUtils.H:23
int nx
Definition: ERF_GridUtils.H:20
int ny
Definition: ERF_GridUtils.H:21
amrex::Real dx
Definition: ERF_GridUtils.H:22
amrex::Real ymin
Definition: ERF_GridUtils.H:25
amrex::Real xmin
Definition: ERF_GridUtils.H:24