layers/density_map.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/density_map.h" | ||
| 2 | |||
| 3 | #include "terraingen/util/checks.h" | ||
| 4 | |||
| 5 | #include <algorithm> | ||
| 6 | #include <cmath> | ||
| 7 | #include <numbers> | ||
| 8 | #include <stdexcept> | ||
| 9 | #include <utility> | ||
| 10 | |||
| 11 | namespace terraingen { | ||
| 12 | |||
| 13 | namespace { | ||
| 14 | |||
| 15 | /// Simpson intervals in log diameter for an areal fraction. | ||
| 16 | constexpr int kArealSteps = 2048; | ||
| 17 | |||
| 18 | /// The share of ground covered by features of `distribution` in [`low`, `high`) metres: the | ||
| 19 | /// integral of pi D^2 / 4 dN/dD, taken in log diameter. | ||
| 20 | 1 | double ArealFraction(const SizeFrequencyDistribution& distribution, double low, double high) { | |
| 21 | 1 | const double a = std::log(low); | |
| 22 | 1 | const double h = (std::log(high) - a) / kArealSteps; | |
| 23 | 2049 | const auto f = [&](int i) { | |
| 24 | 2049 | const double d = std::exp(a + h * i); | |
| 25 | 2049 | return 0.25 * std::numbers::pi * d * d * d * distribution.Incremental(d); | |
| 26 | 1 | }; | |
| 27 | 1 | double sum = f(0) + f(kArealSteps); | |
| 28 | 2048 | for (int i = 1; i < kArealSteps; ++i) { | |
| 29 | 2047 | sum += (i % 2 == 1 ? 4.0 : 2.0) * f(i); | |
| 30 | } | ||
| 31 | 1 | return sum * h / 3.0; | |
| 32 | } | ||
| 33 | |||
| 34 | } // namespace | ||
| 35 | |||
| 36 | 78 | void ValidateDensityMap(const DensityMap& map) { | |
| 37 | 78 | if (!map.field) { | |
| 38 | 2 | throw std::invalid_argument("A density map needs a field"); | |
| 39 | } | ||
| 40 | 109 | if (map.measure != DensityMeasure::Scale && | |
| 41 | 33 | !(IsPositive(map.minDiameter) && map.maxDiameter > map.minDiameter)) { | |
| 42 | 3 | throw std::invalid_argument("A density map's diameters must satisfy 0 < min < max"); | |
| 43 | } | ||
| 44 | 73 | RequirePositive(map.areaUnit, "Density map areaUnit"); | |
| 45 | 72 | } | |
| 46 | |||
| 47 | 536 | double MapMeasure(const DensityMap& map, const SizeFrequencyDistribution& distribution) { | |
| 48 | 536 | switch (map.measure) { | |
| 49 | 9 | case DensityMeasure::Scale: | |
| 50 | 9 | return 1.0; | |
| 51 | 525 | case DensityMeasure::Count: | |
| 52 | 525 | return distribution.ExpectedCount(1.0, map.minDiameter, map.maxDiameter); | |
| 53 | 2 | case DensityMeasure::ArealFraction: { | |
| 54 | 2 | const double high = std::fmin(map.maxDiameter, distribution.ValidRange().max); | |
| 55 | 2 | if (!std::isfinite(high)) { | |
| 56 | 1 | throw std::domain_error("An areal fraction needs a finite largest diameter"); | |
| 57 | } | ||
| 58 | 1 | return ArealFraction(distribution, map.minDiameter, high); | |
| 59 | } | ||
| 60 | } | ||
| 61 | ✗ | return 1.0; | |
| 62 | } | ||
| 63 | |||
| 64 | 25 | void ValidateSizeSlopeMap(const SizeSlopeMap& map) { | |
| 65 | 25 | if (!map.field) { | |
| 66 | 1 | throw std::invalid_argument("A size slope map needs a field"); | |
| 67 | } | ||
| 68 | 24 | RequirePositive(map.referenceDiameter, "Size slope map referenceDiameter"); | |
| 69 | 43 | if (!(map.minExponent > 0.0 && map.maxExponent > map.minExponent && | |
| 70 | 20 | std::isfinite(map.maxExponent))) { | |
| 71 | 4 | throw std::invalid_argument("A size slope map's exponents must satisfy 0 < min < max"); | |
| 72 | } | ||
| 73 | 19 | } | |
| 74 | |||
| 75 | 287383 | std::optional<double> SizeSlopeAt(const SizeSlopeMap& map, double x, double y) { | |
| 76 | 287383 | const double value = map.field->ValueAt(x, y); | |
| 77 | 287383 | if (std::isnan(value)) { | |
| 78 | 1 | return std::nullopt; | |
| 79 | } | ||
| 80 | 287382 | double exponent = value; | |
| 81 | 287382 | if (map.measure == SizeSlopeMeasure::MeanDiameter) { | |
| 82 | 2 | const double above = value - map.referenceDiameter; | |
| 83 | 2 | exponent = above > 0.0 ? value / above : map.maxExponent; | |
| 84 | } | ||
| 85 | 287382 | return std::clamp(exponent, map.minExponent, map.maxExponent); | |
| 86 | } | ||
| 87 | |||
| 88 | 2128253 | double MapValueAt(const DensityMap& map, double x, double y) { | |
| 89 | 2128253 | const double value = map.field->ValueAt(x, y); | |
| 90 | 2128253 | return map.measure == DensityMeasure::Count ? value / map.areaUnit : value; | |
| 91 | } | ||
| 92 | |||
| 93 | 12 | DensityScale::DensityScale(DensityMap map, const SizeFrequencyDistribution& distribution) | |
| 94 | 12 | : map_(std::move(map)) { | |
| 95 | 12 | ValidateDensityMap(map_); | |
| 96 | 12 | measure_ = MapMeasure(map_, distribution); | |
| 97 | 12 | if (!(measure_ > 0.0 && std::isfinite(measure_))) { | |
| 98 | 1 | throw std::invalid_argument("A density map must measure some of its size distribution"); | |
| 99 | } | ||
| 100 | 11 | const double top = map_.field->Max(); | |
| 101 | 11 | const double largest = map_.measure == DensityMeasure::Count ? top / map_.areaUnit : top; | |
| 102 | 11 | if (largest / measure_ > max_) { | |
| 103 | 7 | max_ = largest / measure_; | |
| 104 | } | ||
| 105 | 12 | } | |
| 106 | |||
| 107 | 531156 | double DensityScale::At(double x, double y) const { | |
| 108 | 531156 | return Scale(MapValueAt(map_, x, y)); | |
| 109 | } | ||
| 110 | |||
| 111 | 1044551 | double DensityScale::Scale(double value) const { | |
| 112 | 1044551 | if (std::isnan(value)) { | |
| 113 | 2 | return 1.0; | |
| 114 | } | ||
| 115 | 1044549 | return std::clamp(value / measure_, 0.0, max_); | |
| 116 | } | ||
| 117 | |||
| 118 | } // namespace terraingen | ||
| 119 |