• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sxs-collaboration / spectre / 4200690155

pending completion
4200690155

push

github

GitHub
Merge pull request #4747 from knelli2/excision_sphere_name

9 of 9 new or added lines in 3 files covered. (100.0%)

63672 of 66385 relevant lines covered (95.91%)

383640.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.36
/src/Domain/CoordinateMaps/UniformCylindricalFlatEndcap.cpp
1
// Distributed under the MIT License.
2
// See LICENSE.txt for details.
3

4
#include "Domain/CoordinateMaps/UniformCylindricalFlatEndcap.hpp"
5

6
#include <boost/math/special_functions/sign.hpp>
7
#include <cmath>
8
#include <limits>
9
#include <optional>
10
#include <pup.h>
11
#include <sstream>
12
#include <utility>
13

14
#include "DataStructures/Tensor/EagerMath/DeterminantAndInverse.hpp"
15
#include "DataStructures/Tensor/Tensor.hpp"
16
#include "Domain/CoordinateMaps/CylindricalEndcapHelpers.hpp"
17
#include "NumericalAlgorithms/RootFinding/TOMS748.hpp"
18
#include "Parallel/PupStlCpp11.hpp"
19
#include "Utilities/ConstantExpressions.hpp"
20
#include "Utilities/DereferenceWrapper.hpp"
21
#include "Utilities/EqualWithinRoundoff.hpp"
22
#include "Utilities/GenerateInstantiations.hpp"
23

