layers/craters/secondary_model.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/craters/secondary_model.h" | ||
| 2 | |||
| 3 | #include "terraingen/layers/fade.h" | ||
| 4 | #include "terraingen/noise/value.h" | ||
| 5 | #include "terraingen/util/checks.h" | ||
| 6 | |||
| 7 | #include <algorithm> | ||
| 8 | #include <cmath> | ||
| 9 | #include <limits> | ||
| 10 | #include <numbers> | ||
| 11 | #include <stdexcept> | ||
| 12 | |||
| 13 | namespace terraingen { | ||
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | /// Singer et al. (2020, Table 3): d = a R^-b, a = kSingerScale D^kSingerSizeExponent, b = | ||
| 18 | /// kSingerRangeExponent ln D, in km. | ||
| 19 | constexpr double kSingerScale = 0.022; | ||
| 20 | constexpr double kSingerSizeExponent = 1.96; | ||
| 21 | constexpr double kSingerRangeExponent = 0.14; | ||
| 22 | |||
| 23 | /// The outer range of rays as a multiple of their median length (Elliott et al. 2018). | ||
| 24 | constexpr double kLongestRayOverMedian = 1.6; | ||
| 25 | |||
| 26 | /// How strongly secondaries clump at `u` clump wavelengths along a ray keyed `key`: 0 between | ||
| 27 | /// clumps, 1 in them. | ||
| 28 | 30677469 | double ClumpBump(double u, std::uint32_t key) { | |
| 29 | 30677469 | const double n = static_cast<double>(ValueNoise2(static_cast<float>(u), 0.5f, key)); | |
| 30 | 30677469 | return Smoothstep(std::clamp(0.5 + 1.5 * n, 0.0, 1.0)); | |
| 31 | } | ||
| 32 | |||
| 33 | /// The mean of `ClumpBump` along a ray, found once. | ||
| 34 | 1030 | double MeanClumpBump() { | |
| 35 | 4 | static const double mean = [] { | |
| 36 | 4 | constexpr int kSamples = 1 << 16; | |
| 37 | 4 | double sum = 0.0; | |
| 38 | 262148 | for (int i = 0; i < kSamples; ++i) { | |
| 39 | 262144 | sum += ClumpBump(0.0371 * i, 0x434c554du); | |
| 40 | } | ||
| 41 | 4 | return sum / kSamples; | |
| 42 | 1030 | }(); | |
| 43 | 1030 | return mean; | |
| 44 | } | ||
| 45 | |||
| 46 | } // namespace | ||
| 47 | |||
| 48 | 3111001 | double SingerMaxSecondaryDiameter(double diameter, double range) { | |
| 49 | 3111001 | const double d = diameter / 1000.0; | |
| 50 | 3111001 | const double r = range / 1000.0; | |
| 51 | 3111001 | return 1000.0 * kSingerScale * std::pow(d, kSingerSizeExponent) * | |
| 52 | 3111001 | std::pow(r, -kSingerRangeExponent * std::log(d)); | |
| 53 | } | ||
| 54 | |||
| 55 | 3225 | double SingerRangeForSize(double diameter, double size) { | |
| 56 | 3225 | const double d = diameter / 1000.0; | |
| 57 | 3225 | const double logD = std::log(d); | |
| 58 | 3225 | if (!(logD > 0.0)) { | |
| 59 | 2 | return std::numeric_limits<double>::infinity(); | |
| 60 | } | ||
| 61 | 3223 | const double a = kSingerScale * std::pow(d, kSingerSizeExponent); | |
| 62 | 3223 | return 1000.0 * std::pow(a / (size / 1000.0), 1.0 / (kSingerRangeExponent * logD)); | |
| 63 | } | ||
| 64 | |||
| 65 | 482165 | double SecondaryOnsetRange(double diameter) { | |
| 66 | 482165 | const double small = 3000.0; | |
| 67 | 482165 | const double large = 31000.0; | |
| 68 | 482165 | double rim = 1.8; | |
| 69 | 482165 | if (diameter >= large) { | |
| 70 | 5 | rim = 1.3; | |
| 71 | 482160 | } else if (diameter > small) { | |
| 72 | 6 | const double t = std::log(diameter / small) / std::log(large / small); | |
| 73 | 6 | rim = 1.8 + t * (1.3 - 1.8); | |
| 74 | } | ||
| 75 | 482165 | return (0.5 + rim) * diameter; | |
| 76 | } | ||
| 77 | |||
| 78 | 952099 | double ElliottMedianRayLength(double diameter, double coefficient, double exponent) { | |
| 79 | 952099 | return 1000.0 * coefficient * std::pow(0.0005 * diameter, exponent); | |
| 80 | } | ||
| 81 | |||
| 82 | 74 | void ValidateSecondaryCraterOptions(const SecondaryCraterOptions& options) { | |
| 83 | 74 | if (!(options.onsetCount && IsPositive(*options.onsetCount))) { | |
| 84 | throw std::invalid_argument("Secondary craters need an onset count: the sources give their " | ||
| 85 | 2 | "sizes, not their number"); | |
| 86 | } | ||
| 87 | 72 | if (!(IsPositive(options.countFraction) && IsPositive(options.sizeSlope))) { | |
| 88 | 3 | throw std::invalid_argument("The secondary count fraction and size slope must be positive"); | |
| 89 | } | ||
| 90 | 69 | if (!(IsPositive(options.minDiameter) && options.maxDiameter > options.minDiameter)) { | |
| 91 | 2 | throw std::invalid_argument("Secondary diameters must satisfy 0 < min < max"); | |
| 92 | } | ||
| 93 | 67 | if (options.outerRange && !IsPositive(*options.outerRange)) { | |
| 94 | 1 | throw std::invalid_argument("The secondary outer range must be positive"); | |
| 95 | } | ||
| 96 | 66 | ValidateCraterSelection(options.selection, "Primary"); | |
| 97 | 64 | if (!(IsPositive(options.cellSizeFactor) && IsPositive(options.maxExpected))) { | |
| 98 | 1 | throw std::invalid_argument("The cell size factor and expected limit must be positive"); | |
| 99 | } | ||
| 100 | 63 | const SecondaryRays& rays = options.rays; | |
| 101 | 63 | ValidateRayLayout(rays.layout); | |
| 102 | 122 | if (!(IsInUnitInterval(rays.share) && IsInUnitInterval(rays.clustering) && | |
| 103 | 60 | rays.minHalfAngleDegrees > 0.0 && rays.minHalfAngleDegrees < 90.0 && | |
| 104 | 59 | IsPositive(rays.clusterSpacing))) { | |
| 105 | throw std::invalid_argument("Secondary rays need a share and clustering in [0, 1], a floor " | ||
| 106 | 4 | "angle in (0, 90) degrees and a positive cluster spacing"); | |
| 107 | } | ||
| 108 | 58 | const SecondaryShapes& shapes = options.shapes; | |
| 109 | 58 | ValidateValueDistribution(shapes.ellipticity); | |
| 110 | 112 | if (!(IsInUnitInterval(shapes.elongatedShare) && shapes.ellipticity.Lowest() >= 1.0 && | |
| 111 | 55 | std::isfinite(shapes.ellipticity.Highest()))) { | |
| 112 | throw std::invalid_argument("Secondary shapes need a share in [0, 1] and a bounded " | ||
| 113 | 2 | "ellipticity of at least 1"); | |
| 114 | } | ||
| 115 | 55 | } | |
| 116 | |||
| 117 | 481133 | double SecondaryOuterRange(double diameter, const SecondaryCraterOptions& options) { | |
| 118 | 481133 | if (options.outerRange) { | |
| 119 | 2 | return *options.outerRange * diameter; | |
| 120 | } | ||
| 121 | 481131 | return kLongestRayOverMedian * ElliottMedianRayLength(diameter); | |
| 122 | } | ||
| 123 | |||
| 124 | 1080 | std::optional<SecondaryRayField> SecondaryRayField::Make(const Crater& primary, | |
| 125 | const SecondaryRays& rays) { | ||
| 126 | 1080 | if (!rays.enabled) { | |
| 127 | 49 | return std::nullopt; | |
| 128 | } | ||
| 129 | 1031 | SecondaryRayField field; | |
| 130 | 1031 | field.rays = RayDirections(primary, rays.layout); | |
| 131 | 1031 | if (field.rays.empty()) { | |
| 132 | 1 | return std::nullopt; | |
| 133 | } | ||
| 134 | 1030 | field.diameter = primary.diameter; | |
| 135 | 1030 | field.onset = SecondaryOnsetRange(primary.diameter); | |
| 136 | 1030 | field.share = rays.share; | |
| 137 | 1030 | field.sinFloor = std::sin(rays.minHalfAngleDegrees * std::numbers::pi / 180.0); | |
| 138 | 1030 | field.clustering = rays.clustering; | |
| 139 | 1030 | field.spacing = rays.clusterSpacing; | |
| 140 | 1030 | field.meanClump = MeanClumpBump(); | |
| 141 | 1030 | return field; | |
| 142 | 1031 | } | |
| 143 | |||
| 144 | 45984311 | double SecondaryRayField::Width(const RayDirection& ray, double range) const { | |
| 145 | 45984311 | return std::fmax(diameter * ray.sinHalfWidth, range * sinFloor); | |
| 146 | } | ||
| 147 | |||
| 148 | 32687417 | double SecondaryRayField::Profile(const RayDirection& ray, double range, double azimuth) const { | |
| 149 | 32687417 | const double delta = std::remainder(azimuth - ray.azimuth, 2.0 * std::numbers::pi); | |
| 150 | 32687417 | if (std::cos(delta) <= 0.0) { | |
| 151 | 19390523 | return 0.0; | |
| 152 | } | ||
| 153 | 13296894 | const double across = range * std::sin(std::fabs(delta)) / Width(ray, range); | |
| 154 | 13296894 | return std::exp(-across * across); | |
| 155 | } | ||
| 156 | |||
| 157 | 30415325 | double SecondaryRayField::Clump(const RayDirection& ray, double range) const { | |
| 158 | 30415325 | const double u = std::log(range / onset) / spacing; | |
| 159 | 30415325 | return (1.0 - clustering) + clustering * ClumpBump(u, ray.key) / meanClump; | |
| 160 | } | ||
| 161 | |||
| 162 | 2934190 | double SecondaryRayField::MeanProfile(double range) const { | |
| 163 | 2934190 | double sum = 0.0; | |
| 164 | 35621607 | for (const RayDirection& ray : rays) { | |
| 165 | 32687417 | sum += std::sqrt(std::numbers::pi) * Width(ray, range); | |
| 166 | } | ||
| 167 | 2934190 | return sum / (2.0 * std::numbers::pi * range); | |
| 168 | } | ||
| 169 | |||
| 170 | 2727802 | double SecondaryRayField::Density(double range, double azimuth) const { | |
| 171 | 2727802 | double sum = 0.0; | |
| 172 | 33143127 | for (const RayDirection& ray : rays) { | |
| 173 | 30415325 | sum += Profile(ray, range, azimuth) * Clump(ray, range); | |
| 174 | } | ||
| 175 | 2727802 | return (1.0 - share) + share * sum / MeanProfile(range); | |
| 176 | } | ||
| 177 | |||
| 178 | 6251 | double SecondaryRayField::Peak(double r0, double r1) const { | |
| 179 | 6251 | double peak = 0.0; | |
| 180 | 25004 | for (const double range : {r0, 0.5 * (r0 + r1), r1}) { | |
| 181 | 225141 | for (const RayDirection& at : rays) { | |
| 182 | 206388 | double sum = 0.0; | |
| 183 | 2478480 | for (const RayDirection& ray : rays) { | |
| 184 | 2272092 | sum += Profile(ray, range, at.azimuth); | |
| 185 | } | ||
| 186 | 206388 | peak = std::fmax(peak, sum / MeanProfile(range)); | |
| 187 | } | ||
| 188 | } | ||
| 189 | 6251 | const double clump = (1.0 - clustering) + clustering / meanClump; | |
| 190 | 6251 | return (1.0 - share) + share * 1.1 * peak * clump; | |
| 191 | } | ||
| 192 | |||
| 193 | 8 | SecondaryDensity::SecondaryDensity(const Crater& primary, const SecondaryCraterOptions& options) | |
| 194 | 8 | : sizeSlope_(options.sizeSlope), minSize_(options.minDiameter), maxSize_(options.maxDiameter), | |
| 195 | 8 | diameter_(primary.diameter) { | |
| 196 | 8 | if (!options.selection.Includes(primary) || !options.onsetCount) { | |
| 197 | 1 | return; | |
| 198 | } | ||
| 199 | 7 | onset_ = SecondaryOnsetRange(diameter_); | |
| 200 | 7 | const double outer = SecondaryOuterRange(diameter_, options); | |
| 201 | 7 | if (!(outer > onset_)) { | |
| 202 | ✗ | return; | |
| 203 | } | ||
| 204 | 7 | outer_ = outer; | |
| 205 | 7 | density0_ = *options.onsetCount / (0.25 * std::numbers::pi * diameter_ * diameter_); | |
| 206 | 7 | reference_ = options.countFraction * diameter_; | |
| 207 | 7 | rays_ = SecondaryRayField::Make(primary, options.rays); | |
| 208 | } | ||
| 209 | |||
| 210 | 503568 | double SecondaryDensity::Measure(const Vec2d& offset, double dMin, double dMax, bool cover) const { | |
| 211 | 503568 | const double range = std::hypot(offset.x, offset.y); | |
| 212 | 503568 | if (!(range >= onset_ && range < outer_)) { | |
| 213 | 61930 | return 0.0; | |
| 214 | } | ||
| 215 | 441638 | const double low = std::fmax(dMin, minSize_); | |
| 216 | const double high = | ||
| 217 | 441638 | std::fmin(std::fmin(dMax, maxSize_), SingerMaxSecondaryDiameter(diameter_, range)); | |
| 218 | 441638 | if (!(high > low)) { | |
| 219 | ✗ | return 0.0; | |
| 220 | } | ||
| 221 | 441638 | const double r = 1.0 + (range - onset_) / diameter_; | |
| 222 | 441638 | const double s = sizeSlope_; | |
| 223 | 441638 | double sizes = std::pow(low / reference_, -s) - std::pow(high / reference_, -s); | |
| 224 | 441638 | if (cover) { | |
| 225 | 2 | const double scale = 0.25 * std::numbers::pi * s * std::pow(reference_, s); | |
| 226 | 3 | sizes = s == 2.0 ? scale * std::log(high / low) | |
| 227 | 1 | : scale * (std::pow(high, 2.0 - s) - std::pow(low, 2.0 - s)) / (2.0 - s); | |
| 228 | } | ||
| 229 | 441638 | const double density = density0_ / (r * r) * sizes; | |
| 230 | 441638 | return rays_ ? density * rays_->Density(range, std::atan2(offset.y, offset.x)) : density; | |
| 231 | } | ||
| 232 | |||
| 233 | 503566 | double SecondaryDensity::At(const Vec2d& offset, double dMin, double dMax) const { | |
| 234 | 503566 | return Measure(offset, dMin, dMax, false); | |
| 235 | } | ||
| 236 | |||
| 237 | 2 | double SecondaryDensity::CoverAt(const Vec2d& offset, double dMin, double dMax) const { | |
| 238 | 2 | return Measure(offset, dMin, dMax, true); | |
| 239 | } | ||
| 240 | |||
| 241 | 2 | double SecondaryDensity::Largest() const { | |
| 242 | 2 | if (!(outer_ > 0.0)) { | |
| 243 | ✗ | return 0.0; | |
| 244 | } | ||
| 245 | 2 | return std::fmin(maxSize_, std::fmax(SingerMaxSecondaryDiameter(diameter_, onset_), | |
| 246 | 4 | SingerMaxSecondaryDiameter(diameter_, outer_))); | |
| 247 | } | ||
| 248 | |||
| 249 | 202 | double LargestSecondary(double diameter, const SecondaryCraterOptions& options) { | |
| 250 | 202 | const double onset = SecondaryOnsetRange(diameter); | |
| 251 | 202 | const double outer = SecondaryOuterRange(diameter, options); | |
| 252 | 202 | if (!(outer > onset)) { | |
| 253 | 2 | return 0.0; | |
| 254 | } | ||
| 255 | 200 | return std::fmin(options.maxDiameter, std::fmax(SingerMaxSecondaryDiameter(diameter, onset), | |
| 256 | 200 | SingerMaxSecondaryDiameter(diameter, outer))); | |
| 257 | } | ||
| 258 | |||
| 259 | } // namespace terraingen | ||
| 260 |