24
namespace domain::CoordinateMaps {
25

26
namespace {
27

28
// The function that the inverse map needs to root-find to find rhobar.
29
double this_function_is_zero_for_correct_rhobar(
5,737,180✔
30
    const double rhobar, const std::array<double, 3>& center_one,
31
    const std::array<double, 3>& center_two, double radius_one,
32
    const double radius_two, const double theta_max_one,
33
    const std::array<double, 3>& target_coords) {
34
  const double r_one_cos_theta_one = radius_one * cos(rhobar * theta_max_one);
5,737,180✔
35
  const double lambda =
36
      (target_coords[2] - center_one[2] - r_one_cos_theta_one) /
5,737,180✔
37
      (center_two[2] - center_one[2] - r_one_cos_theta_one);
5,737,180✔
38
  return square(target_coords[0] - center_one[0] -
5,737,180✔
39
                lambda * (center_two[0] - center_one[0])) +
5,737,180✔
40
         square(target_coords[1] - center_one[1] -
5,737,180✔
41
                lambda * (center_two[1] - center_one[1])) -
5,737,180✔
42
         square((1.0 - lambda) * radius_one * sin(rhobar * theta_max_one) +
11,474,400✔
43
                lambda * radius_two * rhobar);
11,474,400✔
44
}
45

46
// min and max values of rhobar in the inverse function.
47
std::array<double, 2> rhobar_min_max(
5,912✔
48
    const std::array<double, 3>& center_one, const double radius_one,
49
    const double theta_max_one, const std::array<double, 3>& target_coords) {
50
  // Choose the minimum value of rhobar so that lambda >=0, where
51
  // lambda is the quantity inside function_to_zero.  Note that the
52
  // denominator of lambda inside that function is always positive.
53
  const double rhobar_min =
54
      target_coords[2] - center_one[2] >= radius_one
5,912✔
55
          ? 0.0
5,912✔
56
          : acos((target_coords[2] - center_one[2]) / radius_one) /
5,796✔
57
                theta_max_one;
5,912✔
58
  return {rhobar_min, 1.0};
5,912✔
59
}
60

61
// Returns whether the map is invertible if the target is on sphere_one.
62
bool is_invertible_for_target_on_sphere_one(
5,800✔
63
    const std::array<double, 3>& center_one,
64
    const std::array<double, 3>& center_two, const double radius_one,
65
    const double radius_two, const double theta_max_one,
66
    const std::array<double, 3>& target_coords) {
67
  const auto [rhobar_min, rhobar_max] =
5,800✔
68
      rhobar_min_max(center_one, radius_one, theta_max_one, target_coords);
5,800✔
69
  if (equal_within_roundoff(rhobar_min, rhobar_max)) {
11,600✔
70
    // Special case where rhobar_min == rhobar_max to roundoff.  This
71
    // case occurs when the target point (assumed to be on sphere_one)
72
    // is at rhobar=1.  In this case there is nothing to test because
73
    // there is only one possible value of rhobar, so there cannot be
74
    // two roots for two different values of rhobar.
75
    return true;
58✔
76
  }
77
  const size_t num_pts = 1000;
5,742✔
78
  const double drhobar = (rhobar_max - rhobar_min) / (num_pts - 1.0);
5,742✔
79
  // Because the target point is on sphere_one, the only root should
80
  // be at rhobar = rhobar_min, where lambda=0.  The actual value of
81
  // the function at rhobar_min may be either sign because of
82
  // roundoff.  So ignore that point, and start at the next point.
83
  // Keep track of the sign, and look for sign changes as we progress
84
  // from point to point.
85
  std::optional<double> sign;
5,742✔
86
  for (size_t i = 1; i < num_pts; ++i) {
5,742,000✔
87
    const double rhobar = rhobar_min + i * drhobar;
5,736,260✔
88
    const double func = this_function_is_zero_for_correct_rhobar(
5,736,260✔
89
        rhobar, center_one, center_two, radius_one, radius_two, theta_max_one,
90
        target_coords);
91
    if (not sign.has_value()) {
5,736,260✔
92
      sign = func > 0.0 ? 1.0 : -1.0;
5,742✔
93
    } else if (func * sign.value() <= 0.0) {
5,730,520✔
94
      // There is another root, so function is not invertible.
95
      //
96
      // Note that the code should rarely get here if the map
97
      // parameters are in range, so it is difficult to add an error
98
      // test because I don't know how to make it get here on purpose;
99
      // therefore we turn off codecov.
100
      return false; // LCOV_EXCL_LINE
101
    }
102
  }
103
  return true;  // No roots found.
5,742✔
104
}
105
}  // namespace
106

107
bool is_uniform_cylindrical_flat_endcap_invertible_on_sphere_one(
59✔
108
    const std::array<double, 3>& center_one,
109
    const std::array<double, 3>& center_two, double radius_one,
110
    double radius_two, double theta_max_one) {
111
  if (center_one[1] == center_two[1] and center_one[0] == center_two[0]) {
59✔
112
    return true;
1✔
113
  }
114
  // Choose the worst direction, which is the direction of x-y-plane
115
  // projection of the difference between center_one and center_two.
116
  const double phi =
117
      atan2(center_one[1] - center_two[1], center_one[0] - center_two[0]);
58✔
118
  const size_t num_pts = 100;
58✔
119
  for (size_t i = 0; i < num_pts; ++i) {
5,858✔
120
    const double theta_one = theta_max_one * (i / (num_pts - 1.0));
5,800✔
121
    if (not is_invertible_for_target_on_sphere_one(
5,800✔
122
            center_one, center_two, radius_one, radius_two, theta_max_one,
123
            {{center_one[0] + radius_one * sin(theta_one) * cos(phi),
5,800✔
124
              center_one[1] + radius_one * sin(theta_one) * sin(phi),
5,800✔
125
              center_one[2] + radius_one * cos(theta_one)}})) {
5,800✔
126
      // Note that the code should rarely get here if the map
127
      // parameters are in range, so it is difficult to add an error
128
      // test because I don't know how to make it get here on purpose;
129
      // therefore we turn off codecov.
130
      return false; // LCOV_EXCL_LINE
131
    }
132
  }
133
  return true;
58✔
134
}
135

136
UniformCylindricalFlatEndcap::UniformCylindricalFlatEndcap(
57✔
137
    const std::array<double, 3>& center_one,
138
    const std::array<double, 3>& center_two, double radius_one,
139
    double radius_two, double z_plane_one)
57✔
140
    : center_one_(center_one),
141
      center_two_(center_two),
142
      radius_one_([&radius_one]() {
228✔
143
        // The equal_within_roundoff below has an implicit scale of 1,
144
        // so the ASSERT may trigger in the case where we really
145
        // want an entire domain that is very small.
146
        ASSERT(not equal_within_roundoff(radius_one, 0.0),
114✔
147
               "Cannot have zero radius_one");
148
        ASSERT(radius_one > 0.0, "Cannot have negative radius_one");
57✔
149
        return radius_one;
57✔
150
        // codecov doesn't understand lambdas in constructors for some
151
        // reason, and says the next line is not covered even though it is.
152
      }()), // LCOV_EXCL_LINE
153
      radius_two_([&radius_two]() {
228✔
154
        // The equal_within_roundoff below has an implicit scale of 1,
155
        // so the ASSERT may trigger in the case where we really
156
        // want an entire domain that is very small.
157
        ASSERT(not equal_within_roundoff(radius_two, 0.0),
114✔
158
               "Cannot have zero radius_two");
159
        ASSERT(radius_two > 0.0, "Cannot have negative radius_two");
57✔
160
        return radius_two;
57✔
161
        // codecov doesn't understand lambdas in constructors for some
162
        // reason, and says the next line is not covered even though it is.
163
      }()), // LCOV_EXCL_LINE
164
      z_plane_one_(z_plane_one),
165
      theta_max_one_([&center_one, &radius_one, &z_plane_one]() {
114✔
166
        const double cos_theta_max = (z_plane_one - center_one[2]) / radius_one;
57✔
167
        ASSERT(abs(cos_theta_max) < 1.0,
57✔
168
               "Plane one must intersect sphere_one, and at more than one "
169
               "point. You probably specified a bad value of z_plane_one, "
170
               "radius_one, or the z component of center_one. "
171
               "Here z_plane_one="
172
                   << z_plane_one << ", radius_one = " << radius_one
173
                   << ", center_one=" << center_one
174
                   << ", and cos_theta_max = " << cos_theta_max);
175
        return acos(cos_theta_max);
57✔
176
      }()) {
57✔
177
  // The code below defines several variables that are used only in ASSERTs.
178
  // We put that code in a #ifdef SPECTRE_DEBUG to avoid clang-tidy complaining
179
  // about unused variables in release mode.
180
#ifdef SPECTRE_DEBUG
181

182
  // For some reason, codecov thinks that the following lambda never
183
  // gets called, even though it is in all of the ASSERT messages below.
184
  // LCOV_EXCL_START
185
  const auto param_string = [this]() -> std::string {
186
    std::ostringstream buffer;
187
    buffer << "\nParameters to UniformCylindricalFlatEndcap:\nradius_one="
188
           << radius_one_ << "\nradius_two=" << radius_two_
189
           << "\ncenter_one=" << center_one_ << "\ncenter_two=" << center_two_
190
           << "\nz_plane_one=" << z_plane_one_;
191
    return buffer.str();
192
  };
193
  // LCOV_EXCL_STOP
194

195
  // Assumptions made in the map.  The exact numbers for all of these
196
  // can be changed, as long as the unit test is changed to test them.
197
  ASSERT(center_two[2] >= center_one[2] + 1.05 * radius_one,
57✔
198
         "center_two[2] must be >= center_one[2] + 1.05 * radius_one, not "
199
             << center_two[2] << " " << center_one[2] + 1.05 * radius_one
200
             << param_string());
201
  ASSERT(center_two[2] <= center_one[2] + 5.0 * radius_one,
57✔
202
         "center_two[2] must be <= center_one[2] + 5.0 * radius_one, not "
203
             << center_two[2] << " " << center_one[2] + 5.0 * radius_one
204
             << param_string());
205
  ASSERT(radius_two >= 0.1 * radius_one * sin(theta_max_one_),
57✔
206
         "radius_two is too small: " << radius_two << " "
207
                                     << 0.1 * radius_one * sin(theta_max_one_)
208
                                     << param_string());
209
  ASSERT(theta_max_one_ < M_PI * 0.35,
57✔
210
         "z_plane_one is too close to the center of sphere_one: theta/pi = "
211
             << theta_max_one_ / M_PI << param_string());
212
  ASSERT(theta_max_one_ > M_PI * 0.075,
57✔
213
         "z_plane_one is too far from the center of sphere_one: theta/pi = "
214
             << theta_max_one_ / M_PI << param_string());
215
  ASSERT(
57✔
216
      is_uniform_cylindrical_flat_endcap_invertible_on_sphere_one(
217
          center_one_, center_two_, radius_one_, radius_two_, theta_max_one_),
218
      "The map is not invertible at at least one point on sphere_one."
219
      " center_one = "
220
          << center_one_ << " center_two = " << center_two_
221
          << " radius_one = " << radius_one_ << " radius_two = " << radius_two_
222
          << " theta_max_one = " << theta_max_one_ << param_string());
223

224
  const double horizontal_dist_spheres =
225
      sqrt(square(center_one[0] - center_two[0]) +
57✔
226
           square(center_one[1] - center_two[1]));
57✔
227

228
  ASSERT(horizontal_dist_spheres <= radius_one_ * sin(theta_max_one_),
57✔
229
         "Horizontal distance between centers is too large: "
230
             << horizontal_dist_spheres << " "
231
             << radius_one_ * sin(theta_max_one_) << param_string());
232

233
  // max_horizontal_dist_between_circles can be either sign;
234
  // therefore alpha can be in either the first or second quadrant.
235
  const double max_horizontal_dist_between_circles =
57✔
236
      horizontal_dist_spheres + radius_one_ * sin(theta_max_one_) - radius_two_;
57✔
237

238
  const double alpha =
239
      atan2(center_two[2] - z_plane_one, max_horizontal_dist_between_circles);
57✔
240
  ASSERT(alpha > 1.1 * theta_max_one_,
57✔
241
         "Angle alpha is too small: alpha = "
242
             << alpha << ", theta_max_one = " << theta_max_one_
243
             << ", max_horizontal_dist_between_circles = "
244
             << max_horizontal_dist_between_circles
245
             << ", horizontal_dist_spheres = " << horizontal_dist_spheres
246
             << param_string());
247
#endif
248
}
57✔
249

250
template <typename T>
251
std::array<tt::remove_cvref_wrap_t<T>, 3>
252
UniformCylindricalFlatEndcap::operator()(
1,157,866✔
253
    const std::array<T, 3>& source_coords) const {
254
  using ReturnType = tt::remove_cvref_wrap_t<T>;
255
  const ReturnType& xbar = source_coords[0];
1,157,866✔
256
  const ReturnType& ybar = source_coords[1];
1,157,866✔
257
  const ReturnType& zbar = source_coords[2];
1,157,866✔
258
  std::array<ReturnType, 3> target_coords{};
1,157,866✔
259
  ReturnType& x = target_coords[0];
1,157,866✔
260
  ReturnType& y = target_coords[1];
1,157,866✔
261
  ReturnType& z = target_coords[2];
1,157,866✔
262

263
  // Use y and z as temporary storage to avoid allocations,
264
  // before setting them to their actual values.
265
  z = sqrt(square(xbar) + square(ybar));  // rhobar in the dox
1,159,970✔
266
  y = radius_one_ *
2,315,732✔
267
          cylindrical_endcap_helpers::sin_ax_over_x(z, theta_max_one_) *
1,159,970✔
268
          (1.0 - zbar) +
1,162,074✔
269
      radius_two_ * (1.0 + zbar);
1,157,866✔
270

271
  x = 0.5 * (y * xbar + center_one_[0] * (1.0 - zbar) +
1,157,866✔
272
             center_two_[0] * (1.0 + zbar));
1,157,866✔
273
  y = 0.5 * (y * ybar + center_one_[1] * (1.0 - zbar) +
1,157,866✔
274
             center_two_[1] * (1.0 + zbar));
1,157,866✔
275
  z = 0.5 *
1,157,866✔
276
      ((radius_one_ * cos(theta_max_one_ * z) + center_one_[2]) * (1.0 - zbar) +
1,157,866✔
277
       center_two_[2] * (1.0 + zbar));
1,157,866✔
278
  return target_coords;
1,157,866✔
279
}
280

281
std::optional<std::array<double, 3>> UniformCylindricalFlatEndcap::inverse(
116✔
282
    const std::array<double, 3>& target_coords) const {
283
  // First do some easy checks that target_coords is in the range
284
  // of the map.  Need to accept points that are out of range by roundoff.
285

286
  // check z >= z_plane_one_
287
  if (target_coords[2] < z_plane_one_ and
117✔
288
      not equal_within_roundoff(target_coords[2], z_plane_one_)) {
2✔
289
    return std::nullopt;
1✔
290
  }
291

292
  // check that target point is at smaller z than circle_two
293
  if (target_coords[2] > center_two_[2] and
136✔
294
      not equal_within_roundoff(target_coords[2], center_two_[2])) {
42✔
295
    return std::nullopt;
1✔
296
  }
297

298
  // check that point is outside or on sphere_one_
299
  const double r_minus_center_one_squared =
300
      square(target_coords[0] - center_one_[0]) +
114✔
301
      square(target_coords[1] - center_one_[1]) +
114✔
302
      square(target_coords[2] - center_one_[2]);
114✔
303
  if (r_minus_center_one_squared < square(radius_one_) and
249✔
304
      not equal_within_roundoff(sqrt(r_minus_center_one_squared),
135✔
305
                                radius_one_)) {
21✔
306
    // sqrt above because we don't want to change the scale of
307
    // equal_within_roundoff too much.
308
    return std::nullopt;
1✔
309
  }
310

311
  // Check if the point is inside the cone
312
  const double lambda_tilde =
313
      (target_coords[2] - z_plane_one_) / (center_two_[2] - z_plane_one_);
113✔
314
  const double rho_tilde =
315
      sqrt(square(target_coords[0] - center_one_[0] -
113✔
316
                  lambda_tilde * (center_two_[0] - center_one_[0])) +
113✔
317
           square(target_coords[1] - center_one_[1] -
113✔
318
                  lambda_tilde * (center_two_[1] - center_one_[1])));
113✔
319
  const double circle_radius =
113✔
320
      radius_one_ * sin(theta_max_one_) * (1.0 - lambda_tilde) +
113✔
321
      radius_two_ * lambda_tilde;
113✔
322
  if (rho_tilde > circle_radius and
114✔
323
      not equal_within_roundoff(rho_tilde, circle_radius,
2✔
324
                                std::numeric_limits<double>::epsilon() * 100.0,
1✔
325
                                radius_two_)) {
1✔
326
    return std::nullopt;
1✔
327
  }
328

329
  // To find rhobar we will do numerical root finding.
330
  // function_to_zero is the function that is zero when rhobar has the
331
  // correct value.
332
  const auto function_to_zero = [this, &target_coords](const double rhobar) {
923✔
333
    return this_function_is_zero_for_correct_rhobar(
923✔
334
        rhobar, center_one_, center_two_, radius_one_, radius_two_,
923✔
335
        theta_max_one_, target_coords);
923✔
336
  };
112✔
337

338
  // Derivative of function_to_zero with respect to rhobar.
339
  const auto deriv_function_to_zero = [this,
20✔
340
                                       &target_coords](const double rhobar) {
380✔
341
    const double r_one_cos_theta_one =
20✔
342
        radius_one_ * cos(rhobar * theta_max_one_);
20✔
343
    const double lambda =
344
        (target_coords[2] - center_one_[2] - r_one_cos_theta_one) /
20✔
345
        (center_two_[2] - center_one_[2] - r_one_cos_theta_one);
20✔
346
    // deriv of r_one_cos_theta_one with respect to rho.
347
    const double d_r_one_cos_theta_one =
20✔
348
        radius_one_ * sin(rhobar * theta_max_one_) * theta_max_one_;
20✔
349
    const double dlambda_drho =
350
        d_r_one_cos_theta_one * (1.0 - lambda) /
20✔
351
        (center_two_[2] - center_one_[2] - r_one_cos_theta_one);
20✔
352
    return -2.0 * dlambda_drho *
40✔
353
               ((center_two_[0] - center_one_[0]) *
20✔
354
                    (target_coords[0] - center_one_[0] -
20✔
355
                     lambda * (center_two_[0] - center_one_[0])) +
20✔
356
                (center_two_[1] - center_one_[1]) *
20✔
357
                    (target_coords[1] - center_one_[1] -
20✔
358
                     lambda * (center_two_[1] - center_one_[1]))) -
20✔
359
           2.0 *
20✔
360
               ((1.0 - lambda) * radius_one_ * sin(rhobar * theta_max_one_) +
20✔
361
                lambda * radius_two_ * rhobar) *
20✔
362
               ((radius_two_ * rhobar -
20✔
363
                 radius_one_ * sin(rhobar * theta_max_one_)) *
20✔
364
                    dlambda_drho +
20✔
365
                (1.0 - lambda) * r_one_cos_theta_one * theta_max_one_ +
20✔
366
                lambda * radius_two_);
20✔
367
  };
112✔
368

369
  // Non-const because might be changed below.
370
  auto [rhobar_min, rhobar_max] =
112✔
371
      rhobar_min_max(center_one_, radius_one_, theta_max_one_, target_coords);
112✔
372

373
  // If rhobar is zero, then the root finding doesn't converge
374
  // well. This is because the function behaves like rhobar^2 for
375
  // small values of rhobar, so both the function and its derivative
376
  // are zero at rhobar==0.
377
  //
378
  // Note that rhobar==0 is not that uncommon, and will occur if there
379
  // are grid points on the symmetry axis.
380
  //
381
  // Similarly, rhobar==1 occurs if there are grid points on the block
382
  // boundary.
383
  //
384
  // So check for the special cases of rhobar==0 and rhobar==1
385
  // before doing any root finding.
386
  const auto this_is_small_if_rhobar_is_near_unity = [this, &target_coords]() {
3,024✔
387
    const double denom = 1.0 / (center_two_[2] - center_one_[2] -
108✔
388
                                radius_one_ * cos(theta_max_one_));
108✔
389
    const double lambda_if_rhobar_is_unity =
390
        (target_coords[2] - center_one_[2] -
108✔
391
         radius_one_ * cos(theta_max_one_)) *
108✔
392
        denom;
108✔
393
    // The function function_to_zero goes like
394
    // first_term + second_term (1-rhobar) when 1-rhobar is small,
395
    // where first_term goes to zero (faster than (1-rhobar)) when
396
    // 1-rhobar is small.
397
    // We don't have a good sense of scale for first_term, so we
398
    // check the size of first_term/|second_term|.
399
    const double temp = radius_one_ * sin(theta_max_one_) +
108✔
400
                        lambda_if_rhobar_is_unity *
108✔
401
                            (radius_two_ - radius_one_ * sin(theta_max_one_));
108✔
402
    const double first_term =
403
        square(target_coords[0] - center_one_[0] -
108✔
404
               lambda_if_rhobar_is_unity * (center_two_[0] - center_one_[0])) +
108✔
405
        square(target_coords[1] - center_one_[1] -
216✔
406
               lambda_if_rhobar_is_unity * (center_two_[1] - center_one_[1])) -
108✔
407
        square(temp);
108✔
408
    // delta_lambda is (lambda-lambda_if_rhobar_is_unity)/(1-rhobar)
409
    // to first order in 1-rhobar.
410
    const double delta_lambda =
411
        -radius_one_ * theta_max_one_ * sin(theta_max_one_) * square(denom) *
108✔
412
        (center_two_[2] - target_coords[2]);
108✔
413
    const double second_term =
414
        2.0 * delta_lambda *
216✔
415
            (lambda_if_rhobar_is_unity *
108✔
416
                 (square(center_two_[0] - center_one_[0]) +
108✔
417
                  square(center_two_[1] - center_one_[1])) -
108✔
418
             (target_coords[0] - center_one_[0]) *
108✔
419
                 (center_two_[1] - center_one_[1]) -
108✔
420
             (target_coords[1] - center_one_[1]) *
108✔
421
                 (center_two_[0] - center_one_[0])) -
108✔
422
        2.0 * temp *
108✔
423
            (-(1.0 - lambda_if_rhobar_is_unity) * radius_one_ * theta_max_one_ *
108✔
424
                 cos(theta_max_one_) -
108✔
425
             lambda_if_rhobar_is_unity * radius_two_ +
108✔
426
             delta_lambda * (radius_two_ - radius_one_ * sin(theta_max_one_)));
108✔
427
    return std::abs(first_term) / std::abs(second_term);
108✔
428
  };
112✔
429

430
  const auto this_is_small_if_rhobar_is_near_zero = [this, &target_coords]() {
1,624✔
431
    const double denom = 1.0 / (center_two_[2] - center_one_[2] - radius_one_);
58✔
432
    const double lambda_if_rhobar_is_zero =
433
        (target_coords[2] - center_one_[2] - radius_one_) * denom;
58✔
434
    // The function we are trying to make zero goes like
435
    // first_term + second_term rhobar^2, and first_term goes to zero
436
    // (faster than rhobar^2) when rhobar is small.  We don't have a good
437
    // sense of scale for first_term, so we check the size of
438
    // first_term/second_term.
439
    const double first_term =
440
        square(target_coords[0] - center_one_[0] -
58✔
441
               lambda_if_rhobar_is_zero * (center_two_[0] - center_one_[0])) +
58✔
442
        square(target_coords[1] - center_one_[1] -
58✔
443
               lambda_if_rhobar_is_zero * (center_two_[1] - center_one_[1]));
58✔
444
    // delta_lambda is (lambda-lambda_if_rhobar_is_zero)/rhobar^2
445
    // to first order in rhobar^2.
446
    const double delta_lambda = radius_one_ * square(theta_max_one_) *
116✔
447
                                square(denom) *
58✔
448
                                (center_two_[2] - target_coords[2]);
58✔
449
    const double second_term =
450
        2.0 * delta_lambda *
116✔
451
            (lambda_if_rhobar_is_zero *
58✔
452
                 (square(center_two_[0] - center_one_[0]) +
58✔
453
                  square(center_two_[1] - center_one_[1])) -
58✔
454
             (target_coords[0] - center_one_[0]) *
58✔
455
                 (center_two_[1] - center_one_[1]) -
58✔
456
             (target_coords[1] - center_one_[1]) *
58✔
457
                 (center_two_[0] - center_one_[0])) -
58✔
458
        square((1.0 - lambda_if_rhobar_is_zero) * radius_one_ * theta_max_one_ +
116✔
459
               lambda_if_rhobar_is_zero * radius_two_);
58✔
460
    return std::abs(first_term) / std::abs(second_term);
58✔
461
  };
112✔
462

463
  if (rhobar_min < 1.e-6 and this_is_small_if_rhobar_is_near_zero() < 1.e-14) {
112✔
464
    // Treat rhobar as zero, if it is zero to roundoff.
465
    // We empirically choose roundoff here to be 1.e-14.  We don't
466
    // really need the rhobar_min < 1.e-6 in the 'if' above, but we
467
    // add it so that we can save the computational expense of calling
468
    // this_is_small_if_rhobar_is_near_zero() when we already know
469
    // that rhobar isn't close to zero. We choose the relatively large
470
    // value of 1.e-6 to err on the side of caution.
471
    const double lambda =
472
        (target_coords[2] - center_one_[2] - radius_one_) /
4✔
473
        (center_two_[2] - center_one_[2] - radius_one_);
4✔
474
    return {{0.0, 0.0, 2.0 * lambda - 1.0}};
4✔
475
  } else if (1.0 - rhobar_max < 1.e-6 and
216✔
476
             this_is_small_if_rhobar_is_near_unity() < 1.e-14) {
108✔
477
    // Treat rhobar as unity, if it is unity to roundoff.
478
    // We empirically choose roundoff here to be 1.e-14.  We don't
479
    // really need the 1-rhobar_max < 1.e-6 in the 'if' above, but we
480
    // add it so that we can save the computational expense of calling
481
    // this_is_small_if_rhobar_is_near_unity() when we already know
482
    // that rhobar isn't close to unity. We choose the relatively
483
    // large value of 1.e-6 to err on the side of caution.
484
    const double lambda =
485
        (target_coords[2] - center_one_[2] -
4✔
486
         radius_one_ * cos(theta_max_one_)) /
4✔
487
        (center_two_[2] - center_one_[2] - radius_one_ * cos(theta_max_one_));
4✔
488
    const double denom =
4✔
489
        1.0 / ((1.0 - lambda) * radius_one_ * sin(theta_max_one_) +
4✔
490
               lambda * radius_two_);
4✔
491
    return {{(target_coords[0] - center_one_[0] -
4✔
492
              lambda * (center_two_[0] - center_one_[0])) *
4✔
493
                 denom,
494
             (target_coords[1] - center_one_[1] -
4✔
495
              lambda * (center_two_[1] - center_one_[1])) *
4✔
496
                 denom,
497
             2.0 * lambda - 1.0}};
8✔
498
  }
499

500
  // If we get here, then rhobar is not near unity or near zero.
501
  // So let's find rhobar.
502
  double rhobar = std::numeric_limits<double>::signaling_NaN();
104✔
503

504
  // The root should always be bracketed.  However, if
505
  // rhobar==rhobar_min or rhobar==rhobar_max, the function may not be
506
  // exactly zero because of roundoff and then the root might not be
507
  // bracketed.
508
  double function_at_rhobar_min = function_to_zero(rhobar_min);
104✔
509
  double function_at_rhobar_max = function_to_zero(rhobar_max);
104✔
510
  if (function_at_rhobar_min * function_at_rhobar_max > 0.0) {
104✔
511
    // Root not bracketed.
512
    // If the bracketing failure is due to roundoff, then
513
    // slightly adjust the bounds to increase the range. Otherwise, error.
514
    //
515
    // roundoff_ratio is the limiting ratio of the two function values
516
    // for which we should attempt to adjust the bounds.  It is ok for
517
    // roundoff_ratio to be much larger than actual roundoff, since it
518
    // doesn't hurt to expand the interval and try again when
519
    // otherwise we would just error.
520
    constexpr double roundoff_ratio = 1.e-3;
20✔
521
    if (abs(function_at_rhobar_min) / abs(function_at_rhobar_max) <
20✔
522
        roundoff_ratio) {
523
      // Slightly decrease rhobar_min.  How far do we decrease it?
524
      // Figure that out by looking at the deriv of the function.
525
      const double deriv_function_at_rhobar_min =
526
          deriv_function_to_zero(rhobar_min);
20✔
527
      const double trial_rhobar_min_increment =
20✔
528
          -2.0 * function_at_rhobar_min / deriv_function_at_rhobar_min;
20✔
529
      // new_rhobar_min = rhobar_min + trial_rhobar_min_increment would be
530
      // a Newton-Raphson step except for the factor of 2 in
531
      // trial_rhobar_min_increment. The factor of 2 is there to
532
      // over-compensate so that the new rhobar_min brackets the
533
      // root.
534
      //
535
      // But sometimes the factor of 2 is not enough when
536
      // trial_rhobar_min_increment is roundoff-small so that the
537
      // change in new_rhobar_min is zero or only in the last one or
538
      // two bits.  If this is the case, then we set
539
      // rhobar_min_increment to some small roundoff value with the
540
      // same sign as trial_rhobar_min_increment.
541
      // Note that rhobar is always between zero and one, so it is
542
      // ok to use an absolute epsilon.
543
      const double rhobar_min_increment =
544
          abs(trial_rhobar_min_increment) >
20✔
545
                  100.0 * std::numeric_limits<double>::epsilon()
20✔
546
              ? trial_rhobar_min_increment
20✔
547
              : boost::math::copysign(
×
548
                    100.0 * std::numeric_limits<double>::epsilon(),
×
549
                    trial_rhobar_min_increment);
20✔
550
      const double new_rhobar_min = rhobar_min + rhobar_min_increment;
20✔
551
      const double function_at_new_rhobar_min =
552
          function_to_zero(new_rhobar_min);
20✔
553
      if (function_at_new_rhobar_min * function_at_rhobar_min > 0.0) {
20✔
554
        // This error should never happen except for a bug, so it
555
        // is hard to make an error test for it.
556
        // LCOV_EXCL_START
557
        ERROR(
558
            "Cannot find bracket after trying to adjust bracketing "
559
            "rhobar_min due "
560
            "to roundoff : rhobar_min="
561
            << rhobar_min << " f(rhobar_min)=" << function_at_rhobar_min
562
            << " rhobar_max=" << rhobar_max << " f(rhobar_max)="
563
            << function_at_rhobar_max << " new_rhobar_min=" << new_rhobar_min
564
            << " f(new_rhobar_min)=" << function_at_new_rhobar_min
565
            << " df(new_rhobar_min)=" << deriv_function_at_rhobar_min
566
            << " new_rhobar_min-rhobar_min=" << new_rhobar_min - rhobar_min
567
            << "\n");
568
        // LCOV_EXCL_STOP
569
      }
570
      // Now the root is bracketed between rhobar_min and new_rhobar_min,
571
      // so replace rhobar_max and rhobar_min and then fall through to the
572
      // root finder.
573
      rhobar_max = rhobar_min;
20✔
574
      function_at_rhobar_max = function_at_rhobar_min;
20✔
575
      rhobar_min = new_rhobar_min;
20✔
576
      function_at_rhobar_min = function_at_new_rhobar_min;
20✔
577
    } else {
578
      // This error should never happen except for a bug, so it
579
      // is hard to make an error test for it.
580
      // LCOV_EXCL_START
581
      ERROR("Root is not bracketed: rhobar_min="
582
            << rhobar_min << " f(rhobar_min)=" << function_at_rhobar_min
583
            << " rhobar_max=" << rhobar_max
584
            << " f(rhobar_max)=" << function_at_rhobar_max);
585
      // LCOV_EXCL_STOP
586
    }
587
  }
588
  // If we get here, root is bracketed. Use toms748, and use the
589
  // function evaluations that we have already done.
590

591
  // Rhobar is between zero and 1, so the scale is unity, so therefore
592
  // abs and rel tolerance are equal.
593
  constexpr double abs_tol = 1.e-15;
104✔
594
  constexpr double rel_tol = 1.e-15;
104✔
595

596
  try {
597
    rhobar =
598
        // NOLINTNEXTLINE(clang-analyzer-core)
599
        RootFinder::toms748(function_to_zero, rhobar_min, rhobar_max,
104✔
600
                            function_at_rhobar_min, function_at_rhobar_max,
601
                            abs_tol, rel_tol);
602
    // This error should never happen except for a bug, so it
603
    // is hard to make an error test for it.
604
    // LCOV_EXCL_START
605
  } catch (std::exception&) {
606
    ERROR("Cannot find root after bracketing: rhobar_min="
607
          << rhobar_min << " f(rhobar_min)=" << function_at_rhobar_min
608
          << " rhobar_max=" << rhobar_max
609
          << " f(rhobar_max)=" << function_at_rhobar_max);
610
    // LCOV_EXCL_STOP
611
  }
612

613
  // Now that we have rhobar, construct inverse.
614
  const double r_one_cos_theta_one = radius_one_ * cos(rhobar * theta_max_one_);
104✔
615
  const double lambda =
616
      (target_coords[2] - center_one_[2] - r_one_cos_theta_one) /
104✔
617
      (center_two_[2] - center_one_[2] - r_one_cos_theta_one);
104✔
618
  const double denom =
104✔
619
      1.0 / ((1.0 - lambda) * radius_one_ * sin(rhobar * theta_max_one_) +
104✔
620
             lambda * radius_two_ * rhobar);
104✔
621
  return {{rhobar *
104✔
622
               (target_coords[0] - center_one_[0] -
104✔
623
                lambda * (center_two_[0] - center_one_[0])) *
104✔
624
               denom,
625
           rhobar *
104✔
626
               (target_coords[1] - center_one_[1] -
104✔
627
                lambda * (center_two_[1] - center_one_[1])) *
104✔
628
               denom,
629
           2.0 * lambda - 1.0}};
208✔
630
}
631

632
template <typename T>
633
tnsr::Ij<tt::remove_cvref_wrap_t<T>, 3, Frame::NoFrame>
634
UniformCylindricalFlatEndcap::jacobian(
4,152✔
635
    const std::array<T, 3>& source_coords) const {
636
  using ReturnType = tt::remove_cvref_wrap_t<T>;
637
  const ReturnType& xbar = source_coords[0];
4,152✔
638
  const ReturnType& ybar = source_coords[1];
4,152✔
639
  const ReturnType& zbar = source_coords[2];
4,152✔
640

641
  auto jac =
448✔
642
      make_with_value<tnsr::Ij<tt::remove_cvref_wrap_t<T>, 3, Frame::NoFrame>>(
3,704✔
643
          dereference_wrapper(source_coords[0]), 0.0);
8,304✔
644

645
  // Use jacobian components as temporary storage to avoid extra
646
  // memory allocations.
647
  get<2, 2>(jac) = sqrt(square(xbar) + square(ybar));
8,752✔
648
  get<2, 0>(jac) = 0.5 * radius_one_ *
4,600✔
649
                   cylindrical_endcap_helpers::sin_ax_over_x(get<2, 2>(jac),
4,152✔
650
                                                             theta_max_one_) *
5,048✔
651
                   (1.0 - zbar);
4,152✔
652
  get<1, 1>(jac) = get<2, 0>(jac) + 0.5 * radius_two_ * (1.0 + zbar);
5,048✔
653
  get<2, 1>(jac) = theta_max_one_ * get<2, 0>(jac);
8,304✔
654
  get<1, 2>(jac) =
4,152✔
655
      0.5 *
4,152✔
656
      (radius_two_ - radius_one_ * cylindrical_endcap_helpers::sin_ax_over_x(
4,600✔
657
                                       get<2, 2>(jac), theta_max_one_));
8,304✔
658
  get<1, 0>(jac) = 0.5 * radius_one_ *
4,600✔
659
                   cylindrical_endcap_helpers::one_over_x_d_sin_ax_over_x(
7,408✔
660
                       get<2, 2>(jac), theta_max_one_) *
9,200✔
661
                   (1.0 - zbar);
4,600✔
662

663
  // Now fill Jacobian values
664
  get<0, 0>(jac) = square(xbar) * get<1, 0>(jac) + get<1, 1>(jac);
16,608✔
665
  get<1, 1>(jac) = square(ybar) * get<1, 0>(jac) + get<1, 1>(jac);
12,904✔
666
  get<0, 1>(jac) = xbar * ybar * get<1, 0>(jac);
12,008✔
667
  get<1, 0>(jac) = get<0, 1>(jac);
4,600✔
668
  get<2, 0>(jac) = -xbar * get<2, 1>(jac);
8,304✔
669
  get<2, 1>(jac) = -ybar * get<2, 1>(jac);
8,304✔
670
  get<0, 2>(jac) =
4,152✔
671
      xbar * get<1, 2>(jac) + 0.5 * (center_two_[0] - center_one_[0]);
8,304✔
672
  get<1, 2>(jac) =
4,152✔
673
      ybar * get<1, 2>(jac) + 0.5 * (center_two_[1] - center_one_[1]);
8,752✔
674
  get<2, 2>(jac) = 0.5 * (center_two_[2] - center_one_[2] -
4,600✔
675
                          radius_one_ * cos(theta_max_one_ * get<2, 2>(jac)));
7,856✔
676

677
  return jac;
4,152✔
678
}
679

680
template <typename T>
681
tnsr::Ij<tt::remove_cvref_wrap_t<T>, 3, Frame::NoFrame>
682
UniformCylindricalFlatEndcap::inv_jacobian(
580✔
683
    const std::array<T, 3>& source_coords) const {
684
  return determinant_and_inverse(jacobian(source_coords)).second;
804✔
685
}
686

687
void UniformCylindricalFlatEndcap::pup(PUP::er& p) {
999✔
688
  p | center_one_;
999✔
689
  p | center_two_;
999✔
690
  p | radius_one_;
999✔
691
  p | radius_two_;
999✔
692
  p | z_plane_one_;
999✔
693
  p | theta_max_one_;
999✔
694
}
999✔
695

696
bool operator==(const UniformCylindricalFlatEndcap& lhs,
384✔
697
                const UniformCylindricalFlatEndcap& rhs) {
698
  // don't need to compare theta_max_one_
699
  // because it is uniquely determined from the other variables.
700
  return lhs.center_one_ == rhs.center_one_ and
384✔
701
         lhs.radius_one_ == rhs.radius_one_ and
384✔
702
         lhs.z_plane_one_ == rhs.z_plane_one_ and
384✔
703
         lhs.center_two_ == rhs.center_two_ and
1,152✔
704
         lhs.radius_two_ == rhs.radius_two_;
768✔
705
}
706

707
bool operator!=(const UniformCylindricalFlatEndcap& lhs,
52✔
708
                const UniformCylindricalFlatEndcap& rhs) {
709
  return not(lhs == rhs);
52✔
710
}
711

712
// Explicit instantiations
713
#define DTYPE(data) BOOST_PP_TUPLE_ELEM(0, data)
714

715
#define INSTANTIATE(_, data)                                                 \
716
  template std::array<tt::remove_cvref_wrap_t<DTYPE(data)>, 3>               \
717
  UniformCylindricalFlatEndcap::operator()(                                  \
718
      const std::array<DTYPE(data), 3>& source_coords) const;                \
719
  template tnsr::Ij<tt::remove_cvref_wrap_t<DTYPE(data)>, 3, Frame::NoFrame> \
720
  UniformCylindricalFlatEndcap::jacobian(                                    \
721
      const std::array<DTYPE(data), 3>& source_coords) const;                \
722
  template tnsr::Ij<tt::remove_cvref_wrap_t<DTYPE(data)>, 3, Frame::NoFrame> \
723
  UniformCylindricalFlatEndcap::inv_jacobian(                                \
724
      const std::array<DTYPE(data), 3>& source_coords) const;
725

726
GENERATE_INSTANTIATIONS(INSTANTIATE, (double, DataVector,
727
                                      std::reference_wrapper<const double>,
728
                                      std::reference_wrapper<const DataVector>))
729

730
#undef DTYPE
731
#undef INSTANTIATE
732

733
}  // namespace domain::CoordinateMaps
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc